Skip to content

Coherence: the rename, the generated reference tables, and the glossary - #7

Merged
borisdev merged 6 commits into
mainfrom
glossary
Oct 6, 2026
Merged

borisdev merged 6 commits into
mainfrom
glossary

Conversation

@borisdev

@borisdev borisdev commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Stacked on #6 (which is stacked on #5). Merge in order.

The README has to stay short, and this change keeps introducing words that carry real meaning a reader can't guess — coherence, well-formedness, stated gap, noise floor, declaration layer. Explaining each inline wears the reader down; dropping them loses the knowledge.

So: define them once, link them on first use.

16 terms, three lines each

The third line is the one worth reading — what the word does not mean here:

term the correction it carries
coherence the parts connect. Not that they support a conclusion — that's only measurable, which is what a battle is for
consistency weaker than coherence, and the gap is the whole problem. An inconsistency usually crashes; an incoherence runs
design rule check chip design's name for this activity. ERC's floating net, dangling output and two drivers on one net map onto three of our rules almost exactly. LVS has no equivalent here, deliberately
soundness van der Aalst's workflow nets. Ours are structural approximations, not a proof — a smaller claim, stated
deep embedding why the design is data, and why every capability follows from that one choice
well-formedness rule UML's own section heading. Not an "invariant" in the strict sense

⛔ The test is the point

A glossary with dead links is worse than none: a reader clicks a term they don't know, lands nowhere, and learns not to click the next one. The mechanism dies quietly — and nothing else here would notice, because a markdown link is not code.

Five tests. All six ways this rots were introduced one at a time, each went red:

mutation caught
a term is renamed, README links rot ✅
an internal cross-reference breaks ✅
the README quietly stops linking a load-bearing term ✅
the glossary is truncated to a stub ✅
two headings collapse to one anchor ✅
the file is deleted outright ✅

The anchor test replicates GitHub's slug algorithm rather than assuming headings and links happen to agree.

⚠️ test_the_readme_actually_links_the_terms_it_leans_on is the unusual one, and it guards the failure this idea invites: the glossary exists, nobody links to it, and the knowledge is parked where no reader will find it. Five terms are required by name — dropping one is allowed, but only on purpose, by editing that list.

250 tests pass. Both generators green.

🤖 Generated with Claude Code

Ubuntu and others added 5 commits October 1, 2026 04:46
…egex (0.3.0)

Closes #4.

`check()` returned `list[str]`. The findings are sentences, so the structure a
caller needs was encoded in the prose:

    hard = [f for f in findings if not f.startswith("NOT CHECKED")]

That prefix match was load-bearing control flow in three call sites here and in
both downstream repos — two kinds of finding wearing one type, told apart by
text. `.claude/rules/checks.md`: NOT CHECKED and 0 FOUND must never render the
same.

`CoherenceFinding` is a `str` SUBCLASS, which is the load-bearing decision:

    check      the producing function's name, e.g. "check_variables"
    about      a node/join/decision name; "source->target" for an edge; the
               strategy name; "" for a finding about the whole design
    blocking   derived — False only for a `NOT CHECKED — …` stated gap

A bool, not a severity enum: two states, no third observed. A frozen dataclass
is tidier and costs a second breaking migration one release after StepSpec;
that is why it loses.

ADDITIVE, and measured rather than asserted:

  · all 221 existing tests pass untouched
  · every finding the test designs produce — 64 of them — is byte-for-byte
    identical to origin/main. `__repr__` is deliberately NOT overridden,
    because the examples print whole lists of findings
  · the five downstream lines #4's comment names as the acceptance bar were
    RUN, unmodified, against this build: nobsmed-v2's
    scripts/prove_workflow_workbench.py (ALL CHECKS PASSED) and
    libs/case_build/tests/test_battle.py (15 passed)

27 findings.append sites in checks.py tagged; message text unchanged at every
one. `GraphSpec._check`'s recursion finding is the 28th and the only producer
outside checks.py — it carries that qualified name because cycle detection
needs the `ancestry` only that method holds.

