Skip to content

Validate the specification: quality gates, schema conformance and a repository-wide sweep - #1

Draft
muraschal wants to merge 20 commits into
mainfrom
claude/new-session-lym57t
Draft

muraschal wants to merge 20 commits into
mainfrom
claude/new-session-lym57t

Conversation

@muraschal

@muraschal muraschal commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Continuous integration checked that Markdown linted and that the invocation surface built. It never validated the 152 YAML files against the JSON Schemas, never resolved the 907 internal links an agent follows, and never noticed an eval scoring a requirement identifier no specification defines. This pull request builds the gate that checks those things, then fixes what the gate found — plus what a systematic read of each surface turned up.

Closes no issue: this is repository-wide hardening rather than one reported defect.

Type of change

  • Protocol core specification (protocol/)
  • ÆON Learn specification or bootstrap (products/learn/)
  • Library package (library/)
  • JSON Schema (schemas/)
  • Eval case or rubric (evals/)
  • Invocation surface (site/learn/)
  • Validation toolchain or release tooling (tools/, scripts/)
  • Documentation, decision record or repository configuration

The five defects that were reaching users

  1. Curated library content was being silently discarded. Eighteen list items across ten packages contained unquoted prose with a colon and a space — - decision hygiene: the reserve clause and the archer model. YAML parsed each as a single-key mapping; lib/packages.ts filters list entries with typeof v === "string". Every one of those lines was dropped from the rendered topic page, with no error and no warning.
  2. Every release shipped an entry contract announcing the previous version. bump-version.mjs matched pinned tags with /v\d+\.\d+\.\d+/g, which never matched the bare banner ÆON Learn 0.3.0 · ÆON Protocol 0.3.0 — no leading v. The banner was therefore never re-pinned. This is precisely the drift the script exists to prevent.
  3. The site's primary controls had no visible focus indicator. focus-visible:outline-none sat inside the shared button variants; as a utility class it beat the base :focus-visible rule in @layer base. The hero call to action, all thirty topic copy chips and the invocation copy button were unusable by keyboard — a WCAG 2.4.7 failure.
  4. An agent reading only the bootstrap omitted two required completion parts. INT-6 explicitly supports running on the bootstrap alone; step 9 listed six of the eight parts LEARN-14 requires, dropping the assessment and the recommended next learning path. llms.txt, the file an agent fetches first, had drifted from the bootstrap in six further places.
  5. RES-8 could be waived by assertion. It required an evidence map "for every sufficiently substantial subject" — a qualifier no eval could falsify.

Requirement identifiers

  • Added (28, all additive): CORE-6, CORE-7, CAP-9, CAP-10, ORCH-9, RES-10, STA-9, STA-10, INT-9, INT-10, INT-11, LEARN-15, LEARN-16, LEARN-D-8, LEARN-R-10, LEARN-R-11, LEARN-R-12, LEARN-C-11, LEARN-C-12, LEARN-C-13, LEARN-S-14, LEARN-A-11, LEARN-A-12, LEARN-A-13, LEARN-AS-10, REN-5, REN-POD-7, REN-ART-9
  • Changed in meaning: RES-8 (strengthened), ORCH-3 (narrowed), REN-POD-1 (relaxed), LEARN-A-11, LEARN-AS-10, LEARN-S-14, EPI-3, STA-5
  • Removed: none — no identifier was renumbered or reused

STA-8 cited INT-6 where it meant INT-8; the document's own reference table and learner.schema.json both already said INT-8. That is a corrected cross-reference, not a changed requirement.

Version impact

  • Major — RES-8 changed what a MUST demands
  • Minor · [ ] Patch · [ ] None

REN-POD-1 moved the other way: it asked for 750–1,200 words per podcast script where the repository's own Charisma manuscripts run 227–387 German words. The specification failed the fixture it abstracts by a factor of three. Duration is now the target, the word count a language-dependent indication.

What an agent sees differently

