Version
v0.26.0
Date
2026-08-24
Highlights
- The landing page ships its new eclipse identity — the reason
lumibase.devlooked unchanged is that Pages only deploys on a tag. - The EN/VI documentation backlog is closed: 146/146 pairs up-to-date, no single-sided pair left.
high-load-cache-readinessP0-P2 code is complete; only the k6 re-measurements remain, and §7 does not gate the 1.0 tag on them.- Five cache-stack correctness gaps closed, including an
immutablemedia promise no purge channel could reach. - The dependency-audit gate is green again, and pnpm 10 can no longer drop the overrides silently.
Security
- Closed two high-severity advisories that were live on
main.nanoid
was pinned below 5.1.16 (GHSA-28wg-ghj8-5hjv — non-secure generators can loop
indefinitely on a negative size) andjs-yamlbelow 4.3.1
(GHSA-5p4m-2wfm-xmqj — quadratic CPU consumption in!!omap). Both were held
down by rootpnpm.overrides, so the bump had to land there rather than in
the individual manifests.pnpm audit --prod --audit-level highis clean
again. - Closed the two dev-only
brace-expansionadvisories.brace-expansion@1
->^1.1.16(GHSA-3jxr-9vmj-r5cp
— exponential-time expansion of consecutive non-expanding{}groups) and
brace-expansion@5->^5.0.8
(GHSA-mh99-v99m-4gvg —
unbounded expansion length crashing the process on OOM). Both are dev-only
—brace-expansionnever enters thepnpm audit --prodtree — so the CI gate
was already green and this closes the standing Dependabot alerts rather than
unblocking a build. Natural drift had already lifted most of the tree to
1.1.18/5.0.9; the straggler was a5.0.7copy held byminimatch@10.2.5.
Keyed per-major, the same shape as the existingnanoid@3/nanoid@5pair,
becausebrace-expansion@1exports the function itself while 5.x exports a
namespace object — collapsing both onto 5.x makesminimatch@3(ESLint and its
plugins) throwTypeError: m is not a function. A new "Known-unfixable alerts"
section in
docs/en/security/dependency-overrides.md
records the two advisories that cannot be closed at all: GHSA-mh99 also matches
the 1.x line by range, andglib@0.18.5is pinned across the Tauri Linux GTK
stack.
Added
-
pnpm drift:check— a guard against overrides overruling manifests.
scripts/check-override-drift.mjsfails CI when an override range does not
intersect a range some workspace package declares directly. The existing
settings:checkcould not catch this class: the two override copies agreed
with each other, they were only both wrong relative to the manifests.
Dependency-free like the parity script — a check on install settings must not
depend on a successful install — and covered bypnpm scripts:test
(node --test), including the vite case, so it cannot degrade into a guard
that always passes. Seedocs/en/security/dependency-overrides.md. -
MCP path-traversal tripwire no longer blind to spliced path segments.
The registry scan inpath-hardening.wiring.test.tsprobed each argument
with the literal..and then looked for..as a path segment of its own.
When a call site splices the value into a larger segment
(`/exports/report-${id}.json`), the probe produced
/exports/report-...json— no..segment — so the field was treated as
path-free and skipped before it ever reached the assertion. The source scan
missed the same shape independently, because its regex only matches an
interpolation directly preceded by/. Both halves of the guard were blind
to the identical call-site shape. Detection now probes with an alphanumeric
sentinel (unchanged byencodePathSegment) and checks the path portion of
the recorded URL, so an argument is found wherever it is spliced. Guard-only
change — no tool or runtime behaviour is affected, and no offender exists on
maintoday (32/32 pass unchanged). -
The landing page is a new visual identity.
apps/landingis restyled from
the orbital-solar-system look into an editorial "eclipse" language: a warm-black
ground, cream ink, a prismatic nebula palette, Archivo display / Literata serif /
DM Mono labels, uppercase bracketed mono captions, dashed rules and numbered
sections. The brand mark is a total solar eclipse with a small spaceship
transiting the moon (logo-mark.svg,EclipseMark/EclipsePhase), and the OG
image matches. The page is now scroll-driven rather than a stack of sections:
EclipseStageis a fixed backdrop where scroll is the eclipse — totality fills
the hero, the stage shrinks to the top-right as the moon slides off through the
product sections (light returns), then returns to centre for a second totality
behind the final CTA, with the spaceship transiting twice on scroll scrub.
Scene/WipeTitle/EclipsePhaseScrubdrive dashed-rule draw, clip-path
title wipes and per-section eclipse-phase glyphs off a--active-ratioper-section
progress value; feature grids get per-column parallax. Smooth scrolling is Lenis
through a context (the header nav useslenis.scrollTo), disabled under
prefers-reduced-motion, which instead renders a dimmed static totality —
hydration-safe viauseStaticMotion. Implemented with framer-motion scrub rather
than THREE/GSAP. Marketing surface only: no API, schema, env or CMS behaviour
change. -
docs/en/roadmap/post-v1.md— the post-1.0 roadmap, written down. The
narrative companion to the Post-v1 Roadmap board: it scores the four candidate
themes against the actual code, records why they are ordered the way they are,
and notes the positioning corrections found while reviewing MCP, GitOps, the
Change Feed and the authorization realms. Pairs with thepost-v1-labelled
issues so "later" has a written definition instead of living in a project board. -
A structural parity gate for translations —
scripts/docs-i18n/check-parity.mjs.
The two existing checkers answer different questions:sync.mjscompares hashes
("same source revision?") andverify-code-refs.mjscompares claims against the
source tree ("is it true?"). Neither can tell you the translation is the same
document. This one does: target actually in the target language, same heading
count and level sequence, same fenced-block count with byte-identical code
(comment lines excluded — those are prose), same inline-code and link-target
multisets, same table row counts, provenance front matter present, body length
inside a truncation band. It is tuned against the real corpus: in-page anchors are
compared by count rather than value, because a translated heading must produce a
different slug, and prose-shaped fences (text,tree,mermaid, untagged) are
compared by count only, because translating an ASCII diagram's labels is correct.
Per-doc<!-- check-parity: allow <check> -->waivers keep a deliberate divergence
visible next to the divergence.stamp-pair.mjsruns it and refuses to stamp
on drift — that is the gate that bites, since stamping is what makes a pair read
"up-to-date" to every other tool. -
The EN/VI translation backlog is closed: 146/146 pairs up-to-date.
detect
went from 87 up-to-date / 59 planned to 146 / 0;check-parityfrom 187 problems
across 76 pairs with 3 single-sided pairs to 52 across 36 with none
single-sided. Single-sided was the reader-facing failure: someone choosing
Vietnamese and being served English. The 52 that remain are cosmetic — comment
text inside code fences, anchor slugs that differ because the headings differ.
docs/.i18n/TASKS.mdkeeps the task table as a record rather than a queue. -
DoD §4a — EN/VI parity is a condition of the PR that breaks it. Two failures
drove this, both of which reachedmain:docs/vi/agent-setup/prompt.mdnever
received thewithDeprecationopt-in rule the EN side had carried since 0.24.x, so
an agent reading the Vietnamese instructions was never told about it — on a file
§4 lists as release surface; and the 60-pair backlog above accumulated to the point
of needing its own translation campaign. §4a makes both locales a checklist item,
states plainly that CI will not catch this (docs-i18n-sync.ymlruns
check-paritywith|| true, so the human is the enforcement), and carries up the
rules that were previously only visible to someone already mid-batch: establish the
translation direction fromdetectrather than guessing (some pairs are
VI-source), never edit the source side to make parity green, and do not translate a
file whose EN source is sitting in an unmerged PR.
Changed
-
The repo guards now run before the commit, not after the push.
version:check,
registry:check,settings:check,drift:checkandscripts:testexisted only as
CI steps, so the feedback loop for a mechanical mistake was commit → push → open PR →
wait for CI → red, even though every one of them runs in well under a second against
files already on disk. They are now a singlepnpm check:all, invoked by
.husky/pre-commitbeforepnpm testso a bad row number or a half-declared
override fails immediately instead of after a full suite run. CI calls the same script
rather than keeping its own list of five steps, so the local and CI guard sets cannot
drift apart — adding a guard tocheck:allarms it in both places at once.
Closes #406. -
The landing page's "Set intent" control now does something. The Content OS
section's Intent-driven, not click-driven card was a static mock: a
btn-solidspan labelled "Set intent" with no handler, over a hardcoded
"82% converged" bar. It is now a working composer
(apps/landing/src/components/IntentComposerViz.tsx) — edit the sentence or
pick a collection preset, and the card compiles it into theintent-rule.v1
payload you wouldPOST /api/v1/agent/intents, evaluates an eight-item sample
against those rules, and runs the beats (compile → evaluate → incident →
reconcile → converged), ending with a copyable payload. Compilation is
deliberately local and labelled as such: the real endpoint is control-plane
admin-only andapps/landingships as a static export, so
apps/landing/src/lib/intent-compile.tsimplements a deterministic matcher
over the same six rule shapes asintent-service.ts. It mirrors the shipped
semantics rather than the marketing phrasing —field_constraintmeasures
characters (drift-service.tscomparesvalue.length), so "50–200 words"
compiles to 300–1200 chars with the conversion surfaced as a warning, and
each fix is attributed to the roleRULE_ROLE_ROUTINGwould route that rule
to. Landing-only: no API, schema, env or CMS behaviour changes. -
Consolidated every open Dependabot upgrade into one lockfile-consistent
change rather than merging them serially, where each merge invalidated the
next PR's lockfile. Majors taken:uuid11→14,vite7→8,nanoid5.0→5.1,
@dnd-kit/sortable8→10,lucide-react0.452→1.28,fast-check3→4,
@testing-library/jest-dom6→7,@cloudflare/workers-types4→5,eslint
9→10,next16.2→16.3. Three breaking changes needed source follow-up, all
behaviour-preserving:@cloudflare/workers-typesv5 declares its own
Buffer: anyand widenedExecutionContext;lucide-reactv1 dropped brand
marks (Githubwas the only removed export in use, across 121 import sites);
andfast-checkv4 replaced the per-unit string builders with
string({ unit }). No runtime, schema, or setup behaviour changes — no
migration or backfill is required on upgrade. -
high-load-cache-readinessP0–P2 complete (tasks 0–21). The programme's code
is now in:PageService+/api/v1/pageswith negative-cache forget on create and
slug rename;runtime.edgeCache(Cloudflarecaches.default, Docker no-op) behind
the runtime abstraction on the deliver path; Cache Provider v2 with tags,
invalidateByTag,onEventand an LRUMemoryCacheProviderplus contract tests;
POST /api/v1/utils/cache/purge; content invalidation and deliver app-cache;
createSwrCache(single-flight / stale-while-revalidate); middleware consolidation;
async audit; aRateLimiterProvider; cache observability; process roles with a
leader lock;flow_runsasync withPrefer: respond-async;itemsindexes and
transactional writes; aperf-k6CI workflow; and ADR-012 removing the unused CDC
CacheInvalidator. The k6 re-measurements are still open (tasks 7.1, 15.1,
21.1) — the code landed, the numbers were not taken, and per Req 0.3 estimated
figures are not allowed in the roadmap table.v1-release-criteria.md§7 classifies
performance baselines as non-blocking for the 1.0 tag; the one real measurement is
DB-query-per-404 = 0.0308. -
Per-request identity lookups de-duplicated (Req 10).
withAuthnow stashes
the resolvedusersrow anduser_sitesmembership on the Hono context
(c.set('principal', …));withSiteMembershipreads that bundle instead of
re-issuing the same queries, andwithStudioAccessreuses theaccessbundle
withSiteMembershipalready computed rather than callingPermissionService.bundle()
a second time. Each guard still falls back to querying when the bundle is absent, so
the middleware stay independently correct and standalone-testable, and guard
semantics are unchanged — a request rejected before is rejected with the same status
now.request-context-bundle.test.tsasserts the principal × route matrix and the
query-count reduction; thesecurity-guards.wiringtripwire is preserved. -
A high-load Phase 0 baseline exists. k6 seed and run tooling under
apps/cms/k6/, with reproducible measurement and runs that continue when a
threshold is advisory. This is what makes the remaining re-measurement tasks
possible at all: Req 0.3 forbids filling the roadmap table with estimates, so
without a baseline no phase could be closed.
Fixed
-
The test suite no longer fails because the machine was busy — and the ReDoS
guard it contained now actually guards something. Two separate time-based
assertions were failing under parallel load on unmodifiedmain, and the
turbo cache was hiding it (pnpm testreplayed a cache hit; only
turbo run test --forceexposed it). TheCloudflareSearchProviderhost
normalization test asserted an absolute< 100msthat the correct linear scan
itself can exceed (133ms observed in CI) — and it timed inputs ending in
slashes, which/\/+$/matches greedily without backtracking, so the test
would have passed with the vulnerable regex in place (measured 0.4ms). It now
compares an adversarial slash run against a benign string of the same length
and bounds the ratio, which load scales on both sides; the separation between
implementations is ~4 orders of magnitude. Confirmed by reintroducing the
regex: the new test fails at 35,699x. React Testing Library's
asyncUtilTimeoutin Studio is also raised from its 1000ms default, which was
an implicit wall-clock budget causing "Unable to find role=..." failures in
suites that pass standalone. The docs search smoke test keeps a ceiling but a
generous one, with a note that it only catches catastrophic regressions.
Closes #408. -
fc.date()generators could emitInvalid Date.noInvalidDate
defaults to false, so bounded date arbitraries still produced NaN
timestamps — the approvals-list ordering property compared them, and the
Studio approval-card arbitrary would have thrown on
new Date(NaN).toISOString(). A latent test bug, surfaced (not caused) by
fast-checkv4's different generation bias. -
Vite 8 was never actually in effect.
pnpm.overrides.vitesat at^7.3.5
whileapps/studioandapps/docsboth declared^8.1.3. pnpm overrides
apply to direct dependencies too, so the override won and the lockfile
importer readspecifier: ^7.3.5 → 7.3.6— both apps were built with Vite 7
for as long as their manifests claimed Vite 8. Raising the override in step
with the manifests flips the toolchain for real (importers now resolve 8.2.2). -
Node floor raised to
>=22.13.0, and.nvmrcpinned to24to match
CI'sNODE_VERSIONinstead of floating on22. The old>=22admitted
22.0–22.12, a range that breaks bothvite8 (needs>=22.12.0) andeslint
10 (needs^22.13.0). Contributors on Node 22.0–22.12 must upgrade.
packages/mcp-serverkeeps>=18: its floor is a contract with consumers of
a published package, not with this toolchain. -
ESLint unified on 10 —
apps/landingwas still on 9 whileapps/consumer
already declared 10.@dnd-kit/coreraised to^6.3.0so@dnd-kit/sortable
10's peer is satisfied by declaration rather than by the lockfile happening to
resolve 6.3.1.apps/docsmoved to@testing-library/jest-dom7 so the
workspace no longer carries two majors of one test library. -
The
immutablemedia cache promise was not one./api/v1/media/:key
transform URLs were servedCache-Control: immutable, max-age=31536000, but
POST /media/:keyoverwrites in place under a caller-chosen key (storage.put,
no existence check). The URL was therefore not a function of the content:
re-uploading different bytes left every browser that had already fetched the old
ones holding them for a year, with no channel able to reach them — neither tag
purge nor CDN purge reaches the browser cache. Uploaded bytes are now fingerprinted
into storage metadata (contentHash), returned asversionand surfaced on reads
asX-Lumi-Media-Version;immutableis served only for a URL pinned with
?v=<contentHash>, and a pin that does not match the stored fingerprint is
downgraded rather than freezing whatever bytes happen to be there. The
original-bytes path, which previously carried noCache-Controlat all and left
each cache to its own heuristic, now getsmust-revalidate+ a weakETag+304
onIf-None-Match. Objects with no stored fingerprint — earlier uploads, and
anything written through the streamingPUT /files/upload/:keyreceiver, which
cannot hash a body it never buffers — degrade to revalidation and never to
immutable. Refs #388. -
VaryomittedHost. It named onlyX-Lumi-Site, while
middleware/tenant.tsresolves the site from that header, fromHost, and
from?site=in development. The query string is already part of the cache key,
butHostwas declared nowhere — leaving correctness on the delivery routes
dependent on every CDN in front of the app keying onHostby convention. Also
documents how far invalidation actually reaches, and adds an operational edge
purge plus an in-process cache layer. -
fast-uriandhonoadvisories in the production tree. The dependency-audit
gate had been red on every branch since
GHSA-7p8r-x3mc-p8w7 was
published — fast-uri host confusion via a backslash authority introducer, high,
>=4.0.0 <4.1.2. Nothing in the repo changed; the advisory did. The root override
read">=3.1.4", written for the earlier 3.x advisory, which resolved happily to
the vulnerable 4.1.1; it now reads">=4.1.2".fast-urireaches the production
tree throughajv@8.honoinapps/cmsalso goes^4.12.32→^4.12.34for
GHSA-8j4g-w8fx-2239 — ReDoS
in the CORS middleware viaAccess-Control-Request-Headers. That one is only
moderate, so--audit-level highwould never have caught it, but this is the
framework every request passes through and the CORS middleware is wired
(apps/cms/src/config/cors.ts), so it takes a patch bump rather than a backlog row. -
pnpm 10 would have silently dropped every override. The
pnpmfield in
package.json— overrides,patchedDependencies,auditConfig— is no longer read
by pnpm 10, which readspnpm-workspace.yamlinstead and only warns once before
ignoring the rest. Nothing was broken yet, becausepackageManagerpins 9.12.0, but
the firstpackageManagerbump would have reverted every security pin and unapplied
thegray-matterpatch without a single error. All three keys are now declared in
both files, andpnpm settings:checkfails the build when the two drift apart.
Refs #295. -
CHANGELOG.mdasserted a release that does not exist. A
## [1.0.0] - 2026-07-11section sat wedged between[0.22.0]and[0.21.0],
which read as 1.0.0 having shipped before 0.22.0, 0.23.0 and 0.24.x. It would also
have broken this release:/releaseinserts the new section directly under
## [Unreleased], so cutting 1.0.0 would have produced two## [1.0.0]headings and
theawkextractor stops at the first — the GitHub Release would have shipped the
stale text. The prose is preserved in.kiro/steering/v1-changelog-draft.mdwith the
list of what it still needs. Also stops CI from stamping translations it never made. -
stamp-pair.mjscould not bootstrap an unstamped pair. Its pre-write gate
treatedcheck-parity'sfront-matterproblems as "the two locales are not the same
document" — but those fire precisely when the target side has notranslatedFrom/
sourceHash, and their printed remedy is to runstamp-pair.mjs. The gate was
unsatisfiable: exit 6, pointing at itself. Found on
security/dependency-overrides.md, whose VI side was a complete and current
translation that merely predated the provenance stamps.front-matteris now
excluded from that gate; every other check still blocks. -
docs/{en,vi}/cdc/architecture.mddocumented a module ADR-012 deleted. Both
locales describedapps/cms/src/modules/cdc/cache-invalidator.ts— its Redis key
scheme, its dedup window, its outage buffer — for a file that no longer exists. The
section now records what actually happened and why (keys omittedsiteId; they never
matched the tag-based keys the read paths use), and points at ADR-012 and the real
invalidation path. This was the onlyverify-code-refsfinding onmain, and it was
a reader-facing inaccuracy rather than a broken link.
Migrations
- None.