New tests, because the 221 are an oracle for the MESSAGES and say nothing about
`check` and `about`:

  · a structural oracle — every `about` is "" or a name resolvable against the
    design the caller handed in. Not a hand-written table of 27 expected
    strings, which is as likely to be wrong as the code it checks
  · `about` follows the SUBJECT, not the loop variable: an undeclared node
    cannot be looked up, so that finding is about the EDGE; a branch missing
    `when=` is about the branch, not the decision
  · the compatibility oracle — the five string operations that must not move
  · a source-scan ratchet, so an append site on a branch nothing provokes
    cannot ship returning a plain `str`

All nine were mutation-tested: eight deliberate defects introduced one at a
time, each caught by exactly the assertion meant to catch it.

⚠️ One correction to #4's premise, found by running it. "The 221 tests assert
finding message substrings, so a wrong message fails loudly" is true of some
messages and not others — rewording the tail of check_reachable's "unreachable
from START" finding keeps the whole suite green, because the test asserts only
`"orphan" in f and "unreachable" in f`. The byte-for-byte diff above is the
check that actually covers the claim; the suite alone does not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
 did not say what it checks, and the type it returns says  — a
word that appeared NOWHERE else in the API. One concept wearing two names is the
thing the naming rule exists to prevent, and the orphan was the half I added.

    spec.check(strategy)              0.2.0
    spec.coherence_check(strategy)    0.3.0

No alias: a missed call site is an AttributeError at the call. Same choice 0.2.0
made when NodeSpec(...) became a TypeError.

 was considered and rejected —  IS the design, so
 restates its own receiver.

UNCHANGED, deliberately: the eleven  functions and .
Both name an individual check, which is what they still are.

103 replacements across 25 files, applied by counted exact-match rather than a blind
sed — every  in the repo was first enumerated and confirmed to be this method.

⚠️  was deliberately REVERTED after the sweep touched it. It
documents what the API was AT 0.2.0, where the method genuinely was .
Renaming it there would make a historical upgrade guide assert something false — a
doc that lies is worse than no doc.

Done inside this PR rather than after it, so 0.3.0 carries ONE migration instead of
0.3.0 and 0.4.0 carrying one each.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-up to 0863362, whose commit message is truncated: it was passed through a
double-quoted shell string and every `backticked` term was eaten as command
substitution. The rationale it should have carried is below. No code in that commit
was affected — only its message.

WHY THE RENAME. `check()` did not say what it checks, and the type it returns says
`Coherence` — a word that appeared NOWHERE else in the API. One concept wearing two
names is what the naming rule exists to prevent, and the orphan half was the one I
added in this PR. `design_check()` was considered and rejected: `spec` IS the design,
so `spec.design_check()` restates its own receiver. There is no alias — a missed call
site is an `AttributeError` at the call, the same choice 0.2.0 made when
`NodeSpec(...)` became a `TypeError`.

Unchanged, deliberately: the eleven `check_*` functions and `CoherenceFinding.check`.
Both name an individual check, which is what they still are.

THE THREE LOOSE ENDS, and none was found by reading the diff:

1. The README's four `spec.coherence_check(...)` samples were MISSED by the sweep.
   Its regex required no `.` before `check()`, which excluded exactly the form the
   README uses. Caught by `test_the_docs_use_the_current_api` — the lint that exists
   because the README's own opening example stopped running once before and nothing
   said so. It went red on the first full run after the rename.

2. `docs/migration-0.3.md` cites the 0.2.0 precedent by name, so it contains
   `NodeSpec(` and that same lint flags it. Added to `_NAMES_THE_OLD_API` rather than
   reworded around: a migration doc necessarily names the API it migrates FROM, and
   `test_the_retirement_exemptions_all_exist` keeps the exemption from going stale.

3. `docs/migration-0.2.md` was REVERTED after the sweep touched it (in 0863362). It
   documents what the API was AT 0.2.0, where the method genuinely was `check()`.
   Renaming it there would make a historical upgrade guide assert something false.

`CHANGELOG.md` keeps three bare `check()` mentions on purpose — they are the breaking
change narrating itself, and one sample is explicitly labelled `# 0.2.0`.