It must build an evidence map in every journey, not only in ones it judges substantial. It has defined behaviour where it previously had none: a failed bootstrap fetch (INT-11), a truncated resumable state block (STA-10), a capability that stops working mid-journey (CAP-10), rejection at the contract gate where silence is explicitly not approval (ORCH-9, STA-9), weak evidence (LEARN-R-12), and a learner who pauses or abandons (LEARN-A-12, LEARN-A-13).

Local gate

$ npm run validate
schemas: ok (schemas meta-validated: 6, documents validated: 61, documents without a schema: 92)
links: ok (documents scanned: 65, relative links checked: 907)
refs: ok (packages checked: 30, catalogue entries: 30)
yaml: ok (files checked: 152)
requirements: ok (requirements defined: 183, references from evals: 166)
5 checks, 0 errors, 0 warnings in 1.63s

$ npm test
# tests 116  # pass 116  # fail 0

$ npx markdownlint-cli2 (CI globs)
Linting: 47 files
Summary: 0 issues in 0 files

$ node scripts/bump-version.mjs --check
all pin sites agree with v0.3.0

$ cd site/learn && npm run lint && npm run typecheck && npm run test && npm run build
Test Files  13 passed (13)
     Tests  639 passed (639)
✓ Generating static pages using 3 workers (135/135)
  • npm run validate passes on a clean clone — verified in a fresh checkout of the committed tree, not only in the working copy
  • markdownlint-cli2 passes
  • npm run lint && npm run typecheck && npm run test && npm run build passes in site/learn/
  • node scripts/bump-version.mjs --check passes

What continuous integration now enforces

Nine workflows replace two, each check reporting as its own status so a red build names the surface that broke.

Workflow Enforces
validate schemas, links, refs, yaml, requirements as separate statuses; validators unit-tested on Node 22 and 24; pinned-URL agreement
docs markdownlint; whitespace, line-ending and UTF-8 hygiene; no emoji in specification content
site lint, types, tests; build with an assertion that llms.txt is published and pins a tag; dependency audit
security gitleaks across the full history; actionlint over every workflow; rejection of any action referenced by a movable tag
codeql security-and-quality queries over the only executable code the repository ships
scorecard OpenSSF supply-chain analysis, published
dependency-review high-severity advisories and network-copyleft licences on pull requests
links scheduled external link check that opens an issue instead of failing a pull request
release refuses to publish when the tag disagrees with the pinned version, or the changelog has no section for it

Every action pin was stale (checkout v5 against v7.0.1, CodeQL v3 against v4, the markdownlint action v20 against v24); each is re-resolved and verified against its own action.yml for the inputs used here. Gitleaks and actionlint are installed from pinned release archives with verified SHA-256 checksums rather than marketplace actions, because the Gitleaks action requires a licence when the repository owner is an organisation.

Three judgement calls worth a reviewer's attention

The CodeQL alert on tools/lib/slug.mjs is fixed, but its exploitability claim does not hold. The result becomes an anchor slug, never HTML, and slugify already drops angle brackets. The single-pass removal is also provably complete for this pattern: matching is leftmost-greedy, so a < only survives when no > follows it. It is fixed anyway, because that correctness depended on a subtle property a lazy quantifier would silently break — and the regression test asserts the invariant that matters (no angle bracket reaches a slug) rather than the behaviour first assumed.

Dependency review needs a repository setting, not a code change. The dependency graph is not enabled, and no pull request can enable it. Rather than deleting the workflow or leaving it permanently red, the job asks the API whether the graph is available: it warns and skips while the feature is off, and begins enforcing by itself once someone enables it under Settings → Code security.

related_packages was left asymmetric on purpose. 43 of 108 edges are one-way. Nothing in the schemas, the protocol or the site asserts symmetry, and forcing it would invent 43 curated relationships — cryptography would grow from one neighbour to seven. The asymmetric edges are listed in the audit for a curator to rule on.

