SeekquarrySeekquarry - Yioop Repo - Seekquarry

/ devlog / dev_process_for_llms.html⤓
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Yioop &mdash; Development process for LLM co-implementers</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 760px;
    margin: 2rem auto; padding: 0 1rem; line-height: 1.5;
    color: #1a1a1a; }
h2 { margin-top: 2rem; border-bottom: 1px solid #ddd;
    padding-bottom: 0.2rem; }
h3 { margin-top: 1.4rem; }
ol, ul { margin: 0.4rem 0; }
li { margin: 0.35rem 0; }
.note { color: #555; font-size: 0.92em; margin: 0.6rem 0 1.2rem; }
.counter { background: #fff4e5; border: 1px solid #f0c890;
    border-radius: 6px; padding: 0.6rem 0.9rem; margin: 1rem 0; }
code { background: #f2f2f2; padding: 0 0.2em; border-radius: 3px; }
</style>
</head>
<body>
<h1>Yioop &mdash; Development process for LLM co-implementers</h1>
<p class="note">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 &mdash; the gold/work sync, the
createdb check, the locale and library rules &mdash; is marked. When
something says "always" or "never," treat it as a hard rule.</p>

<div class="counter">
<b>Round counter.</b> Keep a count of rounds since you last re-read this
file. A round is one exchange: Chris says something, you respond, and that
includes every time he stops you mid-work, however short what he says.
Echo the current count in bold as the first thing in every response. When
the count reaches 10, re-read this whole file before writing your next
response. Write 2-3 sentences for each section in this file briefly
describing it and how you can adhere to that section more closely in the
next round. Then reset the count to 0. A re-read Chris asks for before
the count reaches 10 never resets it: the count goes on, and the review
at 10 is still done. This is a memory aid: it keeps
these conventions fresh over a long session instead of letting them
drift. The same counter runs in atto's
copy of this file.
<p>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.
&ldquo;Calls that free what frees itself are never written&rdquo; 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.</p>
<p>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.</p>
<p>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:</p>
<ol>
  <li><b>Every lapse in the ten rounds, named, with what stops it
    recurring.</b> Not a general resolution to do better: the rule that
    was broken, the round it was broken in, and the mechanical guard or
    changed habit that catches it next time. A lapse Chris had to find
    and report is worth naming first.</li>
  <li><b>What the ten rounds show about the process itself.</b> Where
    rounds went to waste, which check caught a fault late that could have
    caught it early, which step cost more than it returned. Say what to
    change in this file, and change it in the same patch.</li>
</ol>
</div>

<h2>How changes move (Yioop-specific): gold and work</h2>
<p>Yioop is developed against a two-clone discipline:</p>
<ul>
  <li><b>gold</b> is the confirmed line of history. Its
    <code>confirmed-head</code> tag marks the latest confirmed commit.</li>
  <li><b>work</b> is a throwaway clone of gold where changes are made. It
    is re-cloned from gold after every gold update, with the configured
    <code>work_directory</code> restored into it.</li>
</ul>
<p><b>Setting up gold, and keeping it current, from seekquarry.com.</b> The
confirmed history lives at
<code>https://www.seekquarry.com/group/2/yioop-repo.git</code>, 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: <code>git clone --depth 200
https://www.seekquarry.com/group/2/yioop-repo.git</code> into the gold path.
That is enough recent history for context without pulling the whole
repository. Tag <code>confirmed-head</code> at the clone's tip.</p>
<p>Each round you deliver one patch. Gold then advances not by pulling from
the remote each round &mdash; that has proven unreliable over the shallow
clone &mdash; 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 &mdash;
<code>git apply</code> the delivery, commit it in gold with that message, and
move <code>confirmed-head</code> to the new commit &mdash; 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 <code>confirmed-head..HEAD</code> diff needs.</p>
<p><b>gold is a vault, not a workbench.</b> Exactly two operations ever
touch gold: cloning it to make work, and advancing it by applying a patch
Chris has confirmed and moving <code>confirmed-head</code> to it, as above.
Nothing else, ever. Never run a build, a test, a
golden regeneration, a <code>ConfigureTool</code> command such as
<code>createdb</code>,
or any other command inside gold; never create a <code>work_directory</code>
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 &mdash; even just to check a
result or to get back in sync &mdash; that urge is the process breaking;
stop, and do it in work.</p>
<p>The loop each round, and this must be done <b>prior to any new work</b>:
first settle gold. If Chris's reply shows the commit message for the patch
you delivered last round, apply that patch to gold, move
<code>confirmed-head</code> to the new commit, re-clone work, and restore
<code>work_directory</code>; 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 &mdash; that
would discard the in-progress changes; instead apply the confirmed patch to
gold, move <code>confirmed-head</code> to that commit in both gold and work,
and only then cut. Cut a delivery as <code>git diff
confirmed-head..HEAD</code>, and dry-run it with <code>git apply
--check</code> on a fresh clone checked out at <code>confirmed-head</code>
before presenting. Apply patches forward only &mdash; 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.</p>

<h3>Two scripts that keep gold in step with what Chris committed</h3>
<p>Saying gold advanced is not advancing it. The failure this guards
against is writing &ldquo;gold is now at X&rdquo; 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
<code>devlog/</code> remove the judgment from the reporting, and a ledger
file records what has been delivered and what has been settled.</p>
<ul>
  <li><code>devlog/devscripts/settle_gold.sh "commit message"</code> 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 <code>confirmed-head</code>, and marks it settled. Either way it
    prints gold's head read back from the tree, and any patch still
    waiting. <b>The gold line in your response is that printed output,
    pasted.</b> 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 <code>grep</code> on that output once hid the
    gold line, and a round then went out with no gold line to paste.</li>
  <li><code>settle_gold.sh --withdraw name-of-patch</code> 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.</li>
  <li>A delivered patch Chris neither confirms with a commit message nor
    rejects is folded into the next patch. That round's settle is
    <code>--withdraw</code> 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.</li>
  <li><code>devlog/devscripts/cut_patch.sh name-of-patch</code> 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.</li>
</ul>
<p>The ledger lives outside the repository (by default
<code>/home/claude/gold_ledger.txt</code>) and holds one line per patch:
its path, its name, and <code>delivered</code> or <code>settled</code>.
Both scripts take <code>GOLD</code>, <code>WORK</code>, <code>OUT</code>
and <code>LEDGER</code> from the environment, so a sandbox laid out
differently only needs those set.</p>

<h3>A bug that outlives a conversation</h3>
<p>A conversation can reach its limit mid-chase. When one does, write the
state of the chase as a file and hand it over &mdash; 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.</p>

<h2>Setting up the dev environment (Yioop-specific)</h2>
<p>Beyond the gold/work clones above, a work clone needs a configured
<code>work_directory</code> before it will build or run. Let <i>php</i> 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
<code>work_directory</code> into the fresh work clone, point Yioop
at it with <code>php src/configs/ConfigureTool.php work-dir</code>, then
build the two SQLite databases with
<code>php src/configs/ConfigureTool.php createdb</code>, which should print
&ldquo;Create DB succeeded.&rdquo; Without a configured
<code>work_directory</code>, <code>Config.php</code> stops short of defining
its later constants (for example <code>MAX_GROUP_PAGE_LEN</code>) and
createdb dies with an undefined-constant error &mdash; that failure is the
environment, not your patch. Strings and resources are always read from
<code>src/locale</code> first. <code>work_directory/app/locale</code>
(<code>LOCALE_DIR</code>) may be absent, or hold only the strings and
resources a site changed, which take the place of the shipped ones.
Nothing writes to <code>src/locale</code>: TokenTool and the Manage Locales
screen write to <code>app/locale</code>.</p>
<h3>Tools that ship with Yioop</h3>
<ul>
  <li><code>src/executables/CodeTool.php</code> is the general-purpose tool.
    Run a test script with <code>php src/executables/CodeTool.php unit
    &lt;TestFile&gt;</code> (or <code>unit list</code> /
    <code>unit ClassName methodName</code> to target cases); check docblocks
    with <code>needsdocs &lt;file&gt;</code>; and check the 80-column rule with
    <code>longlines &lt;file&gt;</code> (multibyte-aware, so it is the
    authority over a raw byte count).</li>
  <li>Yioop itself can be run under its own web server via the index.php
  script in the root directory of the project, or under another web server
  such as nginx or Apache.</li>
  <li>There are many Command-line tools that can easily be installed in this
    environment:
    <code>lynx</code> and <code>curl</code> for fetching and reading pages as
    text, plus <code>node</code> and <code>npm</code>.  Rather than say don't
    have the tool you want, try to install it.</li>
  <li>For a fault in an index or in search results, run
    <code>php src/executables/QueryTool.php '&lt;query&gt; i:&lt;timestamp&gt;'
    explain</code> and <code>php src/executables/ArcTool.php dict
    &lt;bundle&gt; &lt;word&gt;</code> 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.</li>
  <li>For a slow page or file, measure before changing anything. The
    excimer sampling profiler builds here from its source with
    <code>phpize</code> and loads with <code>php -d extension=...</code>;
    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.</li>
</ul>
<h3>Getting a headless browser</h3>
<p><code>wkhtmltoimage</code> and <code>wkhtmltopdf</code> 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 <code>var()</code>,
<code>clamp()</code>, and some <code>@media</code> 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.</p>
<p>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
<code>lynx -dump</code>, the cache off, settings put back and
<code>stop_test_servers.sh</code> at the end.</p>
<p>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.</p>
<p>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 <code>php index.php
secure-start</code>, <code>secure-restart</code> and
<code>secure-stop</code>. 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 <code>secure-stop</code> and check that no
<code>index.php</code> process is left.</p>
<p>A change to how the server takes in a request, such as an upload or a
git push, is walked over TLS with <code>secure-start</code> or
<code>secure-terminal</code>, 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.</p>
<p>A walk reads a page as text with <code>lynx -dump</code>, 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
<code>page_name</code> query parameter the name is percent-encoded twice;
a name encoded once answers &ldquo;page does not exist&rdquo; for a page
that is there. A page read through <code>/p/</code> needs one
encoding.</p>
<p>Say which screens a walk covered. &ldquo;Checked in a browser&rdquo; 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
<code>createdb</code>. 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
<code>php src/configs/ConfigureTool.php locale en-US</code>.</p>
<p>For that, drive a scriptable Chrome from <code>puppeteer-core</code>, 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 &mdash; check
<code>~/.cache/puppeteer</code> and <code>/opt/pw-browsers</code> for a
<code>chrome</code> or <code>chrome-headless-shell</code> binary and point
<code>executablePath</code> at it (do not hard-code a version in the path).
Downloading one through <code>@puppeteer/browsers install</code> 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
<code>--no-sandbox --disable-dev-shm-usage</code> (not
<code>--single-process</code>, which breaks the DevTools pipe), set the viewport
to a real phone width &mdash; about 360px, not 500px, which is wider than a
phone and hides overflow &mdash; then read <code>getBoundingClientRect</code>
and <code>getComputedStyle</code> inside <code>page.evaluate</code>, 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
<code>wkhtmltoimage</code>, 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.</p>
<p>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 <code>page.setCacheEnabled(false)</code>, set once when the page is
made.</p>
<p><b>Where a fault is reported in a named browser, that browser is the one
to reproduce it in.</b> 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.</p>

<h2>Presenting Options</h2>
<ul>
    <li>Avoid fixed list choices as Chris views these with suspicion like
        you are deliberately trying to limit his options.</li>
    <li>If you still have a choice of things you are presenting to Chris, you
        should always present them as plain text, never present a choice
        gadget of any kind.</li>
</ul>

<h2>Coding conventions (shared across projects)</h2>
<ul>
  <li><b>80 columns.</b> No line over 80 characters. When a patch touches a
    file, fix every pre-existing long line in it too (autogenerated files,
    such as <code>src/configs/PublicHelpPages.php</code>, are the
    exception).</li>
  <li><b>Braces always</b> on every <code>if</code>, <code>else</code>,
    and loop body.</li>
  <li><b>An inline comment is written only where the code would be
    hard to understand without it.</b> It says what the code does, not
    the fault it fixes or how the code used to behave. A comment such as
    &ldquo;a name ending in a newline stopped the worker&rdquo; is about
    a past issue. It does not explain the line under it. The
    history of a fix goes in the devlog and the commit message.</li>
  <li><b>A comment running over more than one line inside a function
    or a method body uses <code>/* ... */</code></b>, never a run of
    <code>//</code> lines, and carries no vertical stripe of stars.
    A single-line <code>//</code> comment is allowed and is what
    Coding.pdf uses in its own examples, including the <code>//no
    break</code> that marks a switch case falling through to the
    next.</li>
  <li><b>Whitespace that carries nothing is never added.</b> A change
    that alters only the spaces or newlines in what a page sends a
    browser is a fault to put right, not a difference to accept: it
    marks the output without meaning anything to a reader. Where markup
    moves between methods, the marks that switch between code and markup
    go where the bytes the page sends stay as they were.</li>
  <li><b>No dead code.</b> If a change makes nearby code a no-op or
    pointless, delete it and say so.</li>
  <li><b>No magic numbers.</b> Every literal &mdash; size, limit, timeout,
    conversion factor &mdash; gets a named constant with a docblock; grep
    <code>Config.php</code> 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.</li>
  <li><b>A standard mathematical or statistical object keeps the name the
    literature gives it.</b> Look the name up before naming anything that
    stands for one, and never invent your own. Write <code>Zipf
    distribution</code>, <code>quantile function</code>, <code>continuity
    correction</code>, <code>inverse transform sampling</code>. 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.</li>
  <li><b>No single-letter variables</b> except loop indices <code>i</code>,
    <code>j</code>, <code>k</code>, in both PHP and JavaScript, and
    <code>$e</code> for the thing a <code>catch</code> takes, which
    reads the same way to everyone.</li>
  <li><b>No type or unit suffixes on names</b> (<code>_arr</code>,
    <code>_str</code>, <code>_int</code>, <code>_obj</code>,
    <code>_bool</code>, <code>_list</code>, <code>_secs</code>,
    <code>_ms</code>, <code>_bytes</code>, ...). Put the unit in the
    docblock: <code>HEARTBEAT_POLL</code>, not
    <code>HEARTBEAT_POLL_SECS</code>. (<code>_info</code>, <code>_max</code>,
    <code>_name</code> read as words and are fine.)</li>
  <li><strong>Renaming is done one method at a time</strong>, taking in
    the docblock, and refuses where the new name is already used anywhere
    in that span. A rename applied across a whole file by pattern will
    land a local name on top of a parameter taken by reference; that is
    how <code>$posting_list</code> renamed to <code>$postings</code>
    overwrote <code>addDocIndexPostings(&$postings)</code> 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.</li>
  <li><b>A method is replaced by its line span</b>, read from the file
    with <code>grep -n</code> 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.</li>
  <li><b>A new name comes from words already in the code or from the
    words Chris uses.</b> A coined term such as "outbox" for requests
    queued to a worker is not used. A comment that needs a new term
    defines it where it first appears.</li>
  <li><b>A method name never begins with an article.</b> Not
    <code>aSmallBatchIsOfferedAgainTestCase</code> but
    <code>smallBatchIsOfferedAgainTestCase</code>; the same for
    <code>an</code> and <code>the</code>. <code>cut_patch.sh</code>
    refuses a patch that adds one.</li>
  <li><b>A method whose body is one or two lines is inlined where it is
    used.</b> Such a method, with the docblock it then needs, makes the
    code look as though something of substance happens there when nothing
    does: a reader follows the call, finds one expression, and comes back
    no wiser. Write the expression where it is needed, and give it a
    local name if a name was what was wanted.</li>
  <li>A new value or setting goes the way the code already takes its
    kind, such as a profile value through <code>p()</code> and
    <code>updateProfile</code>, before anything new is written.</li>
  <li><b>No method that only hands back a field.</b> A method whose whole
    body returns one expression of a property, such as a count of it, is
    not written: read the property where it is needed, and make it public
    if it must be.</li>
  <li><b>Docblocks everywhere.</b> Every class and method has a docblock
    whose first sentence a layperson can read: what it does and why. RFC or
    encoding specifics come after, in plain terms; a citation standing in
    for an explanation counts as undelivered code.</li>
  <li><b>Read <code>Coding.pdf</code> in the project files</b> 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.</li>
</ul>

<h2>What a docblock has to say</h2>
<p>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.</p>
<ul>
<li><b>Name the thing in the first sentence.</b> A method's docblock
    opens with the method's name, a property's with the property's name:
    <code>deriveSyncSamples works out which frames can be decoded on
    their own</code>, <code>$slice_id stores which slice of the picture
    this is</code>. Never open with a bare verb (<i>Gives the frame
    width</i>) or a pronoun (<i>This is the frame width</i>): a reader
    asks what gives, and what is.</li>
<li><b>Say what the thing does, and how and when it is used.</b> A
    list of parameters is not a description. A method that reads a
    file's settings says who calls the method, and why the caller must
    do so before anything else is read.</li>
<li><b>Whole sentences in Simplified English</b>
    (<a href="https://simple.wikipedia.org/wiki/Simplified_English"
    >simple.wikipedia.org/wiki/Simplified_English</a>): a controlled
    vocabulary, short sentences, the active voice, and one thought to a
    sentence. No fragments, no aphorisms, no clause built out of a
    standard's own grammar. A sentence says who does what to what.</li>
<li><b>A word that is not English gets said in words, in every docblock
    that uses it.</b> Not once per file. A name from a standard
    (<code>I_PCM</code>, <code>residual_block_cavlc</code>,
    <code>movi</code>) is jargon wherever it stands.</li>
<li><b>A clause number is not a description.</b> A docblock opening
    <i>9.3.1.2 engine initialization</i> tells a reader nothing. Say
    what the thing does, then name the clause at the end if it
    helps.</li>
<li><b>Never restate the name.</b> <i>Whether qpprime y zero transform
    bypass</i> above <code>$qpprime_y_zero_transform_bypass</code> adds
    nothing. Where a name needs that much explaining, the name is wrong,
    so rename the value.</li>
<li><b>A poor name shows up as a poor docblock.</b> Where the
    description of <code>$compression</code> reads "the four letters the
    stream header gives for the codec", the value wants to be called
    <code>$compression_name</code>. Short and abbreviated method names
    (<code>te</code>, <code>ue</code>, <code>moreRbspData</code>) carry
    the same signal.</li>
<li><b>No blank lines among the tags</b>, and every
    <code>@param</code> and <code>@return</code> carries words, not
    only a type.</li>
<li><b>Types stay plain.</b> Write <code>array</code>, not
    <code>array&lt;int,array{0:int,1:int,2:bool}&gt;</code>.</li>
<li><b>A docblock reads on its own.</b> Never lean on the docblock
    above: <i>SHORT_EDGES is the same for one of the short
    stretches</i> leaves a reader who arrived at that line with
    nothing. Say the thing again in full, however close the neighbor
    sits.</li>
<li><b>Documentation you pass gets brought up to standard, one file
    a patch.</b> When a change touches files of the Yioop project, pick
    one of them and fix every docblock in it that breaks one of these
    rules, folding the fix into the same patch; the other touched files
    wait for a later patch that touches them. Code that came in from a
    separate project shipped alongside Yioop, such as the atto servers,
    is left alone unless Chris asks, since its own copy is the master.
    Its names, comments and layout are not changed to meet these rules or
    the audit. A change to it is the smallest that does the job, and the
    audit's findings in it are reported as already there.</li>
</ul>
<p><code>devlog/devscripts/check_docwords.sh &lt;file&gt;...</code> 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. <code>cut_patch.sh</code> runs it over every PHP file
a patch touches and refuses to cut while one fails.</p>

<h2>Yioop-specific rules</h2>
<ul>
  <li><b>Yioop runs on Linux, macOS and Windows.</b> Any mechanism a
    change rests on &mdash; a transport, a system call, a shell
    construct, a kind of file &mdash; is checked against all three
    before anything is built on it, and the response says which were
    checked. A unix socket was chosen as the way a web server that had
    given up root could ask a privileged process to start a program,
    and it was built over several rounds before anyone asked: PHP has
    no unix transport on Windows, and the approach was lost.
    <code>CrawlDaemon</code> 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 &mdash; giving up privilege is one &mdash; 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 <code>CrawlDaemon</code>, such as its
    <code>stopProcess</code>. An atto server cannot call Yioop code, so
    it takes such a function from Yioop as a start option, as
    <code>WORKER_START</code> and <code>WORKER_STOP</code> do. A shell
    command names its null device by platform: <code>NUL</code> on
    Windows, <code>/dev/null</code> elsewhere.</li>
  <li><b>Locale keys are string literals.</b> Never put a variable or a
    ternary inside <code>tl()</code>; extract the key to a variable first.
    The locale extractor only matches <code>tl('literal_key')</code>.</li>
  <li><b>Library classes are pure logic.</b> Nothing in
    <code>src/library/</code> may touch a model or the database, call
    <code>tl()</code>, or escape/sanitize. Components, controllers, and
    helpers may. The atto servers (<code>WebSite.php</code>,
    <code>MailSite.php</code>) go further: no <code>L\</code> or
    <code>C\</code> references and no <code>Config</code> reads at all, so
    keep any conversion constants local to the file, named
    <code>X_PER_Y</code> with units in the docblock. Know decimal versus
    binary: <code>metricToInt</code> is decimal (powers of 1000) and does
    not match PHP's binary <code>memory_limit</code> "M" (1024&times;1024).</li>
  <li><b>A component never calls another component.</b> Components
    are the arms of a controller, and one arm reaching into another is
    a split left half done. Where two components need the same method,
    it goes into a base component both extend, or into a model where
    the work is reading or writing, or into a library class where it is
    pure logic. SocialComponent is that base for everything about a
    group. Where a component is split, decide the home of every
    method first, from what calls it, and only then move anything.
    Moving whatever the last measurement named does not converge: a
    method drags in what it calls, so each pass makes new
    crossings.</li>
  <li><b>Schema changes update the createdb seeds.</b> Any column add or
    remove needs the matching explicit column-list INSERTs in
    <code>src/configs/ConfigureTool.php</code>, the code behind its
    <code>createdb</code> command; positional seeds break on a
    fresh install.</li>
  <li><b>Once a schema version is gold, never change it in place.</b> If a
    patch that bumped <code>DATABASE_VERSION</code> 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
    <code>DATABASE_VERSION</code> 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.</li>
  <li><b>The database stores raw functional values.</b> Do not store
    <code>htmlspecialchars</code>-escaped text; that breaks functional use
    and double-escapes at display. Controllers escape for the view at
    render time.</li>
  <li><b>SQL stays portable</b> across SQLite, MySQL, Postgres, Oracle, and
    DB2. Avoid backend-specific syntax; <code>VARCHAR</code> widths matter
    off SQLite. There are two SQLite databases, <code>public_default.db</code>
    and <code>private_default.db</code> &mdash; never say just
    "default.db."</li>
  <li><b>No new file is made for Yioop to keep or write at run
    time unless Chris asks for it.</b> A log, a state file or a marker
    file goes where its kind already goes: a program's lines go to
    <code>index.log</code> or the program's existing log, and what a
    daemon must say about itself goes in its lock file. Chris turned down
    <code>Launcher.log</code> and <code>IndexServerPort.txt</code> on
    23 October; <code>LauncherUser.txt</code> is the same kind of
    file.</li>
  <li><b>A shipped data file keeps its size and format unless Chris
    agrees first.</b> Making a file larger to make code faster, such as
    storing a compressed dictionary uncompressed, is a trade only he
    decides. In the round of 27 October that made the Chinese
    term dictionary and part-of-speech weights larger and added a video
    test file, all of it was taken back.</li>
  <li><b>A temporary file goes under Yioop's own temporary folder,
    <code>C\TEMP_DIR</code></b> in the work directory, never under the
    system's, so nothing Yioop writes is outside the tree it is given.
    <code>cut_patch.sh</code> refuses a patch that adds a
    <code>sys_get_temp_dir()</code> call.</li>
  <li><b><code>PublicHelpPages.php</code> is autogenerated</b> &mdash;
    never patch it directly. Deliver wiki help source as a
    <code>.txt</code> 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.</li>
  <li><strong>A call is spelt exactly as the method it reaches is
    spelt.</strong> PHP does not care about the case of a method name, so
    <code>getCurrentmachine()</code> reaches
    <code>getCurrentMachine()</code> 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.</li>
  <li><strong>A lookup that gives -1 for a miss says so in its
    docblock</strong>, and every caller compares the result against zero.
    <code>GroupModel::getGroupId</code> and
    <code>RoleModel::getRoleIdWithDb</code> both give -1 for a name
    nothing answers to. -1 read as a yes or no is a yes, so a caller
    testing <code>if (!$id)</code> 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.</li>
</ul>

<h2>Tests</h2>
<p>As code is developed, unit tests should be created. Unit tests live
in <code>tests/</code>; a case is a method named
<code>&lt;name&gt;TestCase()</code> (not <code>test&lt;Name&gt;</code>) and
asserts with <code>assertEqual</code>, <code>assertTrue</code>, and the
like. Run one with <code>php src/executables/CodeTool.php unit
&lt;TestFile&gt;</code>, and list or target cases with <code>unit
list</code> / <code>unit ClassName methodName</code>. 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&nbsp;seconds &mdash; it usually means unintended I/O. When patching a
locale, edit <code>src/locale/&lt;tag&gt;/configure.ini</code> directly; it
takes effect immediately, no extraction step. Before and after
translating a Yioop locale, run <code>devlog/devscripts/check_locale.sh
&lt;tag&gt;</code>: 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 <code>*Test.php</code>, and experiments, in files
named <code>*Experiment.php</code>. 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
<code>src/library</code>. No code writes a PHP file and then runs it, and
no code runs a string of PHP with <code>php -r</code>. The one exception is
the profile, <code>Profile.php</code>, which Yioop writes as settings. A PHP
process is started from a script already in the tree, through CrawlDaemon.
<code>devlog/devscripts/check_php_writes.sh</code> names any write of PHP
code, any write to a <code>.php</code> path other than Profile.php, and any
<code>-r</code> 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 &mdash; seed the exact value the code will pick, then assert its
row still lands &mdash; since a fast machine hands out distinct timestamps
and never reproduces the collision on its own.</p>
<p>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.</p>

<h2>The audit before every patch</h2>
<p>Run these as separate checks and report PASS or FAIL for each. The
CodeTool commands used are noted where they apply.</p>
<ol>
  <li><code>php -l</code> on every touched file.</li>
  <li><code>CodeTool.php needsdocs &lt;file&gt;</code> returns 0 &mdash;
    every class and method has a docblock.</li>
  <li>No new single-letter PHP variables except <code>i/j/k</code>.</li>
  <li>No banned identifier suffixes.</li>
  <li>No <code>//</code> comments.</li>
  <li>No new single-letter JavaScript variables except
    <code>i/j/k</code>.</li>
  <li><code>CodeTool.php longlines &lt;file&gt;</code> clean &mdash; 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.</li>
  <li><code>devlog/devscripts/check_deprecated.sh</code> clean on every touched
  file
    &mdash; nothing this PHP passes in silence that Chris's PHP reports.</li>
  <li>Every <code>if</code> body is braced.</li>
  <li>No banned words and no dead code.</li>
  <li>A fresh <code>php src/configs/ConfigureTool.php createdb</code> against
    a configured
    SQLite work directory prints "Create DB succeeded" (verify the USERS
    column count and the root/public rows).</li>
  <li>A patch that edits a generated or data file loads it and counts
    what came back. <code>php -l</code> passes happily on data that was
    never read, so it says nothing about whether the entries are
    reachable: entries added to
    <code>PublicHelpPages.php</code> 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.</li>
  <li>For any patch touching a screen or an endpoint, the server under test
    shows errors and reports every level, and the whole of what comes back
    is read, from its first character. Errors are turned on through the
    project's own settings where it has them, never with flags of your own
    on the command line, so the server runs as it does for Chris. In Yioop
    that is <code>php src/configs/ConfigureTool.php debug +error</code>,
    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.</li>
</ol>

<h2>Verify before you state</h2>
<ul>
  <li>Read the actual code before asserting how it behaves. If it is not in
    view, say so &mdash; never present a guess as a mechanism.</li>
  <li>A cause written into a plan entry, a test's docblock or a commit
    message is a claim like one in a reply. It is read off the code or a
    log in the same round. A plan entry once said a landing page was
    queued again by each link to its host, before the code that queues
    links had been read, and Chris had to ask how the copies got in.</li>
  <li>A command handed to Chris to run is run here first, on the same kind
    of output. A <code>grep -c '^http'</code> once went out to count
    QueryTool's results; QueryTool starts each url with
    <code>URL:</code>, so it counted 6 of 100.</li>
  <li>Grep for a named helper before claiming it does not exist. Chris
    wrote the codebase; a function he names usually is there.</li>
  <li><b>A name you put in front of Chris is itself a claim.</b> A
    command, path, file, or method named in a response is one this round
    ran or listed; a name recalled rather than read is a guess, and it
    goes in only with what produced it beside it. That is mechanical:
    either a tool result in the round shows the name or it does not.
    <code>php src/executables/Yioop.php</code> 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.</li>
  <li>For anything visual (CSS, JS layout), do not guess at output: render
    it headless and measure computed styles against a reference commit.
    For server behavior, run it and confirm before presenting.</li>
  <li>For a fault that only shows in a real deployment or between two
    machines, the first move is to make the running system say what it is
    doing, then read that, before changing anything. Where one attempt to
    reproduce it in the sandbox fails, the round's deliverable is that log
    line and nothing else; a guess made on a machine that cannot show the
    fault costs a round and settles nothing. The calling arc
    turned on this: a relay that logged only a still-up line hid three
    separate causes in a row, and each became plain the round its own log
    line was added. Add the logging or the trace first; fix the symptom
    once the system has shown which layer is at fault. A slow or unreachable
    web server is such a fault. Before any change to how it runs requests,
    every process that runs them logs the request it is running. The next
    report then names the request a busy or stopped process was on. A
    change to how searches run is called a fix only after a replay. The
    searches of the reporting site's own log are replayed here against
    its index.
    The reply gives the answered, refused and timed-out counts. It gives
    them for the same load before and after the change. A limit on how
    long a request may run is first measured against the slowest request
    in the reporting site's own logs. A change to how requests are given
    to workers reports how many requests each worker answered. A
    reproduction takes its sizes and times from the reporting site's own
    logs. It also takes the site's server settings, such as the number of
    web server workers and PHP's <code>memory_limit</code>: 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.</li>
  <li><b>A fault Chris reports from a browser that the sandbox does not
    show is answered with a log line.</b> A screenshot of a network panel
    with a failed request is such a report. Where one attempt to
    reproduce it here fails, the patch makes the server log what it did
    with that request, at each layer the request passes through, and the
    reply names the line to look for. A round once went to a speedup in
    place of that line, and the fault stayed three rounds more. Try the
    protocols a real install offers, HTTP/2 and HTTP/3 as well as
    HTTP/1.1, since a sandbox server started with a context of its own
    may offer only HTTP/1.1.</li>
  <li><b>A reading must not disturb what it reads.</b> A probe that
    builds a form, reloads a screen, or presses a control on the way to
    its measurement can make a broken thing look whole. A save read fine
    for two rounds because the probe itself gathered the form and so
    filled the field the save needed. Take the measurement the way a
    reader would, with nothing else touching the page.</li>
  <li><b>A page named as slow or faulty is given by its full URL, from the
    log, so Chris can fetch it.</b> A domain is not an example: "ikea.com:
    53 pages" gave him nothing to test. The URL is read off a grep of the
    log in the same round, with the time or fault the log gives
    beside it.</li>
  <li><b>A number put in front of Chris says in the same sentence what it
    counts.</b> &ldquo;de 48, bn 29, ar 50&rdquo; under a column called
    Findings meant nothing to him, because a finding was never defined.
    Name the thing counted (keys missing, values whose placeholders
    differ, pages with no translation), and define any word the response
    coins before its first number.</li>
  <li>If a change might make nearby code slower or redundant, say so
    plainly rather than hiding it.</li>
  <li><b>A retry, a re-read, or any fix that reads something again is
    checked against every cache between the code and the disk before it
    is called a fix.</b> A read that failed because a list was stale
    fails the same way when the list is read again from the same cache,
    and the retry then costs the failed read several times over. Name
    each cache in the path and say which the fix clears; where the
    answer is none, the retry is not a fix.</li>
  <li><b>A fault that a restart clears points to a cache.</b> The first
    step names every in-memory cache on the request's path and checks each
    one, before any theory about protocols. A regenerated thumbnail was
    served with the new length and the old bytes for three rounds, while
    HTTP/2 and HTTP/3 were blamed; the front process's cache of small
    files held the old picture.</li>
  <li><b>A change to how results are ranked is written in the scoring
    model the code already uses</b>: divergence from randomness, through
    <code>WordIterator::divergenceScore</code>, with field weights as in
    <code>frequencyScoring</code>. 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.</li>
  <li><b>Before writing that something was not reproduced or not
    measured here, list the uploads and the scratch data folders</b> and
    say what was looked at. A round said real slow pages and a real
    dictionary were out of reach while both sat in the uploads.</li>
  <li><b>An attachment is searched for every number the question turns
    on before anything is asked for.</b> A results page held each
    result's rank in its hover text, and a round went to asking for a
    QueryTool run that would have shown the same thing.</li>
</ul>

<h2>Delivering a patch</h2>
<ul>
  <li><strong>Never run for more than 5 to 10 minutes without a
    response to Chris.</strong> Chris set this on 14 October, after a
    round ran an hour and he could not tell what it had done. Past ten
    minutes, stop, say in plain words what was found and what is next,
    and carry on from there in the next round.</li>
  <li><strong>A round is timed.</strong> Its first command after the
    settle is <code>date -u</code>, 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 <code>settle_gold.sh</code> writes when the round began
    beside the round count, and <code>cut_patch.sh</code> refuses a cut
    made before nine minutes unless run with <code>SOLVED=1</code>, 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.</li>
  <li><strong>A fault the round finds in code it reads is fixed in the
    same patch, with a test, whether or not the request needs it.</strong>
    A test that fails on the code before the fix shows the fault was
    reached. A reply never carries a fault marked as found and not
    changed: one in how a redirect's urls were kept went out that way,
    and Chris held the patch back until it was fixed.</li>
  <li><strong>Never reuse a patch's name.</strong> The name becomes
    Chris's commit message, so a patch recut after a withdrawal gets a new
    name, never one already delivered. Two deliveries under one name let
    him apply the older one: on 30 September he committed the round 8
    no-forced-locale-copy, not its recut, and gold no longer matched his
    tree.</li>
  <li><strong>Run the round until what Chris asked for is solved, or
    until the tool budget runs out.</strong> Solving it usually means
    producing a patch, and that is how such a round ends. Where the
    budget runs out first, say that the tool budget ran out and ask him
    to say continue, so the work carries on next round from where it
    stopped. This is the one question a round ends on; every other doubt
    is settled by acting, not by asking. Never hand him part of the work
    to look at; he reviews completed work only.</li>
  <li>A reply opens by naming each part of the request and what was done
    for it; a part with nothing done is named, never left out.</li>
  <li>A patch that creates a file names it in the reply with its
    <code>git add</code> line, since <code>git commit -a</code> leaves a
    new file out.</li>
  <li><code>cut_patch.sh</code> prints the <code>git add</code> line of
    each file a patch creates; the reply pastes those lines under the
    patch's name.</li>
  <li><strong>The round before a review is an ordinary round.</strong> A
    review due next round puts nothing off: the work is run until solved or
    the budget ends, as in any round. A fix once went unbuilt, with a
    command handed to Chris in its place, because the next round was the
    review.</li>
  <li><b>Where Chris's message can be read two ways, act on the likelier
    reading rather than asking which he meant.</b> A question costs a whole
    round every time; a guess costs a round only when it is wrong, and
    nothing when it is right. So guess, say in one line which reading was
    taken and what was done on it, and let him correct it. This holds for a
    reply that shows no commit message, for a request whose scope is
    unclear, and for a fault report that does not name the file: read the
    evidence, pick the reading it favors, and carry on.</li>
  <li><strong>When Chris asks a question, the first sentence of the
    reply answers it,</strong> naming what was asked about; checks and
    details follow. Three rounds once went to one question because the
    answer sat under the patch and its checks.</li>
  <li><strong>A reply ends with the to-do items still open,</strong> one
    short bullet for each: what Chris has asked for and is not yet done,
    and what a round found and left for later. An item done this round is
    left off. The list lets Chris see at a glance what is still owed
    without reading back through earlier rounds.</li>
  <li><b>Budget the tool calls before starting, and reserve the last ones
    for delivery.</b> Cutting the patch, checking the response words and
    presenting the file take three or four calls, and they are set aside
    first. A patch written to the outputs folder but never presented does
    not reach Chris: he sees a continue button and nothing else, and the
    whole round is lost. Where work is written and then checked, as a
    block of translated strings is written and then compared with the
    English, write it and check it in the same call; a separate call for
    each halves what fits. Chris prefers the largest patch that fits in
    one round, so size the round's work to a complete piece that fits,
    such as every string of one locale, rather than starting a piece that
    cannot finish and delivering nothing.</li>
  <li>Prefer one larger, complete, testable patch over a run of tiny ones.
    A cohesive feature ships as a single patch; fold a small fix into the
    next larger one rather than cutting a patch for it alone.</li>
  <li><strong>A patch carries every part of the request it
    answers,</strong> including any item the work finds that the request
    needs to be done. Such an item is never listed as not in this patch
    while the round has time left; the round runs on until it is in, or to
    ten minutes, and the reply then says what remains. A clone fault was
    once answered by two patches that each left sending the reply as it is
    built for later, and Chris would not take either as complete.</li>
  <li>Present the pass/fail audit, a plain-English sentence or two on what
    the patch does, and exactly what to run and look for to check it.</li>
  <li>Every code patch also carries its devlog update &mdash; the next plan
    item checked off with a short how-it-was-done note &mdash; committed
    inside the same patch.</li>
  <li>Do not check off a devlog item until Chris says it is done. When he
    does say to check an item, check it in the arc plan <em>and</em> update
    the master to-do file <code>devlog/v10/yioop_v10_todo.html</code> at the
    same time: mark or adjust the matching phase and backlog-item entry
    there (its <code>box</code>/<code>part</code>/checked markers) so the
    arc plan and the master list never drift out of sync.</li>
</ul>

<h2>Devlog</h2>
<h3>What the markers beside a plan item mean</h3>
<p>An arc plan marks each item with one of three, and they say who the item
is waiting on:</p>
<ul>
  <li><b>&#9654; being worked on.</b> Started, not implemented. Nothing to
    look at yet.</li>
  <li><b>&#9873; implemented, waiting to be reviewed.</b> The code is
    written and the patch delivered, but Chris has not yet said it is
    right. This is where an item sits between delivery and his word.</li>
  <li><b>&#10003; done.</b> Chris has reviewed it and said so. Only he
    moves an item to this marker; never set it yourself.</li>
</ul>
<p>A checkmark may be followed by <b>&#10007;</b>, 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 <b>&#10007;</b> for a backlog item that was dropped.</p>
<p>A further mark, <b>?</b>, 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. <code>check_devlog.sh</code> refuses a plan
with a bare part under a marked item.</p>
<p>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.</p>
<h3>Where and how work is logged</h3>
<p>Work is logged under <code>devlog/v10/</code> in dated arc folders named
<code>YYYY-MM-DD Arc Name/</code>, 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.</p>

<p>A plan is one nested ordered list and nothing else. These rules are not
suggestions.</p>
<ol>
<li><b>One outline, no headings inside it.</b> The whole plan body is a
    single <code>&lt;ol&gt;</code> with nested <code>&lt;ol&gt;</code>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.</li>
<li><b>Never type a number or a letter.</b> Every label comes from
    <code>list-style-type</code> in the stylesheet, one rule per depth:
    <code>upper-roman</code>, then <code>upper-alpha</code>, then
    <code>decimal</code>, then <code>lower-alpha</code>. A typed label
    goes stale the moment an item is inserted.</li>
<li><b>Levels run I, then A, then 1, then a.</b> A step within section VI
    is VI.A, a part of that step is VI.A.1, and a part of that is
    VI.A.1.a.</li>
<li><b>Each entry is a full-sentence title in
    <code>&lt;strong&gt;</code>, then one to three sentences saying what
    changed, then two or three on how it was done and what to
    remember.</b> The title does not count toward those sentences. No
    sentence runs past about twenty words. No paragraph tags inside an
    entry.</li>
<li><b>An entry is a deliverable, not a round or a commit.</b> Several
    commits fixing one thing are one entry. A round spent chasing a wrong
    diagnosis is not an entry at all. If a reasonable person could not
    tell from the title what changed on the site, it is not an
    entry.</li>
<li><b>Group related work under one entry with its parts nested.</b> Nine
    commits making git clone work over HTTP are one entry with five
    parts, not nine entries.</li>
<li><b>Only Chris sets a checkmark, and a parent is not done until every
    part under it is.</b> Implemented work waits at
    <span class="review">&#9873;</span> however sure you are of it.</li>
<li><b>Nothing unstarted carries a marker.</b> Planned work is unmarked;
    <span class="next">&#9654;</span> means being worked on now.</li>
</ol>

<p>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
<code>list-style-type</code> at each depth, because markup that parses is
not the same as an outline that numbers correctly.</p>

<p>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
<code>devlog/v10/yioop_v10_todo.html</code> first, then the current arc's
plan. Each finished arc from <code>2026-06-16A Mail Refactor Arc</code>
onward carries a three-to-four sentence &ldquo;Arc Post-Mortem Summary&rdquo;
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.</p>

<h2>Spelling</h2>
<p>Use American spelling throughout &mdash; code, comments, devlog prose,
and what you write to Chris. Write <em>color</em>, <em>behavior</em>,
<em>gray</em>, <em>normalize</em>, not <em>colour</em>, <em>behaviour</em>,
<em>grey</em>, <em>normalise</em>. This matters for identifiers as much as
prose: CSS spells it <code>color</code>, so a custom property named
<code>--colour-background</code> reads against the language it is written
in.</p>
<p><code>devlog/devscripts/check_spelling.sh &lt;file&gt;...</code> looks
for the British spellings that keep coming back and names the file and
the line. <code>cut_patch.sh</code> 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.</p>
<h2>Words and phrases to avoid at All Costs</h2>
<p>These words and phrasings should be excluded from all of your output
    even transient.</p>
<ul>
  <li>Avoid at all costs <b>harness</b>; <b>ctor</b> (write "constructor");
    <b>rails</b>; <b>sidecar</b> (say ".flags/.date companion files");
    <b>fixture</b> (say "test set-up"); <b>nit</b>;
    <b>goldens</b> (say "expected-output files");
    <b>slug</b> (say "page name" or "url name"); <b>seamless</b>
    (unnecessary adjective that forces Chris to take a drink which is bad for
    his health)</li>
  <li><b>proof</b>/<b>proven</b>/"I proved" only if can give a formal
    proof in lean. For empirical results (say "evidence").</li>
  <li>Avoid at all costs, lead in phrases that imply dishonesty:
      <b>to be honest</b>, <b>honestly</b>, <b>genuinely</b>, <b>plainly</b>
    <b>actually</b>, "the honest finding," "straight with you." The whole
    family is banned, not just the wordings listed here. Any other framing
    that asserts honesty or directness as a quality of what you are about
    to say reads the same way: "let me be straight," "straight about,"
    "let me be honest about," "my honest read." All of these imply you
    are usually trying to be dishonest. It is better
    to avoid unnecessary phrases to begin and adhere as close as possible
    to simple English. Simple English means avoiding jargon and saying a
    thing once in ordinary words. It is not license to write like a
    fortune cookie: no aphorisms, no portentous short sentences, no
    dropping the words that carry the meaning. Say who does what to what,
    name the page or the control an end user would see, and give the
    numbers you measured.</li>
  <li>Avoid at all costs words that grant Chris a point as though the
      granting were yours to do: <b>fair</b>, "fair enough", "fair point",
      "that's fair". Saying a thing he told you is fair presupposes that
      finding it unfair was open to you. Answer with what you now know or
      what you will change, never with a verdict on what he said.
  </li>
  <li>Avoid presuppositional phrasings. For example, "Send me the commit hash
      once you've applied and confirmed it, ..." implies that I will find your
      code suitable rather than give me a chance to evaluate it fairly.
  </li>
</ul>

<h2>Keeping those phrases out on a small context window</h2>
<p>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
&mdash; 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 <em>how</em> you are speaking rather than
<em>what</em> you found, cut the frame and state the fact. A second trick is
that the content is always sayable without the label &mdash; "I verified X
on a standalone page, not the live view" needs no "honesty note." Delete the
announcement; keep the substance.</p>
<h3>A guard that does not depend on remembering</h3>
<p>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
&ldquo;let me be plain&rdquo; and &ldquo;I will say so rather than dress it
up&rdquo; 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.</p>
<p>The rule to hold while composing, rather than after: <b>no clause may say
how you are speaking</b>. 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 &ldquo;I will say
the probe measured nothing&rdquo; but &ldquo;the probe measured
nothing&rdquo;.</p>
<p><code>devlog/devscripts/check_words.sh draft.txt</code> applies both halves
&mdash;
the fixed list of words and the shape &mdash; 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.</p>
<p>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 &ldquo;To check&rdquo;
line saying what Chris runs to check it.</p>
<p>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.</p>

<h2>Acronyms and citations</h2>
<ul>
  <li>All acronyms should be spelt out in full the first time it
    appears in a round &mdash; write "Content-Security-Policy (CSP)", then
    "CSP" afterward, not "CSP" cold. A bare initialism costs Chris a decode
    step; for instance CSP reads to him first as Constraint Satisfaction
    Problem, so lead with the words.</li>
  <li>For an RFC (a numbered internet standards document), the first time
    you cite one give both the number and, in plain words, what it covers
    &mdash; "RFC 6265, the HTTP cookie specification", not just
    "RFC 6265".</li>
  <li>The same holds for a tool, a library or any other proper name that
    may be uncommon: the first time it appears in a round, say in a few
    words what it is and what it does &mdash; "excimer, a sampling
    profiler for PHP that records which functions a program spends its
    time in", not "excimer" cold.</li>
</ul>

<h2>Running things in this environment</h2>
<ul>
  <li>Work within the environment you are given; do not ask Chris to widen
    it. When a host is not in the network allowlist, a tool is not
    installed, or a file is out of reach, that is a limit to work around,
    not a permission to request. Say plainly what you cannot see or reach
    and reason from what you do have, or ask Chris for the piece of data
    itself (paste the page, the config, the log), rather than asking him to
    open the sandbox up for you. Asking for more access reads as trying to
    push past the guardrails.</li>
  <li>No command reaches a host outside the sandbox for the project's
    data or history; Chris does not use GitHub. Test data of a reported
    size is made here, as a repository of several thousand commits was
    made from the counts Chris gave.</li>
  <li>Chris's Mac runs tcsh; commands for it are written in tcsh syntax
    (no bash-style loops; single-quote any string containing
    <code>!</code>). 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.</li>
  <li>Start an atto or PHP server so it cannot hang the session: give it a
    self-terminating timeout that cannot be trapped, e.g. <code>timeout -s
    KILL 12 php index.php ... &lt;/dev/null &gt;/tmp/log 2&gt;&1
    &</code>, and give <code>curl</code> a <code>--max-time</code>.
    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.</li>
  <li>Test servers are stopped with a script kept outside the
    repository, /home/claude/stop_test_servers.sh. It kills by id only
    processes whose program is php and whose command names index.php. A
    kill loop typed into a call matched that call's own shell six times,
    and each such call returned nothing.</li>
  <li>A scratch script that loads <code>Config.php</code> sets
    <code>display_errors</code> to <code>stderr</code> 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 <code>git show HEAD:&lt;file&gt;</code> and puts
    the new copy back in the same call.</li>
  <li>Free a busy port before a fresh run with <code>fuser -k
    8080/tcp</code>; a stale server on the port answers with old code and
    hides the change under test.</li>
  <li><b>No process runs more than ten minutes.</b> Chris set this on
    23 October, after a full test run was started with a 50 minute
    timeout. Every command that can run long is started under
    <code>timeout 600</code>; work that needs longer is split into runs
    that each fit.</li>
</ul>

<h2>Functions that do nothing, which must never be written</h2>
<p class="note">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 &mdash; delete the call and let the value go
out of scope.</p>
<ul>
  <li><code>imagedestroy()</code> &mdash; no effect since 8.0, deprecated
    8.5. A GdImage frees itself.</li>
  <li><code>curl_close()</code> &mdash; no effect since 8.0, deprecated
    8.5. A CurlHandle frees itself.</li>
  <li><code>xml_parser_free()</code> &mdash; no effect since 8.0,
    deprecated 8.5.</li>
  <li><code>finfo_close()</code> &mdash; no effect since 8.1, deprecated
    8.5.</li>
  <li><code>shmop_close()</code> &mdash; no effect since 8.0, deprecated
    8.5.</li>
  <li><code>openssl_pkey_free()</code>, <code>openssl_x509_free()</code>
    &mdash; no effect since 8.0, deprecated 8.5.
    <code>openssl_free_key()</code> was deprecated outright in 8.0.</li>
</ul>
<p>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.</p>

<h2>Deprecations your PHP will not tell you about</h2>
<p class="note">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.</p>
<p><strong>Caught here already</strong> &mdash; a clean local run means
these are clean:</p>
<ul>
  <li>Creation of a property that was never declared (8.2). Declare every
    property a class assigns, tests included; a test's
    <code>setUp</code> assigning <code>$this->whatever</code> needs
    <code>public $whatever;</code> on the class with a docblock.</li>
  <li>Passing <code>null</code> to an internal function's parameter that
    wants a string or an int (8.1).</li>
  <li>An implicit conversion from a float to an int that loses
    precision (8.1). A local run reports this only where the line
    actually runs, so a branch the tests do not reach stays quiet here
    and speaks on Chris's machine; one such call reached him from
    <code>addDifferentialPrivacy</code>, where the area under a curve was
    handed to <code>rand()</code>. 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.</li>
  <li><code>${var}</code> inside a string (8.2); write
    <code>{$var}</code>.</li>
  <li><code>utf8_encode()</code> and <code>utf8_decode()</code> (8.2).</li>
</ul>
<p><strong>Silent here, loud on Chris's machine</strong> &mdash; these have
to be watched for by eye, because a clean local run says nothing about
them:</p>
<ul>
  <li><code>ReflectionMethod::setAccessible()</code> and
    <code>ReflectionProperty::setAccessible()</code> (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.</li>
  <li>A parameter given a default of <code>null</code> without being
    marked as taking one (8.4): <code>function f(int $number = null)</code>
    wants <code>?int $number = null</code>. This is the easiest of these
    to write without noticing.</li>
  <li>The <code>E_STRICT</code> constant (8.4), which names a level of
    error that no longer happens.</li>
</ul>
<p>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
<em>whole</em> 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.</p>
<h3>Looking for them mechanically</h3>
<p>Watching for the silent ones by eye failed even in the round after this
list was re-read: three <code>setAccessible</code> calls went out in test
code and landed on Chris's machine as notices. A list to remember is not a
guard. <code>devlog/devscripts/check_deprecated.sh</code> looks for every
pattern above
that a text search can find &mdash; the reflection calls, a parameter
defaulting to null without being marked as taking one,
<code>E_STRICT</code>, the utf8 pair, <code>${var}</code> in a string, and
the calls that free what frees itself &mdash; 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
<code>longlines</code> and <code>needsdocs</code>, and it exits non-zero so
it can gate a cut. The two it cannot see &mdash; an undeclared property and
a null passed to an internal function &mdash; a local run does report, so
between the two nothing on the list is left to memory.</p>
<p>The script finds only what it has a pattern for. When Chris reports a
notice this PHP does not print, its pattern goes into
<code>check_deprecated.sh</code> in the same patch.</p>
</body>
</html>