Document reusable review capabilities and publish compressed comic
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
@@ -141,6 +141,7 @@ async function main() {
|
||||
"/",
|
||||
"/docs/00-endor-concepts-draft",
|
||||
"/figures/endor-architecture.svg",
|
||||
"/figures/endor-review-gate.webp",
|
||||
"/docs/pretext/01-chapter",
|
||||
"/materials/pretext/verify.cjs",
|
||||
"/materials/pretext/results.chrome156.json",
|
||||
@@ -184,13 +185,30 @@ 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(/<img\b[^>]*\bsrc="\/figures\/endor-review-gate\.webp"/g)].length,
|
||||
2,
|
||||
"The comic preview and lightbox must use WebP",
|
||||
);
|
||||
assert.ok(!purpose.includes("endor-review-gate.png"));
|
||||
const comic = await request("/figures/endor-review-gate.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/endor-review-gate.webp")),
|
||||
);
|
||||
assert.equal((await request("/figures/endor-review-gate.png", { headers })).status, 404);
|
||||
const assets = new Set(
|
||||
[...home.matchAll(/(?:src|href)="(\/_next\/static\/[^" ]+)"/g)].map((m) =>
|
||||
m[1].replaceAll("&", "&"),
|
||||
|
||||
@@ -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(/<img\b[^>]*\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 committed = await readFile(destination);
|
||||
if (!encoded.equals(committed)) throw new Error(`${relative} is stale. Run pnpm --filter @endor/docs images and commit the output.`);
|
||||
} 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)`);
|
||||
}
|
||||
@@ -16,4 +16,21 @@ 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,防止修改原稿或策略后遗漏更新产物。检查同时拒绝公开目录里的原始位图及 Markdown 图片或 HTML img 直接引用的原始 PNG、JPEG。SVG 无需转成位图。新增位图时一起提交原稿、生成的 WebP 与制作记录,沿用共同策略。
|
||||
|
||||
@@ -16,6 +16,51 @@
|
||||
|
||||
这一定位希望形成的体验是:**对象池构成持续可用的工作世界,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 的继承机制。
|
||||
|
||||
## 一对图:我们希望改变什么
|
||||
|
||||

|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"maxWidth": 1600,
|
||||
"quality": 82,
|
||||
"maxBytes": 196608
|
||||
}
|
||||
+1
-1
@@ -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 test:images && pnpm run figures && pnpm run build",
|
||||
"test:auth": "pnpm --filter @endor/docs run test:auth"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+4
-3
@@ -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:
|
||||
|
||||
@@ -6,6 +6,7 @@ uv sync --project tools/figures --locked
|
||||
pnpm run lint
|
||||
pnpm run typecheck
|
||||
pnpm --filter @endor/docs run test:auth
|
||||
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
|
||||
|
||||
+1
-1
@@ -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", "/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"]))
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
@@ -0,0 +1,19 @@
|
||||
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.
|
||||
Generate 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.
|
||||
Reference in New Issue
Block a user