Left for a human, deliberately

  • 83 of 197 controversies render with no positions on the site, because two incompatible data shapes coexist. Collapsing the named-side form into a flat list would destroy the label saying whose position it is. This needs a curator to choose the target shape, and the schema and packages.ts to move with it.
  • No requirement says what a conforming agent does when a subject involves individualised professional advice, though several library packages carry "not medical advice" or "not investment advice". That is a policy decision with a versioning cost.
  • CAP-3 is not scoreable by any eval. No transcript distinguishes a verified capability from a correctly guessed one. Recorded in evals/learn/coverage.md rather than papered over with a vague case.
  • Seventeen free-form common-misconceptions.status values sit outside the documented four-value legend; remapping any of them changes an epistemic claim about a specific misconception.

Checklist

  • No requirement depends on the proprietary behaviour of one model vendor; the core's vendor list is now framed as test baselines under INT-2
  • Every new or changed requirement is observable in a transcript; evals/learn/coverage.md states which are scored and which are not
  • Specifications and the JSON Schemas they describe change together
  • The bootstrap and site/learn/public/llms.txt agree again, in six places where they had drifted
  • Files under products/learn/examples/charisma/ remain byte-identical (git diff origin/main is empty for that path)
  • Every source cited is real and reachable
  • CHANGELOG.md records the change under Unreleased, including the plain statement that this is a major release
  • Prose follows the house style, now enforced by a check

🤖 Generated with Claude Code

https://claude.ai/code/session_0115djDoBfqBbioBWYxzHTjH

Continuous integration checked two things: that Markdown linted and that the
invocation surface built. Nothing validated the 129 YAML files against the five
JSON Schemas, no check resolved the internal links an agent actually follows,
and nothing noticed when an eval referenced a requirement identifier that no
specification defined.

Nine workflows replace the two. The specification gate reports each check as its
own status, so a red build names the surface that broke:

- validate: schemas, links, refs, yaml and requirements as separate statuses,
  the validators unit-tested on Node 22 and 24, and bump-version --check so the
  bootstrap, llms.txt and the site's VERSION constant cannot drift apart.
- docs: markdownlint, whitespace and encoding hygiene, and the house rule that
  specification content carries no emoji.
- site: lint, types and tests; build with an output report and an assertion that
  llms.txt is published and pins a release tag; a dependency audit that blocks on
  a high advisory in shipped code and only reports one in the toolchain.
- security: gitleaks across the full history, actionlint over every workflow, and
  a check that rejects any action referenced by a movable tag.
- codeql, scorecard, dependency-review and a scheduled external link check that
  opens an issue rather than failing somebody's pull request.
- release: refuses to publish when the tag disagrees with the pinned version or
  the changelog has no section for it.

Every action pin was stale, so each is re-resolved to the current release and
verified against its own action.yml for the inputs used here. Gitleaks and
actionlint are installed from pinned release archives with verified SHA-256
checksums rather than through a marketplace action, because this repository's
owner is an organisation and the Gitleaks action requires a licence for one.

Dependabot now watches the actions and both npm manifests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115djDoBfqBbioBWYxzHTjH
@vercel

vercel Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
aeon-learn Ready Ready Preview Sep 8, 2026 6:03pm UTC

Request Review

@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

Two of the new checks failed on their first run for reasons unrelated to the
repository's content.

The house-style check ran a JavaScript snippet inside a single-quoted shell
argument, and shellcheck read its template literals as failed shell expansions
(SC2016), which failed actionlint. The snippet now builds its strings by
concatenation, so nothing in it looks like a shell expansion. This was missed
locally because actionlint only runs shellcheck when shellcheck is installed,
and it was not; it is now part of how these workflows are verified.

Dependency review failed because the dependency graph is not enabled for this
repository, which no pull request can change. The job now asks the API whether
the graph is available: it warns and skips while the feature is off, and starts
enforcing on its own once somebody enables it, rather than sitting permanently
red or being deleted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115djDoBfqBbioBWYxzHTjH

Copy link
Copy Markdown
Contributor Author

Continuous integration status on a72abcc

The first run of the new gates was red. Here is every failure, its cause, and what happened to it.

Fixed in this push

Check Cause Fix
workflow lint The house-style check runs a JavaScript snippet inside a single-quoted shell argument; shellcheck read its template literals as failed shell expansions (SC2016). The snippet builds its strings by concatenation, so nothing in it looks like shell syntax.
review dependency changes The dependency graph is not enabled for this repository, which no pull request can change. The job now asks the API whether the graph is available: it warns and skips while the feature is off, and begins enforcing by itself once it is enabled.

workflow lint passed locally before that push and still failed on the runner, because actionlint only invokes shellcheck when shellcheck is installed — and it was not installed here. Both are now part of how these workflows get verified locally, and actionlint with shellcheck on the path returns clean on all nine.

Still red, and expected to be, until the next commit

schemas, links, refs, yaml, requirements, toolchain and pinned release URLs agree all fail for one reason: this branch carries the workflows that invoke the validation toolchain, but not yet the toolchain itself. There is no root package.json, so npm ci has nothing to install, and the committed scripts/bump-version.mjs predates the --check mode the pin job calls.

That is the deliberate shape of a draft mid-sweep, not an unknown failure. The commit carrying tools/, the root manifest and the rest of the repository-wide changes turns all seven green together; each is verified locally before it lands.

One item that needs a repository setting, not a code change

Dependency review stays inactive until Dependency graph is enabled under Settings, Code security. The workflow is written and correct; it simply has nothing to read until then.


Generated by Claude Code

…ed itself

The core specification carried requirements a conformance eval could not score
and cross-references that pointed at the wrong rule.

RES-8 required an internal evidence map "for every sufficiently substantial
subject". That qualifier is unfalsifiable: an agent could waive the map by
assertion and no eval could contradict it. The map is now required for every
journey, and how much it holds scales with the subject instead. An agent that
relied on the qualifier now fails conformance, so this breaks a MUST.

ORCH-3 required that each phase consume the artefact of the phase before it,
which the first phase cannot do, having no predecessor. It now applies to every
phase after the first, and the pipeline says the invocation is the input to
phase 1 rather than a phase of its own.

STA-8 cited INT-6 where it meant INT-8; the document's own reference table and
the learner schema both already said INT-8.

Eleven identifiers cover behaviour that was specified nowhere: what conformance
to the core means and how a product layers on it (CORE-6, CORE-7), a capability
that cannot be determined or that disappears mid-workflow (CAP-9, CAP-10),
rejection at the contract gate, where silence is not approval (ORCH-9, STA-9), a
recalled citation presented as retrieved (RES-10), an unreadable resumable state
block (STA-10), and a bootstrap fetch that fails (INT-11). INT-9 and INT-10 were
MUST NOTs buried inside INT-3's prose without identifiers of their own.

The vision no longer names model vendors as things the protocol works with:
named runtimes are test baselines under INT-2, never dependencies.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115djDoBfqBbioBWYxzHTjH
An agent that reads only the bootstrap is explicitly supported (INT-6), and step
9 listed six of the eight parts LEARN-14 requires at completion. Such an agent
silently omitted the assessment and the recommended next learning path. Both are
now in the bootstrap.

REN-POD-1 asked for 750 to 1,200 words per podcast script. The manuscripts in
the repository's own worked example run 227 to 387 German words, so the
specification failed the fixture it abstracts by a factor of three. Duration is
now the target and the word count a language-dependent indication.

Two statements contradicted requirements elsewhere in the same specification.
adaptation.md called a retrieval-free journey "a violation in spirit even where
no single MUST fails" when LEARN-A-7 and LEARN-S-9 both do fail. And
specification.md called the Charisma example "the canonical worked example of a
conforming journey" when its own retrospective lists seven gaps.

Seventeen identifiers cover requirements that were normative in wording but
anonymous, and degradation paths that were unspecified: weak evidence, a missing
research capability, a learner who pauses or abandons, honest early closure, and
what a renderer must preserve. Every normative sentence under products/learn now
sits under an identifier or cites one.

Files under products/learn/examples/charisma/ remain byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115djDoBfqBbioBWYxzHTjH
Eighteen list items across ten packages were parsed as single-key mappings
rather than strings, because unquoted prose contained a colon and a space:

  - decision hygiene: the reserve clause and the archer model

YAML read that as {"decision hygiene": "the reserve clause…"}. The site filters
list entries with typeof v === "string", so every one of these curated lines was
dropped from the rendered topic page. No error, no warning, just missing
content. All eighteen are now quoted, byte-for-byte identical in text.

One concept had three names: tier_reason, tier_note and tier_rationale each
mean, verbatim from their own file headers, "one line stating why the entry sits
in that tier". The library README documents tier_rationale, so 587 keys across
eleven packages now use it.

Three statements in the catalogue were not true of the data. The technology
packages do not all carry the alignment note the README claimed;
software-architecture is not "mostly tier 3" but tied, eighteen entries at tier
3 against eighteen at tier 1; and the manifest anatomy omitted description,
which twenty-seven manifests carry and the site renders.

Also normalised: three manifests declared version as a string where the other
twenty-seven used an integer, two file headers declared a role vocabulary their
data never used, and two knowledge maps were missing the misconceptions slot
their twenty-nine siblings carry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115djDoBfqBbioBWYxzHTjH
All five schemas now declare draft 2020-12 and carry an $id on the host agents
actually fetch from. The previous $id pointed at learn.rapold.io, which by
ADR 0002 serves no specification files. The path deliberately does not pin a
tag: a pinned $id renames every schema at each release and adds a fourth pin
site that scripts/bump-version.mjs does not know about, which is exactly the
silent drift that decision record warns against.

Every property now says what it is for and names the requirement behind it, so
an agent reading the schema learns the intent rather than the type.

Constraints the specifications already imply are now enforced. A lesson's ten
slots are all required and non-empty, because an empty boundary defeats
LEARN-S-6. A curriculum module requires evidence, counterposition and example;
prerequisites and retrieval_question stay optional only because the frozen
Charisma fixture has neither, and the module description says so. Package
identifiers, learning paths and related packages match the library's slug
pattern. Person lenses have a real shape with a tier enum. The learner profile's
eleven discovery dimensions are named and typed instead of an untyped blob.

Verified against every instance in the repository: thirty manifests, the
Charisma curriculum, thirteen eval capability blocks and twelve learner
profiles all validate, and eleven deliberately invalid documents are rejected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115djDoBfqBbioBWYxzHTjH
The check emitted "::error file=$file::does not end with a newline". GitHub
attaches that to the right file as an annotation, but anyone reading the raw
log sees only the message and has to guess which of 250 files it means. Each
message now repeats the path in its text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115djDoBfqBbioBWYxzHTjH
The validation toolchain tests itself against a fixture repository whose files
are malformed on purpose — broken links, bad anchors, invalid YAML. All three
documentation gates would have flagged them as defects, so markdownlint, the
whitespace check and the house-style check now skip tools/__fixtures__.

The two exclusions in this workflow now exist for opposite reasons: the Charisma
fixture is preserved byte-identical and so cannot be linted into compliance,
while the toolchain fixtures must stay broken to be worth testing against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115djDoBfqBbioBWYxzHTjH
The repository shipped 152 YAML files, six JSON Schemas and 875 internal links,
and nothing checked any of it. A schema violation, a link an agent would follow
to a file that no longer exists, or an eval scoring a requirement identifier no
specification defines could all reach main unnoticed.

node tools/validate.mjs runs five checks, each also runnable alone so a red
build names the surface that broke rather than saying validation failed:

- schemas: meta-validates every schema against draft 2020-12, then validates
  every data file against the schema its path implies.
- links: resolves every relative link and image across the Markdown, and every
  in-document anchor against the headings that exist, using GitHub's slug rules
  including its de-duplication counter.
- refs: library cross-reference integrity — the catalogue matches the packages
  on disk, related_packages and prerequisites name packages that exist, every
  package has a light and a dark visual, and each id matches its directory.
