diff --git a/.dockerignore b/.dockerignore index 7da7097..557f4ea 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,4 +6,5 @@ **/*.pyc **/*.tsbuildinfo .artifacts +apps/docs/public/figures/**/*.webp .env* diff --git a/.gitignore b/.gitignore index 53e450f..785350d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ __pycache__/ !.env.example .artifacts/ + +# Browser raster derivatives are generated on the confirmed remote runner. +/apps/docs/public/figures/**/*.webp diff --git a/apps/docs/package.json b/apps/docs/package.json index 0b11c46..a8e7658 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -3,8 +3,10 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "NEXT_TELEMETRY_DISABLED=1 next dev --hostname 127.0.0.1", - "build": "NEXT_TELEMETRY_DISABLED=1 next build && node scripts/build-auth.mjs", + "dev": "node scripts/build-doc-images.mjs && NEXT_TELEMETRY_DISABLED=1 next dev --hostname 127.0.0.1", + "build": "node scripts/build-doc-images.mjs && NEXT_TELEMETRY_DISABLED=1 next build && node scripts/build-auth.mjs", + "images": "node scripts/build-doc-images.mjs", + "test:images": "node scripts/build-doc-images.mjs --check", "start": "NEXT_TELEMETRY_DISABLED=1 next start --hostname 127.0.0.1", "lint": "eslint . --max-warnings=0", "typecheck": "NEXT_TELEMETRY_DISABLED=1 next typegen && tsc --noEmit", @@ -29,6 +31,7 @@ "eslint": "^9.39.5", "eslint-config-next": "16.4.3", "postcss": "8.5.31", + "sharp": "0.35.5", "tailwindcss": "4.3.3", "typescript": "~5.9.3" } diff --git a/apps/docs/scripts/auth-cli.ts b/apps/docs/scripts/auth-cli.ts index c4c704b..de914bd 100644 --- a/apps/docs/scripts/auth-cli.ts +++ b/apps/docs/scripts/auth-cli.ts @@ -141,6 +141,8 @@ async function main() { "/", "/docs/00-endor-concepts-draft", "/figures/endor-architecture.svg", + "/figures/endor-review-gate.webp", + "/figures/endor-review-desk.webp", "/docs/pretext/01-chapter", "/materials/pretext/verify.cjs", "/materials/pretext/results.chrome156.json", @@ -184,13 +186,41 @@ async function main() { const searchIndex = await searchResponse.json(); assert.ok(searchIndex.some((doc: { slug: string; searchText: string }) => doc.slug === "pretext/04-chapter" && doc.searchText.includes("getSegmentMetrics"))); let home = ""; + let purpose = ""; for (const route of routes) { const response = await request(route, { headers }); assert.equal(response.status, 200, route); if (route === "/") home = await response.text(); + if (route === "/docs/04-endor-purpose-draft") purpose = await response.text(); } assert.ok(home.includes("资料库")); assert.ok(home.includes("/docs/pretext/00-overview")); + // Inline preview and zoom dialog must both use the compressed browser asset. + assert.equal( + [...purpose.matchAll(/]*\bsrc="\/figures\/endor-review-gate\.webp"/g)].length, + 2, + "The comic preview and lightbox must use WebP", + ); + assert.ok(purpose.includes('aria-label="上一张图片"')); + assert.ok(purpose.includes('aria-label="下一张图片"')); + assert.ok(purpose.includes('aria-label="显示第 1 张图片" aria-current="true"')); + assert.ok(purpose.includes('aria-label="显示第 2 张图片"')); + assert.equal( + [...purpose.matchAll(/]*\bsrc="\/figures\/endor-review-desk\.webp"/g)].length, + 0, + "The second slide should not download until selected", + ); + for (const name of ["endor-review-gate", "endor-review-desk"]) { + assert.ok(!purpose.includes(`/figures/${name}.png`)); + const comic = await request(`/figures/${name}.webp`, { headers }); + assert.equal(comic.status, 200); + assert.match(comic.headers.get("content-type") || "", /image\/webp/); + assert.deepEqual( + Buffer.from(await comic.arrayBuffer()), + readFileSync(join(__dirname, `public/figures/${name}.webp`)), + ); + assert.equal((await request(`/figures/${name}.png`, { headers })).status, 404); + } const assets = new Set( [...home.matchAll(/(?:src|href)="(\/_next\/static\/[^" ]+)"/g)].map((m) => m[1].replaceAll("&", "&"), diff --git a/apps/docs/scripts/build-doc-images.mjs b/apps/docs/scripts/build-doc-images.mjs new file mode 100644 index 0000000..8989bf7 --- /dev/null +++ b/apps/docs/scripts/build-doc-images.mjs @@ -0,0 +1,62 @@ +import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import sharp from "sharp"; + +const root = fileURLToPath(new URL("../../../", import.meta.url)); +const sources = path.join(root, "tools/figures/assets"); +const output = path.join(root, "apps/docs/public/figures"); +const check = process.argv.includes("--check"); +const policy = JSON.parse(await readFile(path.join(root, "content/image-policy.json"), "utf8")); + +async function files(directory) { + const entries = await readdir(directory, { withFileTypes: true }); + return (await Promise.all(entries.map((entry) => { + const name = path.join(directory, entry.name); + return entry.isDirectory() ? files(name) : [name]; + }))).flat().sort(); +} + +// Originals stay outside public; both inline and lightbox images use the WebP. +for (const name of await files(path.join(root, "apps/docs/public"))) { + if (/\.(?:png|jpe?g|tiff?|bmp)$/i.test(name)) { + throw new Error(`Original raster exposed in public: ${path.relative(root, name)}. Move it to tools/figures/assets and run images.`); + } +} +for (const name of await files(path.join(root, "content/docs"))) { + if (!name.endsWith(".md")) continue; + const content = await readFile(name, "utf8"); + const images = [ + ...content.matchAll(/!\[[^\]]*\]\(([^\s)>]+)/g), + ...content.matchAll(/]*\bsrc=["']([^"']+)/gi), + ]; + for (const [, source] of images) { + if (/\.(?:png|jpe?g|tiff?|bmp)(?:[?#]|$)/i.test(source)) { + throw new Error(`${path.relative(root, name)} embeds an original raster: ${source}. Use the compressed WebP.`); + } + } +} + +await mkdir(output, { recursive: true }); +for (const source of await files(sources)) { + if (!/\.(?:png|jpe?g|tiff?|bmp)$/i.test(source)) continue; + const relative = path.relative(sources, source).replace(/\.[^.]+$/, ".webp"); + const destination = path.join(output, relative); + const original = await readFile(source); + const encoded = await sharp(original) + .rotate() + .resize({ width: policy.maxWidth, withoutEnlargement: true }) + .webp({ quality: policy.quality, effort: 6 }) + .toBuffer(); + if (encoded.length > policy.maxBytes) { + throw new Error(`${relative}: ${encoded.length} bytes exceeds the ${policy.maxBytes}-byte image budget.`); + } + if (check) { + const generated = await readFile(destination); + if (!encoded.equals(generated)) throw new Error(`${relative} is stale. Regenerate browser images on the confirmed remote runner.`); + } else { + await mkdir(path.dirname(destination), { recursive: true }); + await writeFile(destination, encoded); + } + console.log(`${check ? "Verified" : "Generated"} ${relative}: ${original.length} → ${encoded.length} bytes (${(100 * (1 - encoded.length / original.length)).toFixed(1)}% smaller)`); +} diff --git a/apps/docs/src/components/FigureGallery.tsx b/apps/docs/src/components/FigureGallery.tsx new file mode 100644 index 0000000..6fad60a --- /dev/null +++ b/apps/docs/src/components/FigureGallery.tsx @@ -0,0 +1,66 @@ +import { useId, useState } from "react"; +import { ArchitectureFigure } from "./ArchitectureFigure"; + +type Slide = { source: string; alt: string }; + +export function FigureGallery({ slides }: { slides: Slide[] }) { + const [active, setActive] = useState(0); + const viewport = useId(); + const slide = slides[active]; + if (!slide) return null; + + function move(offset: number) { + setActive((current) => (current + offset + slides.length) % slides.length); + } + + return ( + { + if ((event.target as HTMLElement).closest("dialog")) return; + if (event.key === "ArrowLeft" || event.key === "ArrowRight") { + event.preventDefault(); + move(event.key === "ArrowLeft" ? -1 : 1); + } + }} + > + + + + + move(-1)}> + ‹ + + + {slides.map((item, index) => ( + setActive(index)} + > + + + ))} + + move(1)}> + › + + + + 第 {active + 1} 张,共 {slides.length} 张:{slide.alt} + + + ); +} diff --git a/apps/docs/src/pages/docs/[...slug].tsx b/apps/docs/src/pages/docs/[...slug].tsx index c92dd88..b4f7207 100644 --- a/apps/docs/src/pages/docs/[...slug].tsx +++ b/apps/docs/src/pages/docs/[...slug].tsx @@ -17,6 +17,7 @@ import remarkGfm from "remark-gfm"; import { listDocs, readDoc, type Doc, type DocLink } from "@/lib/docs"; import { Shell, Navigation } from "@/components/Shell"; import { ArchitectureFigure } from "@/components/ArchitectureFigure"; +import { FigureGallery } from "@/components/FigureGallery"; import { CodeBlock } from "@/components/CodeBlock"; import { collectionEntries, collectionLabel } from "@/lib/collections"; import { Icon } from "@/components/Icon"; @@ -102,15 +103,30 @@ export default function DocPage({ - node?.children.some( + p: ({ children, node }) => { + const slides = node?.children.flatMap((child) => + child.type === "element" && child.tagName === "img" && + typeof child.properties.src === "string" && + child.properties.src.startsWith("/figures/endor-") + ? [{ source: child.properties.src, alt: String(child.properties.alt ?? "") }] + : [], + ) ?? []; + const onlyImages = node?.children.every((child) => + (child.type === "element" && child.tagName === "img" && + typeof child.properties.src === "string" && + child.properties.src.startsWith("/figures/endor-")) || + (child.type === "text" && child.value.trim() === ""), + ); + if (slides.length > 1 && onlyImages) return ; + return node?.children.some( (child) => child.type === "element" && child.tagName === "img", ) ? ( {children} ) : ( {children} - ), + ); + }, img: ({ src, alt }) => typeof src === "string" && src.startsWith("/figures/endor-") ? ( diff --git a/apps/docs/src/styles/globals.css b/apps/docs/src/styles/globals.css index 8c7971f..7f165ba 100644 --- a/apps/docs/src/styles/globals.css +++ b/apps/docs/src/styles/globals.css @@ -882,6 +882,57 @@ button { border-radius: 0; background: transparent; } +.figure-gallery { + margin: 28px 0; +} +.figure-gallery .architecture-preview { + margin: 0; +} +.figure-gallery-controls, +.figure-gallery-dots { + display: flex; + align-items: center; + justify-content: center; +} +.figure-gallery-controls { + gap: 20px; + margin-top: 12px; +} +.figure-gallery-arrow, +.figure-gallery-dot { + display: grid; + place-items: center; + min-width: 36px; + min-height: 36px; + padding: 0; + border: 0; + border-radius: 50%; + color: #57534e; + background: transparent; + cursor: pointer; +} +.figure-gallery-arrow { + font-size: 28px; + line-height: 1; +} +.figure-gallery-dot span { + width: 7px; + height: 7px; + border-radius: 50%; + background: #d6d3d1; +} +.figure-gallery-dot[aria-current="true"] span { + background: #2563eb; +} +.figure-gallery-arrow:hover, +.figure-gallery-dot:hover { + background: #f5f5f4; +} +.figure-gallery-arrow:focus-visible, +.figure-gallery-dot:focus-visible { + outline: 2px solid #2563eb; + outline-offset: 2px; +} .image-lightbox { width: 100vw; height: 100dvh; diff --git a/content/docs/03-figures.md b/content/docs/03-figures.md index fccbd00..29739ee 100644 --- a/content/docs/03-figures.md +++ b/content/docs/03-figures.md @@ -16,4 +16,27 @@ pnpm figures CI 会重新生成图片并检查与提交版本一致。Python 依赖保留独立缓存,不进入生产运行镜像。 -图形应使用本地字体与资源,并明确标注草案状态。未来可按具体需求加入自动布局、PNG 转换或 TypeScript SVG 工具。 +图形应使用本地字体与资源,并明确标注草案状态。未来可按具体需求加入自动布局或 TypeScript SVG 工具。 + +## 位图默认发布压缩版本 + +SVG 保留可编辑矢量格式;漫画、照片等位图的原始 PNG、JPEG 放在 `tools/figures/assets`,不放进 `public`。浏览器只使用生成的 WebP,正文、点击放大和图片适应屏幕都使用同一个压缩地址。 + +共享策略位于 `content/image-policy.json`:默认最大宽度 1600 像素、WebP 质量 82、每张图最多 192 KiB,不放大小图。修改策略后重新生成并检查公式、细线和小字是否清晰。 + +以下涉及图像编码与构建产物生成的命令在已确认的远程 CI/CD runner 上执行;未获明确许可时不在用户本机运行。 + +```sh +pnpm --filter @endor/docs images +pnpm --filter @endor/docs test:images +``` + +原图相对 `tools/figures/assets` 的路径会映射到 `apps/docs/public/figures` 下同名的 `.webp`。例如 `endor-review-gate.png` 对应 `/figures/endor-review-gate.webp`。文档只嵌入后一个地址,制作原稿不随网页发布。 + +远程生产构建会运行压缩脚本;CI 先生成 WebP,再重新编码核对产物与原稿、策略是否一致。WebP 是构建派生文件,不必在本机生成或提交;Git 保存原稿、制作记录与共享策略。检查同时拒绝公开目录里的原始位图及 Markdown 图片或 HTML img 直接引用的原始 PNG、JPEG。SVG 无需转成位图,仍按前述流程提交可编辑输出。 + +## 同一位置切换多张配图 + +把两张或更多 `/figures/endor-` 图片写在同一段,中间换行但不插入空行,正文会将它们呈现为图组。左右按钮和键盘左右方向键可以切换,下方圆点表示当前图片,也可点击直接选择。图片保留点击放大能力。 + +图组一次只挂载当前图片,切换后才加载下一张压缩图,不预先下载整组原图。 diff --git a/content/docs/04-endor-purpose-draft.md b/content/docs/04-endor-purpose-draft.md index a35d3a1..772f695 100644 --- a/content/docs/04-endor-purpose-draft.md +++ b/content/docs/04-endor-purpose-draft.md @@ -16,6 +16,54 @@ 这一定位希望形成的体验是:**对象池构成持续可用的工作世界,Agent 成为其中执行工作的主体,Endor 语言让对象和行为能够被表达、检查与组合。** 用户与 AI 因而可以在同一套对象基础上发起、接续和检查工作。这是本文的设计方向,不表示所有系统能力已经实现。 +## 让产出成为检查下一次产出的帮手 + + + + +两张漫画用检查门与检查台表达同一个目标,可用左右按钮或下方圆点切换。 + +上方把 Coding Agent 比作一台不断吐出工作结果的机器。机器可以持续产出,但人的时间和精力没有同步增长;等待检查的材料越积越多。虚线圈出的 **∫dW** 表示累积的待处理工作,门上的 **C₀** 表示在这个对比中保持不变的检查能力。通过门、带上勾的文件才表示已处理的结果。 + +下方的机器吐出同样的原始文件,改变的是 Endor 如何组织和接续这些产出。人和检查门直接面对的是这次新增的 **dW**;已经处理的文件被组织成有类型、来源、关系与可调用能力的对象,成为图里的蓝色帮手。帮手的箭头指向检查门,表达它们为后续检查提供支持,而不是让新生成的结果自己证明自己正确。 + +例如,已确认的需求可以成为验收条件;稳定的接口约定可以帮助发现不兼容修改;图片发布规则可以检查下一份文档有没有直接嵌入原图。**过去的产出从待处理的负担,逐步变成帮助人处理新工作的能力。** 新结果是否符合要求,仍需要适用的检查逻辑与人的判断。 + +门上的 **C₀ + k∫dW** 表示我们希望实现的积累效应:在原有能力上,增加由历史成果转化而来的可复用检查能力。这里的积分、微分和系数 k 是设计隐喻,不是性能公式或实测结论;只有经过确认、适合复用并保持有效的成果才能贡献能力,产出数量不等于正确性,实际检查也可能需要重新覆盖受影响的历史对象。 + +> **设计目标**:这张漫画表达希望建立的工作组织方式,不表示当前 Endor 已能自动把任意文件变成检查工具。要兑现它,需要保存来源和依赖、表达检查规则、执行检查,并把失败与待确认事项呈现给人。 + +## 从压缩图片想到的继承:让要求成为共同能力 + +这张漫画也带来一个具体要求:**文档在浏览器中默认使用压缩图,原图留作制作与归档,不因打开正文或放大图片就下载原图。** 如果很多文档都需要这条规则,用户不应该每次写文档时重新提醒一次。 + +单次 Coding Agent 对话中的“记得压缩图片”,通常首先是一项需要读取、理解和落实的指令。指令可以放进项目约定、技能或模板,也可以让 Agent 写出共享压缩脚本;这些都能减少重复劳动。但一句自然语言要求本身,并没有自动建立“这类文档如何派生、谁沿用默认行为、哪个文档允许覆盖”的类型与行为关系。这里讨论的是这种**可持续、可检查的继承**,不是说 Agent 不能编写使用继承的程序。 + +### 为什么 Endor 的设计能承载继承 + +在现有设计中,文档、图片、类型定义、函数和 Agent 都进入同一对象体系。对象具有可引用的身份,类型可以表达对象的结构与能力,函数可以表达处理行为;继承、实例化与代码依赖可以保存为有不同含义的关联。因此,“默认如何发布文档图片”可以被表达为一个可引用的共同定义,以及读取和执行它的行为,而不只是一段随对话流逝的提醒。 + +沿着这个设计方向,可以把图片发布要求放到共享文档类型或其呈现策略上:普通文档沿用“优先压缩图”的默认行为,需要精确查看像素的专业文档再显式采用另一种策略。具体通过类型继承、策略对象委托还是能力组合实现,需要结合语言规则确定;不是因为对象之间有一个链接,就天然获得了继承。 + +| 层次 | 压缩图例子中承担的职责 | +| --- | --- | +| 共同定义 | 保存默认图片尺寸、编码质量、字节预算及浏览器应使用的派生版本 | +| 继承或委托关系 | 让采用共同定义的文档持续引用它,明确哪些行为来自共同来源 | +| 实际处理能力 | 根据策略生成压缩版本,保留它与原图的来源关系,并让呈现入口选择压缩版本 | +| 检查与覆盖 | 检查新产出是否符合默认要求;特例显式选择覆盖,并能说明生效的规则来自哪里 | + +关键不只是“记住用户偏好”,而是**规则有归属,使用者有关系,行为可以执行,结果能够检查**。当 Agent 新建同类文档时,它使用已有文档能力与策略,就能让之前确认的要求继续参与这次工作;人主要判断本次差异和真正的例外。这也是漫画里历史成果增强检查门的一个具体例子。 + +定义和依赖是对象,使系统有基础追踪受影响的文档,并通过名称、类型与调用检查发现部分不一致;图片编码与发布仍需实际运行相应能力。“一切皆对象”本身不保证每份文档都调用正确的渲染器,也不自动决定规则修改后是否追溯更新历史文档。 + +> **语义边界**:继承与委托、覆盖与冲突规则仍在[语言设计问题集的对象模型部分](/docs/language-design/05-questions)讨论中。父定义更新如何生效、文档是否绑定特定版本、多来源冲突如何处理,需要明确后才能成为稳定承诺。这里解释 Endor 为什么有承载这类继承的共同基础,以及希望形成的体验,不宣称这些规则已全部实现。 + +### 本站现在怎样落实这条规则 + +Tabularium 目前用共享构建策略落实这项要求:原图保存在非公开的制作目录,脚本统一生成有尺寸、质量与字节预算的 WebP,文档正文和放大查看都使用这个压缩版本。新增文档直接引用生成的图片,不需要逐篇重新实现压缩;构建检查会拒绝公开原始位图或直接嵌入原始 PNG、JPEG。 + +这是今天在文档仓库里可以执行和验证的共享规则,具体工作流见[图形与脚本](/docs/03-figures)。它示范了“把一次提醒变成后续工作可复用的能力”,并不表示 Tabularium 这个 Next.js 网站已经运行了 Endor 的继承机制。 + ## 一对图:我们希望改变什么  diff --git a/content/image-policy.json b/content/image-policy.json new file mode 100644 index 0000000..7967d1e --- /dev/null +++ b/content/image-policy.json @@ -0,0 +1,5 @@ +{ + "maxWidth": 1600, + "quality": 82, + "maxBytes": 196608 +} diff --git a/package.json b/package.json index 94f4079..4623585 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "lint": "pnpm --filter @endor/docs run lint", "typecheck": "pnpm --filter @endor/docs run typecheck", "figures": "uv run --locked --project tools/figures endor-figures", - "check": "pnpm run lint && pnpm run typecheck && pnpm run test:auth && pnpm run figures && pnpm run build", + "check": "pnpm run lint && pnpm run typecheck && pnpm run test:auth && pnpm --filter @endor/docs run images && pnpm --filter @endor/docs run test:images && pnpm run figures && pnpm run build", "test:auth": "pnpm --filter @endor/docs run test:auth" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a68050..ec308af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: postcss: specifier: 8.5.31 version: 8.5.31 + sharp: + specifier: 0.35.5 + version: 0.35.5(@types/node@22.20.5) tailwindcss: specifier: 4.3.3 version: 4.3.3 @@ -2876,8 +2879,7 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@img/colour@1.1.0': - optional: true + '@img/colour@1.1.0': {} '@img/sharp-darwin-arm64@0.35.5': optionalDependencies: @@ -5184,7 +5186,6 @@ snapshots: '@img/sharp-win32-ia32': 0.35.5 '@img/sharp-win32-x64': 0.35.5 '@types/node': 22.20.5 - optional: true shebang-command@2.0.0: dependencies: diff --git a/scripts/check-container.sh b/scripts/check-container.sh index 13dd0ea..7ec2013 100644 --- a/scripts/check-container.sh +++ b/scripts/check-container.sh @@ -6,6 +6,8 @@ uv sync --project tools/figures --locked pnpm run lint pnpm run typecheck pnpm --filter @endor/docs run test:auth +pnpm --filter @endor/docs run images +pnpm --filter @endor/docs run test:images cp -R apps/docs/public/figures /tmp/expected-figures pnpm run figures uv run --locked --project tools/figures python -m unittest discover -s tools/figures/tests -v diff --git a/scripts/smoke.py b/scripts/smoke.py index a1f4267..9296a3b 100644 --- a/scripts/smoke.py +++ b/scripts/smoke.py @@ -27,7 +27,7 @@ def verify(base: str) -> None: assert status == 200 and json.loads(health) == {"status": "ok"} status, _, login = get("/login") assert status == 200 and "输入动态码" in login.decode() - for path in ("/", "/docs/00-endor-concepts-draft", "/figures/endor-architecture.svg", "/docs/pretext/01-chapter", "/materials/pretext/verify.cjs", "/materials/pretext/results.chrome156.json"): + for path in ("/", "/docs/00-endor-concepts-draft", "/figures/endor-architecture.svg", "/figures/endor-review-gate.webp", "/figures/endor-review-desk.webp", "/docs/pretext/01-chapter", "/materials/pretext/verify.cjs", "/materials/pretext/results.chrome156.json"): status, headers, _ = get(path) assert status == 307, path target = urllib.parse.urlparse(urllib.parse.urljoin(base, headers["Location"])) diff --git a/tools/figures/assets/endor-review-desk.png b/tools/figures/assets/endor-review-desk.png new file mode 100644 index 0000000..b80e6dc Binary files /dev/null and b/tools/figures/assets/endor-review-desk.png differ diff --git a/tools/figures/assets/endor-review-desk.txt b/tools/figures/assets/endor-review-desk.txt new file mode 100644 index 0000000..2f3f5ba --- /dev/null +++ b/tools/figures/assets/endor-review-desk.txt @@ -0,0 +1,14 @@ +Endor inspection desk comic — approved second slide + +Source: built-in image_gen; physics inspection-bench variant with corrected top-row flow. +Original: exec-432e0f3d-d01d-4658-b5d5-0c63db95e472.png +The user selected this alongside the gate variant for one carousel. + +The two identical machines emit raw papers. Above, a dashed integral dW boundary +encloses the backlog BEFORE one person's fixed-capacity C0 inspection desk. +Below, a dashed dW boundary encloses the one new paper being inspected. Earlier +checked outputs become structured document helpers that contribute additional +magnifiers to the Endor desk, whose capacity is C0 + k integral dW. + +Store this PNG as the production master outside public. The confirmed remote +runner generates /figures/endor-review-desk.webp using content/image-policy.json. diff --git a/tools/figures/assets/endor-review-gate.png b/tools/figures/assets/endor-review-gate.png new file mode 100644 index 0000000..10a4ac8 Binary files /dev/null and b/tools/figures/assets/endor-review-gate.png differ diff --git a/tools/figures/assets/endor-review-gate.txt b/tools/figures/assets/endor-review-gate.txt new file mode 100644 index 0000000..d250c48 --- /dev/null +++ b/tools/figures/assets/endor-review-gate.txt @@ -0,0 +1,20 @@ +Endor review gate comic — approved source + +Source: built-in image_gen, physics gate variant, approved by the user on 2026-10-28. +Original: exec-380f8db3-6450-4981-97df-473c13a62223.png +The PNG is the production master, not a public browser asset. +The confirmed remote runner generates the browser WebP with: +pnpm --filter @endor/docs images + +Visual specification: +Two rows, white background, charcoal outlines, blue accents, identical coding +machines emitting identical raw sheets. Top: accumulated unchecked sheets inside +a dashed integral dW boundary, one person and a fixed C0 inspection gate, checked +output after the gate. Bottom: one new sheet inside a dashed dW boundary directly +inspected by a stick figure at an Endor gate. Gate capacity is C0 + k integral dW. +Checked output becomes a structured document helper, joins historical helpers, +and blue arrows from helpers feed the gate to increase inspection capability. +No dialogue or explanatory prose. Math is a conceptual metaphor, not measured data. + +The bitmap master retains the approved artwork, including its mathematical +symbols. Browser optimization changes encoding and dimensions only.
+ 第 {active + 1} 张,共 {slides.length} 张:{slide.alt} +
{children}