Skip to content

content(blog/svg): fail clipped drop shadows and grow 27 filter regions, translate four alt texts, fit the Runtime ring - #248

Merged
hotlong merged 36 commits into
mainfrom
claude/pm-dispatch-objectos-ju9td1
Oct 15, 2026
Merged

hotlong merged 36 commits into
mainfrom
claude/pm-dispatch-objectos-ju9td1

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #224

Branch. This PR carries 3 commit(s) from the seat's linear stack, replayed onto main @ 311a349. The replay is patch-equal to git diff 8cb5bde lin/issue-224, with byte-identical diffs. Head: f498430. The staged SHAs quoted below are the pre-replay commits; their content is unchanged.

Three fixes, all found while #221 was staged:

  • drop shadows cut off by their own filter region in 27 blog SVGs;
  • placeholder alt text on four illustrations in five locales;
  • a gold ring running off its card in the natural-language-to-app-metadata covers.

1. Drop shadows cut off by their filter region

A filter paints nothing outside its filter region. 27 blog SVGs kept the default-looking region x="-20%" y="-20%" width="140%" height="150%" on cards too short for the shadow they cast (dy 16–18, stdDeviation 20–24). That left a hard straight edge across the shadow, mostly just under the card.

  • Measured, not listed: 81 of 181 filtered elements had part of their shadow on the canvas but outside the region. 35 of them cut within 1.5σ of the shadow's edge, which reproduces the card's count exactly (35 elements, the same 12 SVGs). The other 46 are shallower cuts the card's threshold did not count.
  • The fix: every cut filter's region now holds the full 3σ reach of every element that uses it. Regions are rounded out to 10% and only ever grow. Nothing else in any SVG changed in this commit: 27 lines, one filter element per file.
  • Visual identity, proven in Chromium: every one of the 78 filtered SVGs renders pixel-identically to the same SVG with every region made unbounded (x/y -500%, width/height 1100%); 0 of 78 differ. Before and after differ in exactly 14 SVGs, and those pixels are exactly the ones the unbounded render adds: the removed hard edge and nothing else.
SVG filter cut elements deepest cut new region (x y width height) Chromium step before (of 255)
ai-wrote-your-app-dare-to-merge/review-loop-en.svg #sh 4 2.67σ -20% -20% 140% 160% 0
ai-wrote-your-app-dare-to-merge/review-loop.svg #sh 4 2.17σ -20% -30% 140% 170% 0
airtable-style-ai-builder/cover-en.svg #shadow 1 2.75σ -30% -20% 160% 150% 0
airtable-style-ai-builder/cover.svg #shadow 1 2.75σ -30% -20% 160% 150% 0
automation-cross-system-flows/cover-en.svg #shadow 2 0.85σ -20% -40% 140% 210% 7
automation-governed-flow-changes/cover-en.svg #shadow 3 1.30σ -20% -40% 140% 200% 3
automation-trigger-model/cover-en.svg #shadow 1 0.74σ -20% -50% 140% 230% 8
beyond-agentforce-copilot-open-runtime/cover-en.svg #shadow 4 1.84σ -20% -30% 140% 180% 1
beyond-agentforce-copilot-open-runtime/cover.svg #shadow 4 1.84σ -20% -30% 140% 180% 1
business-app-in-16k-tokens/cover-en.svg #shadow 1 2.14σ -20% -30% 140% 170% 0
business-app-in-16k-tokens/cover.svg #shadow 1 2.14σ -20% -30% 140% 170% 0
enterprise-ontology-race-open-vs-closed/cover-en.svg #shadow 5 0.28σ -20% -70% 140% 280% 14
enterprise-ontology-race-open-vs-closed/cover.svg #shadow 5 0.28σ -20% -70% 140% 280% 14
forward-deployed-engineer-tools/cover-en.svg #shadow 2 2.27σ -20% -30% 140% 170% 0
forward-deployed-engineer-tools/cover.svg #shadow 2 2.27σ -20% -30% 140% 170% 0
forward-deployed-engineer-vs-solutions-architect/cover-en.svg #shadow 4 2.35σ -30% -20% 160% 150% 0
mcp-governed-tool-layer/cover-en.svg #shadow 2 1.90σ -20% -30% 140% 180% 0
mcp-governed-tool-layer/cover.svg #shadow 2 1.90σ -20% -30% 140% 180% 0
mcp-governed-tool-layer/tool-call-lifecycle-en.svg #sh 4 2.17σ -20% -30% 140% 170% 0
mcp-governed-tool-layer/tool-call-lifecycle.svg #sh 4 2.17σ -20% -30% 140% 170% 0
natural-language-to-app-metadata/cover-en.svg #shadow 2 0.91σ -30% -50% 160% 220% 7
natural-language-to-app-metadata/cover.svg #shadow 2 0.91σ -30% -50% 160% 220% 7
objectos-automation-engine/cover-en.svg #shadow 4 0.95σ -20% -40% 140% 210% 6
objectos-automation-engine/run-record-en.svg #s2 6 1.34σ -30% -40% 160% 200% 3
ontology-vs-semantic-layer-vs-knowledge-graph/cover-en.svg #shadow 1 1.15σ -20% -40% 140% 200% 4
why-ai-agent-pilots-fail-four-layers/cover-en.svg #shadow 5 0.16σ -20% -70% 140% 290% 15
why-ai-agent-pilots-fail-four-layers/cover.svg #shadow 5 0.16σ -20% -70% 140% 290% 15

2. The gate: svg-text-fit rule 3

scripts/svg-text-fit.mjs (from #221, run by pnpm svg:lint and by pnpm build) gains a third rule. For every element with a filter it takes three things:

  • the object bounding box, which is what objectBoundingBox units are fractions of;
  • the painted box, strokes included;
  • the feDropShadow's offset and blur.

It fails unless the shadow's 3σ reach lies inside the filter region wherever that reach lands on the canvas. 3σ is the bound Skia puts on a blur; past it, the shadow is under 0.14% of its flood opacity. A cut past the canvas edge is invisible and passes. Each finding names the side, the depth in σ and the shadow's opacity at the cut.

A filter that is not a single feDropShadow fails as unmeasurable, and so do primitive subregions, primitiveUnits, href and a missing filter id. A new kind of filter cannot pass unread. Rule 3 has no allowlist. package.json is untouched: the rule lives in the script the build already runs.

  • Red on stack/www-wave1 (7f69d97), using the final script: svg-text-fit: FAIL — 81 drop shadows cut off by their filter region on the canvas (112 SVGs, 2244 text boxes, 181 filtered elements, tolerance 2px, 1 allowlisted), exit 1, in 27 SVGs.
  • Green at 69d9877: svg-text-fit: OK — no text box crosses the shape under it or runs under a shape painted over it, and no drop shadow is cut off by its filter region (112 SVGs, 2244 text boxes, 181 filtered elements, tolerance 2px, 1 allowlisted), exit 0.
  • Text rules unchanged: the old and the new script print byte-identical --verbose clearance lines for all 2244 text boxes.
  • Synthetic cases (scratch, not committed) behave as specified:
    • a short card fails on all four sides;
    • the same card with a userSpaceOnUse region passes;
    • a card whose bottom cut falls past the canvas edge reports only the on-canvas sides;
    • a CSS filter: rule is read;
    • feGaussianBlur and a missing filter id both fail as unmeasurable.

3. Alt text in de, es, fr, ja and ko

The 20 placeholder alts ("Konzeptdiagramm", "Diagrama conceptual", "Schéma conceptuel", 「概念図」, "개념 다이어그램") now carry #221's English description, translated, in each post's own terms. They keep "AI" as the posts write it, and use governte, gobernada or gouvernée for the object layer. Each was checked against the image itself. Zero placeholder alts remain in content/.

crm-ai-understands-customers

  • de: Illustration: Ein Kundendatensatz bildet den Mittelpunkt eines Knotenpunkts und ist mit Kontakten, einer Aktivitätszeitleiste, Pipeline-Diagrammen, Bestellungen und mit einer Warnung markierten Supportfällen verbunden, während ein AI-Assistent mit dem verknüpften Datensatz arbeitet
  • es: Ilustración: un registro de cliente ocupa el centro de un nodo central, conectado con contactos, una línea de tiempo de actividad, gráficos del pipeline, pedidos y casos de soporte marcados con una advertencia, mientras un asistente de AI trabaja a partir del registro conectado
  • fr: Illustration : une fiche client occupe le centre d'un hub, reliée aux contacts, à une chronologie d'activités, à des graphiques de pipeline, aux commandes et à des tickets de support signalés par un avertissement, tandis qu'un assistant AI travaille à partir de cette fiche connectée
  • ja: イラスト:顧客レコードがハブの中心にあり、連絡先、活動タイムライン、パイプラインのグラフ、受注、警告マーク付きのサポートケースとつながっている。AI アシスタントは、このつながった顧客レコードをもとに作業している
  • ko: 일러스트: 고객 레코드가 허브 중앙에 있고 연락처, 활동 타임라인, 파이프라인 차트, 주문, 경고 표시가 붙은 지원 케이스와 연결되어 있으며, AI 어시스턴트가 이 연결된 레코드를 바탕으로 작업합니다

extend-existing-systems-with-ai

  • de: Illustration: Eine bestehende Datenbank und Anwendung links sind über eine gesperrte, schreibgeschützte Verbindung an eine governte Objektschicht in der Mitte angebunden; sie prüft Filter, Identität und Freigabe, bevor sie rechts Dashboards, eine mobile App, Workflows, APIs und einen AI-Agenten bedient
  • es: Ilustración: una base de datos y una aplicación existentes, a la izquierda, se conectan mediante un enlace bloqueado de solo lectura a una capa de objetos gobernada en el centro, que comprueba filtros, identidad y aprobación antes de servir a la derecha paneles, una app móvil, flujos de trabajo, APIs y un agente de AI
  • fr: Illustration : une base de données et une application existantes, à gauche, se connectent par un lien verrouillé en lecture seule à une couche d'objets gouvernée au centre, qui vérifie filtres, identité et approbation avant d'alimenter, à droite, des tableaux de bord, une application mobile, des workflows, des API et un agent AI
  • ja: イラスト:左側の既存のデータベースとアプリが、鍵のかかった読み取り専用の接続を通じて、中央のガバナンスされたオブジェクト層につながる。オブジェクト層はフィルター、ID、承認を確認してから、右側のダッシュボード、モバイルアプリ、ワークフロー、API、AI エージェントにデータを提供する
  • ko: 일러스트: 왼쪽의 기존 데이터베이스와 앱이 잠금 표시가 있는 읽기 전용 연결을 통해 가운데의 거버넌스가 적용된 객체 레이어에 연결되고, 이 레이어는 필터, 신원, 승인을 확인한 뒤 오른쪽의 대시보드, 모바일 앱, 워크플로, API, AI 에이전트에 데이터를 제공합니다

low-code-vs-ai-native-app-platform

  • de: Illustration, die zwei Architekturen gegenüberstellt: Links verheddern sich Seiten, Server, Datenbanken und Skripte unter Warnschildern; rechts versorgt ein zentrales Modell verbundener Objekte Ansichten, Dashboards, Workflows, Rollen, Berechtigungen, Daten und APIs über klare Linien
  • es: Ilustración que contrasta dos arquitecturas: a la izquierda, páginas, servidores, bases de datos y scripts enredados entre sí bajo señales de advertencia; a la derecha, un modelo central de objetos conectados alimenta vistas, paneles, flujos de trabajo, roles, permisos, datos y APIs mediante líneas limpias
  • fr: Illustration opposant deux architectures : à gauche, pages, serveurs, bases de données et scripts enchevêtrés sous des panneaux d'avertissement ; à droite, un modèle central d'objets reliés alimente vues, tableaux de bord, workflows, rôles, permissions, données et API par des liaisons nettes
  • ja: 2 つのアーキテクチャを対比したイラスト:左側では、ページ、サーバー、データベース、スクリプトが絡み合い、あちこちに警告マークが出ている。右側では、つながったオブジェクトからなる中央のモデルが、整ったラインでビュー、ダッシュボード、ワークフロー、ロール、権限、データ、API を支えている
  • ko: 두 아키텍처를 대비한 일러스트: 왼쪽에는 페이지, 서버, 데이터베이스, 스크립트가 경고 표시 아래 뒤엉켜 있고, 오른쪽에는 연결된 객체로 이루어진 하나의 중앙 모델이 깔끔한 선으로 뷰, 대시보드, 워크플로, 역할, 권한, 데이터, API를 공급합니다

manufacturing-legacy-systems-ai

  • de: Illustration: Alte Werksserver und eine Fertigungshalle mit Maschinen, Förderbändern und einem Lager speisen eine gemeinsame Schicht aus Geschäftsobjekten, Beziehungen und Datenbanken; ein AI-Assistent arbeitet auf dieser Schicht, meldet Anomalien und arbeitet Checklisten ab, und eine Berechtigungsprüfung hindert ihn daran, ein System zu erreichen, auf das er nicht zugreifen darf
  • es: Ilustración: servidores heredados de la planta y una nave de producción con máquinas, cintas transportadoras y un almacén alimentan una única capa de objetos de negocio, relaciones y bases de datos; un asistente de AI trabaja desde esa capa para señalar anomalías y ejecutar listas de verificación, y una comprobación de permisos le impide llegar a un sistema que no puede tocar
  • fr: Illustration : d'anciens serveurs d'usine et un atelier fait de machines, de convoyeurs et d'un entrepôt alimentent une même couche d'objets métier, de relations et de bases de données ; un assistant AI travaille à partir de cette couche pour signaler les anomalies et dérouler des checklists, et un contrôle des permissions l'empêche d'atteindre un système auquel il n'a pas le droit de toucher
  • ja: イラスト:古い工場サーバーと、設備・コンベヤー・倉庫からなる現場が、業務オブジェクト、リレーション、データベースからなる 1 つのレイヤーに集約される。AI アシスタントはこのレイヤーをもとに異常を検知し、チェックリストを実行する。権限チェックが、触れてはいけないシステムへのアクセスを止めている
  • ko: 일러스트: 오래된 공장 서버와 설비, 컨베이어, 창고로 이루어진 생산 현장이 비즈니스 객체, 관계, 데이터베이스로 된 하나의 레이어로 모이고, AI 어시스턴트는 이 레이어에서 이상을 감지하고 체크리스트를 실행하며, 권한 검사가 접근해서는 안 되는 시스템에 닿지 못하게 막습니다

4. The Runtime ring stays inside its card

In natural-language-to-app-metadata's cover-en.svg and cover.svg, the Runtime card's gold cycle ring has radius 90, stroke 5 and sits 172px down the card. It reached 264.5px down a 248px card, so it ran 16.5px past the card's bottom edge onto the page background.

The card is now 290px tall. That centres the ring in the space under the card title, with about 26px clear above and below. The ring, the title, the two lines inside the ring and the card's top edge do not move: one attribute per file.

In Chromium the before/after difference is confined to the card's lower edge and its shadow (x 794–1151, y 364–509). The drop-shadow gate still passes: the card's filter region scales with its box, and the margins are now top 7.3σ and bottom 8.5σ.

The stroke rule is a gap, not part of this PR. svg-text-fit models fills, not strokes, so nothing gates this class. A naive stroke rule was prototyped (scratch, not committed): a stroked, unfilled shape must stay inside the filled shape under its centre. Of 185 stroked shapes, it flagged 20 in 11 SVGs before this fix and 18 in 9 after. Most are connectors drawn across cards on purpose, 29–256px past their "host". The remaining few small overruns (6.5px in the conversational-app-iteration covers, 5.9px in eu-ai-act-runtime-audit/cover-en.svg) were not looked at. Telling a connector from an overrun needs a host model the text rule does not have, so it is not a small, well-bounded addition.

Verification (at 69d9877, clean tree)

  • pnpm content:lint: ✓ content lint passed (339 files, 44 glossary terms checked), exit 0
  • pnpm content:lint --published: same verdict, exit 0
  • pnpm svg:lint: OK line above, exit 0
  • pnpm check: Result (143 files): 0 errors, 0 warnings, 0 hints, exit 0
  • pnpm build: exit 0, with these verdicts:
    • 880 page(s) built
    • ✓ content lint --dist passed (478 built blog pages checked)
    • ✓ content lint --dist links passed (89567 internal links in 880 built pages resolve to built files)
  • pnpm seo:smoke: SEO smoke test passed (870 HTML pages checked), exit 0
  • Built alt attributes were read back from dist/ for all 20 pages.
  • All 27 changed SVGs ship byte-identical in dist/_astro/ (matched by hash).

Screenshots (looked at)

  • Native size, every changed SVG: before and after, on the light (#f3f7f2) and the dark (#101713) page: 108 renders. Each of the 14 SVGs whose pixels changed also got a native-scale crop of the changed area, plain and contrast-stretched ×6. In the before crop the band is visible without stretching, for example under the five vendor cards in enterprise-ontology-race and under the four layer cards in why-ai-agent-pilots. After, the shadows fade smoothly. All 27 render correctly on both page themes; every changed SVG has an opaque background.
  • Article pages, 1440×750 and 360×800, light and dark: the 22 article pages whose cover changed, plus the 5 with a changed body diagram. For the 14 visibly changed pages, a before shot as well: the page as built, with the image request answered by the stack file. The band shows at 1440. At 360 the step shrinks to at most 13 levels and is hard to see. No layout change anywhere.
  • Also looked at: the blog list at 1440 and 360, and /de/blog/crm-ai-understands-customers/ in dark mode.
  • Runtime ring, before (cee0607) and after (69d9877): both natural-language covers at native size on the light and the dark page. The blog-list card in /en/blog/ (1440 and 360, light and dark) and /zh-Hans/blog/ (1440 and 360). The article cover in en (1440 and 360, light and dark) and zh-Hans (1440 and 360). After, the ring sits inside the card everywhere, including the cropped list card at 360.

Acceptance notes

  • 13 SVGs change with no visible Chromium difference. Their deepest cut lies 1.9σ to 2.75σ out, where Chromium's blur happens to round to zero. They are fixed because the gate's bound is 3σ ("fully inside"), not today's Chromium threshold; the seat chose to keep the 3σ bound. To undo that later, lower SHADOW_REACH to 2 and revert those 13 one-line changes; the gate header records the calibration.
  • Stroke overruns are not gated. The prototype's untriaged hits are listed in section 4.
  • Not measured: engines other than Chromium.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr

claude added 30 commits October 5, 2026 12:39
… definition post (slot 2026-10-08)

Post 1 of the positioning wave: the executable business ontology defined
from the objectstack README and the Business Ontology concepts page,
quoted rather than paraphrased — the six-kind core mapped onto the
bundled CRM example, the four projections with a removal test to tell
core from projection, the three things it is not, where code goes, and
the four promises led by Executable. ObjectOS appears only as the
canonical T4 + E1. Eight locales (zh-Hant generated by gen:zh-hant), one
cover and two diagrams, in English text and in Chinese text for the
zh-Hans/zh-Hant pair.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
The post was written for the Thursday 2026-10-08 slot and dated for it,
but it was never merged that day, so the slot passed with the post still
unpublished. Under the publishing-cadence rules in AGENTS.md, a post's
`date` is its planned slot and merge time is publish time; the
forward-only rule protects the dates of posts that are already live,
and this one is not. The seat therefore moves it to the next free slot,
today, Thursday 2026-10-15, and merges it today.

Only the `date` frontmatter line changes, in the seven hand-maintained
locales (en, zh-Hans, ja, de, es, fr, ko), bare-date format kept and no
`updated` added; zh-Hant is regenerated from zh-Hans with
`pnpm gen:zh-hant`. No other file changes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
Certification, AI governance tiers, MCP setup, templates, Cloud data,
Business Self-Managed, and two locale overstatements, each re-sourced
from objectos and objectstack docs at origin/main:

1. Enterprise card: "SCIM + compliance (SOC 2 / ISO)" -> "SCIM, compliance
   attestations" (license.mdx:72-73; security.mdx:142 says ObjectOS itself
   is not certified).
2. AI page: governance claims written per edition (license.mdx:42-43):
   a taste on Free, in-app AI on Team, AI action audit / guardrails /
   permission scoping from Business. The "0 bypasses" tile, the Ask audit
   item, the "never how it is governed" item, the edition table and the
   two FAQs now say that.
3. MCP page: the npx/--url snippet (no bin, no such flag) is replaced by the
   documented `claude mcp add --transport http` command and the
   `{"type":"http","url":".../api/v1/mcp"}` client config
   (connect-mcp.mdx:26-32, 85-94).
4. Templates page: the real eight-template catalog (todo, contracts,
   procurement, compliance, helpdesk, content, hr, project;
   templates.mdx:19-28) replaces the six invented starters; the decision
   table now carries the documented install / fork / publish paths
   (templates.mdx:33-62). llms.txt follows the page description.
5. Trust center: the lead quotes E1 (Cloud / self-managed split), the
   "Self-hosted" tile becomes "2 editions", the data-residency and
   licence-check items follow index.mdx:63-77 and license.mdx:150-152,
   and the model-training FAQ says only what the Privacy Policy says
   (legal.ts:414-415).
6. Pricing: Business Self-Managed (single node, 10-25 AI seats,
   $540 / AI seat / yr, bring your own model; license.mdx:41, 64-69) is a
   labelled option under Business, the billing note no longer says
   self-managed is Enterprise-only, and the Business governance line names
   the AI governance row. Team/Business headline prices untouched (#214).
7. zh home tile "数据不出网" -> "数据驻留" (= en "Data residency"); ja
   security tile "0 既定の外部テレメトリ" -> the en "Agent inherits user
   identity" tile.

zh-Hans changed with en; zh-Hant for marketing pages and pricing derives
at build time (registry deepS2t / toHant), so no generated file changes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
Article tables on a phone no longer run each row onto one line. Cells
wrap, each column keeps a 6.5rem floor (three columns fit the 328px
column of a 360px phone), and overflow-wrap: anywhere keeps an
overflowing table at exactly its column count x 104px. The widest
English table goes from 2,035px to 328px at 360x800; the 12 tables with
four to six columns still scroll, to at most 625px, with the always-on
scrollbar and trailing-edge fade they already had.

Code blocks get the same two scroll affordances at every width, since
they overflow at desktop sizes too. The marketing decision table drops
its 760px floor on a phone and takes the same wrap, floor and
affordances.

Every scroll container is keyboard-reachable and named: post tables get
tabindex="0" and aria-labelledby their section heading (the article
title when no heading precedes them); code blocks keep Shiki's tabindex
and gain role="group" with a "Code (lang)" name; the decision-table
wrapper becomes a named, focusable group. group, not region, because a
region is a landmark and duplicate names fail axe landmark-unique.

Body images become links to their own asset: one tap opens the diagram
in the browser, where pinch zoom takes its smallest text past 11px. The
link's name is the image's alt text. No SVG changes; desktop geometry of
tables and images is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
The marketing artifact code block is a scroll container; it gets
tabindex="0" and role="group" named by its section heading, like the
other scroll containers, which clears the last axe
scrollable-region-focusable finding on the marketing pages.

The security page's data-residency table had the same 620px floor as the
decision table, which put its third column ("Leaves boundary?") wholly
off-screen on a phone. It takes the same phone block: cells wrap, a
6.5rem column floor, tighter padding, type size and tracking, and the
scrollbar and trailing-edge fade if it still overflows. Its wrapper is
focusable and named by the section heading. Desktop is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…e build

Six visible defects from the 2026-10-06 site audit, plus the gate that
would have caught the broken links.

- Home "Latest insights": the cover's height attribute became CSS
  height:500px and beat aspect-ratio, cutting each 16:9 cover to a 344x500
  sliver. height:auto, a 16:9 box (the covers' own ratio), contain for the
  odd 16:10 cover.
- Blog index: the feature card spanned three rows beside three cards
  (~1470px) with ~670px of content, leaving an ~800px band. It now takes
  its own row, cover beside text from 1024px, the other three below.
- 404: a top-level 404.html (Cloudflare Pages otherwise runs the site as
  an SPA and answers every missing URL 200 with the language redirect) and
  a localized one per locale at <locale>/404.html, moved there by a small
  build hook. noindexed, no canonical, out of the sitemap.
- Cluster pages linked topic hubs not built in de/es/fr/ja/ko. They now
  link the hub really built (this locale's, else English), and
  content-lint --dist fails on any internal link to a page the build did
  not produce.
- Footer links and the drawer's close label come from the i18n copy.
- Alignment: .item > span; align-content:start on capability cards and
  next-page links; no min-height floor on template/compare cards; pricing
  cards on a subgrid; footer brand nowrap; the phone rule now clears the
  odd capability items' border; non-breaking space before card arrows.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
The browser check of the German drawer showed a visible English "Theme"
button beside the localized menu, the same defect as the footer links and
the drawer's "Close" label: hard-coded English in the shared chrome. The
theme toggle's name and tooltip, the language switcher's "Language:"
prefix, the three nav landmark names and the home page's topic-cluster nav
name move into the i18n copy for all eight locales. English output is
byte-identical.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…e to 4.5:1, fix three near-misses

The 11 text lines the audit measured running past their cards, now inside
them with room left for wider system faces:

- self-hosted-runtime-en: the Runtime and AI Agent subtitles and the
  "Only approved minimum context is sent" line wrap to two lines (the
  Runtime line no longer runs over the green connector).
- ai-wrote-your-app-dare-to-merge cover (en and zh): the two captions beside
  the Merge pills wrap to two lines centred on the pill; "+8,142 -0" is
  right-aligned 22px inside its panel in both locales.
- ai-ontology-open-protocol cover-en: the two dark pills widen to 264px and
  the lock moves to the panel's lower right.
- tool-call-lifecycle-en: card 1 rewrapped ("change as Zhang, not as / an
  anonymous superuser").
- natural-language-to-app-metadata cover (en and zh): the spec card widens
  to 350px with 282px pills and sits 14px lower, so it no longer clips the
  subtitle's descenders; the arrows shorten to keep their gaps.
- one-definition-four-surfaces-en: the audit line is reworded to fit.

Contrast: defect-layers-en .none goes from #8a9c95 (2.7:1) to the house
.soft #5d736b.

Near-misses: review-gate-doors-en gives the gap card 12px more height and
the 29/16 numerals 6px more air; self-hosted cover paints the ObjectOS label
after the connectors (both locales), as the runtime diagram already does;
review-loop centres its card row (40px each side, both locales) so card 4's
shadow is no longer clipped.

Also objectos-automation-engine cover-en: the run's meta line ran 40px
under the "selected" tile, hiding "runAs: user"; it drops the "duration:"
label to clear the tile.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…their alt text

Four English and five zh-Hans posts used AI-illustration webp images whose
alt text was only "Concept diagram" / 「概念示意图」. Each alt now says what
the picture shows (customer hub, read-only connection into a governed object
layer, tangled vs. modelled architecture, factory systems feeding an object
layer, legacy stack to structured model). The images are unchanged.
zh-Hant regenerated with pnpm gen:zh-hant.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…ed over it

scripts/svg-text-fit.mjs measures every <text> in content/blog's SVGs and
fails when its box runs more than 2px past the filled shape under its centre,
or when a filled shape painted after it covers more than 2px of it both ways.
The #193 sweep only caught overflow that also dropped contrast, and a
tightest-rectangle measure sees an overflowing line's whole background as
its container; this gate measures each line against the shape it sits on.

It runs without a browser, since CI installs none: text is laid out with
Arial advance widths (scripts/lib/svg-text-metrics.mjs, generated from the
metric-compatible Liberation faces by scripts/gen-svg-text-metrics.mjs) and
the browser's text-box rules. Against headless Chromium on the same files
it agreed to within Chromium's whole-pixel rounding and found the same
crossings. A deliberate crossing goes in the documented allowlist; a stale
entry fails the gate.

Wired into `pnpm build` after content-lint --published (so CI's Build step
runs it) and exposed as `pnpm svg:lint`.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
Measured with DejaVu Sans (the default sans-serif on Debian/Ubuntu
desktops, about 15% wider than Arial), two of the refitted lines still ran
past their shapes: the spec pills on the natural-language cover widen to
290px (both locales), and tool-call-lifecycle card 1 keeps its original
words, rewrapped as 'change as "Zhang"—not / an anonymous superuser'.

The gate's header states its Chromium agreement as "2px", not "2.0px".

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…E1 attribution across 18 posts

Post-side update sweep for the 2026-10-03 positioning (work-order item 3):

- CTA: the stale `npm i -g @objectstack/cli && os start` block in 15 en
  posts and their authored locales becomes the objectstack README path —
  `npm create objectstack@latest my-app && cd my-app`, point a coding
  agent at it, `npx os dev --ui`. `how-to-self-host-objectstack` keeps
  `os start` (the production server), as does the mid-article operator
  line in `enterprise-ontology-race-open-vs-closed`.
- Descriptor: "the commercial production platform" / 「生产平台」 and the
  ja/de/es/fr/ko equivalents in ai-ontology-open-protocol,
  beyond-agentforce-copilot-open-runtime,
  enterprise-ontology-race-open-vs-closed, forward-deployed-engineer-tools
  and self-hosted-ai-app-platform become positioning[locale].descriptor
  verbatim ("the commercial runtime environment built on ObjectStack").
- Attribution: extend-existing-systems-with-ai scopes "ObjectOS runs in
  your environment" to self-managed (ObjectOS Enterprise) or the open
  runtime per E1 and objectos license.mdx (Cloud operates the model);
  objectos-action-tools attributes the Action mechanism (opt-in exposure,
  confirmation, permissions, audit) to the open ObjectStack runtime and
  positions ObjectOS with the descriptor. objectos-agent-permission-
  boundaries already attributed everything to ObjectStack; untouched.
- `updated: 2026-10-06T10:00:00+08:00` on every corrected file; no `date`
  touched, no slug changed. zh-Hant regenerated with gen:zh-hant.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…ullet matches the FAQ

The bullet ended "Business data and prompts never leave your perimeter",
which is false once an external AI provider is configured — the FAQ
answer in the same post already says so. It now states the same claim
as the FAQ: business data stays in your database; prompts go only to the
AI provider you configure (on ObjectOS Cloud the model is operated for
you); permissions decide what the model sees. en and zh-Hans carried the
false sentence; ja, de, es, fr and ko had no sentence about prompts and
now carry the corrected claim too. zh-Hant regenerated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…re menus, tap targets

Contrast. --muted is #626c66 light (5.43:1 on --surface) and #8a968d dark
(5.42:1); the light accent moves to #3d58f0 (4.67:1 on --surface-sunk);
the focus ring is the solid accent (5.44:1 light, 7.06:1 dark) instead of
a 28% tint (1.5:1). The home, evidence, security, glossary and cluster
accents get dark-theme overrides (green #3fbf8f, blue #8fa3ff, amber
#e3a54a, red #f08a8a, all >= 6.9:1 on the dark surface) and the light
amber darkens to #93600f (5.34:1). The status pill reads theme tokens
(published 5.03:1 light, 6.19:1 dark on its tint), the pricing Team CTA
is --surface on --accent (7.06:1 dark, was 2.36:1), and code blocks use
Shiki's github-dark-default (comments 6.15:1, were 3.04:1).

Keyboard. The mobile drawer moves focus to its close button, makes the
page behind it inert, keeps Tab inside it and returns focus to the menu
button on close. The header dropdowns drop role=menu/menuitem for the
disclosure pattern (aria-controls, plain links, Escape returns focus,
focus leaving a menu closes it).

Tap targets. The hamburger, drawer close, header CTA and wordmark are
44px tall below 900px; footer links 24px (44px on a phone); the glossary
and post back links 44px without moving the text; card title links cover
their cards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…posts

`[ObjectOS](https://www.objectos.ai)` in business-app-in-16k-tokens (1 per
locale) and forward-deployed-engineer-tools (2 per locale) pointed at the
root language-redirect page, so every click took a meta-refresh hop before
landing. Each locale now links its own home (`/en/`, `/de/`, ...); zh-Hant
regenerated with gen:zh-hant (it rewrote `/zh-Hans/` to `/zh-Hant/`). 24
links in 16 files; no `date` or `updated` touched — a link target is not a
substantive revision.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…hubs, OG

The site-wide audit on #219 measured, on a build of this tree's base:

- Sitemap: 190 noindexed fallback pages listed. The sitemap filter now
  reads each built page and lists it only when it is indexable and its own
  canonical (astro.config.mjs) — one rule for every fallback producer
  instead of a second copy of each registry.
- Pricing/privacy/terms/refunds in ja/de/es/fr/ko rendered the English
  copy, self-canonical and in every locale's hreflang (20 pages); security
  in those five locales inherits five English sections. They now follow
  the marketing fallback rule through src/lib/fallback.ts: noindex,
  canonical to /en/, hreflang only for authored locales, the switcher
  still offering every locale. Authored-ness is derived, not listed: a
  copy table entry that is the English object is a fallback, and a
  security locale that inherits any English field is one.
- Base takes `contentLocale`: a fallback body gets `<main lang="en">`
  (marketing, glossary, pricing, legal; 210 pages had English under
  lang="de"/"ja"/...), og:locale follows it. The security page marks each
  inherited English part `lang="en"`; glossary fallbacks mark the
  reader-locale article links with their locale.
- The 35 non-English cluster pages are one template per locale: noindex,
  canonical to the English cluster, out of hreflang and llms.txt.
- Pillar pages: every footer links the five clusters (FooterClusters),
  and each post a cluster lists as its reading path links back to it.
- Topic hubs listing fewer than 3 posts (51) are noindexed and out of
  hreflang; every hub gets BreadcrumbList and ItemList JSON-LD and a
  "<topic> · Articles · ObjectOS" title.
- Root `/`: absolute hreflang, canonical /en/, no noindex beside the
  meta refresh. SecurityPage links its post with the trailing slash.
- public/_headers caches the hashed /_astro/* files as immutable.
- og:locale uses en_US/zh_CN/zh_TW/...; og:image:alt everywhere and
  width/height when known; Organization gets @id and sameAs (the GitHub
  organization only); SoftwareApplication gets offers from the pricing
  page's own dollar plans.
- Titles: marketing titles that already name ObjectOS no longer append
  " · ObjectOS" (27 indexable doubles); the blog index takes the home
  page's heading for it instead of "Articles · ObjectOS".

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
… lint warns at 60/160

seo:smoke reads every built page once and asserts invariants over the
whole build instead of per-registry URL lists, so a new fallback producer
that gets the rule wrong fails without this script learning about it:

1. every sitemap URL is a built, indexable, self-canonical page;
2. every canonical and hreflang target is indexable and self-canonical,
   and a page that canonicalizes elsewhere is noindexed;
3. every block of prose in <main> is written in the language its nearest
   `lang` declares (scripts/lib/text-lang.mjs: script for CJK, distinctive
   stopwords for en/de/es/fr; too little text is "cannot tell", never a
   pass), with a per-locale control so it cannot measure nothing;
4. no internal link, canonical, og:url or hreflang redirects (missing
   trailing slash, `/`, `.../index.html`, a `_redirects` source).

Plus the root page (canonical /en/, absolute hreflang, no noindex beside
the refresh), `_headers` (rule present, every /_astro/ name hashed),
og:locale values, og:image:alt, the Organization @id/sameAs, hub
BreadcrumbList/ItemList, and the clusters: English in llms.txt and the
sitemap, the templated locales noindexed and canonical to English.

On the base build this reports 190 noindexed sitemap URLs, 215 pages
whose <main> is in another language than declared (25 indexable), and 9
redirecting link targets on 24 pages; on this branch's build it passes.

content-lint: a title wider than 60 columns or a description wider than
160 (CJK counted double) is a warning. Posts first published before the
check are counted in one summary line; `--snippets` lists them all.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…uled

The home page's SoftwareApplication listed Offers derived from the pricing
page ($20/$45 per AI seat billed annually). That price is under a pending
maintainer decision (www#214: $20/$45 annual on www vs $24/$54 monthly on
docs), so structured data must not assert either until it is ruled. The
rest of the item-10 changes stay: the publisher still references the
Organization @id.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…ith srcset

The post header cover is the LCP element on a phone, and 80 posts used a
1.5-1.9 MB PNG for it (ai-agent-workbench: 9.96 s LCP at 390x844, 4x CPU,
slow 4G). Card thumbnails on the blog index, topic hubs and the home page
downloaded the same full-size file, about 18.8 MB for one scroll of
/en/blog/.

A PNG or JPG cover now goes through <Image> as WebP with a width list and
`sizes` per slot:

- post header: 480-1600w, `(min-width: 908px) 860px, calc(100vw - 2rem)`;
  still eager with fetchpriority=high, since it is the LCP element;
- ArticleCard: 360-1280w, sized for the three-column grid, the two-column
  tablet grid and the phone column; the feature card's cover is eager, as it
  opens the blog index above the fold;
- the home page's latest-posts grid: 360-1080w.

The ai-agent-workbench cover at 1280w is 58 KB (the original is 1.62 MB);
a 1:1 crop of the 1280w WebP against the original resized to 1280 shows no
visible loss (PSNR 39 dB). SVG covers are a few KB and scale themselves, so
they keep a plain <img> on the original.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
209 posts (41 English) named an SVG as og:image, twitter:image and
BlogPosting.image, and X, LinkedIn, Facebook and Slack draw no card for an
SVG; the PNG-cover posts named a 1.5-1.9 MB 1672x941 file.

`scripts/og-images.mjs` runs in `pnpm build` after `astro build` and
renders one 1200x630 JPEG per cover into dist/og/blog/<post>/. The URL rule
lives in scripts/lib/og-image.mjs, shared with BlogPost.astro, which now
names that file in og:image, twitter:image and BlogPosting.image, with
og:image:width/height 1200x630 and the post title as og:image:alt. The URL
carries a hash of the cover's bytes, so an edited cover gets a new URL the
networks re-fetch. The step is driven from the built HTML and fails the
build when a page names a share image no cover maps to, or declares a size
that is not the file's.

SVG text: sharp draws it through librsvg/Pango/fontconfig, which use the
machine's fonts. The covers ask for -apple-system, Segoe UI, Inter, Arial
and Menlo, and CI's ubuntu-latest has no CJK font, so the Chinese covers
would have shipped as rows of boxes. The rasterizer now sees only bundled
OFL faces (scripts/lib/og-fonts.mjs): Arimo and Cousine (the Arial and
Courier New metrics `pnpm svg:lint` measures with), Noto Sans SC, Noto Sans
Symbols 2, Noto Sans Math and Noto Emoji. Fontsource ships them as WOFF2,
which the FreeType in sharp's libvips cannot read (every glyph drew as a
box), so wawoff2 decodes them to TrueType in a temp dir that a generated
fonts.conf points at. Before rendering, every character in each SVG cover's
text is checked against the faces' cmaps; one that no face has fails the
build with the cover, code point and line. On this tree that check first
found U+21B6 and U+2298, which is why Noto Sans Math is in the set.

An SVG cover is drawn whole at 1120x630 and widened by repeating its edge
columns (two covers put a caption in the bottom 23px a centre crop would
cut; no cover draws in its outer 40px). A PNG/JPG cover is an illustration
without text and is centre-cropped. Rendering SVG text on several libvips
threads in one process crashed (SIGSEGV or "SVG rendering failed" in 4 of 6
runs), so SVGs are rendered in three worker processes, one at a time each.

Result: 87 images, 5.2 MB, 12 s of the build; zh-Hans, zh-Hant, ja and ko
rasters looked at, no missing glyph.

New devDependencies, all build-time only: wawoff2 (MIT) and six Fontsource
packages (OFL-1.1). sharp was already a dependency.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…, dim it in dark mode

The 1,732,355-byte hero PNG was loaded eagerly by the 120 marketing
evidence pages, where it renders in a 415x340 box, and lazily by the home
page; it was also the default og:image of every page without its own.

- src/assets/objectos-home-hero.png (moved from public/images/) goes
  through <Image> as WebP with srcset (400-1586w). The box crops the 1.6:1
  image with object-fit: cover, so `sizes` asks for the drawn width, not the
  box's: 545px on a desktop, about 1.2x the box on a phone.
- Marketing evidence pages: measured at all six AGENTS.md viewports, the
  hero's top is at 172px on a desktop (the LCP element there) and at
  713-781px on an 800-844px phone, so it is in the first screen everywhere
  and stays eager, now with fetchpriority=high. Home: it sits under the
  hero and the video at every viewport, so it stays lazy.
- Dark theme: the white render was a glaring block on the dark page. There
  is no dark variant of the render, so the image itself is dimmed
  (brightness 0.6, contrast 1.12; three strengths compared side by side in
  the hero panel). No layout CSS changes.
- Base.astro: the default share image was the 1586x992 PNG by its public
  path, which the move removes. It is now getImage() of the same asset, a
  1200x630 JPEG (94 KB). og-images.mjs now checks every og:image that is a
  file of this site against the size its page declares, the default
  included, and fails on one that is not in dist/.

The hero component code no longer reads the import's width, so Astro drops
the 1.7 MB original from dist/ (nothing references it).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…m at

A standard card's 16:10 thumb crops the 16:9 cover with object-fit: cover,
which draws the image 1.11x the card's width: a 343px card at 1366 needs
381px of image, and `sizes` said 360px, so the 360w candidate was
stretched and visibly soft next to the original. `sizes` now names the
drawn width (400px on a desktop, 55vw - 2rem on the two-column tablet
grid; the phone's 2:1 thumb is drawn at the card's width) and the list
starts at 400w.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
The reading-path cards on the 40 cluster pages still used the original
cover: /en/ai-native-app-platform/ loaded four original JPGs (240-370 KB
each) into 180px-wide slots. A raster cover now goes through Image as
WebP, 400-1080w. `sizes` is the drawn width, the same way as the
ArticleCard thumbs: above 640px the image is a 180px (150px at 641-900px)
column as tall as the card's text, measured 190-227px, and object-fit:
cover fills it by height, so a 1.6:1 or 16:9 cover is drawn 304-364px
wide; `sizes` says 400px. On a phone the card stacks and the 16:9 image
is drawn at the column width. SVG covers keep the plain img.

Only the frontmatter (the import and the width list replacing coverSrc)
and the card markup change; no CSS.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…ns to hold theirs

A filter paints nothing outside its region, and 27 blog SVGs set regions
(x/y -20%, width 140%, height 150%) too small for the shadow they cast:
81 of 181 filtered elements had part of their feDropShadow on the canvas
but outside the region, 35 of them within 1.5σ of the shadow's edge.
The worst left a hard horizontal edge, a 15-of-255-level step, across
the shadow just under its card.

svg-text-fit gains rule 3: for every filtered element it takes the object
bounding box, the painted box, and the shadow's offset and blur, and fails
unless the shadow's 3σ reach lies inside the filter region wherever it
lands on the canvas. Anything but a single feDropShadow fails as
unmeasurable. Red on stack/www-wave1 (81 cut shadows in 27 SVGs), green
after.

Each cut filter's region now holds the full 3σ reach of every element
that uses it, rounded out to 10% and only ever grown. Headless Chromium
renders every filtered SVG identically to the same SVG with unbounded
regions; 14 SVGs change pixels, all outside the old regions.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
… de, es, fr, ja and ko

The de, es, fr, ja and ko versions of crm-ai-understands-customers,
extend-existing-systems-with-ai, low-code-vs-ai-native-app-platform and
manufacturing-legacy-systems-ai still gave their AI illustrations the
placeholder alt "Konzeptdiagramm" / "Diagrama conceptual" / "Schéma
conceptuel" / 「概念図」 / "개념 다이어그램". Each now carries the
descriptive alt the English and zh-Hans posts got, translated, in the
post's own terms (AI, governte / gobernada / gouvernée objects). The
images are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…card

In natural-language-to-app-metadata's cover-en.svg and cover.svg, the
Runtime card's gold cycle ring (radius 90, stroke 5, centre 172px down
the card) reached 264.5px down a 248px card: it ran 16.5px past the
card's bottom edge onto the page background. The card is now 290px
tall, which centres the ring in the space under the card title with
about 26px clear above and below. The ring, the title, the two lines
inside the ring and the card's top edge do not move.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants