Skip to content

Releases: shoddymills/shoddy

v2.6.0

Choose a tag to compare

@github-actions github-actions released this 22 Aug 16:08
dfa2cc8

Changed

  • Fettler has moved to its own repository. The file, search and edit
    tools for AI assistants now live — and release — at
    shoddy-fettler, with
    documentation at
    shoddymills.github.io/shoddy-fettler.
    This repository no longer ships fettle; every Fettler link in the docs
    points at the new home.
  • Releases are made differently now. The tag is the version: the
    Release workflow stamps the number from the tag, so there is no
    version-bump commit and no release branch — a release is cut from main
    after a pull request CI has proven. The maintainer procedure is
    WORKFLOW.md,
    the reasoning
    RELEASING.md.
  • ./build.ps1 check (and the build.sh twin) is a new verb running the
    seven fast verify gates — docs, errors, permissions, host-blind, suites,
    twins, lanes — in seconds; ./build.ps1 test remains the whole suite.
    The Toolchain page
    documents the full verb set.
  • ./scripts/shoddy-status.ps1 says where a piece of work has got to — the
    branch and whether it is clean, how far it sits from main, whether it
    has been pushed, the state of its pull request, CI's verdict on the
    commit, whether the release notes for a version exist and are committed,
    and whether that tag is still free — then names the single command that
    comes next. It is the one script here that reports rather than acts: the
    only thing it writes is the remote-tracking refs it fetches, and it
    always exits 0, because a status tool that fails on incomplete work
    becomes a gate. ./scripts/shoddy-status.sh is the twin.

Fixed

  • The julian, ephemeris and clock core suites ran only in the
    retired release gate, never in ./build.ps1 test. The suite roster is
    now a single copy (scripts/suites.mjs) read by the build, the display
    runner and the verify gate alike, so a suite can no longer fall between
    them.
  • New .ps1 scripts were committed without their execute bit.
    scripts/shoddy-commit.* staged the bit on new .sh files only, while
    verify-permissions.js applies it to every tracked file that opens
    #! — so a new PowerShell script fell straight through the window the
    .sh case exists to close. Six had, and are corrected here. Both twins
    now test the shebang, which is the rule the gate itself applies.

v2.5.1

Choose a tag to compare

@github-actions github-actions released this 21 Aug 16:35

The disclosure screen's category words have changed, and the old ones are
refused.
If any .fettler.json or .fettler.local.json on your machine
names phi, pii or sci in a screen, it will stop working when you
install this release. Two of the three new words are a straight rename; one
of them splits in two.

Was Write instead
pii identifiers
sci scientific
phi identifiers for the labelled patterns, clinical for model screening of clinical text — it was doing duty for both

The old words are refused rather than translated, when the configuration
is read, with a message naming the replacement:

'phi' is no longer a screening category; the labelled identifier patterns are
'identifiers', and clinical-model screening is 'clinical'

Translating them silently would have kept them alive in configurations
indefinitely, still promising what they always over-promised — which is the
whole reason for the change.

Why the words changed

phi and pii claimed more than the screen delivers. A category called
phi reads as "protected health information is covered here". What its
pattern tier actually found was a record number under an MRN: label and a
date under a DOB: label. Patient names, addresses, conditions and the
clinical prose around them were never matched by anything unless you had
installed a model — and nothing said so.

The honest sentence, which the documentation now leads with: out of the
box the screen catches structured identifiers — a social security number,
a Luhn-valid card, a formatted phone number, an email address, a labelled
record number and a labelled date of birth. Names, addresses, conditions and
free text are caught only by a model you install, and the manifest plus
roots name exactly which model that is.

The new words say which is which. identifiers is the pattern tier and
takes no model, ever. clinical, legal and scientific are model tiers
and screen nothing at all until you install one.

Added

  • fettle roots names the model doing the judging, and never hides it.
    Where a grant screens a model-backed category, roots now reports that
    category on its own line: the checkpoint and revision read from the
    model's manifest.json, or no model installed - reads here will refuse
    where the directory holds none.

    records       /work/records
                  can: list read
                  screen: identifiers clinical
                    clinical: obi/deid_roberta_i2b2 @ a1b2c3d
    

    With no "models" directory declared at all, one line says so rather than
    letting the category words imply a protection that is not running:

    no "models" directory is declared, so clinical, legal, scientific screen
    nothing here - only the identifier patterns run
    

    The JSON answer carries the same facts under screening, because the MCP
    front end never sees the text one — and that is the front end where a
    model is the thing reading the answer. Manifest reading is read-only and
    tolerant: an unreadable or incomplete manifest reports as no model rather
    than taking the whole answer away, since roots is very often the call
    somebody makes because something is already wrong.

  • scripts/shoddy-branch.* replaces scripts/shoddy-feature.*, and adds
    a bug verb.
    bug NAMENN cuts bug/<origin-feature>NN off an
    up-to-date main, so a fix carries the name of the feature it belongs to
    and its number: bug/pudsey01 is the first fix to work that shipped from
    feature/pudsey. The two digits are not optional — bug/pudsey refuses.
    ship runs from feature/* and bug/* alike and merges each the same
    way. The script was renamed because feature was never the only thing it
    cut.

Changed

  • A pattern-only screen never starts the sidecar. A scope screening
    identifiers and nothing else has no question to put to a model, so
    burler is not consulted, not started, and its absence is not a refusal.
    Previously the category words made this hard to reason about; now it
    follows from them. A tree screening only identifiers never needs a
    models directory at all.

  • Every pattern finding reports as identifiers. A refusal that used to
    read 2 in phi now reads 2 in identifiers. The count and the category
    are still all it says — never the text it found.

  • The documentation says what the screen catches, rather than implying
    more.
    Screening
    gains a column naming the model family behind each category and stating
    that the manifest and roots name the exact checkpoint; the
    four-categories tables, the worked roots output, the refusal examples
    and every configuration snippet carry the new words. The claim that the
    first tier "already carries most of" the personal-identifier category is
    gone — it was the clearest form of the over-promise.

  • The workstation diagram draws all eleven denied built-ins. Monitor,
    BashOutput and TaskOutput were added to the deny list in v2.5.0 and
    never drawn. Monitor is a shell — it runs what it is handed in the same
    environment Bash does — and the two output readers start nothing and
    write nothing but hand back the output of work already done, bytes that
    reached the model without passing the tree boundary, the secret scan or
    the disclosure screen. The figure's aria-label names them too.

Fixed

  • A worked example on the screening page tripped the screen it
    documents.
    The page carried a literal labelled date of birth as sample
    text, so the date-of-birth detector matched it and any screened tool
    reading the page was refused — the documentation for the feature was
    unreadable to anything the feature was switched on for. The example now
    describes the shape instead of spelling it.

v2.5.0

Choose a tag to compare

@github-actions github-actions released this 20 Aug 22:23

Fettler can now refuse to hand back regulated personal data.

v2.4.0 taught fettle to read Excel and Word and to search inside those
and PDFs — which is to say it taught it to open exactly the formats
health, consumer-report and education records live in. Everything it
returns lands in an assistant's context, and from there in a transcript,
a log, and whatever ships those onward. Nothing inspected that payload on
its way out.

Now a tree can declare a disclosure screen, and any detection in a
screened category refuses the whole response. It is off everywhere until
a tree asks for it, and most trees never will.

Added

  • A tree or a scope may declare "screen". true screens all four
    categories — phi, pii, legal, sci. A bare list includes and a
    - list excludes
    from the full set, so ["phi", "legal"] screens
    those two and ["-sci"] screens everything else. Mixing the forms in
    one list is refused when the file is read
    , naming the ambiguity:
    ["phi", "-sci"] reads either as "phi only" or as "everything but
    sci", and those differ by two whole categories, so a rule picking one
    would silently withhold a screen from somebody who believed they had
    asked for it. A misspelt category is refused for the sharper version of
    the same reason — it would quietly subtract one.

  • A scope's screen replaces the tree's, exactly as its can does,
    which is what lets a scope carve out an anonymised subdirectory. A
    scope that says nothing about it inherits
    , because every scope
    written before this existed says nothing, and reading that silence as
    "screen nothing here" would punch a hole in the first tree anybody
    switched it on for.

  • It judges the disclosure, not the file. A document may hold a
    record number on page 40; a read of page 2 that discloses none is
    served. This is the same choice Secrets makes in judging what a write
    adds rather than what a file contains, and for the same reason —
    judging the whole file locks the very trees people most need help in,
    and the way round that is to turn the screen off. A search is judged
    as the hit records it is about to return, per file, including whatever
    --context brings with them.

  • Tier one needs nothing installed. Structural detectors run in
    process with no model and no new dependency: a social security number
    with the issuing authority's own invalid ranges excluded, a card that
    passes Luhn, a phone number, an email address, and — under their own
    labels, because no issuer agrees on a shape — a medical record number
    and a date of birth. They are narrow deliberately. Nine bare digits are
    an order number far more often than an identifier, and a check that
    cries wolf is a check somebody switches off.

  • burler, a second executable, hosts the models. The trade's word
    for whoever picks the faults out of finished cloth before it leaves the
    mill. It is a separate program for one reason: BERT inference needs
    ONNX Runtime, which ships native per-RID binaries, and Fettler's
    package allowlist admits only pure managed assemblies so that fettle
    can publish self-contained and single-file. The allowlist did not
    change, and the test that enforces it did not change.
    Neither project
    references the other in either direction — the line-delimited JSON wire
    is the whole contract, and a protocol test on each side asserts it
    independently, in the verify-twins spirit: twins proven equivalent by
    test rather than by sharing.

  • The sidecar is lazy, warm and bounded. It starts on the first
    screened disclosure, is held so a warm answer costs milliseconds rather
    than a model load, and is killed once it has been idle — a tool sitting
    in an editor all day should not hold hundreds of megabytes for a
    document somebody read at breakfast.

  • A new screened outcome and exit code 13, beside credential and
    12. They are mirror images — one is a write going in, the other a
    payload coming out — and a script branching on "a secret was involved"
    would take entirely the wrong action for each.

  • roots reports the screen where one is on, because a boundary a
    caller cannot read is one they can only learn by being refused.

  • doctor now reports what the deny list cannot see. The deny list
    closes tool names, and two things sit outside any list of names.
    2.17 names every other MCP server registered beside fettler
    their tools are not called Read, so nothing setup writes says
    anything about what they may reach. It reads the machine in front of
    it, so a server installed tomorrow is reported tomorrow, and it names
    rather than judges: most of them are wanted. 2.18 reports any tool
    the configuration names that this fettle's inventory does not
    know
    — because check 2.8 saying nothing about a tool reads exactly
    like a clean bill of health, and is not one. Every report now prints
    the date the inventory was drawn
    , so a list that has fallen behind
    the client is visible instead of silently passing.

Changed

  • The exclusion list gains three names — Monitor, BashOutput and
    TaskOutput — so setup writes eleven denies rather than eight.

    Monitor is a shell: its own description says it runs the command it
    is handed in the same shell environment Bash does, so denying Bash
    and leaving it open denied the word and not the thing. BashOutput and
    TaskOutput are readers, and were missed for being neither an editor
    nor a shell — they start nothing and write nothing, but they hand back
    the output of work already done, and those bytes reach the model
    without passing the tree boundary, the secret scan or the disclosure
    screen. TaskOutput settles it by naming its own alternative: Read
    on the task's output file, a tool already on the list.

  • build.ps1 test / build.sh test run both test projects. burler
    has its own, so that each side of the pipe asserts the wire without
    being able to see the other's types — a shared DTO assembly would make
    the two agree by construction, and then the one day they should
    disagree nothing would say so.

  • build.ps1 publish / build.sh publish produce two archives per
    OS.
    burler is optional and only wanted by somebody who has switched
    the screen on, so folding it into fettle's archive would make every
    download pay for ONNX Runtime to get a feature most trees never use.
    fettle looks for it beside itself, so the two unpack into one
    directory when both are wanted.

Notes

  • No model weights are distributed by this project, in any archive.
    Four quantised models run about 400 MB against a fettle download
    measured in single-digit MB. A person fetches the checkpoints they want
    and names the directory as "models" — usually in
    .fettler.local.json, since where they sit is a fact about a disk
    rather than about a project. Each carries a manifest.json recording
    the checkpoint, its revision, its licence and where it came from, and
    burler refuses to start a category without one.

    That is a licensing position as much as an engineering one. Several
    clinical de-identification checkpoints derive from data-use agreements
    restricting redistribution, and some capable legal-domain checkpoints
    are ShareAlike. Redistributing none of them means no obligation
    attaching to their redistribution is incurred.

  • Naming that directory is the act that makes the second tier
    load-bearing.
    Before it is named, tier one is the whole screen and a
    clean payload is served — nobody has claimed a model is installed, so
    there is no second tier to have failed. Once it is named, a screened
    category whose model is missing refuses.

  • Everything that can go wrong refuses. A sidecar that will not
    start, a model that will not load, a malformed answer and an inference
    that outran its clock all deny the disclosure and say which. An image
    in a screened scope is refused too: nothing here can read one, and a
    photographed discharge summary is precisely what the screen exists to
    stop. Serving content because the check broke is the one outcome this
    must never produce.

  • The refusal never repeats what it found. A category and a count. A
    refusal naming the patient it found would write that name into the log
    and the transcript — the exact harm the screen exists to prevent,
    delivered by the screen itself. Where the model found each entity
    crosses the pipe and stops there.

Documentation

  • Screening
    is a new page: deciding whether you need it, switching on the tier that
    needs nothing installed, adding the sidecar, choosing and preparing
    models, and proving a refusal refuses. It is the procedure; the
    reference behind it is below.

  • The disclosure screen
    is a new section on the Fettler reference page, with the configuration
    forms, what each tier catches, every refusal it can produce, and what
    the screen deliberately does not do. The setup page gains a short
    section on switching it on, and the exit-code table gains 13.

  • Two limits are written down rather than left to be assumed.
    FCRA and FERPA are not covered categories — a clinical model knows
    clinical text and a scientific one knows scientific text; neither knows
    a student transcript or a credit report, and only the structured
    identifiers in those regimes are caught. And this is a safety net,
    not a boundary
    : models miss entities, the patterns miss anything
    written a way they do not expect, and a number split across a line
    break defeats every one of them. A clean verdict is evidence of absence
    and never a certificate of it. Anyone who believes a screened tree
    cannot leak has been misled, and that belief is the actual hazard.

v2.4.0

Choose a tag to compare

@github-actions github-actions released this 20 Aug 16:02

A Fettler release, and a pass over the repository's own scripts.

fettle reads Excel and Word now, and searches inside them - along with the
PDFs it could already open. Not being able to read a workbook or a document
meant converting one by hand before an assistant was allowed to look at it,
which is a chore rather than a workflow.

And search can see inside all three. It could not see inside a PDF at all
before this, which made for the worst kind of gap: the file opened by name and
was invisible to every search, so a pattern that was plainly in the document
came back as a calm, confident zero.

Added

  • Spreadsheets are read. .xlsx and .xlsm come back a line per row,
    sheet by sheet, in the order the workbook lists its sheets rather than the
    order the files happen to sit in. Formulas are rendered as well as the
    values they last worked out to
    - a search for VLOOKUP is a question
    about how a sheet works and would find nothing at all in the cached
    results. Dates are rendered as dates, both calendars and the 1900 leap
    year Excel invented, because a workbook stores one as a day count and a
    workbook full of unsearchable numbers is not much better than a shut one.

  • Word documents are read. .docx and .docm come back as their
    paragraphs and tables, with headings kept as headings - and the headings are
    what a hit is cited under, since Word does not store where its pages break.
    Tables render a row at a time, footnotes and endnotes are included because
    they carry real content, and text removed by a tracked change is left out:
    nobody reading the document can see it, so nothing should find it.

  • Neither adds a dependency. Both formats are a zip full of XML, and the
    zip and XML readers were already here in the shared framework. The one
    package fettle bundles is still PdfPig, still only for PDF.

  • Documents are searched, not skipped. search renders a document to text
    and searches that, like any other file. Hits carry the page, sheet or
    heading they were found under, so a result can be turned back into somewhere
    to look. Nothing is unpacked to disk.

  • A reader mechanism, so the list can grow. A document type is one small
    class saying which extensions it claims and how to turn bytes into text plus
    landmarks; read, search and the kind reported for a file all follow from
    that one registration. It is a table compiled in, not a plugin loader:
    running a declared, named command already needs an explicit execute
    grant, so loading unnamed code in process would be a hole straight through
    the boundary the tool exists to draw.

  • write, edit and replace refuse a document by name. What comes back
    for one is a rendering rather than the file, with the sheets, the formulas
    and the structure left behind in the making of it. Writing that text back
    would not edit the document, it would replace it with a fraction of itself
    still wearing a name that says it is a document. The check sits in the one
    function every verb that writes text passes through, so there is no verb to
    forget; extract is deliberately not caught by it, since that writes a
    document's own bytes.

  • Search says when it did not look. An answer reports how many documents
    it left shut, in the text form and as documents_skipped in JSON. A file
    that was never opened is not a file with nothing in it, and the two used to
    be spelled the same way. --no-documents (no_documents over MCP) asks for
    the old behaviour deliberately; documents are searched by default. Anything
    over 32 MB is skipped and counted rather than rendered.

  • A declared task's command line can name a value the configuration
    supplies.
    A task still takes no arguments from its caller - that is the
    whole point of one - but writing {like-this} in its run line fills it
    from a replacements block, and an undeclared name is refused rather than
    passed through. The value can live in .fettler.local.json, which is not
    checked in, so the committed file declares the task every checkout shares
    and the overlay supplies what changes per person and per moment. A value
    containing spaces stays one argument: the command line is split before it
    is filled, never after.

  • scripts/shoddy-display.ps1 / .sh runs the three suites the gate is
    not allowed to run - seedscribbler, seedturtle, seedplotter. They open
    a real window, and GLFW needs a platform to create one on, which a hosted
    runner does not have. The script reads the same TST_EXCLUSIONS list the
    gate excludes them by, so a fourth windowed suite cannot be added there and
    forgotten here, and it refuses on a machine with no display rather than
    failing somewhere inside GLFW.

  • scripts/shoddy-commit.ps1 / .sh stages and commits. It refuses on
    main, which is the point of it rather than a courtesy, and -Force is the
    exception that has to be typed out.

  • scripts/verify-twins.js, now a preflight step, proves every script in
    the repository has its twin and that both offer the same verbs. The pair
    had drifted before - -y against -Yes - while the documentation went on
    calling them interchangeable.

  • tutorials/spiro/build.ps1, the twin that was missing. The tutorial is
    meant to be copied out of the tree, so it finds the mill by itself rather
    than borrowing the repository's shared helper.

Fixed

  • Non-ASCII text sent to fettle was corrupted on Windows. Console.In
    decodes stdin with the console's input code page, 437 or 850 on a default
    install, so the three UTF-8 bytes of an em dash were read as three separate
    characters and written back out as UTF-8 in their own right: six bytes where
    there should be three. The round trip was self-consistent, so the hash
    matched and nothing downstream could tell. It reached further than writing:
    a search for a non-ASCII pattern matched nothing and said so calmly.

  • A new .sh was committed without its executable bit. verify-permissions
    walks tracked files, so the window between writing a script and adding it was
    invisible to it - which is how v1.8.0 shipped four scripts that way and one of
    them took the Release workflow down with Permission denied. shoddy-commit
    now stages an unseen shell script with git add --chmod=+x.

  • shoddy-release.ps1 tested the wrong thing for "am I at the repo root".
    It compared paths that Git for Windows spells differently; it now asks git
    directly.

  • shoddy-release.ps1 wrote Directory.Build.props through Windows
    PowerShell's -Encoding utf8, which emits a BOM.
    The version bump now
    writes UTF-8 without one.

  • The shell twins accepted only -y where the PowerShell scripts document
    -Yes,
    and only as the first argument. Both twins now take either
    spelling, anywhere in the line.

  • sed -i in shoddy-release.sh meant something different on BSD sed
    from GNU sed. The version bump no longer uses it.

Changed

  • The gate runs its PowerShell steps under PowerShell 7. Windows
    PowerShell 5.1 turns anything a native program writes to stderr into a
    terminating error the moment a caller captures both streams, and writes a
    BOM where it is told not to. pwsh is probed once and used everywhere, with
    a fall back to powershell where it is absent.

Documentation

  • WORKFLOW.md and RELEASING.md pointed at shoddy-release.ps1 as the way
    to release.
    It is the primitive underneath, not the front door: it builds,
    tests, tags and pushes, while the doc gates, three of the four test lanes and
    the green-receipt check are all skipped. Both pages now name
    scripts/shoddy.ps1 release X.Y.Z, with a table of exactly what the wrapper
    adds.

  • The display-suite gap is written down in WORKFLOW.md beside the command
    that covers it, rather than being an exclusion list nobody reads.

v2.3.2

Choose a tag to compare

@github-actions github-actions released this 18 Aug 12:55

A Fettler release: fettle serve now applies edits to its declaration
without a restart ΓÇö a permission you revoke is gone on the very next
request.

Fixed

  • Editing .fettler.json or .fettler.local.json did nothing to a
    running server.
    serve read the declaration once at launch and held
    that reading for the life of the process, so a task declared
    mid-session did not appear until the client was restarted ΓÇö and, the
    direction that matters more, a permission removed mid-session stayed
    granted. The CLI never had this fault: every command is a fresh
    process and reads the file anew. The server now does what the CLI
    does ΓÇö both files are re-read before every request ΓÇö so an edit
    binds on the next call: a granted task appears, a revocation takes.

    The re-read is unconditional rather than gated on a modification
    time, so the behaviour is deterministic. A malformed edit refuses
    every request, naming the fault, until it is fixed ΓÇö never falling
    back to the last good reading, since a boundary held wider than the
    file states is the one direction the tool must never fail in. A
    declaration that goes missing refuses too, rather than re-discovering
    one: the file found at launch is the file, for the life of the
    server. --root declares a boundary with no file behind it, and that
    one alone stays as launched.

Documentation

  • The Fettler README's registration section now states the re-read
    contract in place of "a boundary fixed at that moment", and the
    server's MCP instructions tell the model an edit to the two files
    binds on the next request.

v2.3.1

Choose a tag to compare

@github-actions github-actions released this 18 Aug 00:03

A Fettler release: the guardrail closes a route it had been leaving open, the
two verbs that configure a machine can now run on a machine that has not been
configured yet, and the documentation gains a quick start.

Changed

  • fettle setup now denies the shell as well as the built-in file tools.
    The list it writes went from six entries to eight: Bash and PowerShell
    join Read, Write, Edit, NotebookEdit, Grep and Glob.

    A shell is a complete route round the boundary, and not a second-order one ΓÇö
    node x.js writes anything anywhere and is not a writing command at all,
    so no blocklist of commands could ever close it. Denying the two tool names
    is as short, closed and platform-identical as denying the other six; it is
    the commands behind them that never close, and those are yours to name.

    This stops your build and test commands the first time you run it. That
    is the deny working, and the point at which you decide what to let through.
    Allow back only what the project needs ΓÇö "allow": ["Bash(git status:*)"] ΓÇö
    or better, declare them as tasks in .fettler.json, which run inside the
    boundary. Never allow a command that writes files.
    The exclusion list
    has the reasoning and both recipes.

  • doctor check 2.8 covers all eight names. A project with the six file
    tools denied and the shell wide open used to report a clean bill of health ΓÇö
    not because that was judged safe, but because nothing looked. It is now a
    finding, and doctor exits 1 rather than 0.

  • doctor check B.17 exempts a narrowed shell allow. allow: Bash(git status:*) beside deny: Bash is exactly the shape setup now asks for, and
    B.17 matched on the tool name ΓÇö so it reported the recommended configuration
    as a conflict and named "remove the allow" as the fix, which is the one edit
    that breaks it. A narrowed allow on Read is still flagged: reading is
    precisely what Fettler replaces.

Fixed

  • fettle setup and fettle doctor could not run before .fettler.json
    existed ΓÇö and setup --local is what creates it.
    Every verb resolved the
    boundary before dispatch, so the file needed to run setup was the file
    setup writes, and a first run in a new project exited 2. Both verbs now
    fall back to the current directory when nothing at all is declared. The
    fallback is read-only, reaches only a compiled-in list of well-known
    configuration paths, and does not apply to any other verb; a configuration
    that is present and malformed is still refused rather than fallen back from.

  • fettle setup --hooks installed nothing when the deny list was already
    complete.
    The hook sat after an early return taken whenever there was
    nothing to deny ΓÇö so asking for it on an already-configured project, or at
    --global without --deny, reported success and wrote no hook. The deny
    list and the hook are now decided independently and saved once.

  • fettle setup --global left you with no working project. A global run
    declares no tree and therefore writes no .fettler.json, and said so
    nowhere. It now names the per-project command that creates one.

Added

  • Quick Start ΓÇö
    install it, register it for the machine, turn it on in a project, check it.
    Four steps, one command each, with the real output of each and a table
    routing every unclean doctor verdict and finding to the page that clears
    it. It is the second page in the Fettler sequence; the install and project
    pages are the reference behind it.

Documentation

  • fettler-boundary.html is now
    fettler-install.html.

    The v2.3.0 release notes link to the old name and that link no longer
    resolves; what it pointed at is now
    Fettler §7.

  • The three Fettler pages were reordered so each answers one question and the
    steps appear in the order they depend on each other. Page one is what Fettler
    is and asks you to type nothing; the install page is the machine, with the
    MCP front end gathered into one section at the end rather than opening the
    page; the project page is .fettler.json first, because every other step
    needs it.

  • Thirteen machine pages carried breadcrumbs pointing at catalog anchors that
    no longer existed, and eleven of them displayed a category name that does
    not exist at all. Both halves fixed, and the checker that missed them now
    compares the label as well as the link.

  • scripts/verify-docs.js dropped any navigation item carrying a class of its
    own, so an item could vanish from every page at once with the consistency
    check still passing.

  • Every code block on the Fettler pages has the site's copy button, and the
    Fettler link moved to the top navigation row, which had room for it.

v2.3.0

Choose a tag to compare

@github-actions github-actions released this 17 Aug 02:24

Added

  • Fettler ΓÇö file,
    search and edit tools for a model and a script, with no shell in between.

    The project is Fettler; the command is fettle. It performs the operations
    an assistant actually carries out on a source tree ΓÇö find, search, read,
    write, edit, replace, move, copy, delete, extract, and run a declared task ΓÇö
    as one program with a command line and an MCP front end over one dispatcher.
    It ships as a self-contained single-file download per OS, attached to this
    release beside the .vsix and sparky. It needs no .NET on the target
    machine and none of Shoddy to run.
  • A boundary you declare, and nothing outside it exists.
    A path outside every declared tree is refused with exit 4 ΓÇö identically
    whether or not it is there
    , so a refusal cannot be used to ask what is on a
    disk. There is no working directory, no path that climbs out, and no shell
    to reach past it with. So an assistant cannot drift into a sibling checkout,
    a vendored copy or an archived branch and answer from it fluently. Damage is
    the obvious half; grounding is the half that bites quietly.
  • Seven permissions, per tree and per folder. list read create update rename delete execute, stated in a .fettler.json a person writes. A scope
    replaces what its tree grants rather than adding to it, so it can take a
    permission away ΓÇö which is what makes "may reorganise, may never destroy"
    sayable, and what makes a folder inside a readable tree genuinely invisible.
    execute is never a default anywhere, including the tree the configuration
    sits in. See the boundary.
  • The tool cannot write the files that say what it may do. .fettler.json
    and .fettler.local.json are refused by every write path at every
    permission level, with their own outcome governed and exit code 11.
    Without it the permission model is decoration: the escape was two commands
    long ΓÇö write a task declaration, then run what you just wrote.
  • Nor the files that say what the assistant may do. .mcp.json,
    .claude/settings.json, .claude/settings.local.json and
    .vscode/mcp.json refuse on the same terms. No boundary has to be breached
    to reach one: a project's assistant configuration lives in the project, so
    the file governing the assistant and a file the assistant may write were the
    same file. A model that can edit its own deny list has no deny list. Matched
    on the last two segments, so a bare settings.json and
    .vscode/settings.json stay ordinary; CLAUDE.md is left out because it
    persuades rather than permits. setup still writes all of them.
  • A write that would introduce a credential is refused ΓÇö outcome
    credential, exit 12. It judges the diff, not the file: only a secret
    absent from the previous content stops a write, so a config that already
    holds a key stays editable, which is the difference between a rule people
    keep and a rule people switch off. Two tiers ΓÇö issued credentials whose
    shape their issuer fixed, and a secret-shaped name assigned a long,
    high-entropy, whitespace-free value that is not a reference, so an empty
    password and a value naming an environment variable both pass. The
    refusal never repeats what it found
    : a line number and a detector name
    only, because a message naming the secret writes it into the log and the
    transcript. --allow-credential is offered by the command line and by
    nothing else. It is a safety net, not a boundary ΓÇö base64 the value or
    split the string and every rule is defeated. It stops the ordinary
    accident, not an adversary.
  • fettle doctor answers, in one screen, whether Fettler is registered
    with each client at each level, whether the binary it names can be
    launched, and what on this machine still lets an assistant go round the
    boundary ΓÇö a pre-approved sed -i, an allow on a built-in editor, an
    instructions file that has never heard of Fettler. It writes nothing.
    fettle setup writes the wiring; it is the one verb a model is never
    offered, because it names the command that launches the server.
    Setting it up.
  • It reads more than source. Notebooks as cells, PDFs as text per page,
    images as facts plus a native image part, and archives as a manifest ΓÇö
    --member NAME reads one entry out without unpacking anything. --tail N
    is the end of a log without needing its length. extract unpacks into a
    declared tree, refused whole or not at all, with zip slip refused by
    construction rather than by a check bolted on.
  • One version, stamped on every binary the release builds. The mill's
    banner, sparky's serverInfo and fettle --version now answer the same
    number, from one place. Before this, nothing stamped a version at all.

Changed

  • The gate covers all four lanes, not one of them. scripts/shoddy.ps1 gate now proves core, fettler, the MCP host and the MAUI host, plus the
    harness's own self-test, both shipped archives driven from outside the
    repository, and the headless floor. Three of those four suites used to run
    nowhere but a workflow
    , and every one of those workflows is path-filtered
    ΓÇö so a change outside their paths triggered none of them.
    Directory.Build.props, which sets the version every one of those binaries
    reports, matched no filter at all. The workflows remain as the backstop; the
    gate is no longer a claim about one quarter of the repo.
  • A receipt names the tree it was earned against, not the commit.
    gate --resume used to key on the SHA alone, and on a dirty tree it skipped
    all 72 steps and printed a pass in a tenth of a second, describing a tree
    from before the morning's work. publish was never at risk ΓÇö it demands a
    clean tree and a receipt for the exact commit it will tag ΓÇö but a maintainer
    reading that pass was being told something false. Receipts now key on the
    commit plus a digest of everything uncommitted.

Fixed

  • Debug and Release stopped sharing one mill's bin/. Every
    configuration wove to the same mills/<name>/bin DLL, so a Release build
    starting while the previous step's test host was still exiting failed its
    final move with "Access to the path is denied". Neither existing guard
    reached it: MillGate serializes writers across processes, and
    weave-beside-then-rename protects a compiler reading the previous weave ΓÇö
    but the holder here had the assembly loaded, and Windows holds a loaded
    assembly against rename until its process exits. No lock can fix that, so
    the configurations stop sharing the path instead. Set ShoddyBinScope on a
    lane that builds one mill in two configurations at once; it is empty by
    default, so nothing else changes. See
    hosting.
  • The Fettler page claimed an atomicity the code does not promise. A batch
    resolves all-or-nothing ΓÇö every file read, hash-checked and every
    substitution located before the first byte moves ΓÇö but the writing itself
    can still fail halfway on a full disk or a file another process holds open.
    The page now says so, and says what you get instead: the file it stopped at
    and how many had already been written, which identifies the survivors
    exactly rather than leaving them to be guessed.
  • mills/pac-vt100/mill.manifest had lost julian. A generated file
    describing the mill as it used to be, and the only thing in the repository
    that noticed was the MAUI host lane's build ΓÇö which nothing local ran and
    which CI only runs when hosts/maui/** changes. A file in mills/ was
    breaking a lane in hosts/, connected by nothing that would ever fire.
    verify-manifests.js now checks every manifest that exists on every gate
    run.

v2.2.0

Choose a tag to compare

@github-actions github-actions released this 15 Aug 05:26

Added

  • machines/sinq.shoddy
    — Shoddy INtegrated Query: the half of a query the rest of the library could
    not say, which is the word by. Ordering by a key and by several keys
    (OrderBy, OrderByDesc, OrderWith, ByKey, ThenBy, Descending),
    grouping (GroupBy), joining (JoinOn, GroupJoinOn), the set words
    (Distinct, DistinctBy, Union, Intersect, Except), walking a record
    type that holds itself (Descendants, Offspring, SelectMany), the
    extremes by a key (MinBy, MaxBy, and their Opt twins), and the total
    twins that answer an Option instead of aborting (FirstOpt, LastOpt,
    SingleOpt, FindOpt). It re-declares nothing: Map, Filter, Fold and
    Sort are still builtins and Sum is still stats'.
  • Sorting by a key at last. Sort is a builtin, ascending-only, and takes
    no selector — handed records it stops the program with
    SORT expects all NUMBERs or all STRINGs. OrderByDesc(staff, Pay) is now
    an ordinary line. The sort is a stable merge sort, so equal keys keep source
    order and ThenBy means what it says.
  • No adapter for arrays, vectors or matrices, because none is needed. A
    Matrix's Cells is already an array and an array is already a sequence, so
    a matrix is queried through the same words as a list.
  • seed-sinq puts thirteen of them at the reckoner keyboard in halifax and
    sparky alike — SORTBY, SORTBYDESC, GROUPBY, MINBY, MAXBY,
    FINDFIRST, TAKEWHILE, SKIPWHILE, DISTINCT, UNION, INTERSECT,
    EXCEPT and CHUNK. The eight that take a selector take a quoted program:
    { 1 2 3 4 } [ 2 MOD ] SORTBY.
  • A worked example that the gate runs. tst/sinq-demo.shoddy queries a
    customer/order graph, a catalogue, a self-holding org tree and a matrix in
    one program, and is reproduced line for line on the machine's page — so the
    page cannot show code that no longer compiles.

Changed

  • Five machines dropped code they should never have had to write. alg's
    hand-rolled insertion sort is gone and its canonicalisation is n log n;
    json and xml both carried a written apology that Sort would not sort a
    list of pairs, and both are gone with the per-key re-lookups they forced;
    reckoner's WORDS is a GroupBy where it was three Defs; bool's
    Quine–McCluskey dedupes with Distinct and scores each implicant once
    instead of twice.
  • seq's documented cost model was wrong, and is corrected everywhere. A
    Shoddy list is contiguous underneath, so Rest hands back a copy of the
    tail and a First/Rest walk is quadratic — 74 ms at 12,500 elements and
    3,585 ms at 50,000. Lists are not cons cells. The machine header, the
    seq page and both
    AI grounding pages said otherwise and now say this.
  • Only self-tail-recursion becomes a loop, which is now written down
    where it can be found. Mutual recursion costs a frame per element like any
    other call, and the ceiling is about 20,000 frames — a hard process kill with
    no line number. sinq's own suite runs fifty thousand elements for exactly
    this reason; it is the only suite in the tree big enough to catch either
    fault.

v2.1.0

Choose a tag to compare

@github-actions github-actions released this 13 Aug 00:48

Breaking

  • fin no longer declares its own calendar. The FinDate record and its
    six date words are gone; the day-count and bond words now take a JulDate
    from the new julian
    machine. Build one with JulOf(y, m, d) and add Include "julian.shoddy"
    the arithmetic is unchanged, so prices and yields come out identical.
  • At the reckoner prompt, the FINDATE family is now spelled JUL*
    JULDATE, JULDATEOF and friends, from the new seed. Same date-dict shape,
    so the day-count and bond words take the result exactly as before; only the
    spelling of the making words changed, and a session that tries the old name
    is told the new one.

Added

  • machines/julian.shoddy
    — the calendar as arithmetic, and the one place the Gregorian leap rules are
    spelled: checked and total date builders, day arithmetic, weekdays, and the
    two day-number epochs (the 1970 serial and the astronomical Julian day) one
    constant apart. clock gains ClockDate(), the single bridge from "now"
    into that arithmetic.
  • machines/ephemeris.shoddy
    — the sky as arithmetic: sidereal time, altitude and azimuth for any
    observer, the sun and the moon as sky positions, the moon's phase, age,
    distance, rise and set, and the five naked-eye planets from Keplerian
    elements. Pure throughout; every word takes a Julian day number.
  • seed-julian and seed-ephemeris put both machines at the reckoner
    keyboard — JULDATE and JULDAYNUMBER through EPHMOONRISE — in halifax
    and sparky alike.
  • Numeric ODE solving in eng: a fourth-order Runge–Kutta stepper over
    quotations, scalar (EngOdeSolve) and systems (EngOdeSysAt), for the
    motion problems the physics atlas works through.
  • docs/charter.html
    the charter of the mathematical family: which machine owns what, the layer
    map, and the rules that keep alg, eng, fin, geo, julian, ephemeris, lin,
    math, matrix, neural, random and sparse one family rather than a pile.
  • docs/physics.html
    the physics atlas: where each physics topic lives in the tree, with a worked
    damped-driven oscillator.
  • Every machine page now opens with a history — "A Brief History of…" as
    part of the intro, from the haversine to the ready reckoner, on all
    forty-three pages instead of seventeen.

Changed

  • geo's GeoJulian is now defined over julian's JulDayNumber — one set of
    leap rules in the whole tree, bit-for-bit the same answers.
  • The reckoner seeds are listed once, on the
    reckoner page,
    instead of being repeated in every machine's "Who Uses It" — and that
    listing is now ground-truth-checked, both directions.
  • geo joined the quick reference's Core numerics table, which it had always
    been missing from.

v2.0.3

Choose a tag to compare

@github-actions github-actions released this 11 Aug 23:14

Typing "1/0" ALGSIMPLIFY at a halifax or sparky prompt used to end the
session — not the line, the session, taking the stack, the dictionary, the
registers and the tape with it, including anything defined but not yet
SAVEd. It refuses now, and the next line still answers. Nothing else in the
language changed.

Alongside it, the CI reds that a release run never sees, and the sparky
download finally pointed at from the places people actually start.

Fixed

  • A division by zero inside a typed expression aborted the mill. AlgRead
    was documented as total and was total for syntax only: the parser's /
    branch called the aborting AlgDivOf, so any expression whose divisor came
    out zero killed the process. It now refuses through a guarded twin,
    TryAlgDivOf, and all five seedalg words — ALGSIMPLIFY, ALGEXPAND,
    ALGFACTOR, ALGSOLVE, ALGDESOLVE — inherit the refusal without a line
    of their own changing:

    > "1/0" ALGSIMPLIFY
    ?: ALGSIMPLIFY: DIVISION BY ZERO AT TOKEN 3
    

    The literal /0 was the least of it. The builders fold the divisor while
    the parse is still running, so "1/(1-1)", "x/(y-y)" and "sin(1/0)" all
    reached the same abort with no /0 anywhere in their text — which is why the
    guard had to go in the parser and not into a scan of the string.

  • 0^-1 quietly answered 0. A zero base to a negative exponent is a
    division by zero written another way, and the machine had two answers for one
    question: abort for 1/0, a wrong 0 for 0^-1. Both spellings now refuse
    in the same words, through TryAlgPowOf. A positive exponent still folds to
    zero as it always did, and a symbolic exponent whose sign is unknown is
    now left standing as 0 ^ x rather than guessed at.

  • The fuzz lists had no algebra in them. mills/halifax/test.shoddy and
    mills/sparky/test.shoddy between them carried a good spread of hostile
    regex shapes and not one ALG* line, which is how a whole seed's text
    boundary reached a release without ever being fed a bad string. Both lists
    now include the zero-denominator shapes, and sparky's note recording the
    omission as a known defect is gone with the defect.

  • A weave could be read while it was being rewritten. The mill gate
    serializes writers, but another project's compiler can hold the previous
    weave open as a metadata reference while the winner rewrites it — CS0009
    on the Linux runners, where both projects of a solution build abreast. The
    weave now lands beside its target and renames into place, so a reader sees
    the old file complete or the new one complete and never a truncated one.

  • The host-blindness check refused the compiler's own comment. MillGate
    named a host lane in src/, which is exactly what the check exists to
    forbid; the comment now describes the shape without naming it.

  • The MCP lane asserted a path MSBuild does not write. Its closure checks
    named artifacts/bin/sparky/release, but the project is Sparky.csproj and
    the artifacts layout capitalises the directory. A Linux runner does not
    forgive that the way a Windows workstation does. The step had never executed
    before — every earlier run died upstream of it — so its first run was its
    first test. The publish paths were already explicit, which is why the
    release shipped while this stayed red.

  • The sitemap had silently lost eleven pages — sparky, halifax and tally
    among the mills, and eight machines, several missing for months.
    verify-docs now rebuilds the whole expectation from the tree in both
    directions, so a page without an entry and an entry without a page each name
    themselves. Being ungated is how it rotted.

Changed

  • The sparky download is offered where people start. The release page
    carried it and nothing pointed at it. The README's install section, the docs
    home and the setup page now each offer it beside the .vsix, and the
    release workflow records a sparky deployment per release so the repository
    home's Deployments sidebar lists it next to the Pages site.

Added

  • TryAlgDivOf(a, b) and TryAlgPowOf(b, e) in
    alg.shoddy
    Result-answering twins of the two builders that can refuse. The aborting
    forms are defined over them and keep their messages exactly, so existing
    callers are untouched and there is one set of rules rather than a builder and
    a validator that can drift apart.