235 tests pass. `parity --check` green, `examples.greeting` and
`examples.ladder.stage9_decision` both run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… docstrings

The README said what the library is FOR and never what it checks. A reader could not
tell whether `coherence_check()` was three trivial rules or eleven real ones, and the
data language — the whole reason the design is inspectable — was never written down.

TWO GENERATED TABLES, in the README where they were asked for:

    the data language     every word you declare a design with, grouped
    well-formedness       every rule coherence_check() enforces, split by whether it
                          needs a strategy

    python3 -m workflow_workbench.reference --write
    python3 -m workflow_workbench.reference --check

NO DESCRIPTION IS AUTHORED. Every cell is the first line of the thing's own docstring,
or — for the two unions, which have none of their own — the names of their members.
A table cannot claim what the code does not say.

What IS authored is the GROUPING: which word sits under "boxes" and which under
"wires". That is curation, it is source, and it is a literal in `reference.py`. The
source/derived split is the point, so there is a test asserting GROUPS carries names
only.

THE README OPENING NOW NAMES THE FAILURE, NOT THE AUDIENCE. Boris's framing: an agent
left to run free produces work that RUNS AND IS INCOHERENT — each piece locally fine,
the whole not adding up. That is a reason to care; "for developing workflows with an
AI coding agent" was only a statement of who it is for. Four problems, one declaration
answering all four: incoherence, strategies that can only be compared and not asserted,
reuse, and visibility.

⚠️ The visualization claim is narrowed to what is true. pydantic-graph DOES emit
mermaid — `build_mermaid_graph` in `graph_builder.py` — so "they have none" would have
been false. Measured: it is not exported from their `__init__`, and it takes a BUILT
graph's internals, so every implementation must exist first. Two honest differences
then, not a feature list: ours reads the declaration, and ours can draw two strategies
at once.

Rebased onto the coherence-finding branch, because these tables name the method and
that branch renames it. #6 is stacked on #5.

ALSO FIXED AT SOURCE, not worked around:

  · `_Start`'s first docstring line ended mid-sentence on "so `mypy` can narrow a", and
    `_End`'s was "See `_Start`." Both are now standalone sentences, because the table
    lifts that line verbatim. `test_every_first_docstring_line_stands_alone` makes a
    wrapping summary a test failure rather than a fragment in the README.
  · the union rows needed their pipes ESCAPED — a bare `|` is a column separator, so
    `StepSpec | JoinSpec | DecisionSpec` rendered as three empty cells. Invisible in
    the source string; the test asserts the rendered row.
  · the generated prose said "`NodeSpec(...)` is a `TypeError`", which
    `test_the_docs_use_the_current_api` greps for and flags. Reworded at the source
    rather than exempting the README — that lint exists because the README's own
    example once stopped running and nothing said so, and the README is the last file
    that should be exempt from it. It has now caught me twice in this change, both
    times correctly.

`docs/well-formedness.md` is deleted: the tables live in the README now, and two homes
for one generated table is the duplication this repo keeps a drift test to avoid.

Six ways this can rot were introduced one at a time, and each went red: hand-editing a
README row, a docstring changing without regeneration, a new export left out of the
table, a stale exclusion naming something no longer exported, GROUPS growing a
hand-written description, and the unions losing their pipe escaping.

245 tests pass. `parity --check` and `reference --check` both green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README has to stay short, and this change keeps adding words that carry real
meaning a reader cannot guess — coherence, well-formedness, stated gap, noise floor,
declaration layer. Explaining each inline wears the reader down; dropping them loses
the knowledge. So: define them once, link them on first use.

16 terms. Each is three lines at most, and the third is the one worth reading — what
the word does NOT mean here:

  coherence        the parts CONNECT. Not that they support a conclusion — that is
                   only measurable, which is what a battle is for
  consistency      weaker than coherence, and the gap is the whole problem. An
                   inconsistency usually crashes; an incoherence RUNS
  design rule chk  chip design's name for this activity. ERC's floating net,
                   dangling output and two-drivers-on-one-net map onto three of our
                   rules almost exactly. LVS has no equivalent here, deliberately
  soundness        van der Aalst's workflow nets. Our checks are STRUCTURAL
                   approximations of it, not a proof — a smaller claim, stated
  deep embedding   why the design is data, and why every capability follows from it
  well-formedness  UML's own section heading. NOT an "invariant" in the strict sense