- yaml: parses, two-space indent, no tabs, no trailing whitespace, one final
  newline, and no duplicate mapping keys, which YAML resolves silently by
  dropping the earlier value.
- requirements: builds the registry of requirement identifiers, reports
  duplicates, and reports identifiers an eval references that nothing defines.

115 unit tests cover the parts that are easy to get subtly wrong — the anchor
slugger above all — against a fixture repository that is malformed on purpose.

Current result on this tree: 5 checks, 0 errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115djDoBfqBbioBWYxzHTjH
… real gate

scripts/bump-version.mjs matched pinned tags with /v\d+\.\d+\.\d+/g, which never
matched the bare version banner "ÆON Learn 0.3.0 · ÆON Protocol 0.3.0" in the
bootstrap and llms.txt, because that banner carries no leading v. Every release
therefore shipped an entry contract announcing the previous version — exactly
the drift the script exists to prevent. It only warned on a missing pin site,
and wrote files as it went.

It is now declarative: every pin site declares the minimum occurrences it must
carry, the script counts before substituting and verifies afterwards that no
stale tag or banner survived, and it writes nothing at all unless every site
passes. A half-re-pinned tree can no longer be tagged. Both scripts gained a
read-only --check mode, and the continuous integration gate runs it.

CONTRIBUTING told contributors to run markdownlint with globs that differed from
the ones CI used, so a locally clean tree could still fail. It is rewritten
around npm run validate, with a table of what each check enforces, verified
against the checks' source rather than assumed.

README carried five counts that were wrong and a <br/ > typo that would have
rendered as literal text in the first diagram. SECURITY gained a threat model,
a prompt-injection section covering injection through merged repository content
and through sources an agent fetches while researching, and a safe harbour.

Also: an .editorconfig agreeing with .gitattributes and the house-style checks,
two architecture decision records for decisions already made but never written
down, a decision-record template, and scripts/README.md documenting the release
procedure and every pin site.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115djDoBfqBbioBWYxzHTjH
…nscored

The six behavioural cases shared an implicit shape that nothing enforced, so a
case could name a requirement identifier no specification defines and no check
would notice. schemas/eval-case.schema.json now captures that shape — identity,
scenario, preconditions, the requirements under test, the observable
expectations and the rubric — and all cases validate against it. The
requirements check in the validation toolchain refuses any identifier an eval
scores that nothing defines, so a dangling reference is now a red build.

Nine cases cover requirements that were specified but unscored, chosen by what
would silently produce a bad learning journey rather than by count: weak
evidence, pause and resume, a failed assessment, renderer degradation, the
LIB-1 guarantee that a topic package never caps the subjects ÆON teaches,
rejection at the contract gate, invocation and bootstrap, session anatomy, and
the completion package.

evals/learn/coverage.md publishes the traceability matrix in both directions,
so the requirements no case exercises are visible rather than folklore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115djDoBfqBbioBWYxzHTjH
The bullet cited ADR 0002 by a filename it does not have. The link check caught
it on the committed tree, which is the check working as intended.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115djDoBfqBbioBWYxzHTjH
Comment thread tools/lib/slug.mjs Fixed
The last defect the new whitespace and encoding check found anywhere in the
repository. A file without a final newline makes the next change to it show as
two modified lines in every diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115djDoBfqBbioBWYxzHTjH
…he changelog

Four of the placeholders the changelog carried are now written from what
actually landed, including the plain statement the release needs: RES-8 changed
what a MUST demands, so the release carrying this is a major one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115djDoBfqBbioBWYxzHTjH
Type checking, linting and tests for the invocation surface, from the agent
pass over site/learn: stricter TypeScript settings, accessibility linting via
eslint-plugin-jsx-a11y, typescript-eslint for type-aware rules, and jsdom so
component behaviour can be tested rather than only its modules.

Six new test files cover what would actually break in production: route and
slug resolution, package loading against missing or malformed YAML, SEO and
Open Graph metadata generation, rendered markup, client component behaviour and
colour contrast in both themes. A use-reduced-motion hook lets the animated
components respect the visitor's setting.

npx tsc --noEmit passes on this tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115djDoBfqBbioBWYxzHTjH
CodeQL flagged the single-pass tag removal in the heading slugger as incomplete
multi-character sanitisation. The exploitability claim does not hold here: the
result becomes an anchor slug, never HTML, and slugify already drops every
character outside letters, numbers, marks, underscore, hyphen and whitespace,
angle brackets included.

The pattern is nevertheless worth making robust rather than subtly correct.
Matching is leftmost-first and greedy, so a "<" only survives when no ">"
follows it and a second pass provably changes nothing — a property that a lazy
quantifier would silently break. Repeating until stable makes the behaviour
independent of the pattern, and each pass strictly shortens the string.

The regression test asserts the invariant that actually matters: however tags
are nested, no angle bracket reaches the slug, because a slug carrying one
could never match the anchor GitHub generates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115djDoBfqBbioBWYxzHTjH
The button's accessible name came from resolvedTheme, which next-themes does
not know until it has mounted. Before that, the control sat on the dark theme
the site defaults to while announcing "Switch to dark theme" — a screen-reader
user reading the header before hydration was told the opposite of the truth.

Both names now sit in the markup, and the same dark: variant that already chose
the icon chooses which name is exposed. The inactive one is hidden, so it is
absent from the accessibility tree rather than merely invisible; the active one
is sr-only, so nothing changes on screen. That removes the mount flag and the
hydration gap with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115djDoBfqBbioBWYxzHTjH
llms.txt is the file an agent fetches first, and it is supposed to mirror
products/learn/bootstrap.md. Six substantive changes had landed in the
bootstrap and not here, so the document an agent actually reads was the weaker
of the two:

- Discovery did not ask the agent to state back the values it reuses, infers or
  defaults, so a wrong inference stayed invisible to the learner.
- Research told an agent without web access to lower its confidence, but not
  that the journey still runs, and said nothing about a subject whose popular
  claims turn out to rest on weak evidence.
- Completion listed six of the eight parts LEARN-14 requires, omitting the
  assessment itself and the recommended next learning path.
- Counterpositions had to be serious, but not researched rather than imagined,
  nor stated at their strongest.
- Nothing said what to do when a learner pauses or goes quiet, so an abandoned
  journey had no defined ending.
- Nothing said what to do when the specification links cannot be fetched, and
  nothing said that a subject with no topic package is the ordinary case.

Also restores the em dash and the ÆON ligature in package.json's description,
which had been rewritten as \u escapes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115djDoBfqBbioBWYxzHTjH
The invocation-surface entry, the site's regression-test coverage, and the
three verification markers resolved: the toolchain gained no check beyond the
five already listed, and the schema counts in README.md and schemas/README.md
agree, the former by naming the six kinds without a number that could drift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115djDoBfqBbioBWYxzHTjH
CodeQL reported two warnings for a superfluous argument passed to the
IntersectionObserver constructor. The constructor is the real one's; the double
in the reveal test declared only a callback parameter, so the options argument
the components pass had nowhere to go.

Dropping it was not only a type mismatch. Both components configure a
rootMargin — minus 80 pixels for the scroll reveal, plus 120 for the particle
field — and that margin is what decides when content reveals. The double
discarded it and hard-coded rootMargin to the empty string, so deleting the
configuration entirely would have left all 639 tests passing.

The double now accepts and records the options, and the test asserts the margin
the component depends on. Verified by reverting the rootMargin in reveal.tsx:
the test fails, and passes again once restored.

Reproduced locally with CodeQL 2.26.4 running the same security-and-quality
suite the workflow runs: two warnings before, zero after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115djDoBfqBbioBWYxzHTjH

This branch was successfully deployed

1 active deployment
Preview — 253eb32d Deployed Sep 8, 2026 by vercel[bot]
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.

3 participants