⛔ THE TEST IS THE POINT. A glossary with dead links is worse than none: a reader
clicks a term they do not know, lands nowhere, and learns not to click the next one.
The mechanism dies quietly, and nothing else in this repo would notice — a markdown
link is not code.

Five tests, and all six ways this rots were introduced one at a time and each went
red: renaming a term so README links break, breaking an internal cross-reference,
the README quietly dropping a load-bearing link, the glossary truncated to a stub,
two headings collapsing to one anchor, and the file deleted outright.

The anchor test replicates GitHub's slug algorithm rather than trusting that headings
and links happen to agree.

⚠️ `test_the_readme_actually_links_the_terms_it_leans_on` is the unusual one, and it
guards the failure this whole idea invites: the glossary exists, nobody links to it,
and the knowledge is parked where no reader will find it. Five terms are required by
name. Dropping one is allowed — dropping it on purpose, by editing that list.

250 tests pass. Both generators green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 1, 2026 19:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The link checks silently ignore malformed fragments, and one glossary claim contradicts the supported built-graph comparison API.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
What changed in this PR

Adds a centralized glossary and automated link-integrity checks for terminology used throughout the documentation.

Changes:

  • Adds definitions for 16 domain terms.
  • Links key README terminology to glossary entries.
  • Adds tests for glossary existence, anchors, references, and required README links.
File Description
README.md Links specialized terms to glossary definitions.
docs/​glossary.md Defines project terminology and distinctions.
tests/​test_glossary.py Validates glossary links, anchors, and required references.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/test_glossary.py
GLOSSARY = ROOT / "docs" / "glossary.md"

#: `[text](... glossary.md#anchor)`, from anywhere, at any relative depth.
LINK = re.compile(r"\[([^\]]+)\]\(([^)]*glossary\.md)#([a-z0-9-]+)\)")
Comment thread docs/glossary.md
Comment on lines +63 to +64
**Why it matters here.** Every capability in this library — checks, diagrams, diffs, battles —
exists because there is an artifact to read. None of them is possible over a shallow embedding.
Audited by diffing README.md against origin/main and reading every REMOVED line,
rather than trusting that a rewrite kept what mattered. Two were real losses, not
rewordings:

1. "Specify the workflow and its data contracts, inspect its diagram, then have the
   agent implement the steps." The development-sequence table still covers this, but
   the one-line version is what tells a reader the VERB ORDER before they commit to
   reading a table. Back, as "What you do:".

2. "The specification keeps the workflow understandable." Dropped entirely, and it is
   the half of Boris's framing about agent output being hard to READ — a disservice to
   holding the thing in your memory, separate from it being wrong.

   Restored as a claim with a mechanism instead of an adjective: the declaration's size
   is set by the SHAPE of the workflow, not by the complexity of the steps. A step body
   can grow to 500 lines; its StepSpec stays one. Measured across this repo's examples:
   11 to 43 declaration lines, whatever is bound into them.

   ⚠️ Stated as absolute lines, not as a ratio. The ratio over these files is 13-32%,
   but the files are mostly `__main__` demo printing, so the percentage measures the
   demos rather than the claim. The bound is the real property.

250 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@borisdev borisdev changed the title A glossary, linked from the prose, with a test that kills dead links Coherence: the rename, the generated reference tables, and the glossary Oct 1, 2026
Copilot AI balanced review requested due to automatic review settings October 1, 2026 21:01
@borisdev
borisdev changed the base branch from well-formedness-rules to main October 1, 2026 21:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Structured finding metadata can become inconsistent or unresolvable, and several migration messages remain contradictory or stale.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
Previously missed (7)

In code that hasn't changed since last review

Medium severity Make blocking a computed read-only property

workflow_workbench/​checks.py:83

blocking is described as derived from the message, but this public slot is writable. After f.blocking = False, the field disagrees with both the message and blocking([f]), despite _is_blocking being documented as preventing drift. Make it a computed read-only property instead of stored mutable state.

This issue also appears on line 86 of the same file.

Medium severity Attribute extra-binding findings to the supplied strategy

workflow_workbench/​checks.py:290

For an extra binding, n is explicitly not declared by this design, so using its name as about violates the new guarantee that this field is a resolvable handle in the design supplied by the caller. Point this finding at the supplied strategy, which owns the invalid binding, rather than at the foreign node.

This issue also appears on line 306 of the same file.

Medium severity Scope nested finding handles to the parent design

workflow_workbench/​checks.py:441

Propagating child findings unchanged breaks the documented about invariant for the public root call. If a child has an incoherent node named inner_orphan, parent.coherence_check(...) returns about="inner_orphan", although that name cannot be resolved in the parent design handed to the method; this is the exact ambiguity the new structural handle is intended to remove (compare tests/test_workflow_spec.py:331-349). Nested findings need parent/path qualification or another explicit scope mechanism.

Medium severity Only report successful table rewrites when rc is zero

workflow_workbench/​reference.py:221

When either marker is missing, the loop sets rc = 1 and skips that block, but this unconditional success message still claims both tables were rewritten. Gate the message on rc == 0; the earlier marker-specific error is sufficient on failure.

Low severity Scope upgrade guidance to the return-type change

CHANGELOG.md:65

This upgrade instruction contradicts the breaking rename documented immediately above: 0.2 callers must replace spec.check(...) before moving to 0.3, otherwise they get the stated AttributeError. Keep the “nothing to do” wording scoped only to the finding return-type change, not to the release upgrade as a whole.

Low severity Update example label to use coherence_check

examples/​parallel.py:77

The displayed label still uses the removed check API even though the expression calls coherence_check. Running this example therefore prints outdated migration guidance.

Low severity Rename stale GraphSpec._check test documentation

tests/​test_subgraph.py:605

This newly added test documentation still names GraphSpec._check, but the method was renamed to GraphSpec._coherence_check in this change. The stale name sends readers to a method that no longer exists.

@borisdev
borisdev merged commit 5e740f1 into main Oct 6, 2026
1 check passed
borisdev added a commit that referenced this pull request Oct 6, 2026
…— and five rounds of review (#5 #6 #7 #8)

Contains #5, #6 and #7 — the stack is linear, so this one merge brings all four.

- #5 `coherence_check()` returns `CoherenceFinding`, so an agent can branch instead of regex
- #6 both reference tables in the README, generated from the package's own docstrings
- #7 the glossary, linked from the prose, with a test that kills dead links
- #8 the thesis, `examples/contestable.py`, `StepSpec.problem`, the implement-a-workflow skill

Merged as a MERGE COMMIT, not a squash, so the eight commits stay in history.

## Five rounds of Copilot review, worked

20 inline comments were on record unread when consolidation started. Across five rounds:
14 live / 2 stale on round one, 5 on round two (three of them my own regression), pickle on
round three, a stale docstring on round four, and two previously-missed on round five.

⚠️ **Every single finding was a CHECK narrower than its claim, not code that was wrong.** The
dead-link regex skipped malformed links; the "every rule" table enumerated 11 of 12; the retired
-token map had no `.check(`; the compatibility oracle's five string operations omitted pickle;
the rule-count guard scanned one phrasing in one file; a UI test drove one of three render
surfaces. The code was mostly right. The things asserting it was right were the defects.

266 -> 278 tests. Both generators green. Bundle reproduces byte-for-byte from `npm run build`.

## One finding deliberately open

Nested findings propagate an `about` relative to the child design, so it is not resolvable by a
caller holding only the parent. Both readings are right about different things, and reconciling
them means `about` carrying a path across a boundary — a semantic change to the field. Tracked in
#9 with the limit documented in-code at both sites rather than left overstating.
@borisdev
borisdev deleted the glossary branch October 6, 2026 20:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants