Compare commits

...

4 Commits

Author SHA1 Message Date
codex-bot a80f6568ff fix: restore main-only CI policy and remove branch validation workflow 2026-10-30 16:36:20 +08:00
codex-bot 534795617d fix: decouple report checks from legacy snapshots and tolerate slow dependency downloads
Verify database-backed documents / verify (pull_request) Failing after 4m0s
2026-10-30 16:32:16 +08:00
codex-bot d57e9b77d7 feat: make the site database authoritative for documents and catalog
Verify database-backed documents / verify (pull_request) Failing after 6m6s
2026-10-30 16:23:54 +08:00
Zayden-Jung 25c5e83d08 Merge pull request '新增 Smalltalk-80 现代化研究导读与源码研究入口' (#13) from codex/smalltalk-guide into main
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 6m56s
Reviewed-on: #13
2026-10-30 06:19:43 +00:00
78 changed files with 554 additions and 5233 deletions
+4
View File
@@ -8,3 +8,7 @@
.artifacts .artifacts
apps/docs/public/figures/**/*.webp apps/docs/public/figures/**/*.webp
.env* .env*
**/*.sqlite
**/*.sqlite-*
**/*.db
+1
View File
@@ -22,6 +22,7 @@ jobs:
with: with:
github-server-url: ${{ gitea.server_url }} github-server-url: ${{ gitea.server_url }}
persist-credentials: false persist-credentials: false
fetch-depth: 0
- name: Verify with persistent caches or reuse verified image - name: Verify with persistent caches or reuse verified image
run: python3 scripts/ci.py run: python3 scripts/ci.py
- name: Deploy latest main with health checks and rollback - name: Deploy latest main with health checks and rollback
+5
View File
@@ -14,3 +14,8 @@ __pycache__/
# Browser raster derivatives are generated on the confirmed remote runner. # Browser raster derivatives are generated on the confirmed remote runner.
/apps/docs/public/figures/**/*.webp /apps/docs/public/figures/**/*.webp
# Application data and content exports belong in private persistent storage.
*.sqlite
*.sqlite-*
*.db
+2 -2
View File
@@ -1,9 +1,9 @@
# Tabularium working agreements # Tabularium working agreements
- 本仓库专门维护 Endor 文档与制图脚本。支持本地开发;CI/CD 使用现有 ces-control Runner,在 main 通过验证后自动部署。不得修改其他项目、共享 Runner 配置或清理共享 Docker 缓存。 - 本仓库专门维护 Endor 文档与制图脚本。支持本地开发;CI/CD 仅在 main 使用现有 ces-control Runner,验证通过后自动部署;工作分支与 PR 不运行 CI,确认 Runner 不构成扩大触发范围的授权。不得修改其他项目、共享 Runner 配置或清理共享 Docker 缓存。
- Web 技术栈为 TypeScript、React、Tailwind CSS、Next.js Pages Router;不要切换到 App Router。 - Web 技术栈为 TypeScript、React、Tailwind CSS、Next.js Pages Router;不要切换到 App Router。
- 使用 pnpm 管理 JavaScript workspace,使用 uv 管理 `tools/figures` 的 Python 项目,保留两套锁文件。 - 使用 pnpm 管理 JavaScript workspace,使用 uv 管理 `tools/figures` 的 Python 项目,保留两套锁文件。
- Markdown 正文放在 `content/docs`,Python 图形源码放在 `tools/figures/src/endor_figures`,可复现的 SVG 输出放在 `apps/docs/public/figures`。 - 文档正文、目录元数据与修订记录以站点现有 SQLite 数据库为唯一权威源,使用 docs 管理工具直接更新;不得把新正文、目录 JSON、数据库文件或日常内容快照提交到仓库。历史 Git 文档只用于显式的一次性迁移,不作为运行时种子。Python 图形源码放在 `tools/figures/src/endor_figures`,可复现的 SVG 输出放在 `apps/docs/public/figures`。
- 未确认的架构提议明确标注为草案,不能表述为现有实现。 - 未确认的架构提议明确标注为草案,不能表述为现有实现。
- 本地开发与预览只监听 `127.0.0.1`。部署容器监听 `0.0.0.0:3000`,服务器入口默认 18135,可通过独立 production.env 配置。 - 本地开发与预览只监听 `127.0.0.1`。部署容器监听 `0.0.0.0:3000`,服务器入口默认 18135,可通过独立 production.env 配置。
- 对 gitea.entroia.com 仅使用 codex-bot,提交身份为 Codex Bot <codex-bot@gitea.entroia.local>。SSH 使用隔离配置:`ssh -F /dev/null -o IdentitiesOnly=yes -i /Users/zzyang/.codex/credentials/gitea-entroia/codex_entroia_ed25519 -p 22222`。 - 对 gitea.entroia.com 仅使用 codex-bot,提交身份为 Codex Bot <codex-bot@gitea.entroia.local>。SSH 使用隔离配置:`ssh -F /dev/null -o IdentitiesOnly=yes -i /Users/zzyang/.codex/credentials/gitea-entroia/codex_entroia_ed25519 -p 22222`。
+52 -45
View File
@@ -1,55 +1,67 @@
# Endor · Tabularium # Endor · Tabularium
Endor 文档与制图 monorepo。Tabularium 取古罗马档案馆之意。支持本地开发,并由 Gitea Actions 在 ces-control 上验证与部署。 Endor 文档站、研究工作台与制图程序。应用代码在 Git 中评审;全部文档正文、目录元数据和内容修订以站点 SQLite 数据库为唯一权威源。日常写作不需要 commit、构建或部署。
## 本地开发 ## 存储与读取
需要 Node.js 22.13+(推荐 LTS)、pnpm 11.25、Python 3.11+ 和 uv。 沿用 `AUTH_CONFIG_FILE` 同目录的 `login-state.sqlite` 和 `/auth` 持久挂载。`documents` 存正文及分类、合集、分组、摘要、草案与推荐状态;`document_aliases` 保留旧地址;`document_revisions` 保存每次修改;`document_imports` 标记一次性迁移。没有第二套文档数据库。
首次安装依赖需要访问软件包源。本地开发服务只监听 127.0.0.1。
首页、阅读页、目录和全文搜索按请求查询数据库,使用 `private, no-store`。旧文件式地址与改名前的语义地址通过数据库别名跳转,保留查询参数;不再在构建阶段枚举文档。Markdown 原文通过受认证保护的 `/api/documents/source?slug=...` 获取。
仓库不保存日常正文、目录 JSON、数据库文件或内容快照;历史 Git 记录仅用于迁移。页面不再读取 `content/docs` 或 `content/catalog.json`。Markdown 与 GFM 表格仍可用,不解析原始 HTML。图片在写入时检查压缩格式;图形程序与版本化静态资源仍在代码仓库,本轮不迁移媒体存储。研究业务数据留待下一步。
## 在服务器上管理文档
部署安装 `/opt/endorphin-tabularium-docs/docs`,通过当前容器执行管理 CLI。需要服务器的运维权限;不增加可公开写入的 API,不改变现有动态码登录。
```sh ```sh
pnpm install --frozen-lockfile sudo /opt/endorphin-tabularium-docs/docs list
uv sync --project tools/figures --locked sudo /opt/endorphin-tabularium-docs/docs get endor/endor-purpose-draft
pnpm dev sudo /opt/endorphin-tabularium-docs/docs history 04-endor-purpose-draft
``` ```
访问 http://127.0.0.1:3000 ,先使用验证器动态码登录。首次本地使用需按下文初始化私有认证配置;缺少配置时正文关闭访问。站点无外部字体或 CDN 资源。 新建或修改使用标准输入传入完整 JSON。必填字段:`slug`、`sourceSlug`、`title`、`category`、`collection`、`group`、`description`、`draft`、`featured`、`content` 和 `revision`。新建时 `revision: 0`;修改时先 get 并使用读到的 revision,避免覆盖别人刚写入的版本。`sourceSlug` 是稳定标识,修改公共 slug 时保持它不变,旧地址继续可用。调整目录直接修改 category、collection、group 等字段。
```text ```sh
apps/docs/ Next.js Pages Router + React + TypeScript + Tailwind CSS sudo /opt/endorphin-tabularium-docs/docs get endor/endor-purpose-draft > /private/work/purpose.json
content/docs/ Markdown 文档源,支持目录,路径成为 /docs/<path> 路由 # 在私有文件中修改正文或目录字段,保留读到的 revision。
tools/figures/ 独立 Python 项目,使用 drawsvg 编程制图 sudo /opt/endorphin-tabularium-docs/docs save < /private/work/purpose.json
``` ```
## 命令 路径仅为操作示例,应选择实际存在且受保护的目录。运行成功后刷新网页即可看到正文、导航和搜索更新。版本冲突时重新读取并合并修改;不要把旧 revision 改成新数字强行覆盖。草案是现有展示标记,不是按分支或用户隔离的访问控制。
- `pnpm dev`:本地开发,修改 Markdown 后刷新页面即可读取新内容。 新增正文不提交回 Git。奇思妙想这类内容同样通过数据库写入;代码分支与数据发布是不同动作,不能因为迁移程序合并就自动发布其他未合并分支的内容。
- `pnpm figures`:运行 Python 制图,更新 `apps/docs/public/figures`。
- `pnpm lint` / `pnpm typecheck`:前端静态检查。
- `pnpm build`:构建文档站。
- `pnpm start`:在本地预览已构建站点。
- `pnpm check`:依次执行 lint、类型检查、制图与构建。
站点默认浅色,使用本地系统字体。全站搜索支持正文关键词、`⌘/Ctrl K`、方向键与 Enter;代码块提供复制与反馈。桌面提供分组导航和滚动高亮的页内目录,小屏可展开文档菜单与页内目录。首页入口、草案提示、前后篇导航均引用现有 Markdown 内容和资料目录配置。顶部提供文档切换,阅读页左侧只展示当前文档,并有独立图标、名称与返回入口;搜索框标明当前文档范围。界面主要通过留白、轻渐变与状态反馈分层,支持减少动态效果的系统偏好。 ## 首次迁移与备份
参考设计截图与研究记录保存在 `design/references`;浏览器验收截图与检查说明保存在 `design/verification`。这些文件不作为站点静态资源发布。 迁移前主线的 50 篇正文及目录可从固定历史提交 `25c5e83d08eae4277fe429a692955bd3bb2e717f` 完整导出:
文档标题取首个 `# 标题`,文档排序按相对路径。支持多层目录(例如 `content/docs/pretext/01-chapter.md` 对应 `/docs/pretext/01-chapter`);旧文档地址保持可用。`content/catalog.json` 管理合集、分类、摘要、草案与推荐,新增资料无需修改页面代码。首页推荐最多3篇,目录每页8条,可按合集与标题筛选;全站搜索仍覆盖全文。相邻篇目限于当前合集。 ```sh
python3 scripts/export-legacy-documents.py --output .artifacts/documents-migration.json
```
Pretext 教程位于 `content/docs/pretext`,包含导读与12章,来自本地 Pretext 研究成果。源码链接锁定上游研究版本;实验脚本与原始记录位于 `apps/docs/public/materials/pretext`,和正文一样经过登录验证。普通 Markdown 与 GFM 表格可用,嵌入图使用 `/figures/<name>.svg`。不解析原始 HTML。 导出文件位于 Git 忽略目录、权限 0600,不进入镜像。脚本也支持显式 `--ref`,但不能用未评审分支替换生产迁移基线。它不会在站点读取时执行。CI 使用历史导出验证全部正文与目录的逐项一致性,构建镜像只含程序,不含文档快照或数据库。
语言设计问题集位于 `content/docs/language-design`,是第三个资料合集,包含总览与原编号02–14的13个分类,共271项议题。原讨论已确认26项,其余保持待讨论;来源映射和本次状态记录位于 `content/language-design-sources.json`。维护时同步章节、总览计数与来源状态;不会自动从聊天同步,也不把设计记录等同于已实现功能。 首次部署先备份整个共享数据库,再显式事务导入到空文档表。普通部署使用 `import --if-empty`,已有内容时跳过,不以历史数据覆盖数据库编辑;重复同一导入也不会重置内容。导入失败则事务整体回滚,既有登录与研究表保留,部署中止。
生产构建会生成文档路由;新增或修改文档后重新运行 build。开发模式按请求读取内容。 ```sh
main 推送通过 CI 验证后自动部署;Pull Request 只验证,不部署。工作流也支持手动触发。 sudo /opt/endorphin-tabularium-docs/docs backup /auth/manual-backup.sqlite
sudo /opt/endorphin-tabularium-docs/docs export /auth/current-documents.json
```
## Endor 概念图 backup 是包含登录、研究、正文、别名和修订的完整 SQLite 一致性备份;export 只导出当前正文与目录,作为空库初始化/外部迁移快照,不代替完整备份或恢复修订历史。输出路径均在容器内,文件已存在时拒绝覆盖。备份含认证状态,应留在私有挂载或受控备份位置,不能放到公开静态目录、仓库或日志。
`content/docs/00-endor-concepts-draft.md` 解释全球入口、区域网关与 Runtime、分片、CDN、身份及跨区恢复的概念设计,页面为 `/docs/00-endor-concepts-draft`。 代码回退不会撤销数据库编辑,但回退到旧文件式站点会显示旧正文。恢复完整数据库会同时影响登录和研究状态,必须先停止写入、保留当前备份并由管理员明确选择恢复点;部署脚本不会自动恢复数据库或删除新内容。
图形源码位于 `tools/figures/src/endor_figures/architecture.py`,由 `pnpm figures` 生成 `apps/docs/public/figures/endor-architecture.svg`。Python 负责可复现的节点、全连接和复制路径,正文直接显示图片,点击可放大;大图支持原尺寸查看、滚动和 Escape/空白处关闭。无需外部图标、字体或绘图服务。图中的数量是示意,身份服务/CDN 接入属于建议,所有未实现的关系均以草案说明。 ## 代码验证
拓扑检查:`uv run --locked --project tools/figures python -m unittest discover -s tools/figures/tests -v`。容器检查流程会运行同一检查。 使用 React、Next.js Pages Router、TypeScript 和 Tailwind CSS。JavaScript 用 pnpm,制图 Python 用 uv。编译、构建、打包和构建产物全部在已确认的 ces-control Runner 执行;`pnpm test:docs` 等测试脚本会调用 esbuild,也只能在 Runner 执行。
工作分支与 PR 不运行 CI,仅做不会触发编译、构建的轻量检查。用户手动合并 main 后,才在 ces-control 执行 lint、类型检查、数据库事务/版本测试、现有认证与研究测试、图形检查、构建与容器冒烟;验证通过才沿用既有部署流程。
冒烟使用隔离数据库卷:完整导入后逐项核对全部正文与目录,验证新建、更新、重命名、搜索立即变化及容器重启后的持久化,并覆盖全部文档地址、认证、媒体和原文端点。测试卷不会触及生产数据库。
`tools/figures/src/endor_figures` 与 `apps/docs/public/figures` 仍保存图形源码及可复现的静态资源。图形构建也在远程执行。
## 动态码登录与二维码 ## 动态码登录与二维码
@@ -68,13 +80,7 @@ sudo /opt/endorphin-tabularium-docs/auth verify
`qr`只允许交互终端显示;`qr-png`用于通过SSH将PNG直接重定向到私有本地文件,不在CI中运行。二维码等同登录凭据,不放入本仓库或网页。只有明确执行 `auth rotate --confirm` 才更换密钥并使旧二维码、会话失效;重新显示或下载二维码不会轮换。 `qr`只允许交互终端显示;`qr-png`用于通过SSH将PNG直接重定向到私有本地文件,不在CI中运行。二维码等同登录凭据,不放入本仓库或网页。只有明确执行 `auth rotate --confirm` 才更换密钥并使旧二维码、会话失效;重新显示或下载二维码不会轮换。
本地先构建CLI,再初始化独立的本地测试配置: 构建、打包与会触发构建的测试只在用户确认的 ces-control Runner 上执行。本地不执行 `pnpm dev`、`pnpm build`、`pnpm typecheck` 或打包测试。
```sh
pnpm build
AUTH_CONFIG_FILE=/private/tmp/tabularium-auth/config.json node apps/docs/.next/standalone/apps/docs/auth-cli.cjs init
AUTH_CONFIG_FILE=/private/tmp/tabularium-auth/config.json AUTH_COOKIE_SECURE=false pnpm dev
```
当前服务器入口为HTTP,与参考项目一致使用 `AUTH_COOKIE_SECURE=false`;配置HTTPS后应在独立production.env设置 `AUTH_COOKIE_SECURE=true`。HTTP本身不加密动态码和会话传输。 当前服务器入口为HTTP,与参考项目一致使用 `AUTH_COOKIE_SECURE=false`;配置HTTPS后应在独立production.env设置 `AUTH_COOKIE_SECURE=true`。HTTP本身不加密动态码和会话传输。
@@ -87,7 +93,7 @@ AUTH_CONFIG_FILE=/private/tmp/tabularium-auth/config.json AUTH_COOKIE_SECURE=fal
公司明细、收入观测、原始证据和数据核查记录。所有页面与 公司明细、收入观测、原始证据和数据核查记录。所有页面与
`GET /api/research/ai-companies` 均使用文档站现有的登录认证。 `GET /api/research/ai-companies` 均使用文档站现有的登录认证。
研究数据与登录状态**共用文档站现有的 SQLite 数据库文件**: 研究业务数据本轮暂不迁移,仍沿用版本化 JSON 导入机制。研究数据、文档与登录状态**共用文档站现有的 SQLite 数据库文件**:
`AUTH_CONFIG_FILE` 同目录下的 `login-state.sqlite`。该文件名是历史名称,未创建第二个数据库。 `AUTH_CONFIG_FILE` 同目录下的 `login-state.sqlite`。该文件名是历史名称,未创建第二个数据库。
`login_state` 保留登录限流与重放状态,新增的 `research_*` 表保存数据版本、分类、 `login_state` 保留登录限流与重放状态,新增的 `research_*` 表保存数据版本、分类、
公司、经营指标、证据与核查记录。沿用现有 `/auth` 持久挂载,数据库不会写入镜像或公开目录。 公司、经营指标、证据与核查记录。沿用现有 `/auth` 持久挂载,数据库不会写入镜像或公开目录。
@@ -121,9 +127,9 @@ MRR×12仅作为显示单位转换,不改写为公司ARR;公司ARR、估算
平台同步时间、连接过期状态、业务描述的可编辑性和产品/法律主体未去重限制均保留。 平台同步时间、连接过期状态、业务描述的可编辑性和产品/法律主体未去重限制均保留。
覆盖审计页位于 `/docs/other-research/ai-company-research/coverage`,分类链接 `overview?category=<id>` 的筛选数据。 覆盖审计页位于 `/docs/other-research/ai-company-research/coverage`,分类链接 `overview?category=<id>` 的筛选数据。
`python3 scripts/audit-ai-research.py --check` 检查候选池逐条计数与覆盖报告一致性;CI在ces-control运行此检查,未更改触发范围。 `python3 scripts/audit-ai-research.py --check` 检查候选池逐条计数与来源审计一致性;CI在ces-control运行此检查。
收入快照报告位于 `/docs/other-research/ai-company-research/income-snapshot`,样本名称链接 `overview?company=<id>` 的公司明细。 收入快照报告位于 `/docs/other-research/ai-company-research/income-snapshot`,样本名称链接 `overview?company=<id>` 的公司明细。
`python3 scripts/summarize-ai-income.py --check` 独立复核统计与报告一致性;不触发编译或构建。数据变更后先审查方法,再运行不带参数的命令更新固定快照。 `python3 scripts/summarize-ai-income.py --check` 独立复核统计与参考快照一致性;不触发编译或构建。数据变更后先审查方法,再运行不带参数的命令生成 `.artifacts/reports` 下的候选正文,使用 docs 管理工具显式更新数据库;不再生成受 Git 管理的正文。
默认7日样本14个产品;90日窗口新增Gojiberry的8月历史点,营销样本的均值和CV随之明显变化。 默认7日样本14个产品;90日窗口新增Gojiberry的8月历史点,营销样本的均值和CV随之明显变化。
这提示先核查时间与样本构成,不能将高离散度直接解释为造假。两个近名营养应用按不同App Store ID分别建档。 这提示先核查时间与样本构成,不能将高离散度直接解释为造假。两个近名营养应用按不同App Store ID分别建档。
@@ -152,17 +158,18 @@ AI 关键词可能出现在业务背景中,收入、买家和交付状态仍
## CI/CD 与缓存 ## CI/CD 与缓存
`.gitea/workflows/ci-deploy.yml` 使用已有 ces-control Runner,不修改共享 Runner 配置。 文档内容已退出构建输入。若需对照研究数据检查数据库中的报告正文,可先导出当前文档,再给研究脚本传入 `--document-export`;默认 `--check` 只核对研究数据本身的统计和审计不变量,避免把历史迁移快照继续当作日常正文源。
当前工作流仅在 main 推送或针对 main 的手动运行时执行。工作分支PR完成本地静态核查后,
完整类型、测试、构建与容器检查在用户手动合并后的主线CI运行;报告须区分未运行与已通过。
1. 对实际构建输入计算 SHA-256:前端源码、Markdown、图片、Python 制图源码、依赖锁文件、Dockerfile 和构建脚本。 `.gitea/workflows/ci-deploy.yml` 使用已有 ces-control Runner,不修改共享 Runner 配置。
工作流仅在 main 推送或针对 main 的手动运行时执行,工作分支与 PR 不触发 CI。确认 ces-control 是构建位置,不构成新增分支 CI 的授权;报告须区分轻量检查与主线 CI。
1. 对实际构建输入计算 SHA-256:前端源码、图片、Python 制图源码、研究业务快照、依赖锁文件、Dockerfile 和构建脚本。
2. 命中 `verified-<scope>-<摘要>` 镜像时跳过依赖安装和 Next.js build,只做认证、实际路由与资源冒烟检查。 2. 命中 `verified-<scope>-<摘要>` 镜像时跳过依赖安装和 Next.js build,只做认证、实际路由与资源冒烟检查。
3. 输入变化时运行 lint、类型检查、制图一致性检查与构建。pnpm store、uv 和 `.next/cache` 使用持久 Docker 卷,main 与每个 PR 独立;额外保留 Next.js SWC 下载缓存。 3. 输入变化时运行 lint、类型检查、制图一致性检查与构建。pnpm store、uv 和 `.next/cache` 使用持久 Docker 卷,main 与每个 PR 独立;额外保留 Next.js SWC 下载缓存。
4. 生产镜像只包含 Next.js standalone 产物;Python 留在检查容器,不进入运行镜像。镜像通过动态码认证、重放/限流、重启持久化、健康、全部文档、JS/CSS、SVG 和 404 检查后才标记为 verified。 4. 生产镜像只包含 Next.js standalone 产物;Python 留在检查容器,不进入运行镜像。镜像通过动态码认证、重放/限流、重启持久化、健康、全部文档、JS/CSS、SVG 和 404 检查后才标记为 verified。
5. 部署前检查是否仍为最新 main,使用 `--no-build` 启动,失败恢复上一版镜像。 5. 部署前检查是否仍为最新 main,使用 `--no-build` 启动,失败恢复上一版镜像。
根 README、工作流或部署配置等非构建输入变化不会触发 Next.js 重建;Markdown 正文变化会更新静态页面,仍需要一次增量构建。首次构建必须安装依赖并完整生成产物。缓存可能因基础镜像、工具链或锁文件变化而失效;不要清理共享 Docker 缓存或卷。 根 README、工作流或部署配置等非构建输入变化不会触发 Next.js 重建;数据库中的正文和目录变化直接通过请求读取,不触发任何构建。首次构建必须安装依赖并完整生成产物。缓存可能因基础镜像、工具链或锁文件变化而失效;不要清理共享 Docker 缓存或卷。
默认入口为服务器的 **18135** 端口。容器监听 3000,使用独立 Compose 项目 `endorphin-tabularium-docs`。部署前先检查端口是否空闲(或属于本服务),不会停止其他服务以抢占端口。服务器配置位于 `/etc/endorphin-tabularium-docs/production.env`: 默认入口为服务器的 **18135** 端口。容器监听 3000,使用独立 Compose 项目 `endorphin-tabularium-docs`。部署前先检查端口是否空闲(或属于本服务),不会停止其他服务以抢占端口。服务器配置位于 `/etc/endorphin-tabularium-docs/production.env`:
+2 -1
View File
@@ -11,7 +11,8 @@
"lint": "eslint . --max-warnings=0", "lint": "eslint . --max-warnings=0",
"typecheck": "NEXT_TELEMETRY_DISABLED=1 next typegen && tsc --noEmit", "typecheck": "NEXT_TELEMETRY_DISABLED=1 next typegen && tsc --noEmit",
"test:auth": "node scripts/test-auth.mjs", "test:auth": "node scripts/test-auth.mjs",
"test:research": "node scripts/test-research.mjs" "test:research": "node scripts/test-research.mjs",
"test:docs": "node scripts/test-docs.mjs"
}, },
"dependencies": { "dependencies": {
"next": "16.4.3", "next": "16.4.3",
+28 -6
View File
@@ -18,6 +18,7 @@ import {
totp, totp,
isLoginRedirect, isLoginRedirect,
} from "../src/lib/auth"; } from "../src/lib/auth";
import { readDocument, saveDocument } from "../src/lib/docs";
import { consumeCode, initializeStore } from "../src/lib/auth-store"; import { consumeCode, initializeStore } from "../src/lib/auth-store";
async function main() { async function main() {
@@ -132,11 +133,6 @@ async function main() {
join(__dirname, ".next/BUILD_ID"), join(__dirname, ".next/BUILD_ID"),
"utf8", "utf8",
).trim(); ).trim();
const routes = Object.keys(
JSON.parse(
readFileSync(join(__dirname, ".next/prerender-manifest.json"), "utf8"),
).routes,
) as string[];
for (const path of [ for (const path of [
"/", "/",
"/docs/00-endor-concepts-draft", "/docs/00-endor-concepts-draft",
@@ -160,7 +156,7 @@ async function main() {
path, path,
); );
} }
for (const path of ["/api/private", "/api/unknown", "/api/search", "/api/research/ai-companies"]) for (const path of ["/api/private", "/api/unknown", "/api/search", "/api/documents/source?slug=endor/endor-purpose-draft", "/api/research/ai-companies"])
assert.equal((await request(path)).status, 401, path); assert.equal((await request(path)).status, 401, path);
assert.equal( assert.equal(
( (
@@ -209,6 +205,32 @@ async function main() {
assert.equal(destination.searchParams.get("from"), "legacy"); assert.equal(destination.searchParams.get("from"), "legacy");
assert.ok(legacy.headers.get("cache-control")?.includes("no-store")); assert.ok(legacy.headers.get("cache-control")?.includes("no-store"));
} }
const routes = ["/", ...searchIndex.map((doc: { slug: string }) => `/docs/${doc.slug}`)];
const raw = await request("/api/documents/source?slug=endor/endor-purpose-draft", { headers });
assert.equal(raw.status, 200);
assert.ok((await raw.text()).startsWith("# Endor"));
if (process.env.DOCUMENT_SMOKE_TEST) {
const sourceSlug = "ci-live-document";
const previous = readDocument(sourceSlug);
if (command === "exercise") {
const first = saveDocument({ slug: "endor/ci-live-document", sourceSlug,
title: "Runtime document", category: "Endor", collection: "Endor 文档", group: "CI",
description: "Isolated smoke test", draft: true, featured: false,
content: "# Runtime document\n\nInitial body" }, previous?.revision ?? 0);
assert.equal((await request(`/docs/${first.slug}`, { headers })).status, 200);
saveDocument({ ...first, slug: "endor/ci-live-document-renamed", content: "# Runtime document\n\nUpdated without rebuilding" }, first.revision);
}
const stored = readDocument(sourceSlug);
assert.ok(stored?.content.includes("Updated without rebuilding"), "DB content survives restart");
const live = await request(`/docs/${stored.slug}`, { headers });
assert.equal(live.status, 200);
assert.ok((await live.text()).includes("Updated without rebuilding"));
const alias = await request("/docs/endor/ci-live-document?live=1", { headers });
assert.equal(alias.status, 308);
assert.ok(alias.headers.get("location")?.includes("ci-live-document-renamed?live=1"));
const updatedIndex = await (await request("/api/search", { headers })).json();
assert.ok(updatedIndex.some((doc: { searchText: string }) => doc.searchText.includes("Updated without rebuilding")));
}
let home = ""; let home = "";
let purpose = ""; let purpose = "";
for (const route of routes) { for (const route of routes) {
+5
View File
@@ -7,3 +7,8 @@ await build({
format: "cjs", format: "cjs",
outfile: ".next/standalone/apps/docs/auth-cli.cjs", outfile: ".next/standalone/apps/docs/auth-cli.cjs",
}); });
await build({
entryPoints: ["scripts/docs-cli.ts"], bundle: true, platform: "node", target: "node22",
format: "cjs", outfile: ".next/standalone/apps/docs/docs-cli.cjs",
});
-13
View File
@@ -23,19 +23,6 @@ for (const name of await files(path.join(root, "apps/docs/public"))) {
throw new Error(`Original raster exposed in public: ${path.relative(root, name)}. Move it to tools/figures/assets and run images.`); 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 }); await mkdir(output, { recursive: true });
for (const source of await files(sources)) { for (const source of await files(sources)) {
+44
View File
@@ -0,0 +1,44 @@
import { readFileSync, writeFileSync } from "node:fs";
import { openSiteDatabase } from "../src/lib/site-database";
import { allDocuments, readDocument, saveDocument, importDocuments, documentHistory } from "../src/lib/docs";
process.umask(0o077);
const [command, argument] = process.argv.slice(2);
try {
switch (command) {
case "backup": {
if (!argument) throw Error("Provide a private backup filename");
const db = openSiteDatabase();
try { db.prepare("VACUUM INTO ?").run(argument); } finally { db.close(); }
console.log("Full site database backup created; existing files are never overwritten"); break;
}
case "list":
console.log(JSON.stringify(allDocuments().map(({ content: _, ...doc }) => { void _; return doc; }), null, 2)); break;
case "get": {
const doc = readDocument(argument ?? "");
if (!doc) throw Error("Document not found");
console.log(JSON.stringify(doc, null, 2)); break;
}
case "save": {
const { revision, ...input } = JSON.parse(readFileSync(0, "utf8"));
const doc = saveDocument(input, revision);
console.log(JSON.stringify({ slug: doc.slug, revision: doc.revision })); break;
}
case "history":
console.log(JSON.stringify(documentHistory(argument ?? ""), null, 2)); break;
case "import": {
const bundle = JSON.parse(readFileSync(0, "utf8"));
console.log(JSON.stringify({ imported: importDocuments(bundle, argument === "--if-empty") })); break;
}
case "export": {
if (!argument) throw Error("Provide a private export filename");
writeFileSync(argument, JSON.stringify({ format: "tabularium-documents-v1", documents: allDocuments() }),
{ flag: "wx", mode: 0o600 });
console.log("Document export created; existing files are never overwritten"); break;
}
default: throw Error("Usage: docs backup <private-file> | list | get <slug> | save < input.json | history <sourceSlug> | import [--if-empty] < export.json | export <private-file>");
}
} catch (error) {
console.error(error instanceof Error ? error.message : "Document operation failed");
process.exitCode = 1;
}
+14
View File
@@ -0,0 +1,14 @@
import { build } from "esbuild";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
const dir = await mkdtemp(join(tmpdir(), "tabularium-docs-tests-"));
try {
const file = join(dir, "docs.test.cjs");
await build({ entryPoints: ["tests/docs.test.ts"], bundle: true,
platform: "node", target: "node22", format: "cjs", outfile: file });
const result = spawnSync(process.execPath, ["--test", file], { stdio: "inherit" });
process.exitCode = result.status ?? 1;
} finally { await rm(dir, { recursive: true, force: true }); }
+134 -37
View File
@@ -1,50 +1,147 @@
import fs from "node:fs/promises"; import { randomUUID, createHash } from "node:crypto";
import path from "node:path"; import { openSiteDatabase } from "./site-database";
import catalog from "../../../../content/catalog.json";
const directory = path.resolve(process.cwd(), "../../content/docs");
const validSlug = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)*$/;
type Metadata = { slug?: string; category?: string; collection?: string; group?: string; description?: string; draft?: boolean; featured?: boolean };
export type DocLink = { export type DocLink = {
slug: string; sourceSlug: string; category: string; title: string; searchText?: string; slug: string; sourceSlug: string; category: string; title: string; searchText?: string;
group: string; collection: string; description: string; draft: boolean; featured: boolean; group: string; collection: string; description: string; draft: boolean; featured: boolean;
revision: number;
}; };
export type Doc = DocLink & { content: string }; export type Doc = DocLink & { content: string };
export type DocInput = Omit<Doc, "revision" | "searchText">;
export type DocumentExport = { format: "tabularium-documents-v1"; documents: Doc[] };
export const validSlug = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)*$/;
function database() {
const db = openSiteDatabase();
db.exec(`
CREATE TABLE IF NOT EXISTS documents (
id TEXT PRIMARY KEY, slug TEXT NOT NULL UNIQUE, source_slug TEXT NOT NULL UNIQUE,
revision INTEGER NOT NULL, payload TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS document_aliases (
alias TEXT PRIMARY KEY, document_id TEXT NOT NULL REFERENCES documents(id)
);
CREATE TABLE IF NOT EXISTS document_revisions (
document_id TEXT NOT NULL REFERENCES documents(id), revision INTEGER NOT NULL,
payload TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY(document_id, revision)
);
CREATE TABLE IF NOT EXISTS document_imports (
digest TEXT PRIMARY KEY, document_count INTEGER NOT NULL, created_at TEXT NOT NULL
);
`);
return db;
}
export function validateDocument(input: DocInput): void {
if (!input || typeof input !== "object") throw Error("Invalid document");
for (const key of ["slug", "sourceSlug"] as const)
if (typeof input[key] !== "string" || !validSlug.test(input[key])) throw Error(`Invalid ${key}`);
if (input.slug.split("/").some((part) => /^\d+-/.test(part))) throw Error("Use a semantic public slug");
for (const key of ["title", "category", "collection", "group"] as const)
if (typeof input[key] !== "string" || !input[key].trim()) throw Error(`Missing ${key}`);
if (typeof input.content !== "string" || typeof input.description !== "string" ||
typeof input.draft !== "boolean" || typeof input.featured !== "boolean") throw Error("Invalid document fields");
if (input.content.length > 2_000_000) throw Error("Document exceeds content limit");
const images = [
...input.content.matchAll(/!\[[^\]]*\]\(<?([^\s)>]+)/g),
...input.content.matchAll(/<img\b[^>]*\bsrc=["']([^"']+)/gi),
];
if (images.some(([, source]) => /\.(?:png|jpe?g|tiff?|bmp)(?:[?#]|$)/i.test(source)))
throw Error("Use compressed WebP images instead of original rasters");
}
function write(db: ReturnType<typeof database>, input: DocInput, expectedRevision: number): Doc {
validateDocument(input);
const existing = db.prepare("SELECT id, revision FROM documents WHERE source_slug=?")
.get(input.sourceSlug) as { id: string; revision: number } | undefined;
if ((existing?.revision ?? 0) !== expectedRevision) throw Error("Document revision conflict");
const id = existing?.id ?? randomUUID();
const doc: Doc = { slug: input.slug, sourceSlug: input.sourceSlug, category: input.category,
title: input.title, group: input.group, collection: input.collection, description: input.description,
draft: input.draft, featured: input.featured, content: input.content, revision: expectedRevision + 1 };
const payload = JSON.stringify(doc);
// Aliases remain reserved after renaming; collision rolls back the whole write.
db.prepare(`INSERT INTO documents VALUES (?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET
slug=excluded.slug, revision=excluded.revision, payload=excluded.payload`)
.run(id, doc.slug, doc.sourceSlug, doc.revision, payload);
for (const alias of new Set([doc.slug, doc.sourceSlug])) {
const prior = db.prepare("SELECT document_id FROM document_aliases WHERE alias=?")
.get(alias) as { document_id: string } | undefined;
if (prior && prior.document_id !== id) throw Error(`Document alias collision: ${alias}`);
db.prepare("INSERT OR IGNORE INTO document_aliases VALUES (?,?)").run(alias, id);
}
db.prepare("INSERT INTO document_revisions VALUES (?,?,?,?)")
.run(id, doc.revision, payload, new Date().toISOString());
return doc;
}
export function saveDocument(input: DocInput, expectedRevision: number): Doc {
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0) throw Error("Provide expected revision");
const db = database();
try {
db.exec("BEGIN IMMEDIATE");
try { const doc = write(db, input, expectedRevision); db.exec("COMMIT"); return doc; }
catch (error) { db.exec("ROLLBACK"); throw error; }
} finally { db.close(); }
}
export function readDocument(slug: string): Doc | null {
if (!validSlug.test(slug)) return null;
const db = database();
try {
const row = db.prepare(`SELECT d.payload FROM documents d JOIN document_aliases a
ON a.document_id=d.id WHERE a.alias=?`).get(slug) as { payload: string } | undefined;
return row ? JSON.parse(row.payload) as Doc : null;
} finally { db.close(); }
}
export function allDocuments(): Doc[] {
const db = database();
try {
return (db.prepare("SELECT payload FROM documents ORDER BY source_slug").all() as { payload: string }[])
.map((row) => JSON.parse(row.payload) as Doc);
} finally { db.close(); }
}
export async function readDoc(slug: string): Promise<Doc> { export async function readDoc(slug: string): Promise<Doc> {
if (!validSlug.test(slug)) throw new Error("Invalid document slug"); const doc = readDocument(slug);
const sourceSlug = Object.entries(catalog.documents).find(([, metadata]) => metadata.slug === slug)?.[0] ?? slug; if (!doc) throw Error("Document not found");
const content = await fs.readFile(path.join(directory, `${sourceSlug}.md`), "utf8"); return doc;
const title = content.match(/^# (.+)$/m)?.[1]?.trim() ?? slug;
const collection = catalog.collections.find((c) => !c.path || sourceSlug.startsWith(`${c.path}/`))!;
const metadata: Metadata = (catalog.documents as Record<string, Metadata>)[sourceSlug] ?? {};
if (!metadata.slug || !metadata.category || !validSlug.test(metadata.slug) || metadata.slug.split("/").some((part) => /^\d+-/.test(part))) {
throw new Error(`Register a semantic slug and category in content/catalog.json: ${sourceSlug}`);
}
return { slug: metadata.slug, sourceSlug, category: metadata.category, title, content, searchText: content,
group: metadata.group ?? collection.title, collection: metadata.collection ?? collection.title,
description: metadata.description ?? collection.description,
draft: metadata.draft ?? false, featured: metadata.featured ?? false,
};
} }
export async function listDocs(): Promise<DocLink[]> { export async function listDocs(): Promise<DocLink[]> {
async function walk(folder: string): Promise<string[]> { return allDocuments().map(({ content: _, ...doc }) => { void _; return doc; });
const entries = await fs.readdir(path.join(directory, folder), { withFileTypes: true });
const files = await Promise.all(entries.map(async (entry) => {
const relative = folder ? `${folder}/${entry.name}` : entry.name;
if (entry.isDirectory()) return walk(relative);
if (entry.isFile() && relative.endsWith(".md") && validSlug.test(relative.slice(0, -3)))
return [relative.slice(0, -3)];
return [];
}));
return files.flat();
} }
const docs = await Promise.all((await walk("")).sort().map(async (slug) => {
const { content: _, searchText: __, ...doc } = await readDoc(slug); // Explicit, atomic bootstrap only. Runtime reads never import repository data.
void _; void __; export function importDocuments(bundle: DocumentExport, onlyIfEmpty = false): number {
return doc; if (bundle?.format !== "tabularium-documents-v1" || !Array.isArray(bundle.documents) || !bundle.documents.length)
})); throw Error("Invalid or empty document export");
if (new Set(docs.map((doc) => doc.slug)).size !== docs.length) throw new Error("Duplicate public document slug"); bundle.documents.forEach(validateDocument);
return docs; const digest = createHash("sha256").update(JSON.stringify(bundle)).digest("hex");
const db = database();
try {
db.exec("BEGIN IMMEDIATE");
try {
const imported = db.prepare("SELECT digest FROM document_imports WHERE digest=?").get(digest);
if (imported) { db.exec("COMMIT"); return 0; }
const count = (db.prepare("SELECT COUNT(*) AS n FROM documents").get() as { n: number }).n;
if (count) {
if (!onlyIfEmpty) throw Error("Bootstrap requires an empty document database");
db.exec("COMMIT"); return 0;
}
for (const doc of bundle.documents) write(db, doc, 0);
db.prepare("INSERT INTO document_imports VALUES (?,?,?)").run(digest, bundle.documents.length, new Date().toISOString());
db.exec("COMMIT"); return bundle.documents.length;
} catch (error) { db.exec("ROLLBACK"); throw error; }
} finally { db.close(); }
}
export function documentHistory(sourceSlug: string): Doc[] {
const db = database();
try {
return (db.prepare(`SELECT r.payload FROM document_revisions r JOIN documents d
ON r.document_id=d.id WHERE d.source_slug=? ORDER BY r.revision`).all(sourceSlug) as { payload: string }[])
.map((row) => JSON.parse(row.payload) as Doc);
} finally { db.close(); }
} }
@@ -0,0 +1,22 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { readDocument } from "@/lib/docs";
// The proxy protects Markdown just like the rendered document and search index.
export default function handler(request: NextApiRequest, response: NextApiResponse) {
response.setHeader("Cache-Control", "private, no-store");
if (request.method !== "GET") {
response.setHeader("Allow", "GET");
return response.status(405).end();
}
const slug = request.query.slug;
if (typeof slug !== "string") return response.status(400).json({ error: "缺少文档地址" });
try {
const doc = readDocument(slug);
if (!doc) return response.status(404).end();
response.setHeader("Content-Type", "text/plain; charset=utf-8");
response.setHeader("X-Content-Type-Options", "nosniff");
return response.status(200).send(doc.content);
} catch {
return response.status(503).json({ error: "文档数据库暂时不可用" });
}
}
+7 -6
View File
@@ -1,5 +1,5 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { listDocs, readDoc } from "@/lib/docs"; import { allDocuments } from "@/lib/docs";
// The proxy authenticates this endpoint, including index content. // The proxy authenticates this endpoint, including index content.
export default async function handler(request: NextApiRequest, response: NextApiResponse) { export default async function handler(request: NextApiRequest, response: NextApiResponse) {
@@ -8,9 +8,10 @@ export default async function handler(request: NextApiRequest, response: NextApi
response.setHeader("Allow", "GET"); response.setHeader("Allow", "GET");
return response.status(405).end(); return response.status(405).end();
} }
const docs = await listDocs(); try {
const index = await Promise.all(docs.map(async (doc) => ({ const index = allDocuments().map(({ content, ...doc }) => ({ ...doc, searchText: content }));
...doc, searchText: (await readDoc(doc.slug)).content, return response.status(200).json(index);
}))); } catch {
response.status(200).json(index); return response.status(503).json({ error: "文档数据库暂时不可用" });
}
} }
+15 -15
View File
@@ -1,7 +1,6 @@
import type { import type {
GetStaticPaths, GetServerSideProps,
GetStaticProps, InferGetServerSidePropsType,
InferGetStaticPropsType,
} from "next"; } from "next";
import Head from "next/head"; import Head from "next/head";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
@@ -15,7 +14,7 @@ import {
} from "react"; } from "react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import { listDocs, readDoc, type Doc, type DocLink } from "@/lib/docs"; import { listDocs, readDocument, type Doc, type DocLink } from "@/lib/docs";
import { Shell, Navigation } from "@/components/Shell"; import { Shell, Navigation } from "@/components/Shell";
import { ArchitectureFigure } from "@/components/ArchitectureFigure"; import { ArchitectureFigure } from "@/components/ArchitectureFigure";
import { FigureGallery } from "@/components/FigureGallery"; import { FigureGallery } from "@/components/FigureGallery";
@@ -26,16 +25,17 @@ import { headingsFor } from "@/lib/navigation";
type Props = { doc: Doc; docs: DocLink[] }; type Props = { doc: Doc; docs: DocLink[] };
const AICompanyResearch = dynamic(() => import("@/components/AICompanyResearch")); const AICompanyResearch = dynamic(() => import("@/components/AICompanyResearch"));
export const getStaticPaths: GetStaticPaths = async () => ({ export const getServerSideProps: GetServerSideProps<Props> = async ({ params, res, resolvedUrl }) => {
paths: (await listDocs()).map(({ slug }) => ({ params: { slug: slug.split("/") } })), res.setHeader("Cache-Control", "private, no-store");
fallback: false,
});
export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
const slug = Array.isArray(params?.slug) ? params.slug.join("/") : params?.slug; const slug = Array.isArray(params?.slug) ? params.slug.join("/") : params?.slug;
if (typeof slug !== "string") return { notFound: true }; if (typeof slug !== "string") return { notFound: true };
const docs = await listDocs(); const doc = readDocument(slug);
if (!docs.some((d) => d.slug === slug)) return { notFound: true }; if (!doc) return { notFound: true };
return { props: { doc: await readDoc(slug), docs } }; if (slug !== doc.slug) {
const query = resolvedUrl.includes("?") ? resolvedUrl.slice(resolvedUrl.indexOf("?")) : "";
return { redirect: { destination: `/docs/${doc.slug}${query}`, permanent: true } };
}
return { props: { doc, docs: await listDocs() } };
}; };
function textOf(value: ReactNode): string { function textOf(value: ReactNode): string {
return Children.toArray(value) return Children.toArray(value)
@@ -49,7 +49,7 @@ function textOf(value: ReactNode): string {
export default function DocPage({ export default function DocPage({
doc, doc,
docs, docs,
}: InferGetStaticPropsType<typeof getStaticProps>) { }: InferGetServerSidePropsType<typeof getServerSideProps>) {
const headings = headingsFor(doc.content); const headings = headingsFor(doc.content);
const [active, setActive] = useState(""); const [active, setActive] = useState("");
@@ -231,9 +231,9 @@ export default function DocPage({
<div className="article-end"> <div className="article-end">
<a <a
href={`https://gitea.entroia.com/Zayden-Jung/endorphin-tabularium-docs/src/branch/main/content/docs/${doc.sourceSlug}.md`} href={`/api/documents/source?slug=${encodeURIComponent(doc.slug)}`}
> >
查看文档源码 ↗ 查看 Markdown 原文 ↗
</a> </a>
</div> </div>
<nav className="pagination" aria-label="相邻文档"> <nav className="pagination" aria-label="相邻文档">
+6 -5
View File
@@ -1,4 +1,4 @@
import type { GetStaticProps, InferGetStaticPropsType } from "next"; import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
import Head from "next/head"; import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
@@ -7,11 +7,12 @@ import { Shell, Navigation } from "@/components/Shell";
import { Icon } from "@/components/Icon"; import { Icon } from "@/components/Icon";
import { collectionEntries, collectionIcon, collectionLabel, collectionTone } from "@/lib/collections"; import { collectionEntries, collectionIcon, collectionLabel, collectionTone } from "@/lib/collections";
export const getStaticProps: GetStaticProps<{ docs: DocLink[] }> = async () => ({ export const getServerSideProps: GetServerSideProps<{ docs: DocLink[] }> = async ({ res }) => {
props: { docs: await listDocs() }, res.setHeader("Cache-Control", "private, no-store");
}); return { props: { docs: await listDocs() } };
};
const PAGE_SIZE = 8; const PAGE_SIZE = 8;
export default function Home({ docs }: InferGetStaticPropsType<typeof getStaticProps>) { export default function Home({ docs }: InferGetServerSidePropsType<typeof getServerSideProps>) {
const [collection, setCollection] = useState(""); const [collection, setCollection] = useState("");
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
-13
View File
@@ -1,11 +1,5 @@
import { NextResponse, type NextRequest } from "next/server"; import { NextResponse, type NextRequest } from "next/server";
import { authConfig, SESSION_COOKIE, validSession } from "./lib/auth"; import { authConfig, SESSION_COOKIE, validSession } from "./lib/auth";
import catalog from "../../../content/catalog.json";
const legacyRoutes = new Map(Object.entries(catalog.documents).map(([source, metadata]) => [
`/docs/${source}`, `/docs/${metadata.slug}`,
]));
export function proxy(request: NextRequest) { export function proxy(request: NextRequest) {
const path = request.nextUrl.pathname; const path = request.nextUrl.pathname;
const headers = { const headers = {
@@ -54,13 +48,6 @@ export function proxy(request: NextRequest) {
); );
} }
} }
// Keep legacy URLs behind authentication and out of static generation.
const destination = legacyRoutes.get(path);
if (destination && ["GET", "HEAD"].includes(request.method)) {
const target = request.nextUrl.clone();
target.pathname = destination;
return NextResponse.redirect(target, { status: 308, headers });
}
return NextResponse.next({ headers }); return NextResponse.next({ headers });
} }
if (path.startsWith("/api/") || !["GET", "HEAD"].includes(request.method)) { if (path.startsWith("/api/") || !["GET", "HEAD"].includes(request.method)) {
+99
View File
@@ -0,0 +1,99 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { allDocuments, documentHistory, importDocuments, readDocument, saveDocument,
type DocInput, type DocumentExport } from "../src/lib/docs";
const input: DocInput = { slug: "endor/example", sourceSlug: "00-example", title: "Example",
category: "Endor", collection: "Endor 文档", group: "讨论", description: "Example description",
draft: true, featured: false, content: "# Example\n\nOriginal body" };
function isolated(run: (directory: string) => void) {
const directory = mkdtempSync(join(tmpdir(), "tabularium-docs-"));
const previous = process.env.AUTH_CONFIG_FILE;
process.env.AUTH_CONFIG_FILE = join(directory, "config.json");
try { run(directory); } finally {
if (previous === undefined) delete process.env.AUTH_CONFIG_FILE;
else process.env.AUTH_CONFIG_FILE = previous;
rmSync(directory, { recursive: true, force: true });
}
}
function bundle(documents: DocInput[]): DocumentExport {
return { format: "tabularium-documents-v1", documents: documents.map((doc) => ({ ...doc, revision: 1 })) };
}
test("writes and metadata persist across independent connections; no repository seed", () => isolated(() => {
assert.deepEqual(allDocuments(), []);
const saved = saveDocument(input, 0);
assert.deepEqual(readDocument(input.slug), saved);
assert.deepEqual(readDocument(input.sourceSlug), saved);
const changed = saveDocument({ ...input, content: "# Revised", collection: "新合集", group: "新目录" }, 1);
assert.equal(changed.revision, 2);
assert.equal(allDocuments()[0].collection, "新合集");
assert.deepEqual(documentHistory(input.sourceSlug).map((doc) => doc.content), [input.content, "# Revised"]);
}));
test("optimistic concurrency prevents lost updates and requires explicit revision", () => isolated(() => {
saveDocument(input, 0);
assert.throws(() => saveDocument({ ...input, content: "Lost" }, 0), /conflict/);
assert.throws(() => saveDocument(input, NaN), /expected revision/);
assert.equal(readDocument(input.slug)?.content, input.content);
assert.equal(documentHistory(input.sourceSlug).length, 1);
}));
test("renames preserve aliases and alias collisions roll back document and revision", () => isolated(() => {
saveDocument(input, 0);
saveDocument({ ...input, slug: "endor/renamed" }, 1);
assert.equal(readDocument(input.slug)?.slug, "endor/renamed");
assert.throws(() => saveDocument({ ...input, sourceSlug: "another", slug: input.slug }, 0), /collision/);
assert.equal(allDocuments().length, 1);
assert.equal(readDocument("another"), null);
assert.equal(documentHistory(input.sourceSlug).length, 2);
}));
test("bootstrap is atomic and repeat imports never replace DB edits", () => isolated(() => {
const snapshot = bundle([input]);
assert.equal(importDocuments(snapshot), 1);
saveDocument({ ...input, content: "Written directly to DB" }, 1);
assert.equal(importDocuments(snapshot), 0);
assert.equal(importDocuments(bundle([{ ...input, content: "Stale release" }]), true), 0);
assert.throws(() => importDocuments(bundle([{ ...input, content: "Other" }])), /empty/);
assert.equal(readDocument(input.slug)?.content, "Written directly to DB");
}));
test("colliding bootstrap leaves no partial rows and can be retried", () => isolated(() => {
assert.throws(() => importDocuments(bundle([input, { ...input, sourceSlug: "other" }])));
assert.deepEqual(allDocuments(), []);
assert.equal(importDocuments(bundle([input])), 1);
}));
test("migration preserves unrelated authentication and research tables", () => isolated((directory) => {
const db = new DatabaseSync(join(directory, "login-state.sqlite"));
db.exec("CREATE TABLE login_state(value TEXT); INSERT INTO login_state VALUES ('preserved'); CREATE TABLE research_datasets(value TEXT); INSERT INTO research_datasets VALUES ('research');");
db.close();
importDocuments(bundle([input]));
const reopened = new DatabaseSync(join(directory, "login-state.sqlite"));
assert.equal(reopened.prepare("SELECT value FROM login_state").get()?.value, "preserved");
assert.equal(reopened.prepare("SELECT value FROM research_datasets").get()?.value, "research");
assert.deepEqual(reopened.prepare("PRAGMA foreign_key_check").all(), []);
reopened.close();
}));
test("invalid content cannot enter the database", () => isolated(() => {
for (const invalid of [ { ...input, slug: "../private" }, { ...input, slug: "endor/01-example" },
{ ...input, content: "![original](/figures/raw.png)" }, { ...input, category: "" } ])
assert.throws(() => saveDocument(invalid, 0));
assert.deepEqual(allDocuments(), []);
assert.equal(readDocument("../private"), null);
}));
test("missing database configuration fails closed", () => {
const previous = process.env.AUTH_CONFIG_FILE;
delete process.env.AUTH_CONFIG_FILE;
try { assert.throws(() => allDocuments(), /not configured/); }
finally { if (previous !== undefined) process.env.AUTH_CONFIG_FILE = previous; }
});
-315
View File
@@ -1,315 +0,0 @@
{
"collections": [
{
"path": "smalltalk",
"title": "Smalltalk-80 研究导读",
"description": "从消息、类与集合到图形、开发工具和经典 MVC。"
},
{
"path": "pretext",
"title": "Pretext 源码教程",
"description": "从浏览器排版基础到缓存、换行算法与虚拟列表。"
},
{
"path": "language-design",
"title": "语言设计问题集",
"description": "13 个分类的语言设计问题、候选方案与确认记录。"
},
{
"path": "",
"title": "Endor 文档",
"description": "系统设计、项目讨论与工作指南。"
}
],
"documents": {
"ai-company-research/00-overview": {
"collection": "AI 公司研究",
"group": "公司与市场",
"description": "按业务分类查看 AI 公司、经营指标与原始证据,检查收入定义与数据缺项。",
"slug": "other-research/ai-company-research/overview",
"category": "其他研究",
"featured": true
},
"00-endor-concepts-draft": {
"group": "架构设计",
"description": "Endor 的全球入口、区域运行时与对象数据。",
"draft": true,
"featured": true,
"slug": "endor/endor-concepts-draft",
"category": "Endor"
},
"01-getting-started": {
"group": "开始使用",
"description": "本地开发、文档编写与发布。",
"slug": "endor/getting-started",
"category": "Endor"
},
"02-mounting-draft": {
"group": "架构设计",
"description": "节点、对象与统一接入协议。",
"draft": true,
"slug": "endor/mounting-draft",
"category": "Endor"
},
"03-figures": {
"group": "图形档案",
"description": "Python 源码与可编辑 SVG 的制图工作流。",
"slug": "endor/figures",
"category": "Endor"
},
"04-endor-purpose-draft": {
"group": "项目讨论",
"description": "从程序入口走向用户与 AI 的对象操作。",
"draft": true,
"slug": "endor/endor-purpose-draft",
"category": "Endor"
},
"05-object-graphs-gc": {
"collection": "对象关系与垃圾回收",
"group": "运行时基础",
"description": "从对象引用环到标记清除、分代回收与跨代引用,记录内存管理讨论。",
"slug": "language-and-runtime/object-relations/object-graphs-gc",
"category": "语言与运行时"
},
"06-agent-programming-draft": {
"collection": "Agent 编程",
"group": "研究与提案",
"description": "从指令重解释、组合边界与灵活性取舍,讨论 Agent 编程抽象。",
"draft": true,
"slug": "language-and-runtime/agent-programming/from-instructions-to-composition",
"category": "语言与运行时"
},
"07-composable-judgments-draft": {
"collection": "Agent 编程",
"group": "研究与提案",
"description": "比较 LangGraph 与 DSPy,探索判断对象、依据变化与局部重算。",
"draft": true,
"slug": "language-and-runtime/agent-programming/composable-judgments",
"category": "语言与运行时"
},
"pretext/00-overview": {
"description": "理解高性能文本布局的原理,沿着 12 章教程阅读源码。",
"featured": true,
"slug": "source-studies/pretext/overview",
"category": "源码研究"
},
"language-design/00-overview": {
"description": "从名称与作用域到反射与表示,按 271 项议题逐步讨论语言设计。",
"featured": true,
"slug": "language-and-runtime/language-design/overview",
"category": "语言与运行时"
},
"language-design/02-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true,
"slug": "language-and-runtime/language-design/names-and-scope",
"category": "语言与运行时"
},
"language-design/03-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true,
"slug": "language-and-runtime/language-design/values-and-types",
"category": "语言与运行时"
},
"language-design/04-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true,
"slug": "language-and-runtime/language-design/storage-and-lifetimes",
"category": "语言与运行时"
},
"language-design/05-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true,
"slug": "language-and-runtime/language-design/objects-and-members",
"category": "语言与运行时"
},
"language-design/06-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true,
"slug": "language-and-runtime/language-design/expressions-and-operators",
"category": "语言与运行时"
},
"language-design/07-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true,
"slug": "language-and-runtime/language-design/functions-and-calls",
"category": "语言与运行时"
},
"language-design/08-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true,
"slug": "language-and-runtime/language-design/control-flow",
"category": "语言与运行时"
},
"language-design/09-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true,
"slug": "language-and-runtime/language-design/collections-and-access",
"category": "语言与运行时"
},
"language-design/10-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true,
"slug": "language-and-runtime/language-design/errors-and-resources",
"category": "语言与运行时"
},
"language-design/11-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true,
"slug": "language-and-runtime/language-design/modules-and-execution",
"category": "语言与运行时"
},
"language-design/12-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true,
"slug": "language-and-runtime/language-design/concurrency-and-async",
"category": "语言与运行时"
},
"language-design/13-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true,
"slug": "language-and-runtime/language-design/external-interaction",
"category": "语言与运行时"
},
"language-design/14-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true,
"slug": "language-and-runtime/language-design/reflection-and-representation",
"category": "语言与运行时"
},
"pretext/01-chapter": {
"slug": "source-studies/pretext/text-measurement",
"category": "源码研究"
},
"pretext/02-chapter": {
"slug": "source-studies/pretext/prepare-and-layout",
"category": "源码研究"
},
"pretext/03-chapter": {
"slug": "source-studies/pretext/text-segmentation",
"category": "源码研究"
},
"pretext/04-chapter": {
"slug": "source-studies/pretext/measurement-cache",
"category": "源码研究"
},
"pretext/05-chapter": {
"slug": "source-studies/pretext/line-breaking",
"category": "源码研究"
},
"pretext/06-chapter": {
"slug": "source-studies/pretext/height-only-layout",
"category": "源码研究"
},
"pretext/07-chapter": {
"slug": "source-studies/pretext/long-text",
"category": "源码研究"
},
"pretext/08-chapter": {
"slug": "source-studies/pretext/accuracy",
"category": "源码研究"
},
"pretext/09-chapter": {
"slug": "source-studies/pretext/virtual-lists",
"category": "源码研究"
},
"pretext/10-chapter": {
"slug": "source-studies/pretext/performance-verification",
"category": "源码研究"
},
"pretext/11-chapter": {
"slug": "source-studies/pretext/source-reading",
"category": "源码研究"
},
"pretext/12-chapter": {
"slug": "source-studies/pretext/applying-the-design",
"category": "源码研究"
},
"ai-company-research/01-income-snapshot": {
"collection": "AI 公司研究",
"group": "公司与市场",
"description": "查看14个产品的平台MRR分布、均值与离散度、历史点敏感性和逐项证据入口。",
"slug": "other-research/ai-company-research/income-snapshot",
"category": "其他研究"
},
"ai-company-research/02-coverage-and-gaps": {
"collection": "AI 公司研究",
"group": "公司与市场",
"description": "核对候选池、来源偏差、分类与验证完成度,区分全量读取和定向补充。",
"slug": "other-research/ai-company-research/coverage",
"category": "其他研究"
},
"smalltalk/00-overview": {
"slug": "source-studies/smalltalk/overview",
"category": "源码研究",
"group": "阅读地图",
"description": "Smalltalk-80:从消息到交互系统;依据原书章节整理,区分历史机制与补充说明。"
},
"smalltalk/01-messages": {
"slug": "source-studies/smalltalk/messages",
"category": "源码研究",
"group": "语言核心",
"description": "消息与对象:理解执行模型;依据原书章节整理,区分历史机制与补充说明。"
},
"smalltalk/02-syntax-and-blocks": {
"slug": "source-studies/smalltalk/syntax-and-blocks",
"category": "源码研究",
"group": "语言核心",
"description": "语法与块:读懂一段 Smalltalk;依据原书章节整理,区分历史机制与补充说明。"
},
"smalltalk/03-classes-and-methods": {
"slug": "source-studies/smalltalk/classes-and-methods",
"category": "源码研究",
"group": "语言核心",
"description": "类、方法与元类:行为放在哪里;依据原书章节整理,区分历史机制与补充说明。"
},
"smalltalk/04-object-protocol": {
"slug": "source-studies/smalltalk/object-protocol",
"category": "源码研究",
"group": "对象模型",
"description": "对象协议:身份、复制与依赖;依据原书章节整理,区分历史机制与补充说明。"
},
"smalltalk/05-reflection": {
"slug": "source-studies/smalltalk/reflection",
"category": "源码研究",
"group": "对象模型",
"description": "类的架构与反射:系统怎样描述自己;依据原书章节整理,区分历史机制与补充说明。"
},
"smalltalk/06-collections-and-streams": {
"slug": "source-studies/smalltalk/collections-and-streams",
"category": "源码研究",
"group": "类库与执行",
"description": "基础类库:数值、集合与流;依据原书章节整理,区分历史机制与补充说明。"
},
"smalltalk/07-processes": {
"slug": "source-studies/smalltalk/processes",
"category": "源码研究",
"group": "类库与执行",
"description": "进程与同步:协作执行的对象;依据原书章节整理,区分历史机制与补充说明。"
},
"smalltalk/08-graphics": {
"slug": "source-studies/smalltalk/graphics",
"category": "源码研究",
"group": "交互系统",
"description": "图形系统:坐标、位图与绘制;依据原书章节整理,区分历史机制与补充说明。"
},
"smalltalk/09-live-environment": {
"slug": "source-studies/smalltalk/live-environment",
"category": "源码研究",
"group": "交互系统",
"description": "开发环境:浏览、检查与调试;依据原书章节整理,区分历史机制与补充说明。"
},
"smalltalk/10-mvc": {
"slug": "source-studies/smalltalk/mvc",
"category": "源码研究",
"group": "交互系统",
"description": "MVC 与曲线案例:连接数据和界面;依据原书章节整理,区分历史机制与补充说明。"
},
"smalltalk/11-reference": {
"slug": "source-studies/smalltalk/reference",
"category": "源码研究",
"group": "阅读地图",
"description": "术语、练习与原书对照;依据原书章节整理,区分历史机制与补充说明。"
}
}
}
-133
View File
@@ -1,133 +0,0 @@
# Endor 系统架构
Endor 的概念模型把全球访问、区域运行时和对象数据连接起来:请求从统一入口进入,在合适的区域找到 Nexus Runtime,再由它承载的 Aster VM 执行对象操作。PostgreSQL、Aerarium 和 S3 各自维护独立的分片群,在区域内保持高可用,并由统一备份系统追踪所有区域、所有存储类型的分片与恢复材料。
> **架构说明**:本文按当前讨论绘制目标架构,不表示这些组件和协议已经全部实现。图中的区域、节点、分片数量仅为示例;CDN 和身份服务的接入方式是建议方案。
## 架构总览
![Endor 架构:全球入口连接北美、亚洲及其他区域;各区两个 Traefik 与五个 Nexus Runtime 全连接,每个 Nexus 侧下方配一个 Aster VM;Aerarium 分片群分别与 PostgreSQL、S3 分片群全连接,所有分片汇入统一备份系统](/figures/endor-architecture.svg)
图中绿色/蓝色实线表示请求路径或区域内数据依赖,紫色虚线表示身份控制与公钥分发,蓝色点线表示 CDN 回源,金色虚线表示向统一备份系统采集数据。连线表示可达或依赖关系,**不表示请求向所有节点广播**。
## 本地、自部署与平台挂载
![Endor 部署与对象挂载:两组自部署通过 Nginx 或 Traefik 接入,各自包含一到两个 Nexus Runtime、配套 Aster VM 和 Aerarium、PostgreSQL、S3;自部署对象池可挂载到平台,Windows、Linux、macOS、iOS、Android 上的 Tessera 通过本地 Nexus 挂载本地和远端对象](/figures/endor-deployment.svg)
这张图将部署规模收起来,强调对象可以在哪里运行、从哪里挂载。两组自部署示例分别使用一个和两个 Nexus Runtime,每个 Nexus 都带一个 Aster VM;**每个 Nexus 在示例中配一套简单的 Aerarium、PostgreSQL 和 S3**;这用于表达小规模自部署的完整组成。Aerarium 依赖 PostgreSQL 和 S3,右侧平台则将多区域、运行时和存储分片简化为小型集群。
自部署入口可以选择 Nginx 或 Traefik。如果云服务已经提供合适的托管入口、TLS 和路由能力,就可以省去自行部署网关这一环;仍需配置服务暴露和访问权限。
紫色虚线表示**自部署 Aerarium 可以经授权挂载到 Endor 平台端**:平台获得访问源对象池的逻辑入口,源端仍保留数据归属。它不是将整个 PostgreSQL 或 S3 直接公开,也不隐含自动上传、镜像、备份或数据合并。
下方五种系统都运行 Tessera 前端,并带有本地 Nexus Runtime 和对应 Aster VM。本地对象先挂载到自带的 Nexus;本地 Nexus 也可按需挂载自部署或平台端的远程对象池,让 Tessera 通过本地运行时访问它们。蓝色线路表示可选择的远端连接,不要求所有设备同时挂载所有服务。该图延续本文的目标架构约定,不代表五个平台的运行时集成都已完成;远端身份映射、连接协议、写入权限和断线行为仍需继续定义。
## 全球入口与区域入口
客户端包括浏览器、Tessera、SDK 和 CLI。`endorphin API` 是逻辑上的全球流量入口,尚未在本文规定一个真实的域名。全球调度根据健康、延迟、项目归属和数据放置策略选择区域;不能只选择网络最近的区域而忽略对象在哪个分片。
图中展开北美和亚洲两个区域,并用“ … / etc · 其他区域”表示可以继续扩展。每个区域有自己的负载均衡入口,下面是 Traefik 网关,负责按配置进行 TLS、主机名或路径路由。图中的第二个 Traefik 标记为 **HA 副本**:多副本是生产冗余选项,单实例概念部署不要求两个网关。
### 为什么画成二分全连接
每个区域示例中有两个 Traefik 和五个 Nexus Runtime,画出全部十条边,即二分图 **K₂,₅**。网关使用独立的路由分流图标,与 Nexus 的服务器图标区分。每个网关都能把请求路由到任意一个符合条件的健康 Runtime,避免把一个网关固定绑定到一个运行时。
这是逻辑可达关系。实际转发仍需考虑服务发现、健康检查、项目和对象归属,以及有状态连接的路由策略;Runtime 之间并没有因此形成全连接网状网络。
## Nexus Runtime 与 Aster VM
此处采用 **一个 Nexus Runtime 节点配一个 Aster VM** 的概念约定。Nexus 表示对外接入与运行时承载边界,Aster VM 执行对象相关逻辑。图中每台服务器侧下方单独画出一个 VM 芯片,用点线连接到对应 Nexus;节点与 VM 采用错位排布,保持一对一归属。
请求经过区域入口和 Traefik 到达 Nexus,再定位目标分片。图中运行时下方的连接汇入分片路由层,表示节点可以按归属访问区域分片,**不是**「Runtime 1 只能访问分片 1」。对象身份、远程引用、生命周期与调用语义仍需结合[节点与对象挂载草案](/docs/02-mounting-draft)继续定义。
## 三类存储各自独立分片
PostgreSQL、Aerarium Object data pool 和 S3 是三组独立的分片群。图中每组各展开三个分片,数量可以独立增长,分片边界也可以不同;不能将一个 PG 分片、一个 Aerarium 分片和一个 S3 分片打包成固定“三件套”。
| 分片群 | 概念职责 | 分片标识示例 |
| ------------------------- | ------------------------------------------------------------ | ---------------------------- |
| PostgreSQL | 结构化元数据、对象索引及需要事务约束的状态;具体数据模型待定 | NA-PG-01、NA-PG-02、NA-PG-03 |
| Aerarium Object data pool | 对象数据的逻辑组织、对象池状态和访问层;同时依赖 PG 与 S3 | NA-AO-01、NA-AO-02、NA-AO-03 |
| S3 兼容存储 | Blob、文件、快照和适合对象存储的数据;具体格式待定 | NA-S3-01、NA-S3-02、NA-S3-03 |
### 分片群之间的全连接
每个 Aerarium 分片都能按映射访问任意一个 PostgreSQL 分片,也能访问任意一个 S3 分片。图中左右各画一组 **K₃,₃**:Aerarium ↔ PostgreSQL 有九条边,Aerarium ↔ S3 也有九条边;每区合计十八条存储关系边。
这些线表示逻辑可达关系,实际读写仍根据对象归属、分片映射、授权与健康状态选择目标。它们不表示广播,也不表示所有分片保存相同数据。PostgreSQL 和 S3 之间没有凭空增加直接全连接;两种存储通过 Aerarium 的对象模型与版本映射协作。
运行时访问先进入对象池的分片定位层,再由 Aerarium 按映射访问 PG 和 S3。因此不同层可以采用不同的分片数量和容量规划,而无需一一绑定。
### 区域内高可用
每个存储图标下面有一个小型副本图标,用点线连接,表示本区域的高可用副本。生产部署应跨独立故障域放置,具体可以是不同可用区或独立节点。
这是逻辑 HA 要求,各层的实现不同:PostgreSQL 可以使用主备复制,Aerarium 需要自己的状态协调和故障恢复设计,S3 的耐久性与副本由选定的存储系统提供。图不强制三者采用同一种同步复制机制,也不把单纯增加副本等同于已经实现故障自动切换。
## 一个统一的备份系统
备份是覆盖所有区域的总体系统,不按“北美备份亚洲、亚洲备份北美”分成两个固定系统。它统一编排各区域、各存储类型的备份采集、持久化、校验、保留与恢复。物理副本应隔离源区域的故障风险,实际存放位置由备份策略决定;图中不将该系统固定放进任何业务区域。
图中金色线路将每个区域的三类分片群接入同一备份边界,代表采集组内全部分片。统一恢复目录按“区域 + 存储类型 + 分片 ID”追踪每个分片,避免遗漏或将不同层的编号误当作同一个分片。
### 备份中的 PG、Aerarium、S3 分别是什么
| 备份内容 | 保存什么 | 恢复时的用途 |
| --------------- | -------------------------------------------------------------------- | ------------------------------------------ |
| PostgreSQL 备份 | 各 PG 分片的基础备份、WAL 与可恢复时间点等事务恢复材料 | 恢复结构化元数据与事务状态 |
| Aerarium 备份 | 各对象池分片的状态、索引、对象身份和版本映射等恢复材料,具体格式待定 | 重建对象池,并关联 PG 恢复点与 S3 对象版本 |
| S3 备份 | 各 S3 分片的实际对象版本、Blob、文件及快照 | 恢复被对象池引用的实际字节数据 |
这里展示的是**统一系统内三类不同的备份内容与采集能力**,不是每个备份槽里又放一套在线 PG + Aerarium + S3 服务。系统还要维护统一恢复清单、校验和、重试状态,以及各层的版本对应关系。
恢复某个对象或项目时,清单应能定位所需的 PG 分片恢复点、Aerarium 分片状态与 S3 对象版本。因为三类存储独立分片,恢复集合也可能涉及多个不同编号的分片;不能按固定三件套恢复,也不能只备份其中一种存储就宣称整个对象可恢复。
### “最终一定拿到数据”需要怎样的保证
设计目标是:正常访问优先本区域;本地不可用时,仍能找到异地保存的数据。需要区分两类能力:
- **异地在线副本**:已有服务能读取它;追平状态、授权和路由满足要求后,才可承担故障读取。
- **异地备份**:保留了恢复材料,需要恢复 PostgreSQL、重建 Aerarium 并校验对象版本后才能提供服务,通常更慢。
> **保证边界**:异步复制与定期备份不能无条件保证最后一笔已确认写入不会丢失。若源区在复制完成前永久丢失,未到达异地的数据仍可能丢失。当前图承诺的是“已校验恢复点可恢复”的目标,而非已实现的零数据丢失保证。
要实现更强的承诺,需要明确写入确认条件。例如必须等跨区持久化成功才返回写入成功,或者以独立耐久日志作为重放来源,并设计重试、校验、告警与恢复演练。这会增加延迟或实现成本,不能只靠一条“最终一致”虚线获得。
后续设计需要分别确定 **RPO**(可接受的数据丢失窗口)和 **RTO**(恢复服务所需时间),以及备份保留、删除传播、加密密钥和数据跨区合规策略。
## CDN 应该放在哪里
建议把 CDN 作为面向静态和可缓存内容的边缘路径,与动态 API 调度区分:
1. 客户端请求静态前端、文档或允许缓存的对象,优先命中 CDN;命中后直接返回,无需访问 Nexus。
2. 缓存未命中时,CDN 回源到指定的 S3 兼容存储,或专门的资源服务。图中仅示例回源到亚洲区域的 S3,并不把亚洲设为唯一源站。
3. 私有对象可使用短时签名 URL、受控回源或独立授权服务。缓存键、过期、权限隔离和失效策略需要单独设计。
4. 登录、写入、实时连接和个性化动态 API 默认不进入共享缓存。CDN 能代理这些流量,不代表应该缓存这些响应。
CDN 是可替换、可选的加速层,不是数据库副本,也不承担跨区灾难恢复的唯一责任。
## 身份服务是否依赖 Nexus
**建议在逻辑上独立,不把它画成依赖某个业务 Nexus 的服务。** 客户端向身份服务登录或刷新凭证;Nexus 验证访问凭证、项目归属和操作权限后才接受业务请求。
图中紫色线路表示公钥或认证配置的分发关系:使用适当的签名访问令牌方案时,Nexus 可以缓存 JWKS,在本地完成签名及必要声明校验。具体访问令牌格式尚未确定,不能把 OpenID Connect 的 ID Token 直接当作所有 API 的授权凭证。
如果采用不透明令牌、即时撤销或实时策略评估,可能需要在线内省或授权查询;这会引入身份服务的在线依赖,需要单独设计可用性和失败策略。已有令牌可离线验证也不意味着公钥轮换、过期、撤销和权限变化可以不处理。
身份服务本身仍可以使用 PostgreSQL、缓存和密钥管理等基础设施;**逻辑独立不等于没有存储依赖**。将来即使选择在通用 Nexus 基础设施上托管它,也应保留独立的故障和信任边界,避免启动、登录与业务运行时形成循环依赖。
## 继续确定的设计边界
- 全球入口如何发现项目与分片的归属,跨区域请求采用转发还是让客户端重连。
- Nexus 接入职责与 Aster VM 边界、连接亲和性和 Runtime 故障迁移。
- Aerarium 的数据格式、事务边界、对象版本和跨存储一致恢复点。
- 区域内 HA 的实际协议与跨区域 RPO/RTO、故障读取与恢复流程。
- 身份签发者、访问令牌验证与撤销机制,以及私有内容的 CDN 授权。
## 参考依据
以下资料仅用于解释通用组件的能力,不证明 Endor 已经实现本文的概念设计:
- [Traefik:服务与后端负载均衡](https://doc.traefik.io/traefik/master/routing/services/)。
- [OpenID Connect Discovery:配置发现和 JWKS URI](https://openid.net/specs/openid-connect-discovery-1_0.html)。
- [Amazon S3:同区与跨区异步复制](https://docs.aws.amazon.com/AmazonS3/latest/userguide/replication.html)。本文中的 S3 指兼容对象存储,具体部署并未指定 AWS。
-30
View File
@@ -1,30 +0,0 @@
# 开始使用
Tabularium 是 Endor 的文档工作台。文档与图形源码都保存在同一个仓库。
## 启动
在仓库根目录运行:
```sh
pnpm install --frozen-lockfile
uv sync --project tools/figures --locked
pnpm dev
```
浏览器打开 http://127.0.0.1:3000 。
## 添加文档
在 `content/docs` 下添加 Markdown 文件,例如 `04-runtime.md`。文件名使用小写字母、数字和连字符;第一个一级标题是导航标题。在 `content/catalog.json` 的 `documents` 中以源文件路径登记 `slug` 和 `category`,例如 `slug: "endor/runtime"`、`category: "Endor"`。公开地址为 `/docs/endor/runtime`;源文件编号仅用于排序,不进入公开 URL。既有源文件地址保留永久跳转,修改已有公开 slug 时需另行保留旧地址映射。
开发模式下刷新页面读取修改;本地生产预览需要重新构建。
## 检查
```sh
pnpm check
pnpm start
```
本地开发入口只监听 127.0.0.1。main 推送会在 ces-control 上验证并自动部署,服务器入口默认 18135。相同构建输入复用已验证镜像;源码变化时复用依赖与 Next.js 缓存。Pull Request 只验证,不部署。
-25
View File
@@ -1,25 +0,0 @@
# 节点与对象挂载(草案)
> 状态:讨论草案。以下记录当前设计方向,不代表协议已经实现或接口已经定稿。
## 设计方向
每一个 Aster VM 是一个节点。实例之间可以通过统一接入协议挂载对象。
接入对象来源包含 Endor 官方配件,例如 Tessera GUI,以及通过 Endor Python、C、Go、JavaScript 等 SDK 接入的第三方程序。这两类接入采用同一套协议。
## 待确认的术语
| 概念 | 当前提议 | 状态 |
| --- | --- | --- |
| Node | 一个 Aster VM | 用户提出的模型 |
| Endpoint | 统一协议的连接端点 | 术语提议 |
| Export Root | 对外导出的顶级对象入口 | 术语提议 |
| Tessera 顶级对象 | 协议层称 root,挂载名可为 tessera | 尚未确定 |
## 后续设计问题
- 对象的导出、身份、远程引用与生命周期。
- 方法调用、属性访问和事件传播的语义。
- 授权、断连、错误与重新连接。
- 官方配件和不同语言 SDK 的共同协议边界。
-42
View File
@@ -1,42 +0,0 @@
# 图形与脚本
Python 绘图项目位于 `tools/figures`,使用 drawsvg 输出可编辑 SVG。脚本与输出都保留在 Git 中。
```sh
pnpm figures
```
下图是制图工作流的最小示例,用于验证从 Python 源码到文档图片的完整链路。
![制图工作流:Python 源码生成 SVG,再由本地文档引用](/figures/workflow.svg)
## 扩展方式
在 `tools/figures/src/endor_figures` 下保留各张图的生成逻辑,并在 CLI 中调用。输出目录通过源码位置计算,不依赖运行命令时所在的目录。
CI 会重新生成图片并检查与提交版本一致。Python 依赖保留独立缓存,不进入生产运行镜像。
图形应使用本地字体与资源,并明确标注草案状态。未来可按具体需求加入自动布局或 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-` 图片写在同一段,中间换行但不插入空行,正文会将它们呈现为图组。左右按钮和键盘左右方向键可以切换,下方圆点表示当前图片,也可点击直接选择。图片保留点击放大能力。
图组一次只挂载当前图片,切换后才加载下一张压缩图,不预先下载整组原图。
-337
View File
@@ -1,337 +0,0 @@
# Endor 的意义:从程序入口到对象操作
这篇文档用来逐步整理我们为什么要做 Endor。先记录一个切入点:**把对象的操作权交还给用户与 AI**。后续讨论可以在这里补充、修正,暂不把它写成一份已经定稿的项目宣言。
> **讨论草案**:以下是希望探索的系统组织方式。图中进程和对象的数量、对象关系与操作路径都是示意,不表示当前实现或性能测量。
## Endor:面向 AI 的操作系统
**我们可以把 Endor 理解为面向 AI 的操作系统:以对象池组织工作对象,以 Agent 承载和推进任务。** 用户与 AI 不必始终围绕一个个程序和进程组织工作,而是能够围绕对象、关系和可执行的能力继续处理同一件事。
在这个定位下,我们希望把用户需要面对的进程管理,简化为对 **Object data pool(对象数据池)** 中对象及其关系、状态的管理。工作材料和结果不必只存在于某个程序的私有上下文里;它们可以作为有身份、可引用的对象,由不同入口和任务继续使用。对象池是统一的逻辑组织方式,不要求所有对象都位于同一台机器或同一个物理存储实例。
**Agent 充当以往操作系统中进程在任务层面承担的角色**:承载一项正在进行的工作,组织所需对象与能力,保留相关上下文,并推进执行。用户关注的可以是“这项工作处理哪些对象、进行到哪里、下一步做什么”,而不只是“启动了哪个程序、背后有几个进程”。这些是希望建立的任务与生命周期模型,具体的暂停、恢复、调度和状态保留机制仍需定义。
这是一种上层运行环境的组织方式。对象池负责数据与对象关系,Agent 负责组织任务行为,实际执行仍需要运行时、调度和资源隔离。它们不意味着用存储替代全部执行机制,也不要求 Agent 与底层 OS 进程一一对应;一个 Agent 的工作可以包含普通函数调用,普通计算也不必全部改写成 Agent。
这一定位希望形成的体验是:**对象池构成持续可用的工作世界,Agent 成为其中执行工作的主体,Endor 语言让对象和行为能够被表达、检查与组合。** 用户与 AI 因而可以在同一套对象基础上发起、接续和检查工作。这是本文的设计方向,不表示所有系统能力已经实现。
## 让产出成为检查下一次产出的帮手
![Coding Agent 不断产出相同的原始文件:上方待检查文件积累为积分 dW,单人的检查门保持固定能力 C₀;下方 Endor 只把本次新增文件 dW 送到人与检查门前,处理完成的文件变成结构化蓝色帮手,箭头回到检查门,表示历史成果积累为新的检查能力](/figures/endor-review-gate.webp)
![检查台版:上方单人检查台面对积累的待处理文件;下方只检查本次新增文件,已处理文件变成蓝色帮手,向检查台贡献更多检查工具](/figures/endor-review-desk.webp)
两张漫画用检查门与检查台表达同一个目标,可用左右按钮或下方圆点切换。
上方把 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 的继承机制。
## 一对图:我们希望改变什么
![从程序入口到对象操作的对比:上图中 OS 分出九个进程,按二、四、三个分为三组,对应三个程序;对象以进程内的随机小圆圈表示,用户通过程序入口访问。下图中 OS 只分出少量进程,承载 Endorphin 的对象关系网络,用户与 AI 用多条贝塞尔曲线直接指向选定对象;OS、进程区与 Endorphin 都位于 Endor OS 边界内](/figures/endor-purpose.svg)
### 第一张图:用户面对程序
从 OS 出发,许多进程组成不同的程序。每个程序可以对应不同数量的进程;图中分别画了两个、四个和三个。进程区里的许多小圆圈表示对象,它们散布在各进程中。
用户通常通过程序提供的界面访问能力,也可能通过 Web 或 API。这里想强调的是操作入口:用户先选择程序,再在程序提供的功能中寻找自己要做的事。图中的对象仍然存在,但通常隐藏在程序的组织和入口之后。
这是一种用于对照的简化视角,并不是说现有程序都无法开放对象或让外部系统组合能力。
### 第二张图:用户与 AI 面对对象
仍然从 OS 出发,但这次只画少量进程,用来承载 Endorphin。接下来展开的是许多相互连接的小圆圈:对象不再只作为程序内部的实现细节出现,而成为可以被引用、选择和操作的基本单位。
用户与 AI 的多条曲线分别指向部分对象,表达我们希望提供的能力:**在授权范围内,直接找到所需对象,对它们执行操作,并把不同对象的能力组合起来**。图中没有把所有对象都连向用户,因为操作权应当是有范围、可控制的。
OS、进程区与 Endorphin 被共同框在 **Endor OS** 的边界内。这是此处对面向 AI 的整体运行环境的命名:对象池组织工作对象,Agent 承载任务。图中仍保留底层 OS,具体的内核与运行时职责划分尚需继续定义。
## 为什么程序不该是唯一入口
设想我们要把一段会议录像整理成可执行的项目计划。录像在媒体库里,讨论结论写进文档,行动项录入任务工具,补充资料又放在另一个位置。用户需要把时间点、原话、负责人和截止时间反复搬运,才能让这些程序中的内容对上。
困难不只在于点击次数。任务里粘贴的会议摘要,可能无法跟随原始结论的修改;文档里的一段文字,可能没有保留它来自哪个视频片段的关系。用户知道这些内容属于同一件事,系统却往往只保存了分别属于几个程序的数据副本。
AI 参与时也会遇到这种分割。它可能通过 Web 界面、API 或工具调用取得内容,再把结果写进另一套入口。每个入口都能提供有用能力,但对象身份、关联关系和状态变化不一定在这些入口之间保持连续。一次自动化任务成功,也不等于下一次还能准确找到并更新同一对象。
Endor 想探索的是:录像、片段、结论、任务和负责人之间的关系,能否成为可以继续访问和操作的系统结构,而不是只由用户记住、再通过复制粘贴维持。
已有程序也可以用链接、共享数据库、开放 API 或其他方式解决这些问题。Endor 的意义需要通过更连贯的使用体验证明,不能只靠“现有程序彼此隔离”这个前提成立。
## 什么是用户真正想操作的对象
用户想修改的通常不是某个进程里的内存地址,而是有实际含义的东西:一篇文档、一段视频、一个任务、一组设备,或这些内容之间的联系。
在会议整理的例子里,可以先讨论以下对象与关系。这里列的是业务建模示例,不是已经确定的内置类型。
| 对象 | 用户可能希望执行的操作 | 需要保留的关系 |
| --- | --- | --- |
| 会议录像 | 查看、选择片段、引用内容 | 片段来自哪份录像 |
| 讨论结论 | 修订、核对来源、交给其他流程使用 | 依据哪些片段形成 |
| 行动任务 | 修改状态、指派负责人、调整时间 | 对应哪条结论 |
| 项目计划 | 汇集任务、查看进展、调整组织方式 | 包含哪些任务和依赖 |
一个对象能被多个入口使用,不意味着把它复制成多份互不关联的数据。我们希望同一任务可以出现在项目看板、会议记录和个人待办里,入口变化时仍能辨认它是同一个任务。需要快照或独立副本时,也应明确表达这种区别。
对象的身份、当前名称、显示位置和所属进程不能混为一谈。改名、换一个视图或迁移执行位置,是否改变对象身份,应有明确规则。对象删除、替换或不可访问时,保留的引用又如何表现,也是对象模型必须回答的问题。
这不要求把每个临时计算值都做成一个可联网访问的资源。哪些对象需要独立身份、保存和跨端引用,哪些只在一次执行中存在,需要由类型、生命周期和系统成本共同决定。
## “把操作权交还给用户”是什么意思
这里的操作权,首先是让用户能够理解并使用自己的工作对象:找到目标、保留引用、选择合适的入口,并在获准范围内发起操作。它不仅是把程序界面的按钮换成一个通用调用按钮。
- **可定位**:之后仍能找到同一对象,知道名称与引用指向什么。
- **可理解**:能够识别对象的类型、可用操作和当前状态,不必猜测某个界面动作的含义。
- **可操作**:通过明确的输入和操作契约读取、修改或调用能力,并取得可检查的结果。
- **可组合**:把一个对象或操作结果交给其他获准使用它的能力,保留必要的身份和关系。
- **可选择入口**:根据任务选择不同界面或自动化方式,不因更换入口就被迫重新建立全部数据关系。
这些是体验目标,不表示数据所有权、访问权和操作权天然相同。能查看一项任务,不一定能修改;能调用“提交”能力,也不一定能编辑它的实现或直接修改相关的全部资源。对象属于谁、用户此刻能做什么、程序允许产生什么业务效果,需要分别表达。
“交还”也不意味着消除程序提供的业务约束。例如用户调用一个约定好的提交能力,应遵守它的输入、状态和操作范围。把系统的内部存储或任意内存暴露出来,并不能自动带来清晰的对象操作权。
## 程序在 Endor 中还有什么意义
对象成为共同的操作单位后,程序仍然可以提供有价值的组织方式。项目看板擅长安排任务,视频编辑器擅长选择时间片段,专业业务界面则可以把复杂约束组织成容易理解的工作流。
区别在于,我们希望程序不必成为对象唯一的归属入口。同一任务可以由一个界面创建,由另一个界面展示,再交给 AI 协助整理;这些入口使用同一对象身份和约定好的能力,而不是各自建立一份无法关联的任务。
在这个方向上,程序可以承担几种职责:
| 职责 | 对用户的价值 |
| --- | --- |
| 视图与编辑器 | 针对当前任务选择合适的呈现和交互方式 |
| 专业工作流 | 将多个操作、检查和业务步骤组织成可靠流程 |
| 可复用逻辑 | 把一组能力实现为可引用、可调用的程序结构 |
| 与其他系统连接 | 在外部格式和接口与 Endor 对象之间建立明确映射 |
Tessera 可以作为进入对象世界的一种前端;Web 和其他专业程序也可以提供不同入口。这是希望形成的关系,不表示所有现有程序都已经能直接接入,也不表示一个通用界面可以替代全部专业工具。
## 为什么 AI 需要对象,而不只是更多工具
工具让 AI 能够采取动作,对象则让这些动作有一个可以持续引用和理解的目标。以会议整理为例,AI 不只需要“新建任务”这个工具,还需要知道这次处理的是哪次会议、哪条结论、哪些任务已经存在,以及它们之间有什么关系。
如果每次调用只得到一段文本,AI 可能需要反复从描述中重新识别目标。若结果能够保留对象身份、类型和来源关系,后续操作就有机会继续作用于同一目标,而不必每次重建上下文。
我们希望对象模型能够帮助 AI:
- 根据类型和可访问的能力理解目标,而不只依靠界面位置或名称猜测。
- 把操作结果作为后续步骤的输入,保留其来源和关联。
- 在用户、程序和 AI 之间传递同一工作对象,支持继续处理。
- 将“准备修改什么”和“实际修改了什么”组织成可以让用户检查的结果。
这些能力也可以由设计良好的 API 和工具系统提供。Endor 希望进一步让它们成为语言与运行环境中一致的组织方式,减少每个工具单独定义一套对象标识、结果格式和上下文约定的负担。是否真的减少了集成成本,需要在具体工作流里验证。
对象模型不会自动解决 AI 的理解错误,也不会因为对象有类型就保证操作正确。发现能力、理解业务意图、遵守授权和验证结果仍然是不同问题。
## Endor 语言:让对象成为可编程的共同基础
Endor 的意义不能只停留在界面上可以点击一张对象关系图。开发者、用户编写的逻辑和 AI 生成的程序,需要能够表达这些对象、关系与操作,并把它们交给运行环境执行。**Endor 语言应当是连接对象世界与可执行行为的核心部分。**
现有语言文档使用 **Endorphin** 作为语言名称;本文的“Endor 语言”指这套语言在整个项目中的角色。下面依据现有语言设计稿整理其意义,不把设计约定等同于已经完成的软件能力。
### 统一对象模型,而不只是统一调用入口
现有设计以 Object 为共同基础,把数值、文本、容器、引用、类型定义、代码、函数和运行实体纳入对象体系。类型本身也是对象,由 Object 与 Type 的关系组织实例和类型定义。
这使我们能够讨论同一个工作对象的不同层面:它是什么类型,能被什么结构引用,支持哪些操作,由什么规则管理生命周期。类型的意义也不限于检查一个函数参数是否合法,还包括对象怎样组织、使用和呈现。
“一切皆对象”不能直接推导为“一切对象都独立保存、远程暴露或永久存在”。设计稿用 atom 讨论可独立识别、保存和引用的资源单元;哪些对象需要成为这样的单元,应与具体类型和生命周期规则一起确定。
### 身份、引用与命名,让组合能够延续
用户希望反复操作同一任务,语言就需要区分任务本身与当前拿到它的名字或引用。引用应当使程序能够继续关联目标,而不只是把某次显示得到的文本复制到另一个位置。
现有设计由 workspace 管理资源归属,namespace 提供命名和组织入口。资源身份、命名路径与用户身份承担不同职责;代码引用也不自动改变资源归属或取得授权。
对项目意义而言,这种区分让“跨入口使用同一对象”有了可表达的基础。一个对象可以被不同程序引用,用户不必为了更换呈现方式就重新建立它与其他对象的关系。具体的引用失效、资源回收、命名变更和跨端行为,仍应以语言规范及系统设计逐项落实。
### 函数与类型,表达对象能够做什么
有对象身份还不够,语言还需要表达怎样操作对象。类型描述结构与能力,函数表达输入、计算和结果,使可复用逻辑能够被其他程序引用和组合。
会议整理可以被拆成选择片段、形成结论、整理行动项和建立关联等能力。这里的重点不是预先发明一套内置会议 API,而是让业务逻辑能够用可检查的类型和调用关系表达,而不只能藏在某个界面的事件处理里。
函数本身与调用结果也要区分。用户查看一个函数,不代表已经执行;函数可以被引用,也不代表调用者已经具备执行它的授权。界面、AI 与其他程序使用这些能力时,应共享明确的调用含义。
### Preflight 与 inflight,连接定义和实际执行
现有设计把程序处理分为 Preflight 与 inflight:前者负责理解声明和引用、检查名称与类型并建立关联,后者使用实际输入执行逻辑,管理调用、实例和任务的运行状态。
对于 Endor 的目标,这意味着生成或编辑一段逻辑,与实际执行它应当有清楚的边界。AI 写出一段程序,不应仅因生成了文本就被视为已经就绪或已经执行;它仍需要进入相应的检查和运行过程。
这些是语言设计中的阶段职责,不等于本文已经确认所有编译器、虚拟机和平台路径都实现了同样的语义。语言设计与当前实现之间的对应,需要分别核对。
### Agent 也是程序结构的一部分
在“面向 AI 的操作系统”这一定位中,Agent 是任务的执行主体,承担类似传统进程在任务层面的职责。语言需要能够表达它使用的对象、逻辑和上下文,让任务不只是一段外部聊天记录;对象池则为这些任务提供可以持续引用的工作对象。
现有设计将 agent 与 class、function 一起纳入语言资源体系,讨论工具、记忆、状态与运行控制如何在共有模型中组织。AI 因而不只是从语言外部发送指令,也可以通过语言定义和组合行为。
如果 Agent 能引用已有函数、处理类型化结果并保留与对象的关系,它就有机会成为这个对象世界的参与者。人编写的逻辑与 AI 组织的逻辑可以使用共同的资源与调用约定,便于检查、复用和交接。
语言层面的 Agent 结构并不自动授予运行权限。操作身份、委托、任务恢复时的授权检查等仍有待定义,不能从“Agent 在某个 workspace 中”推导出它能以管理员身份执行。
### 从对象类型到界面呈现
语言设计还讨论客户端根据资源或调用结果的类型选择呈现方式,并用普通函数返回可渲染值来表达界面。这让界面与业务逻辑有机会围绕同一对象模型组织,而不是在前后端之间反复重建对应的数据结构。
同一个任务对象,可以在任务列表中显示摘要,在会议记录中显示来源,在专业编辑界面中展示更多操作。不同入口需要适合各自场景的呈现,同时保留对同一对象的关联。
类型驱动呈现不意味着所有对象都自动拥有完整 UI,也不意味着跨设备共享内存或瞬时一致。呈现契约、本地副本、同步状态和失败行为仍需要具体实现。
### 语言应该承担什么,系统又应该承担什么
语言应当把对象、类型、引用和行为表达清楚,让程序可以检查、组合和执行。Nexus、Aster、Aerarium、存储与通信机制负责支撑相应运行能力;不能仅靠增加一个语法结构就宣称已经解决分布式运行、持久化或故障恢复。
对用户来说,成功的结果应当是更容易表达一件事、继续处理同一对象,并把已有能力组合成新的工作流。语言的价值需要落在这种表达与操作的连续性上,而不只在于少写了几个请求或多了一种声明形式。
语言章节会随设计继续整理。当前依据可查阅:[语言概览](https://gitea.entroia.com/Zayden-Jung/Endorphin-Document/src/branch/main/guide/01-overview.md)、[Object 与类型系统](https://gitea.entroia.com/Zayden-Jung/Endorphin-Document/src/branch/main/guide/07-top-level-types/README.md)、[资源访问与呈现](https://gitea.entroia.com/Zayden-Jung/Endorphin-Document/src/branch/main/guide/11-resource-access-and-presentation.md)和[规范状态](https://gitea.entroia.com/Zayden-Jung/Endorphin-Document/src/branch/main/appendices/specification-status.md)。这些资料记录语言设计,不能作为实现完成度的证明。
## 一个完整的使用故事:从会议录像到项目计划
下面是一条用来检验项目意义的目标工作流,不是现有功能演示。它沿用同一次会议,让对象在用户、程序与 AI 之间连续流转。
### 从分散入口开始
用户打开录像,手工记下重要时间点,把结论复制到文档,再切换到任务工具创建行动项。AI 可以协助转写和提炼,但如果交接只剩几段文本,之后仍需要人工确认每个任务来自哪里、是否重复、对应哪一次修改。
当结论变化时,用户要重新找到相关任务和文档。原有工具可以通过链接和集成改善这个过程;我们需要明确比较的是这些已经可用的做法,而不是只和纯手工操作比较。
### 在对象世界中完成同一件事
1. **找到会议**:用户在 Tessera 中打开会议录像对象,选择需要处理的片段,保留它们与原始录像的关联。
2. **交给 AI 整理**:用户要求 AI 基于这些片段形成结论和候选行动项。候选结果继续关联来源,先供用户检查,不把生成内容直接当成已确认计划。
3. **核对并形成任务**:用户修正结论、去掉无效行动项,再通过约定的程序能力建立任务与来源的关系。
4. **换一个入口继续处理**:同一任务出现在项目看板、会议记录和个人工作视图中。不同入口展示不同信息,但引用的是同一任务。
5. **继续修改**:用户调整任务或结论后,可以沿关系找到受影响的内容。哪些变化自动传播、哪些需要重新确认,由具体对象与工作流规则决定。
6. **交接工作**:另一个获准参与的人或 AI 能够继续引用这些对象,了解其来源和当前状态,而不必仅凭上一轮聊天摘要重建整个过程。
这个故事的关键不是“一次指令自动完成全部工作”,而是中途可以检查、修改、换入口并继续处理。Endor 语言应当能表达其中的对象类型、引用和业务能力,前端负责呈现和交互,运行环境则支撑实际的访问与执行。
## 用户怎样保持控制(试写,待确认)
> **本章是待讨论的体验设想,不是已确认的权限规范。** 语言设计已有 workspace 整体授权、资源编辑与程序使用的区分;下面关注用户应能看懂什么、决定什么。操作身份、Agent 委托、撤销和记录的具体机制仍需设计。
“交还操作权”最终需要体现在用户能够作出的决定上。用户应当知道自己正在查看什么、以谁的身份操作、操作会影响哪些对象,以及什么结果已经发生。仅仅让对象可以被调用,而用户看不清影响范围,并不足以实现这个目标。
### 看懂对象、身份与预期影响
在会议整理的例子里,用户应能分辨 AI 正在处理的是哪次会议、哪些片段和哪些候选结果;也应能区分“生成了一份建议”与“已经创建任务”这两种状态。
对于会改变业务状态的操作,可以探索先展示目标、主要输入和预期效果,再让用户判断是否继续。哪些操作需要确认、哪些可以在既定范围内连续执行,应根据任务与风险讨论,不在这里规定所有动作都弹窗确认。
图中让曲线指向某个对象,不表示每个对象、字段或函数都需要一份独立的用户权限表。现有语言设计以 workspace 为整体授权边界,对象定位、代码可视域、入口允许的业务效果与用户授权应各自清楚,避免在这篇意义讨论中另造一套冲突规则。
### 委托给 AI,仍能区分谁在作决定
一个值得探索的目标是:用户可以将某项工作交给 AI,同时理解这次委托的目标、允许产生的效果和停止条件。AI 能引用对象或生成代码,不应自动获得修改任意对象或管理成员的权限。
委托的具体表示、运行身份如何绑定、长期任务是否重新检查授权、用户怎样收回委托,仍未定稿。这里先记录用户需要的体验,不把某种令牌、权限语法或审批流程提前确定为方案。
### 能检查结果,也能面对失败
用户需要区分未开始、执行中、成功、部分完成和失败。若一个流程只完成了部分任务创建,系统应尽量提供足够信息,让用户判断是否继续,而不是只返回一个无法解释的失败提示。
操作记录和来源关系可以帮助检查,但记录哪些内容、保存多久、谁能查看,仍需要权衡。我们可以讨论撤销、补偿操作或恢复到某个版本的能力,不能假定每次操作都可以回滚:已经发生的外部效果可能无法收回,跨系统执行也未必具有一个统一事务。
### 部署选择与控制之间的关系
本地、自部署和平台端提供了不同的运行与数据放置选择。它们可以帮助用户决定在哪里运行对象、由谁维护服务,但部署在自己机器上并不自动解决授权、备份或错误操作问题。
如果用户希望迁出数据、替换前端或更换运行位置,系统需要有可解释的边界。哪些对象、关系和执行状态可以导出,怎样保留引用与依赖,以及离开某个服务后还能使用什么,都应继续讨论,而不是仅用“支持自部署”代替这些问题。
## Endor 的代价、边界与验证标准(试写,待确认)
> **本章提出评估问题,不是已经达成的性能、兼容性或可靠性承诺。** 我们需要用真实工作流判断对象模型带来的收益,是否值得它引入的复杂度。
### 对象共同基础也会带来成本
| 需要承担的代价 | 为什么会出现 | 应当检查什么 |
| --- | --- | --- |
| 对象身份与引用的维护 | 对象被多个入口长期使用,关系不能只存在于一次调用中 | 改名、替换、删除和迁移后,引用是否仍有可理解的行为 |
| 类型与业务语义的协调 | 多个程序要对同一对象的含义形成约定 | 类型演进与不兼容变化是否使其他入口失效 |
| 跨端状态和网络成本 | 本地与远端对象不共享同一块内存 | 延迟、断线、过期状态和部分失败是否会造成错误判断 |
| 运行环境与运维成本 | 对象的执行、保存和访问需要相应基础设施 | 小规模使用是否也必须承担不必要的部署负担 |
| 学习和集成成本 | 开发者需要理解新的语言与对象模型 | 完成同一业务是否比已有语言、API 和工具增加更多工作 |
| 共同基础的依赖风险 | 多个入口可能依赖相同运行时与对象服务 | 故障影响范围、版本兼容和退出路径是否明确 |
图里更少的进程只是组织方式的示意。实际的内存、启动时间、吞吐量、隔离性和故障影响需要分别测量;进程少不天然意味着成本低,也不证明系统更可靠。
### 哪些事情不应勉强统一
专业程序可以保留适合自己的内部结构和交互方式。对于已有明确接口、成熟工作流或特殊执行约束的系统,建立适度的连接也许比全部迁入新的对象体系更合适。
对象关系不应抹平真实边界:一个远端对象可能不可达,一个业务操作可能被拒绝,一次结果更新可能尚未传播。语言可以帮助表达和处理这些情况,不能把它们变成“看起来像本地调用,因此一定立即成功”。
Endor 也不应仅凭对象之间可连接,就宣称用户可以无损迁移全部程序、消除所有格式转换或让 AI 理解全部业务。需要保留什么、转换什么、失去什么,应在具体对象和连接机制中说明。
### 用同一条工作流验证意义
可以先用前面的会议整理故事做一个范围有限的原型,对比现有工具的正常用法与 Endor 原型。两边使用同一份材料、同样的目标和相近的辅助条件;如果现有工具已经有良好的链接与自动化能力,也应纳入比较。
| 想验证的主张 | 可以观察的结果 |
| --- | --- |
| 减少重复组织数据 | 从录像到任务,需要多少次重复录入、复制和人工重建关联 |
| 保留同一对象与来源 | 换入口或重新进入流程后,能否准确找到原任务及其依据 |
| 支持人和 AI 交接 | 人能否修正 AI 的候选结果,AI 能否在修正后继续处理正确目标 |
| 操作效果容易理解 | 用户能否判断哪些只是建议,哪些已经改变业务状态 |
| 组合能力值得复用 | 加入第二种视图或工作流时,需要新增多少适配逻辑 |
| 失败时仍可继续 | 遇到断线、拒绝访问或部分完成后,能否辨认状态并继续工作 |
这些观察需要同时记录用户体验、实现工作量与运行成本。不能只比较演示中最顺利的一次,也不能把更多后台复杂度隐藏起来,再把少几个界面步骤当成全部收益。
### 什么结果意味着需要调整方向
如果保留对象身份仍然离不开大量手工映射,换一个界面仍然要复制一套数据,或者简单任务必须先部署过多基础设施,那么当前设计还没有兑现它的意义。
如果 AI 操作对象之后更难解释结果,或开发者为了接入对象模型付出的工作远高于已有接口带来的成本,也应调整对象粒度、语言边界或集成方式。
这里先不设任意的数值门槛。第一步应是做出可重复的具体工作流,记录收益和问题,再决定哪些目标值得继续投入,哪些假设需要修改。
## 继续整理的方向
这一版先把程序入口、对象、操作权、AI 与语言之间的关系串起来。后续可以用真实工作流修订这些描述,进一步确定对象身份、执行与挂载的语义。
[从给 AI 写教程,到组合 AI 能力](/docs/language-and-runtime/agent-programming/from-instructions-to-composition)进一步记录指令重解释、组合边界与灵活性的三个问题;[LangGraph、DSPy 与可接续的判断对象](/docs/language-and-runtime/agent-programming/composable-judgments)比较现有探索,并提出依据变化后复核与局部重算的原型方向。两篇均为讨论草案,不表示现有语言或运行时已经实现。
[系统架构](/docs/00-endor-concepts-draft)讨论部署与存储,[节点与对象挂载](/docs/02-mounting-draft)讨论运行位置和挂载边界;本文关注它们为何能给用户带来值得验证的变化。
-148
View File
@@ -1,148 +0,0 @@
# 对象关系、循环引用与分代垃圾回收
记录日期:2026 年 10 月 28 日。本文整理一次从“继承是有向无环图,那么 object 的所有关系可以有环吗?”展开的讨论,依次解释对象图、追踪式垃圾回收和分代回收。这里记录通用原理与其他运行时的例子,不代表 Endor 已确定或实现了某种内存管理方案。
## 继承无环,不等于整个对象图无环
**是否允许成环,要看边代表什么关系。** 把所有关系都画在同一张图上,会掩盖不同关系的约束。
| 关系 | 节点与边的含义 | 是否允许有环 |
| --- | --- | --- |
| 类继承 | 类指向父类 | 通常禁止;单继承形成森林,多继承可形成 DAG |
| 原型委托 | 对象沿原型链查找属性 | JavaScript 普通对象的原型链禁止形成环 |
| 普通强引用 | 对象字段指向另一个对象 | 允许,例如父子反向引用、双向链表、社交关系 |
| 所有权或包含关系 | 生命周期由谁管理、谁包含谁 | 由模型规定;树形包含通常限制成环 |
继承的无环约束让“父类”构成可排序的层次,便于定义类型关系、成员查找和初始化顺序。不能简单地说“有环必然让查找无限递归”:查找算法可以检测环,但语言仍可以为了保持继承层次的语义而禁止环。不同语言的检测时机也不同,不一定都在语法或编译阶段。
普通字段引用则可以这样成环:
```js
let parent = { name: "Parent" };
let child = { name: "Child", parent };
parent.child = child;
// parent → child → parent
parent = null;
child = null;
// 假设没有其他强引用,两个对象组成的环现在不可达。
```
双向链表相邻节点的 `next` 和 `prev` 也构成引用环;这不表示“沿 next 前进”一定会回到起点。讨论图是否有环时,需要明确观察哪些边。
JavaScript 对普通对象原型环的限制,可见 [ECMAScript 的 OrdinarySetPrototypeOf 算法](https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-ordinarysetprototypeof)。
## 循环引用影响哪些操作
对图进行递归遍历时,需要用已访问集合等方式防止重复处理。序列化也必须决定怎样表示对象身份和回边。
原生 `JSON.stringify()` 在实际遍历到循环引用时会抛出 `TypeError`,因为 JSON 本身没有表示对象引用的机制。可以按业务需要输出对象 ID,再用独立的关系表记录连接;不能简单丢弃回边后仍声称完整保存了对象图。相关规则见 [ECMAScript 的 SerializeJSONObject 算法](https://tc39.es/ecma262/multipage/structured-data.html#sec-serializejsonobject)。
内存管理上的关键区别是:
- **单纯的引用计数**只看入引用数量。没有外部引用的环内部仍彼此计数,无法仅靠“计数降到零”释放。
- **追踪式 GC**从根出发判断可达性。孤立环没有来自根的路径,可以被回收。
- 引用计数也可以搭配循环检测。CPython 就以引用计数为基础,另有循环垃圾收集器,不能把它直接归为与 V8 相同的标记清除实现。见 [Python 的 gc 文档](https://docs.python.org/3/library/gc.html)。
环并不必然造成内存泄漏。反过来,即使对象关系无环,只要长期缓存等根可达对象一直引用着已经不需要的数据,追踪式 GC 也不会替程序判断它们“业务上已经没用”。
## Tracing GC 如何回收孤立环
Tracing GC(追踪式垃圾回收)把从根沿强引用可达的对象视为需要保留的对象。根可以包括执行栈中仍然存活的引用、全局环境,以及宿主运行时持有的句柄;具体集合由实现决定。源代码里存在一个变量名,并不等于它在任意时刻都一定是 GC 根。
下面用概念上的对象图说明一次回收:
```text
根 R ──→ A ⇄ B 保留:A、B 可从根到达
C ⇄ D 回收候选:C、D 彼此引用,但根到不了
```
### 标记:从根寻找可达对象
最简单的工作队列算法如下。这里假设程序暂停、只有普通强引用,忽略弱引用和终结器处理。
```text
marked = 空集合
worklist = 根所指对象
while worklist 非空:
object = 取出一个对象
if object 已在 marked 中:
continue
把 object 加入 marked
把 object 的所有强引用目标加入 worklist
```
**先记录访问状态,再继续展开引用**,就能避免在 A → B → A 中无限循环。标记结束后,集合中正好包含本次模型下所有根可达对象。工程实现通常使用位图和工作队列等结构,不要求给每个对象增加一个布尔字段。
### 清除:回收未标记对象占用的空间
Mark-and-Sweep(标记清除)是追踪式 GC 的一种实现:完成标记后,遍历管理的堆空间,把未标记对象占用的空间变为可供分配的空闲空间。上图中 C、D 都未被标记,因此环不会妨碍回收。
清除并不保证立刻把内存归还操作系统;运行时可以先保留空间供后续分配。标记状态如何为下一轮重置也是实现细节。
标记清除可能留下不连续的空闲块。Mark-Compact(标记整理)通过移动存活对象并更新引用来减少碎片;移动和整理不一定覆盖每个页面,也不一定每轮执行。V8 的说明可参见 [Orinoco 概览](https://v8.dev/blog/trash-talk)和[标记与清除实现文档](https://github.com/v8/v8/blob/main/docs/heap/marking-and-sweeping.md)。
## 分代回收为什么更快
分代回收利用弱分代假说:在许多程序中,大量对象分配后很快就变得不可达。它是关于工作负载的经验规律,不是所有程序都满足的定理。“活得短”指对象很快不再可达,而不是函数一返回,对象就必然死亡;返回值、闭包和缓存都可能延长对象的生命周期。
运行时把对象按年龄等信息分组,让年轻代更频繁地回收,以减少每次检查整个堆的开销。
| 方面 | 年轻代 | 老年代 |
| --- | --- | --- |
| 常见对象 | 刚分配、尚未晋升的对象 | 已晋升的长期存活对象;部分对象可能直接分配到相关空间 |
| 常见存活特征 | 大量对象很快不可达 | 经筛选后存活率往往较高 |
| 回收思路 | 经常进行较小范围的回收 | 较低频率地进行更大范围的回收 |
| 可能的算法 | 复制、标记清除等 | 标记清除、选择性整理等 |
老年代并不意味着“对象数量少”;长期缓存和不断增长的数据结构都可能使它很大。Minor GC 通常只处理年轻代,Major / Full GC 的具体覆盖范围取决于收集器;不能理解成 Major GC 必然只看老年代。
### 复制回收:只搬运存活对象
经典半空间模型为年轻代准备两个空间。一次回收时,把含有待处理对象的空间称为 From,把接收存活对象的空间称为 To:
```text
回收前 From: [活 A][死][死][活 B][死] To: [空闲]
回收后 From: [整体可复用] To: [活 A][活 B][连续空闲]
```
收集器从根和跨代引用入口出发,把存活对象复制到 To 或晋升到老年代,并修正引用。转发信息保证同一个对象不会被重复复制,也保留共享引用和环。之后两个空间轮换角色;文献在分配前后使用 From / To 名称的时机可能不同。
当存活对象少时,需要搬运的数据也少,而且目标空间天然紧凑。不过要付出备用空间、引用扫描和复制的代价。分代不等于复制:V8 除 Scavenger 外也提供年轻代 Minor Mark-Sweep 路径。见 [V8 GC 文档](https://github.com/v8/v8/blob/main/docs/heap/garbage-collection.md)。
### 晋升:年龄和空间策略共同决定
年轻对象经过回收仍存活,可以按收集器策略晋升到老年代。年龄、目标空间容量、对象大小或页面存活率都可能影响决定。
“熬过两次回收”可以作为某些实现的教学例子,但不是分代 GC 的定义;“To 空间超过 25% 就晋升”也不应作为所有 V8 版本和所有收集器的固定规则。堆大小和阈值需要结合具体版本、配置与工作负载理解。
## 只回收年轻代,如何处理老年代到年轻代的引用
假设根到年轻对象的唯一路径经过老对象:
```text
根 ──→ 老对象 O ──→ 年轻对象 Y
```
如果 Minor GC 既不扫描整个老年代,也不知道 O → Y 这条边,就可能漏掉 Y。常见解决办法是把跨代引用信息记录下来:
1. 程序执行 `oldObject.child = youngObject` 等引用写入时,**写屏障**执行附加的记录逻辑。
2. **记忆集**记录需要重新检查的引用位置,或通过卡表等结构记录包含这些位置的区域。它是收集器维护的元数据,不要求是老年代里的一块固定“小区域”。
3. Minor GC 从根与这些跨代引用入口追踪年轻对象,无需每次完整扫描老年代。扫描入口时还需要检查记录是否仍指向年轻代。
为保证正确性,初始化、晋升等过程产生的新跨代引用也必须由实现维护。写屏障还可能服务于并发或增量标记,不只用于分代记录。V8 的相关说明见[标记与清除文档中的写屏障部分](https://github.com/v8/v8/blob/main/docs/heap/marking-and-sweeping.md)。
记忆集允许保守保留对象:即使 O 已不可达,只要其相关记录本轮仍被作为入口处理,Y 就可能在这次 Minor GC 中暂时存活,等后续更大范围的回收再清理。因此,“不可达对象可以回收”不意味着每次局部回收都会回收所有不可达对象。
## 对 Endor 后续设计的启发
本次讨论留下的设计问题是:应分别规定继承边、普通引用边和所有权边的约束,再决定运行时怎样管理对象生命周期。
- 若普通对象允许互相引用,遍历和持久化需要处理环与共享对象身份。
- 若采用引用计数,需要决定如何处理循环,例如弱引用、显式断环或附加的循环检测。
- 若采用追踪式 GC,需要定义根、可扫描的引用字段,以及弱引用等特殊关系的处理方式。
- 若采用分代策略,需要维护跨代引用,并通过实际对象存活率与暂停时间测量判断收益。
分代回收通常能减少平均回收工作,但不保证没有暂停,也不保证对所有负载都最快。并行、并发和增量执行是另外的优化维度。这些问题应作为后续方案评估项,不能据此推断 Endor 已采用 V8 的方案。
@@ -1,83 +0,0 @@
# 从给 AI 写教程,到组合 AI 能力
记录日期:2026 年 10 月 28 日。本文保存关于 Agent 编程抽象的讨论,重点是指令的重新解释、能力的组合边界,以及约束与灵活性的取舍。
> **讨论草案**:文中的分析和候选机制尚未成为 Endor 语言规范,也不表示 Aster、Hestia 或其他组件已经实现。对外部项目的事实描述附官方来源;设计判断与后续提案分别说明。
## 我们真正追问的是什么
为什么很多 Agent 扩展仍像是给 AI 的教程?传统编程发展出函数、对象、函数式组合和状态管理方法,Agent 是否也应该发展出可以理解、测试和组合的能力单元?
**关键区别在于:代码中的抽象有运行机制支撑,而很多 Agent 抽象的效果,仍然依赖模型理解并遵守一段话。**
函数的局部变量受到作用域规则约束,模块可以控制导出内容,React Hooks 的状态与调用有框架机制支撑。这些机制并不能自动证明业务逻辑正确,却让开发者有明确的执行规则可依赖。
如果在 skill 中写“这段规则只对当前任务有效,不得影响其他任务”,它首先还是一条需要模型遵守的指令。只有宿主系统真正隔离了上下文、存储或权限,才形成相应的执行边界。**把一段话放进文件夹,获得了组织和复用能力,却不等于获得了封装。**
这里不能把 skill 与 MCP 完全等同:Agent Skills 用 Markdown 指导、可选脚本和资源打包任务知识;MCP 还规范工具发现、调用、资源与消息交换,具有真实的协议机制。但协议不替宿主决定模型怎样组织推理。[Agent Skills 规范](https://agentskills.io/specification)、[MCP 架构](https://modelcontextprotocol.io/docs/learn/architecture)
## 第一,结构在每次推理中被重新解释
自然语言把大量原来需要开发者提前定义的步骤,交给模型临时组织。用户说“检查合同,找出对我不利的条款”,模型能够补出搜索、比较、解释等步骤,不必先穷举每个分支。这使教程式扩展具有实际价值:便宜、灵活,也能承载尚未形式化的经验。
**但代价是,结构没有明确保存在程序里,而是在每次推理时由模型重新解释。因此,增加一个 skill,可能改变另一个 skill 的使用方式;换一个模型,原来有效的指令可能表现不同。**
这里说的是仅由自然语言承担的结构。skill 随附脚本、宿主的工具权限和明确的数据流,仍可以由普通代码实现硬边界,不能把整个 Agent 系统都说成没有结构。
需要区分三件事:指导内容是否被正确加载,模型是否按预期理解,执行环境是否强制实施约束。改进第一件事,不自动完成后两件事。
一个值得探索的方向是把“能力的公开输入与输出”“本次可见的数据”“允许产生的外部效果”显式表示。自然语言继续描述意图,系统承担能够检查的边界。模型升级时,对相同任务重新评估,而不是假定文件格式相同就行为兼容。
## 第二,AI 能力还缺少容易验证的组合边界
假设有三个能力:
1. A:查找资料。
2. B:判断资料是否可信。
3. C:写成报告。
把它们串起来很容易。难的是确保:
- B 拒绝的资料不会又被 C 使用。
- C 能区分“原文事实”和“模型推断”。
- A 没找到证据时,后续不会把空缺补成事实。
传统代码也不能自动保证业务正确,但可以控制显式数据流。例如 C 只收到 B 允许传递的证据引用,检索工具也只能读取这一集合。如果三个阶段只是向同一个上下文添加文字,“通过”和“拒绝”可能都只是模型看得到的文本。
**模块之间存在名称上的边界,却未必存在执行上的边界。**
可验证边界不等于“保证模型不知道被拒绝的事情”。模型可能在预训练中已经接触过相关信息,也可能凭一般知识推断。可实现的约束是限制本次输入、检索权限和可接受输出;要求报告中的事实主张指向获准证据,再检查关联是否有效。证据是否真正支持主张,仍需要独立评估或人工判断,不能只检查引用 ID 存在。
还要区分“来源可信”和“支持某个结论”:一份可靠文件可以被错误解读,也可能对另一个问题毫无证明力。审核应该说明对象、版本、问题范围与判断依据。
## 第三,把 AI 约束得足够明确,又可能损失它最有价值的灵活性
把流程完全固定成状态机,可以获得更明确的行为,但也要求开发者提前定义更多场景。反过来,让模型自由决定,就重新依赖它对指令的解释。
这不是无解,而是需要找出:哪些地方应该由系统确定,哪些地方值得留给模型判断。不同任务的答案还不一样,因此很难很快出现一套统一范式。
固定状态机只是取舍的一端。图执行也可以动态分支,模型也可以在受限环境中自主选择路径;不能把所有框架描述成僵硬流程。
一个候选划分是:**把可用材料、权限、预算和结果接纳条件固定下来,把寻找解法的路径留给模型。** 模型可以换检索策略、拆分问题和请求更多材料,但不能通过修改提示词给自己新增写权限,也不能把尚未审核的结果自行标成业务事实。
这仍需要具体任务检验:接纳条件过强可能拒绝有用结果,过弱可能放过错误;某些开放式创作没有唯一验收答案,应保留人工评价和不确定状态。
## 从指导语句到可执行的规则
| 常见指导 | 可以落实的机制 | 仍未解决的部分 |
| --- | --- | --- |
| 不要把资料当成指令 | 分开传递资料与控制配置,资料不能授予工具权限 | 提示注入仍可能影响内容判断,不能只靠分隔符解决 |
| 只记住当前项目 | 按项目划分存储与检索范围,读取时检查身份 | 共享模型本身不因此遗忘已有知识 |
| 交给另一个 Agent 做 | 显式传入对象、权限、预算和返回契约 | 子任务是否理解业务目标仍需评估 |
| 确认以后才能发布 | 发布接口检查有效审批及对应内容版本 | 人的审批是否充分,不能由接口自动证明 |
| 每个结论都要有证据 | 结论关联证据版本并检查引用有效性 | 证据是否充分、结论是否成立仍需判断 |
这些机制有些可以直接用现有语言实现。研究重点应是把它们变成一致、可复用的运行约定,而不是只增加新术语或语法。
## 对 Endor 的意义
Endor 已在设计讨论中把对象池、对象身份和 Agent 任务联系起来。这为表达证据、判断和结果之间的关系提供了切入点,但不能从“一切皆对象”推导出隔离、授权、持久化和业务正确性已经成立。
下一步可以讨论:一个判断是否应成为关联输入版本、检查记录与适用范围的对象?依据变化时,下游工作怎样知道需要复核?同一能力更换模型后,哪些行为需要重新验证?
更深入的外部方案比较、候选机制与原型验收,见[LangGraph、DSPy 与可接续的判断对象](/docs/language-and-runtime/agent-programming/composable-judgments)。项目整体定位见[Endor 的意义](/docs/endor/endor-purpose-draft)。
@@ -1,180 +0,0 @@
# LangGraph、DSPy 与可接续的判断对象
记录日期:2026 年 10 月 28 日。本文从[Agent 编程的三个问题](/docs/language-and-runtime/agent-programming/from-instructions-to-composition)继续讨论:已有探索为什么有价值,边界在哪里,以及 Endor 可以验证什么。
> **研究与设计草案**:官方机制、本文的分析和候选设计在各节中区分。下文没有确认新语言语法、对象内置类型或权限规范;没有修改现有 271 项语言设计议题的确认状态,也没有实现运行时功能。资料按本文日期查阅,具体 API 会演进。
## 两条路线分别解决了什么
LangGraph 把状态、节点和转移显式组织起来;DSPy 把模型任务表示为签名和模块,并根据指标优化程序。它们分别使执行过程更可管理、模型行为更可测量。它们也可以组合使用,并不是相互排斥的完整替代品。
| 维度 | LangGraph | DSPy | 本文进一步提出的问题 |
| --- | --- | --- | --- |
| 组织单位 | 状态、节点、边与子图 | 任务签名、模块与模型程序 | 一项判断能否跨流程保留身份、依据与适用范围 |
| 关键进步 | 显式组织路径、状态与恢复 | 分离任务声明、调用策略与优化 | 如何接纳、复用及作废一个结果 |
| 可获得的检查 | 图结构、状态约束、应用代码检查 | 输出结构、约束验证、数据集上的指标 | 证据版本、权限、业务条件与变化后的复核 |
| 不会自动成立的事情 | 节点执行过不代表业务判断正确 | 得分提高不代表每次都满足真实需求 | 有来源也不代表内容真实或推理正确 |
## LangGraph 为什么好
### 从聊天历史走向明确的执行状态
官方 Graph API 把工作流组织为 State、Nodes 和 Edges,并用 reducer 规定更新怎样合入状态。它还支持输入、输出及内部状态的不同组织方式。这让开发者可以明确决定数据传到哪里,而不必把所有东西拼接成一段聊天。[Graph API](https://docs.langchain.com/oss/python/langgraph/graph-api)
其价值是把一部分程序结构从模型的临时理解中拿回来:例如审核节点产生的允许集合,由普通代码选择后再提供给写作节点。
### 让中断与长期工作可以被管理
checkpointer 保存线程的图状态,store 保存跨线程数据;持久化实现的选择决定重启后是否还能接续。interrupt 支持等待外部输入,恢复时可能重新执行节点开头的代码,因此调用者必须处理重入和副作用。[Persistence](https://docs.langchain.com/oss/python/langgraph/persistence)、[Interrupts](https://docs.langchain.com/oss/python/langgraph/interrupts)
这是真实的执行能力,不只是“请记住进度”的指导。它让人可以在工作中间介入,而不必完全重开任务。
### 保留动态决策空间
LangGraph 有条件转移、动态路由和子图,节点里也可以包含开放式 Agent。不能把它批评成“只能预先画死全部步骤”。其强项在于让确定性逻辑和模型判断出现在一个可组织的执行过程里。[Graph API 的 Command](https://docs.langchain.com/oss/python/langgraph/graph-api#command)
## LangGraph 为什么还没有回答完这个问题
以下是对职责边界的分析,并不是说 LangGraph 无法用应用代码实现这些能力。
**流程状态不自动等于业务世界的有效状态。** checkpoint 可以保存“审核完成”,但被审核文件之后可能已经修改,或者用户已经撤回了授权。恢复执行时是否重读、复核和拒绝过期结果,需要应用定义。
**状态结构不自动等于信息安全边界。** 节点输入过滤有实际价值,但不能仅凭字段叫 private 就假设它不会进入日志、流输出或其他路径;数据访问、工具访问和输出通道仍需检查。一个图也可以做严格隔离,只是隔离不能由图形名称推导。
**检查节点不能天然证明上游结论。** 模型审核也可能犯错,与生成者共享偏差。增加一个 reviewer 节点是一种策略,实际效果要用独立样例和人工核验评估。
**跨任务的判断复用需要共同约定。** 两张图都产生“供应商通过审核”,是否针对同一公司、同一资料版本、同一政策和时间范围?这些业务关系可以加入 state 和数据库,但各应用往往还需自行定义。
因此,应继续利用图的执行能力,同时把跨流程可用的结果关系设计清楚。新做一个流程编辑器,未必解决这些问题。
## DSPy 为什么好
### 把任务声明与求解策略分开
Signature 声明输入、输出及任务意图;Module 组织调用策略。同一任务可以更换求解方式而保留接口。这让“我们要什么结果”和“模型如何尝试完成”更容易分别修改。[Signatures in depth](https://dspy.ai/3.4.1/diving-deeper/signatures-in-depth/)
例如,可以把证据列表到候选结论表示为一个模块,在相同输入集上比较不同策略,而不必重写整个应用。
### 让改进从手调措辞走向可测量的优化
DSPy 的优化器接受程序、指标和样例,根据反馈调整指令、示例,某些优化路径还涉及模型权重。这让提示词成为可优化的实现,而不是只能凭直觉维护的字符串。[DSPy 优化器官方文档源码](https://github.com/stanfordnlp/dspy/blob/main/docs/docs/learn/optimization/optimizers.md)
真正值得借鉴的是对“好不好”的显式追问:必须给出数据和评估方法,才能比较优化前后的程序。
## DSPy 为什么还没有回答完这个问题
**结构类型和业务成立条件不同。** 输出符合字段与类型要求,不说明结论受证据支持。“供应商可靠”可以是合法字符串,却没有包含可靠性的范围、依据和时效。增加业务字段有帮助,字段内容仍需要检验。
**指标是目标的可测量代理。** 如果只奖励引用数量,系统可能更愿意多加引用,而没有提高引用的支持程度。需要分别评估正确性、覆盖、证据支持、拒答、成本和耗时,并使用独立保留样本,避免只追逐一个总分。
**离线优化与在线条件之间存在距离。** 真实对象会变化,工具可能失败,权限可能收回,用户目标也会调整。验证集上的改进不能自动替代每次行动前的状态与授权检查。
**换模型后的可替换性仍需验证。** 接口兼容不等于行为兼容;优化产物需要连同模型配置、数据集与评估记录版本化。更强模型可能减少某些提示策略的必要性,也可能改变结果分布。
这些并不削弱 DSPy 的价值。它提供了进行比较和优化的机制;把它当作业务真理的证明器,才是越过了其能力边界。
## 我们可以补上的一层:让判断成为可接续的对象
候选方向是让 Agent 产出的判断,成为后续程序可以检查、组合和更新的工作对象。重点不在新的提示词容器,而在输出被其他任务使用时仍保留必要关系。
一项判断可以包含以下信息。字段仅用于原型讨论,不是已确认的内置类型。
| 信息 | 例子 | 能检查什么 |
| --- | --- | --- |
| 主张与范围 | 供应商 A 满足本次采购的材料要求 | 区分不同问题,避免把局部结论扩成全面认可 |
| 输入对象及版本 | 营业文件 v3、报价单 v8 | 判断产生时实际使用了哪些材料 |
| 证据关联 | 某项要求对应文件的哪些位置 | 引用是否存在,是否为允许使用的材料 |
| 判断性质 | 原文摘录、计算结果、推断、人工确认 | 不把不同可靠性来源压成一个字符串 |
| 生成与检查记录 | 模型配置、程序版本、检查规则与结果 | 复核、模型替换和实验追溯 |
| 适用条件 | 政策版本、对象版本、复核期限 | 条件变化时识别需要重新检查的结果 |
| 后续使用关系 | 报告段落、采购建议、候选任务 | 沿已记录依赖定位可能受影响的工作 |
这里的“可接纳”始终相对于明确政策与检查过程,不命名为无条件的“已证实事实”。模型自报的置信度也不能当作校准后的正确率。
### 把研究、提案与真实修改分开
模型可以自主探索、重试和比较方案,但先在隔离的候选工作区里产生结果与变更意图。准备提交时,由运行环境检查材料版本、当前授权、接纳条件和影响范围,再实际调用修改能力。
候选工作区可以采用写时复制、事件记录或暂存对象实现,选型尚未确定。它不是整个互联网的快照,也不能回滚已经发出的邮件或外部付款。外部读取可能计费或写日志,也应纳入效果清单;无法模拟的动作需要单独定义提交方式。
## 一个更有吸引力的体验:修改依据,工作能够局部更新
把前文的 A 查资料、B 审核、C 写报告,继续推进到一个用户可感知的场景:
1. AI 用三份资料形成报告,每个需要依据的结论关联具体来源版本。
2. 用户发现第二份资料有误,将其从本次允许集合移除。
3. 系统沿记录的依赖标出相关结论、报告段落和待提交任务;旧结果保留为历史,但不再作为当前有效依据。
4. Agent 可以寻找替代证据、缩小结论或报告材料不足;与这份资料无关且仍满足条件的结果可以复用。
5. 用户看到需要复核的内容、新旧结论和候选变更,确认后再更新正式对象。
**用户感知到的变化是:改一份依据,就能知道哪些判断需要复核,并接着完成这些工作。** 对开发者而言,可复用单元从一段生成文本,变成带输入、条件和检查记录的判断过程。
这借用了电子表格重算、增量构建、数据血缘和版本控制的成熟思想,不声称发明了依赖跟踪。需要验证的新价值是:这些关系能否在用户、模型与不同流程之间共同使用,减少每个应用手写一套更新逻辑的成本。
### 必须承认的限制
- 系统只能沿已记录的依赖传播失效,不能自动理解并发现全部语义影响。模型偷偷使用了未记录输入时,依赖图会不完整;原型应限制数据入口并记录读取,必要时保守地复核整个产物。
- “读取过”不等于“真正依赖”。粗粒度跟踪可能重算过多,细粒度声明可能漏记,需要衡量漏失效与过度失效。
- 版本变化只说明旧条件不再完全匹配,不代表旧结论必然错误。应标为待复核,而不是宣称系统已发现事实错误。
- 重算不保证得到相同结果。回放历史记录与重新调用模型是两回事,外部服务和模型行为也可能变化。
- 本地候选分支可丢弃,不等于全部业务效果可撤销。跨系统提交需要各自支持版本检查、幂等或补偿,否则必须暴露部分完成状态。
## 怎样形成真正的组合边界
下面的概念接口只表达职责,不是 Endor 已有或拟定语法,也不是可运行代码。
```text
查找(问题, 允许的数据源) -> 资料引用与版本 | 材料不足
审核(资料, 问题, 审核政策) -> 本次允许的证据集合 | 需要人工判断
生成(问题, 允许的证据集合) -> 候选结论与证据关联
接纳(候选结论, 检查政策) -> 接纳记录 | 拒绝原因 | 无法确定
准备(接纳记录, 目标对象版本) -> 候选变更
提交(候选变更, 当前授权, 版本前置条件) -> 实际效果记录 | 冲突 | 部分完成
```
这些阶段不要求固定成六次模型调用;查找与生成可以动态循环,审核也可以由确定性规则、模型和人工共同完成。受限的是数据与效果边界,解题路径仍可自主选择。
“接纳记录”不能只是让模型在 JSON 中填写 `accepted: true`。原型可让受信任的检查服务生成记录,并在每次使用时验证记录所属项目、对象版本、政策与有效期。它能证明检查过程发生过,不能证明自然语言结论永远正确。权限应沿用项目既有授权体系,不能通过候选结果自动扩大。
## 与 Endor 对象模型怎样衔接
这项提案与[Endor 的对象操作定位](/docs/endor/endor-purpose-draft)有连接点:材料、判断、候选变更、任务可以保留独立身份和关系,而不只存在于某个聊天上下文或图执行记录中。
候选职责划分如下,仍需逐项设计和确认:
| 层次 | 候选职责 | 不应声称自动完成的事情 |
| --- | --- | --- |
| 语言与类型 | 表达输入、结果分支、对象引用和可调用能力 | 仅靠类型名证明自然语言主张 |
| 对象与存储 | 保存身份、版本、关系和历史 | 自动发现所有语义依赖 |
| 运行环境 | 限制效果、检查当前授权与提交前置条件 | 跨任意外部系统提供统一事务 |
| 模型与模块 | 解释目标、探索方案、生成候选判断 | 自行授予权限或给自己签发可信结论 |
| 检查与人 | 定义接纳条件、核验内容、处理不能确定的情况 | 用一次总分覆盖全部业务风险 |
先用现有语言和接口验证这些机制,再决定哪些值得进入 Endor 语言。不要先把假设写成语法,再用语法存在证明系统有价值。
## 最小原型与公平比较
第一版只选“资料到报告,依据撤回后继续修订”这一条工作流。使用有限的版本化资料集和一个可检查版本的报告存储,不接真实付款或邮件,也不扩展成万能 Agent 平台。
对照组采用认真实现的 LangGraph + DSPy 应用,允许它使用显式输入过滤、持久化、同样的检查器及自定义依赖记录;实验组使用相同模型、材料、预算和检查政策,增加可复用的判断对象与依赖处理层。对照组也能实现相同行为,比较重点是重复代码、集成工作量和变更后的可靠性,而不是把对照组故意降成一个长提示词。
| 实验 | 需要观察的结果 |
| --- | --- |
| 撤回一份材料 | 下游是否还有引用被撤回依据的当前有效结果;受影响项是否漏标 |
| 修改材料中的一个事实 | 哪些产物需要复核,哪些可以复用;是否产生过度失效 |
| 模型替换 | 固定任务上的质量、拒答、成本与耗时怎样变化;既有检查能否复用 |
| 中断与重试 | 是否继续正确任务,重复提交是否被目标端拒绝或去重 |
| 提交前版本变化 | 是否报告冲突或重新验证,避免覆盖用户新修改 |
| 收回写权限 | 接续任务是否重新检查并拒绝写入;历史审批不能覆盖当前授权 |
| 来源不支持结论 | 结构检查能否通过,独立内容审核能否识别错误;如实记录漏检 |
| 增加第二个工作流 | 需要重复编写多少字段、失效传播、权限和接纳逻辑 |
先保存基线数据,再预先确定可接受门槛和独立评估集,不凭最顺利的演示设定标准。分别记录内容质量、来源支持、失效覆盖、过度重算、人工介入、模型与工具成本、延迟和开发工作量;不要把它们合成一个无法解释的“智能分数”。
原型如果不能降低第二条工作流的实现成本,或只是增加很多对象却没有减少错误和复核负担,就说明这层抽象还没有证明价值。如果 LangGraph + DSPy 的直接实现已经足够清晰,则优先沉淀薄库和约定,不必新造完整运行时。
## 当前建议
**保留 LangGraph 对执行的组织能力、DSPy 对模型程序的优化能力;探索让判断的依据、适用条件和后续影响成为共同的对象关系。**
最值得做的演示不是更多 Agent 同时对话,而是用户修改一份依据后,系统解释哪些工作受到影响、哪些仍可复用,并让 AI 在受约束的空间里继续完成修订。这项体验能否用更少的应用专用代码实现,才是继续投入的依据。
@@ -1,64 +0,0 @@
# AI 公司研究
调查 2025—2026 年不同类型 AI 公司的业务、交付能力、规模与商业模式。分类表链接公司数据,公司数据链接经营指标与原始证据。
以下工作台读取研究数据库。首批为公开资料定向试样,成立、公开发布和转型发布分别记录。未披露收入的公司继续保留,避免把收入资料缺失误当成零收入。
[收入样本分布快照](/docs/other-research/ai-company-research/income-snapshot)列出当前平台MRR的分组结果、逐项证据入口和历史样本敏感性。
[资料覆盖与缺项](/docs/other-research/ai-company-research/coverage)区分候选池读取、定向样本、人工复核及尚未核验的字段。
## 商业模式初评:截至 2026-10-30
以下为研究假设,用于设计付费试验,尚不足以给行业排收益名次。规模、交付证据和收入来源必须一起看;目录条目数量不是市场需求,平台样本也存在自愿公开与幸存者偏差。
| 模式 | 工作台可搜索样本 | 当前证据 | 商业试验要测什么 | 主要缺项 |
| --- | --- | --- | --- | --- |
| 企业获客与营销软件 | Gojiberry、Traxy AI、NotFair、ChatSEO | 有平台MRR记录,但同步日期不同 | 有效商机、续费与单客户贡献毛利 | 获客费、客户集中度、退款、留存 |
| 消费者个人应用 | nutrIA、Calyra、Verbi、ABMAXX | 有平台收入与订阅记录;订阅份数不是独立人数 | 渠道成本、后续月份留存、平台抽成后利润 | 取消与退款、月/年套餐结构、重复订阅 |
| 创意软件与制作服务 | Speel.co、Laper、GetEbook AI、White Glove Content | 软件订阅和交付服务必须分组 | 合格交付的计算与人工成本、返工率、重复购买 | 项目收入占比、人工工时、产能 |
| 小企业综合Agent | Runable(AI059) | 公司披露历史ARR里程碑;[官方公告](https://runable.com/blogs/runable-grow) | 限定一个任务,测完整成功率、收费与执行成本 | 当前付费客户、续费、推理成本、人工接管 |
| 行业专业Agent与服务 | LunaBill、Plena Health、Bizmark | 有合同ARR、数量级或ARR自述,日历截止日和交付拆分不完整 | 采购周期、上线工时、失败处理与验收标准 | 合同、经常性与实施费拆分、使用深度 |
| AI辅助工程与研究服务 | Marengo、Harbor | 有首月收入、待交付或多年服务合同;不直接当ARR | 已验收项目毛利、专业人员工时、返工和责任成本 | 合同收入确认、软件/服务拆分、应收账款和交付集中度 |
| 模型、算力与机器人 | Wafer、Induction Labs、Congruent | 可确定产品方向;团队小或融资多不证明单位经济 | 客户真实负载、性能成本、交付与售后成本 | 同口径收入、资本开支、独立性能测试 |
**对我们商业试验的推论:** 先选择能接触真实买家、结果可验收、成本可计量的工作流,比较软件订阅与服务交付的实际毛利,再评估是否扩成综合Agent。当前证据不足以断定某类必然更赚钱;平台数据更适合帮助选择访谈与试验对象。
收入里程碑还需结合成本核查:Runable创始人称毛利为负。[原始采访](https://techcrunch.com/2026/08/26/runable-hits-21m-to-bet-ai-agents-can-go-from-building-businesses-to-growing-them/)
## ARR 分布与财务声明核查
目前可绘制的是按日期窗口、支付平台及收费模式分组的产品 **MRR** 分布。公司自述ARR缺少统一截止日、定义和独立凭证,尚不能形成可靠的行业ARR排名。MRR乘12仅为参考年化;项目收入、融资、近30日收款与ARR分别保存。
| 现象 | 后续核查 | 推断边界 |
| --- | --- | --- |
| 平均值远高于中位数,移除最大值后均值明显下降 | 回查大样本的日期、业务边界和收费模式 | 高离散度不等于造假 |
| ARR与近30日收款差距大 | 查年付、季付、退款、试用、币种及平台计算方法 | 不同指标不应机械相等 |
| 用户很多,付费数缺失 | 取得付费客户、套餐与账期数据,再核ARPA | 不能用注册人数直接反推真实性 |
| 公司、投资方与媒体重复一个数字 | 追到首发声明及独立底账 | 多篇转述不等于独立证据 |
| 声明撤回、口径变更或含并购业务 | 保留历史版本,拆分边界并核对更正原因 | 单个异常无法生成可靠造假概率 |
### 从原文到可比较收入:新增核查实例
以下均为2026-10-30读取的公司自述,读取日不代表收入业务截止日。点击公司可查看分项观测、核查和原始来源。原文仅有美元符号时保留币种待核;未给币种的数字不补成美元。
| 公司 | 原始声明 | 数据表处理 | 对商业模式评估的影响 |
| --- | --- | --- | --- |
| [Risely AI](/docs/other-research/ai-company-research/overview?company=YC30724#research-company-detail) | 上线六周达到8千MRR,原文$ | 保存MRR;无日历截止日,不转换为同期ARR | 高校系统集成还需核实施成本、采购周期与续约 |
| [Hyper](/docs/other-research/ai-company-research/overview?company=YC31548#research-company-detail) | 上线十二天达到1千MRR、采用团队50多 | 币种未列;采用团队不直接当付费客户 | 试点与持续付费需要分开;不直接算每团队收入 |
| [Bizmark](/docs/other-research/ai-company-research/overview?company=YC32200#research-company-detail) | 工厂产品10.8万ARR,原文$ | 保留声明;创始人此前6000万业务收入排除 | 核对软件与定制集成收入、上线成本及客户集中度 |
| [LunaBill](/docs/other-research/ai-company-research/overview?company=YC30997#research-company-detail) | 76.4万contracted ARR,其中42.8万称live revenue,原文$ | 合同承诺、已上线部分分别保存,不相加;后者未擅自补为ARR | 上线转化和已验收用量比总合同承诺更能检验交付;客户追回款项排除 |
| [Plena Health](/docs/other-research/ai-company-research/overview?company=YC31493#research-company-detail) | 七位数committed/contracted ARR | 仅存数量级下界,不填精确100万;八个月/十个月表述保留 | 年度或多年合同需拆上线状态、取消条款和定制实施费 |
| [Brickwise](/docs/other-research/ai-company-research/overview?company=YC30847#research-company-detail) | 销售管线中有超过100万cARR的企业合同,原文$ | 不确认成已签或已实现ARR;融资及前公司业绩排除 | 大合同集中度和签约到上线的转化需要单独核查 |
| [Radley](/docs/other-research/ai-company-research/overview?company=YC33020#research-company-detail) | 收购诊所带来460万年化收入,另有220万合同,原文$ | 收购、交割与并表边界未核,不当纯软件ARR,不把两项相加 | 医疗服务收入需结合医生工时、收购成本与经营毛利,不能直接与SaaS比较 |
| [Misprint](/docs/other-research/ai-company-research/overview?company=YC30302#research-company-detail) | 创始人此前卖卡50万ARR,原文$ | 发布帖明确早于平台构想,排除平台收入 | 卡牌销售额、交易额、平台佣金与软件订阅是不同口径 |
| [Harbor](/docs/other-research/ai-company-research/overview?company=YC31445#research-company-detail) | 未来十二个月18.7万软件合同、月软件收入1.56万、三年193万CRO合同,原文$ | 软件与服务分开,重叠声明不相加;多年合同不均摊为ARR | 从软件扩至服务后,需核项目履约、人员成本和回款周期 |
| [Marengo](/docs/other-research/ai-company-research/overview?company=YC32981#research-company-detail) | 首月7万收入、400万待交付合同,原文$ | 单月收入不自动年化,待交付合同不当确认收入 | 数据中心设计是专业工程服务,需核工程师审核、返工与验收成本 |
| [Tsenta](/docs/other-research/ai-company-research/overview?company=YC31259#research-company-detail) | 三个月达到约300万Annualized,原文$ | 指标没有展开定义,暂不补成ARR | 按申请额度收费,求职结束后的留存与持续付费需单独测量 |
| [Arbital](/docs/other-research/ai-company-research/overview?company=YC32749#research-company-detail) | 累计13.5亿、月约4.35亿交易量,原文$ | 交易量不当收入;没有费率不反推佣金 | 交易量可能包含重复换手,平台抽成、激励和净收入才决定经济性 |
| [Jcode](/docs/other-research/ai-company-research/overview?company=YC32172#research-company-detail) | 每月100万推理支出吞吐,原文$ | 推理支出不当MRR;源码公开与厂商基准分开标记 | 需核工具收费、推理成本承担方和开发者留存 |
| [NebuSec](/docs/other-research/ai-company-research/overview?company=YC32138#research-company-detail) | 40万漏洞奖金,原文$ | 团队与Agent归属、获奖期及收款主体待核,不当ARR | 奖金与安全服务订阅不同;独立漏洞记录及持续合同需分别验证 |
这批记录补充了收入定义差异,尚未增加同日同币种的可比ARR样本。合同承诺、并购业务或履历混入会使领域均值失真;资料有这些差异本身不证明财务造假。
下一轮优先取得统一业务截止日的ARR定义、付费客户变动、留存、收入与现金勾稽、直接成本及可独立复核的来源。缺项留在表内并保持未知。上述核查是研究流程,不是审计意见。
@@ -1,64 +0,0 @@
# AI 公司收入样本分布快照
截至 2026-10-30,7日窗口内有 14 个产品的美元平台MRR可纳入分组描述。创意软件订阅组的样本均值较高、CV也较大;营销软件组与消费者个人应用组的CV接近。各组只有1至5个产品,尚不足以估计行业总体或造假概率。
这是已采集资料的固定快照。[动态工作台](/docs/other-research/ai-company-research/overview)提供筛选与原始证据。窗口按最后同步日计算,不保证所有点同日,也不是1,015条公司/产品记录的收入普查。
## 7日窗口的分组结果
MRR单位为美元每月。CV为样本标准差除以均值;最大值剔除降幅表示移除最高收入产品后均值下降的比例。单个样本不计算离散度。
| 业务及收费模式 | 平台 | 样本数 | 均值 | 中位数 | CV | 最大值剔除降幅 |
| --- | --- | --- | --- | --- | --- | --- |
| 客户与收入运营 · 软件订阅 | Stripe | 4 | 34,815 | 36,723 | 0.469 | 16.9% |
| 内容与创意 · 消费者订阅 | RevenueCat | 1 | 53,101 | 53,101 | 不计算 | 不计算 |
| 内容与创意 · 内容制作服务 | Stripe | 1 | 62,423 | 62,423 | 不计算 | 不计算 |
| 内容与创意 · 软件订阅 | Stripe | 3 | 41,693 | 49,069 | 0.674 | 28.4% |
| 消费者个人应用 · 消费者订阅 | RevenueCat | 5 | 25,349 | 20,483 | 0.471 | 20.9% |
在至少3个样本的组内,内容创意软件订阅的均值较高,但移除最大样本后的下降也最大。营销软件与个人应用的CV仅有微小差别,不据此宣称哪个领域更稳定。两个内容类单样本组虽有更高数值,不能作为典型收入或领域排名。
均值和中位数接近只描述当前采集点,并不证明样本具有代表性。平台公开者、成立年、规模、获客渠道与收费方式均可能影响结果。
## 样本与证据入口
点击产品名称可打开公司明细,再查看平台记录及其限制。连接已过期的近期快照仍按所标日期保留,并不表示连接当前有效。
| 产品 | 成立年 | MRR美元每月 | 最后同步 | 平台 | 连接状态 |
| --- | --- | --- | --- | --- | --- |
| [Boothie](/docs/other-research/ai-company-research/overview?company=MR001#research-company-detail) | 2026 | 53,101 | 2026-10-29 | RevenueCat | 采集时未标过期 |
| [White Glove Content](/docs/other-research/ai-company-research/overview?company=MR002#research-company-detail) | 2026 | 62,423 | 2026-10-29 | Stripe | 采集时未标过期 |
| [Speel.co](/docs/other-research/ai-company-research/overview?company=MR003#research-company-detail) | 2025 | 65,381 | 2026-10-29 | Stripe | 采集时未标过期 |
| [Laper](/docs/other-research/ai-company-research/overview?company=MR004#research-company-detail) | 2025 | 49,069 | 2026-10-29 | Stripe | 采集时未标过期 |
| [GETebook.ai](/docs/other-research/ai-company-research/overview?company=MR005#research-company-detail) | 2025 | 10,629 | 2026-10-29 | Stripe | 采集时未标过期 |
| [TrackAI](/docs/other-research/ai-company-research/overview?company=MR006#research-company-detail) | 2025 | 19,437 | 2026-10-27 | RevenueCat | 已过期的日期快照 |
| [nutrIA(Nutria Inc)](/docs/other-research/ai-company-research/overview?company=MR007#research-company-detail) | 2025 | 18,198 | 2026-10-29 | RevenueCat | 采集时未标过期 |
| [Calyra(平台名Nutria 9e3eaa)](/docs/other-research/ai-company-research/overview?company=MR008#research-company-detail) | 2025 | 46,562 | 2026-10-29 | RevenueCat | 采集时未标过期 |
| [Verbi](/docs/other-research/ai-company-research/overview?company=MR009#research-company-detail) | 2026 | 22,066 | 2026-10-29 | RevenueCat | 采集时未标过期 |
| [AbMaxx](/docs/other-research/ai-company-research/overview?company=MR010#research-company-detail) | 2026 | 20,483 | 2026-10-29 | RevenueCat | 采集时未标过期 |
| [traxy.ai](/docs/other-research/ai-company-research/overview?company=MR011#research-company-detail) | 2026 | 52,438 | 2026-10-29 | Stripe | 采集时未标过期 |
| [NotFair](/docs/other-research/ai-company-research/overview?company=MR012#research-company-detail) | 2026 | 13,376 | 2026-10-29 | Stripe | 采集时未标过期 |
| [Rank Prompt](/docs/other-research/ai-company-research/overview?company=MR015#research-company-detail) | 2025 | 40,112 | 2026-10-29 | Stripe | 采集时未标过期 |
| [ChatSEO](/docs/other-research/ai-company-research/overview?company=MR016#research-company-detail) | 2025 | 33,334 | 2026-10-29 | Stripe | 采集时未标过期 |
## 扩大窗口后的敏感性
营销软件订阅组从7日窗口的4个产品扩大到90日窗口的5个产品,均值从34,815升至112,726美元每月,中位数从36,723变为40,112,CV从0.469升至1.551。
新增点来自[Gojiberry AI](/docs/other-research/ai-company-research/overview?company=AI016#research-company-detail)的8月历史快照,连接已过期。移除该最大值后,均值回到34,815美元,下降69.1%。这是日期与样本构成的敏感性,不能解释为行业风险变化。
## 默认窗口排除的历史记录
| 产品 | MRR美元每月 | 最后同步 | 排除原因 |
| --- | --- | --- | --- |
| [Adspirer](/docs/other-research/ai-company-research/overview?company=MR013#research-company-detail) | 54,684 | 2026-06-29 | 超过7日窗口,保留历史观测 |
| [PROSP](/docs/other-research/ai-company-research/overview?company=MR014#research-company-detail) | 128,005 | 2026-02-05 | 超过7日窗口,保留历史观测 |
| [Gojiberry AI](/docs/other-research/ai-company-research/overview?company=AI016#research-company-detail) | 424,368 | 2026-08-14 | 超过7日窗口,保留历史观测 |
## 对 ARR 与商业模式的含义
MRR乘12只是参考年化。公司声明的ARR、合同管线、近30日收款、一次性额度和服务交付收入仍需分开;支付平台重建指标也不等于财务审计。方法与平台限制见[TrustMRR FAQ](https://trustmrr.com/faq)。
当前更适合选择访谈对象和设计收费试验,不能据最高均值直接决定业务方向。软件订阅优先核留存与推理成本,服务交付优先核人工工时和返工,消费者应用优先核渠道成本、退款和平台抽成。
行业ARR比较仍需补齐同一业务截止日、币种、经常性定义及业务边界。收入日期不完整或同页数值冲突的声明,继续保留在[核查工作台](/docs/other-research/ai-company-research/overview),不补成零值或精确数字。
@@ -1,73 +0,0 @@
# AI 公司资料覆盖与缺项
截至 2026-10-30,研究表含1,015条公司或产品记录。YC候选池有逐条读取审计;其他来源为定向补充。清单尚不能作为全球AI公司普查,也不能将收入未知解释为零。
验收范围已于2026-10-30确认为:以确认的公开来源清单为边界,逐条核对并标明缺项。下方区分已逐条审计的候选池与定向补充来源;已使用某来源不代表该来源已被完整覆盖。
[分类和数据工作台](/docs/other-research/ai-company-research/overview)链接原始证据,[收入样本快照](/docs/other-research/ai-company-research/income-snapshot)展示当前可计算的MRR样本。
## 候选池与公司表的关系
| 环节 | 记录数 | 含义 |
| --- | --- | --- |
| YC批次候选 | 1,428 | 从公开镜像发现,随后读取YC官方页面 |
| 官方页面读取 | 1,428 | 此候选池读取失败0条 |
| 初筛匹配 | 962 | 成立年及AI相关描述通过初筛,尚非逐家商业验证 |
| 与原表重合 | 34 | URL匹配,未再次新增 |
| 新增目录记录 | 928 | 原始入表来源,后续深化不改变发现计数 |
| 年份不符或缺失 | 268 | 此轮不作为成立年匹配项 |
| AI业务待确认 | 192 | 不强行把背景关键词当主营 |
| 成立年与批次冲突 | 6 | 另保留核查记录 |
全表由913条目录基础项和102条个案或平台记录组成。候选池已匹配项之外另有53条记录,包含其他来源及发布/转型对照;其中已有15条原目录记录深化为个案,仍保留目录来源。两种统计边界不同。候选池数字只是该镜像快照的覆盖,Bud等后续定向补充不回填为原候选池已覆盖。
## 成立年与证据深度
| 维度 | 当前记录 | 解读 |
| --- | --- | --- |
| 记录为2025年成立 | 619 | 成立来源和精度见公司明细 |
| 记录为2026年成立 | 391 | 不将产品发布日期代替成立日 |
| 成立年未知或范围外 | 5 | 发布/转型对照单列,不进入两年成立分组 |
| 目录业务分类人工复核 | 160 | 仅复核主营分类,不代表技术或财务审计 |
| 其他目录基础项 | 753 | 规则初分或资料不足,继续待复核 |
| 有团队数字 | 945 | 多为自报Team Size或Employees,时点与口径不统一 |
| 专项技术证据记录 | 7 | 其中3条自述、3条公开材料、1条第三方评测 |
| 公司披露收入线索 | 26 | 含历史、下界、币种不明及产品业务声明,不等于可比ARR |
| 仅平台收入线索 | 16 | 同时有公司披露者计入上一行,不重复计数 |
| 仅第三方估算 | 1 | 不与直接披露混合 |
| 暂无可用收入数字 | 972 | 包含尚未逐家搜索的目录项,不代表没有收入 |
## 业务分类与数据入口
| 业务类别 | 记录数 |
| --- | --- |
| [基础模型](/docs/other-research/ai-company-research/overview?category=C01) | 29 |
| [算力与模型基础设施](/docs/other-research/ai-company-research/overview?category=C02) | 102 |
| [Agent 开发与运行设施](/docs/other-research/ai-company-research/overview?category=C03) | 51 |
| [软件开发](/docs/other-research/ai-company-research/overview?category=C04) | 109 |
| [企业通用工作](/docs/other-research/ai-company-research/overview?category=C05) | 176 |
| [客户与收入运营](/docs/other-research/ai-company-research/overview?category=C06) | 52 |
| [行业专业应用](/docs/other-research/ai-company-research/overview?category=C07) | 321 |
| [内容与创意](/docs/other-research/ai-company-research/overview?category=C08) | 45 |
| [消费者个人应用](/docs/other-research/ai-company-research/overview?category=C09) | 46 |
| [机器人及实体产业](/docs/other-research/ai-company-research/overview?category=C10) | 81 |
| [业务待分类](/docs/other-research/ai-company-research/overview?category=C00) | 3 |
## 已使用来源的分布
1,091条证据记录对应1,077个不同URL。不同URL不自动构成独立验证;同一声明可能由公司、投资方和媒体重复转述。
| 来源 | 证据记录 | 不同URL | 用途及限制 |
| --- | --- | --- | --- |
| YC官方目录 | 984 | 975 | 公司自述的成立、团队与业务;英文及加速器样本偏多 |
| TrustMRR | 18 | 18 | 支付平台重建指标及方法;公开者存在选择偏差,非财务审计 |
| a16z Speedrun | 11 | 10 | 投资机构中的公司介绍及收入声明;存在利益关系 |
| TechCrunch | 3 | 3 | 原始采访或报道;转述公司数字不等于查账 |
| Sacra | 3 | 3 | 第三方估算;与直接披露分开 |
| 其他官网、评测及公开资料 | 72 | 68 | 按每条来源限制理解,不能统一当作独立证据 |
## 尚未满足的研究条件
行业ARR比较仍缺统一业务截止日、币种、经常性定义和业务边界。现有平台MRR可描述已采集产品,不足以代表全部新公司。技术评测只覆盖所列版本与任务,不宜跨领域汇总为统一强弱分数。
下一轮优先补收入线索的日期及合同定义、非YC和非英文来源、规则初分的人工复核,以及停止服务或被收购案例。这样能减少“只见赢家”和“只见融资公司”的偏差。公开来源池之外的未披露公司仍无法证明已被穷尽。
@@ -1,50 +0,0 @@
# 语言设计问题集
从名称与作用域到反射与表示,集中维护语言设计讨论中的问题、候选方案与确认记录。
当前收录 **13 个分类、271 项议题,26 项已确认**。本合集是设计工作底稿,不是已经实现的语言规范。
## 章节目录
| 分类 | 议题数 | 已确认 | 当前问题 |
| --- | ---: | ---: | --- |
| [02 · 名称与作用域](/docs/language-design/02-questions) | 22 | 13 | 02-15 |
| [03 · 值与类型](/docs/language-design/03-questions) | 30 | 13 | 03-14 |
| [04 · 存储与生命周期](/docs/language-design/04-questions) | 20 | 0 | 04-01 |
| [05 · 对象与成员](/docs/language-design/05-questions) | 18 | 0 | 05-01 |
| [06 · 表达式与运算](/docs/language-design/06-questions) | 20 | 0 | 06-01 |
| [07 · 函数与调用](/docs/language-design/07-questions) | 22 | 0 | 07-01 |
| [08 · 控制流](/docs/language-design/08-questions) | 20 | 0 | 08-01 |
| [09 · 集合与访问协议](/docs/language-design/09-questions) | 22 | 0 | 09-01 |
| [10 · 错误与资源](/docs/language-design/10-questions) | 16 | 0 | 10-01 |
| [11 · 模块与程序执行](/docs/language-design/11-questions) | 22 | 0 | 11-01 |
| [12 · 并发与异步](/docs/language-design/12-questions) | 21 | 0 | 12-01 |
| [13 · 外部交互](/docs/language-design/13-questions) | 22 | 0 | 13-01 |
| [14 · 反射与表示](/docs/language-design/14-questions) | 16 | 0 | 14-01 |
## 怎样继续讨论
每项保留原编号、问题范围与验收要求。只有规则、正常示例、边界情况和失败行为明确,并且已有用户确认或明确授权的默认方案时,才勾选完成。建议、语言对照和“已介绍过”均不计为已确认。明确决定不支持某项能力时,也应记录其后果。
已有展开的问题保留 Python、JavaScript/TypeScript、Go、Rust 等语言的对照和原始资料链接;其余议题保留问题与验收要求,后续讨论时再补充对应方案与来源。
复选框是 Markdown 中的维护记录。更新章节后,也要同步本页的数量、当前问题和跨分类约束;网站不会自动从聊天同步。
## 已有决定与待对齐事项
- **名称与作用域**:已确认词法作用域、普通函数局部绑定、块内 `let`、遮蔽、编译时确定赋值检查、`nonlocal`、循环中的独立 `let` 绑定和默认捕获绑定。02-11 的特殊结构作用域仍待核对;02-15 的多个闭包共享读写仍待选择。
- **值与类型**:已确认动态类型、名字绑定对象、显式转换、精确有理数 `Number`、默认分数显示、Unicode 字符串及独立 `Boolean`;条件只接受 `Boolean`。`String * 整数值 Number` 已修订为等价于 `duplicate`,不能继续沿用早期“不支持字符串乘法”的记录。
- **空值仍待确认**:已提出“变量位置存在、当前没有目标对象”的需求;`null` 本身是否为对象尚未得到最终确认。把引用设为 `null` 不会替原对象保活,具体回收机制仍待设计。
- **赋值与对象模型**:04-01、05-01 的原讨论尚未完成,但 03 已记录对象绑定与基本类型按类建模的方向。后续应先核对适用范围,再决定生命周期和对象扩展规则,避免重新默认为全部未定。
- **运算、控制流与集合**:06 的类型组合与条件、08 的真值判断、09 的文本索引应分别对齐 03 的显式转换、仅布尔条件及码点索引规则。
- **错误机制**:03 已确认除零触发算术错误;10 的通用失败模型仍待选择。错误分类、捕获与传播必须交叉核对。
- **执行与类型检查**:03 原讨论记录了解释执行、REPL 与动态类型的方向。名称与确定赋值仍可做执行前检查;不应把动态类型误写成“没有任何执行前检查”。其他章节的“静态或运行时待定”应按具体检查对象重新核对。
跨分类事项保持未完成,直到相关分类明确对齐。本次整理不据此替用户选择新的规则。
## 来源与维护范围
本合集依据用户指定的“语言设计 02”至“语言设计 14”聊天整理,截至 2026-10-28。章节保留原编号,本次没有提供 01 分类的对应语言设计聊天,因此不补造该章。附带的《Chromium 主线 01:导航与文档提交》属于另一主题,未并入。
各章末尾标明原讨论标题;仓库的 `content/language-design-sources.json` 记录对应聊天标识,便于后续追溯。它是本次整理的来源清单,不是自动同步配置。
@@ -1,196 +0,0 @@
# 02 · 名称与作用域
13/22 项已确认。当前推进 02-15;02-11 的特殊结构作用域仍需核对。
本页整理原讨论的问题、验收要求和候选方案。勾选代表讨论中已确认的规则,不代表编译器或运行时已经实现。
## 02-01 词法作用域
- [x] 已确认
名称按定义位置的代码嵌套关系查找;调用者的局部变量不会参与被调用函数的名称查找。
## 02-02 普通变量的基本作用范围
- [x] 已确认
不需要声明关键词;函数内普通变量采用 Python 式作用范围,普通 `if`、`for` 块不限制它。块内赋值后,块外可以使用,前提是该赋值确实发生。
## 02-03 `let` 临时绑定
- [x] 已确认
`let` 声明的变量限定在所属块内;后续赋值仍操作该临时绑定,不扩大作用域;不提供 `var`。
## 02-04 内层遮蔽外层
- [x] 已确认
内层 `let` 可以与外层变量同名。块内读写内层绑定,退出后恢复访问外层绑定,外层值不受这些修改影响。
## 02-05 同层重复 `let`
- [x] 已确认
同一作用域重复声明同名 `let` 报错;修改已有变量使用普通赋值。
## 02-06 `let` 声明前的直接使用
- [x] 已确认
整个块中的同名直接读写都认定为该临时绑定;声明前使用在编译检查阶段报错,不会转而访问外层同名变量。
## 02-07 分支与未初始化读取
- [x] 已确认
确定不能保证读取前已赋值时,编译报错还是运行时报错;覆盖条件分支、可能执行零次的循环和提前退出。
**已确认规则:**采用编译时的确定赋值检查。每条能够到达读取位置的路径都必须先赋值;编译器无法证明这一点,就报错。
## 02-08 普通变量的声明前查找
- [x] 已确认
确定函数后面出现普通赋值时,前面的同名读取算本地未初始化,还是可以访问外层绑定。
**已确认规则:**局部归属按整个函数确定,赋值前读取不能回退到外层同名变量。
## 02-09 同层名称冲突
- [x] 已确认
明确参数、普通变量、`let` 之间哪些组合属于赋值、哪些属于重复声明或冲突。
**已确认规则:**参数后的普通赋值修改参数绑定;参数与函数体同层 let 同名、同层重复 let、重复参数名均编译报错;更内层 let 可以遮蔽;let 之前的同名赋值属于声明前访问,编译报错。
## 02-10 内层函数修改外层绑定
- [x] 已确认
明确普通赋值默认修改哪一层,是否需要专门语法才能修改外层变量。
**已确认规则:**普通赋值默认建立或修改当前函数的局部绑定;通过 `nonlocal` 显式指定外层函数中已有的绑定。
## 02-11 各语法结构的作用域边界
- [ ] 待讨论
列清函数、独立块、循环及其他受支持结构各自如何引入绑定;未决定是否支持的结构标注依赖。依赖:**语法设计**。
## 02-12 循环绑定
- [x] 已确认
确定循环变量和循环体中的 `let` 是每轮建立新绑定,还是多轮复用;说明闭包捕获后的结果。
**已确认规则:**每次进入循环体,创建这一轮自己的 `let` 绑定;普通变量继续复用所属函数中的绑定。循环外不能访问循环体内的 `let`。若支持循环头的 `let`,也采用每轮独立绑定的原则。
## 02-13 声明与初始化顺序
- [x] 已确认
明确 `let` 能否不带初值、初始化表达式中的同名引用如何处理,以及受支持的复合声明按什么顺序建立绑定。依赖:**语法设计**。
**已确认规则:**允许先声明后赋值;初始化前读取自身报错;多目标声明或赋值先求完整右侧,再绑定目标。具体复合语法由语法设计对齐。
## 02-14 闭包捕获什么
- [x] 已确认
确定默认捕获绑定还是当时的值,以及是否提供显式选择。
**已确认规则:**默认捕获绑定;外层重新赋值后,闭包读到新值。需要快照时手动建立独立绑定,不自动深拷贝对象。
## 02-15 闭包修改与共享
- [ ] 待讨论
确定多个闭包是否共享同一外层绑定、哪些修改允许、哪些情况报错。依赖:**可变性规则**。
## 02-16 逃逸闭包的绑定存续
- [ ] 待讨论
确定外层函数返回或代码块退出后,闭包仍可访问什么,以及如何保证访问有效。依赖:**运行时实现、资源管理**。
## 02-17 完整名称查找顺序
- [ ] 待讨论
明确函数外层、模块、内置名称的查找顺序,以及类成员是否参与裸名称查找。依赖:**模块系统、对象与类型模型、内置库设计**。
## 02-18 模块可见性
- [ ] 待讨论
明确默认私有还是公开、如何导出,以及访问不可见名称时的错误。依赖:**模块系统**。
## 02-19 导入名称的绑定与冲突
- [ ] 待讨论
明确导入名能否重新赋值、别名和重名如何处理、导入后观察到的是哪个绑定。依赖:**模块系统**。
## 02-20 跨模块初始化
- [ ] 待讨论
明确循环导入或模块尚未初始化时,读取其名称如何处理。依赖:**模块系统、执行模型**。
## 02-21 类型名与值名的关系
- [ ] 待讨论
确定是否共享命名空间、同名是否冲突、名称如何区分解析。依赖:**类型系统**。
## 02-22 最终一致性验收
- [ ] 待讨论
用一组正常与错误示例交叉检查上述规则,解决跨分类冲突,形成最终规则摘要。
## 当前问题的方案与语言对照
以下沿用原讨论材料;建议和参考语言的做法均不自动成为本语言规则。
接下来是 **02-15:多个闭包能否共同读写同一个绑定**。
```text
fn example() {
count = 0
fn increase() {
nonlocal count
count = count + 1
}
fn read() {
return count
}
increase()
print(read()) // 如果允许共享,输出 1
}
```
各语言的相关规则:
| 语言 | 做法 | 收益与代价 |
|---|---|---|
| **Python** | 多个闭包可以共享绑定;修改外层函数绑定需要 `nonlocal` | 共享状态方便,修改也有明确标记;多个函数的行为会相互影响。[官方说明](https://docs.python.org/3/faq/programming.html#why-am-i-getting-an-unboundlocalerror-when-the-variable-has-a-value) |
| **JS / TS** | 可以共享并修改同一绑定,不需要 `nonlocal`;TS 保持 JS 的运行语义 | 写法直接,读代码时需要留意哪些闭包会修改变量。[官方手册](https://www.typescriptlang.org/docs/handbook/variable-declarations.html) |
| **Go** | 外层函数与闭包共享变量,允许修改 | 共享直接;并发执行时需要另行协调访问。[规范](https://go.dev/ref/spec#Function_literals) |
| **Rust** | 借用捕获受访问限制约束;相互冲突的可变借用和只读借用不能同时存续。共享可变状态需要合适的显式机制 | 编译器能限制冲突访问,但需要更多所有权与借用规则。[官方说明](https://doc.rust-lang.org/book/ch13-01-closures.html#capturing-references-or-moving-ownership) |
我们有两个主要方向:
1. **允许多个闭包共享读写**:上面例子合法,输出 `1`;修改仍要求 `nonlocal`。
2. **限制共享可变捕获**:某些同时持有读写访问的闭包组合被拒绝;具体限制需要与所有权模型一起设计。
我推荐 **1**,与前四种语言的方向一致,也能延续已经采用的 `nonlocal`。这里先决定单线程中的共享行为;并发访问规则属于并发模型。
## 讨论来源
《语言设计 02:名称与作用域》,整理日期 2026-10-28。章节编号沿用原清单。
@@ -1,250 +0,0 @@
# 03 · 值与类型
13/30 项已确认。当前推进 03-14;null 是否为对象仍未确认。其余分类中的类型、赋值和条件规则须与本章已有决定核对。
本页整理原讨论的问题、验收要求和候选方案。勾选代表讨论中已确认的规则,不代表编译器或运行时已经实现。
## 03-01 动态类型
- [x] 已确认
操作根据运行时对象的类型检查;不要求执行前通过静态类型检查。不适用的类型组合在运行时失败。
## 03-02 名字与对象绑定
- [x] 已确认
变量名绑定对象,类型属于对象;`b = a` 共享对象,重新绑定 `a` 不改变 `b`。
## 03-03 隐式转换政策
- [x] 已确认
需要转换类型时必须显式写出;不靠自动转换接受不匹配的操作数,例如 `"41" + 1` 报 `TypeError`。
## 03-04 数值类型与支持范围
- [x] 已确认
对外统一为 `Number`,覆盖整数与有理数,大小自动调整;不引入无穷大、`NaN`、虚数,正负零视为同一个数。精确性及异常运算另见下项。
## 03-05 整数性质与循环次数
- [x] 已确认
记录数值是否为整数;按次数循环要求正整数,零、负数和非整数不满足要求。具体错误形式交由 03-29 统一。
## 03-06 字符串算术操作
- [x] 已确认
确定字符串拼接、减法与重复运算的支持范围。
**已确认规则:**两个 String 可以用 + 拼接,不支持字符串减法;后续已修订为支持 String * 整数值 Number,完全等价于 duplicate,包含相同的参数检查与失败行为。
## 03-07 数值精确性
- [x] 已确认
确定有理数存储和四则运算是否始终精确,是否允许舍入。
**已确认规则:**`1 / 3` 精确保存为 `1/3`;`1/3 + 1/3` 得到 `2/3`;`1/3 * 3` 得到整数 `1`。如何显示另由 03-09 决定。
## 03-08 数值异常
- [x] 已确认
确定除零等不成立的数值运算如何失败,哪些情况交给平台模块。
**已确认规则:**`1 / 0`、`0 / 0`、`1 / (1 - 1)` 都抛出算术错误,停止本次表达式求值。具体错误名称、捕获和传播机制由【错误与异常】及 03-29 对齐。
## 03-09 数值输入与显示
- [x] 已确认
确定小数文字表示什么数、循环小数如何显示,以及显示后能否无损读回。依赖【词法与语法】。
**已确认规则:**数值按精确值解释,默认输出最简分数,分母为正;整数直接显示为整数,零显示为 `0`。
## 03-10 基本类型清单
- [x] 已确认
确定本轮内置哪些类型,以及明确不提供哪些类型。
**已确认规则:**本轮提供 `Number`、`String`、`Boolean`、空值类、`List`、`Map`、`Tuple`、`Set`、`Bytes`、函数类、类型对象所属的类。具体类名暂定。 空值类的对象地位在 03-14 被重新提出,仍待确认;不能据此宣布 null 已确定为对象。
## 03-11 字符串的值模型
- [x] 已确认
确定字符单位、Unicode 处理及索引单位;补齐空字符串等边界。
**已确认规则:**合法 Unicode 标量序列;保留原码点、不自动规范化;默认按码点计数,字素簇使用专门接口;索引从零开始、支持负索引、单个索引越界报错;普通正向切片左闭右开、越界截断、空范围返回空字符串。 创建或解码时拒绝非法编码和孤立代理项,不自动替换或丢弃;原始数据用 Bytes 保存。
## 03-12 字符串重复方法
- [x] 已确认
确定名称、次数参数、零和负数、错误行为及返回值。
**已确认规则:**`s.duplicate(n)` 返回重复后的 `String`,不修改接收者。`n` 必须是非负整数值的 `Number`;零次返回空字符串,负数、非整数及其他类型报错,不做隐式转换。
## 03-13 布尔值与条件转换
- [x] 已确认
确定布尔值是否独立于数字,以及条件位置接受布尔值还是其他值。依赖【控制流】。
**已确认规则:**`Boolean` 独立于 `Number`;条件表达式只接受 `Boolean` 对象。`true`、`false` 可以作为条件,数字、字符串、容器、空值等直接作为条件均报 `TypeError`。具体控制流语法由【控制流】对齐。
## 03-14 空值与缺失
- [ ] 待讨论
确定是否区分空值、缺失、未初始化和无返回值,以及误用时的行为。
## 03-15 复合值规则
- [ ] 待讨论
为选中的容器、元组、记录或枚举等确定成员类型限制、缺失成员及递归结构规则。
## 03-16 用户定义类型的能力
- [ ] 待讨论
确定用户能定义哪些类型,哪些能力仅供内置类型使用。依赖【对象与类型模型】。
## 03-17 类型身份与兼容性
- [ ] 待讨论
确定按名字还是结构认类型、如何处理子类型,以及哪些类型可以互相替代。
## 03-18 运行时类型查询
- [ ] 待讨论
确定如何取得类型、检查类型及检查能力;明确精确类型与子类型检查的区别。
## 03-19 显式转换接口
- [ ] 待讨论
确定转换入口、支持的转换、可能丢失信息的转换及失败行为。
## 03-20 泛型与容器类型约束
- [ ] 待讨论
决定是否支持参数化类型或带成员约束的容器;支持则规定运行时如何检查。
## 03-21 可选类型标注
- [ ] 待讨论
决定是否提供;提供则明确其用途及是否影响运行行为。
## 03-22 顶级类型与顶级类
- [ ] 待讨论
分别决定是否存在能容纳所有值的类型,以及是否存在统一祖先类。
## 03-23 可变性与共享
- [ ] 待讨论
确定哪些对象可变,以及多个名字共享对象时能观察到什么变化。
## 03-24 复制语义
- [ ] 待讨论
确定赋值之外是否提供复制、浅复制和深复制,以及循环引用如何处理。
## 03-25 相等与身份
- [ ] 待讨论
确定内容相等和同一对象如何区分,以及不同类型之间如何比较。
## 03-26 哈希与键的限制
- [ ] 待讨论
确定哪些值可作集合成员或映射键,保证相等、哈希和可变性规则一致。
## 03-27 参数与返回值的绑定
- [ ] 待讨论
确定传参、返回、重新绑定及修改对象的可见效果。依赖【函数与调用】。
## 03-28 显式引用能力
- [ ] 待讨论
决定能否直接取得并传递“变量存储单元”的引用;不支持也可完成此项。
## 03-29 类型相关错误契约
- [ ] 待讨论
统一类型不匹配、值不合法、转换失败等情况的错误分类。依赖【错误与异常】。
## 03-30 跨分类一致性验收
- [ ] 待讨论
用一组正常和失败示例核对类型、转换、对象、运算、函数与控制流规则,消除冲突和阻塞。
## 当前问题的方案与语言对照
以下沿用原讨论材料;建议和参考语言的做法均不自动成为本语言规则。
可以,**你要的空状态表示“变量位置存在,但当前没有指向对象”**。
不过,“避免回收”这里需要区分两件事:
```text
result = null
……之后……
result = calculate()
```
这是**先保留名字,之后再绑定对象**。前面还没有计算结果对象,因此没有对象需要保活。名字什么时候有效,由作用域规则决定。
如果已经有对象,情况不同:
```text
a = 某个对象
b = a
a = null
```
在采用强引用与自动回收的模型中,`b` 仍持有该对象,就不能提前回收它;如果所有有效强引用都消失,对象才可能被回收。**把变量设成 `null` 会解除它原来的引用,不会保护原对象。**具体回收机制属于【运行时与内存管理】,尚未确定。
各语言也区分“空状态”和“对象保活”:
| 语言 | 空状态怎样表达 | 对我们的启发 |
|---|---|---|
| **Python** | `None` 本身是一个对象,名字绑定到它。[对象模型](https://docs.python.org/3/reference/datamodel.html#objects-values-and-types) | 可以保持所有可绑定值都是对象的统一规则。 |
| **JS / TS** | `null` 是独立的原始值,不是对象;TS 另有静态类型约束。[JS 规范](https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types)、[TS 文档](https://www.typescriptlang.org/tsconfig/strictNullChecks.html) | 可以在语言层面明确允许“无对象”的值。 |
| **Go** | 指针变量可以为 `nil`,表示没有指向对象,变量本身仍存在。[语言规范](https://go.dev/ref/spec#The_zero_value) | 接近你描述的“位置存在、目标为空”。 |
| **Rust** | `Option<T>` 的 `None` 表示其中没有 `T` 值,不要求使用空指针实现。[官方说明](https://doc.rust-lang.org/std/option/) | “没有目标对象”的语义不必绑定某种内存布局。 |
这里还有一个需要明确的设计边界:
1. **空引用例外**:名字可以绑定对象,也可以持有 `null`;`null` 不属于对象。
2. **统一对象模型**:`null` 是空值类的对象,用它表示“没有业务对象”。
你说的“没有对象”,更接近 **1**。这会成为之前“所有值都是类的实例”的一个明确例外;方案 **2** 则保留那条统一规则。两者都能实现先声明、后赋值。
**你希望 `null` 本身也不是对象,采用方案 1,对吗?**
## 讨论来源
《语言设计 03:值与类型》,整理日期 2026-10-28。章节编号沿用原清单。
@@ -1,161 +0,0 @@
# 04 · 存储与生命周期
0/20 项已确认。当前推进 04-01;本分类尚未完成选择。
本页整理原讨论的问题、验收要求和候选方案。勾选代表讨论中已确认的规则,不代表编译器或运行时已经实现。
## 04-01 赋值的默认语义
- [ ] 待讨论
明确赋值默认是独立值、共享还是移动;是否按值的类别区分,以及如何识别这种区别。
## 04-02 创建、身份与存储位置
- [ ] 待讨论
明确何时产生新实例、身份是否可观察,以及存储位置和地址是否属于语言保证。依赖:对象与类型模型。
## 04-03 初始化流程
- [ ] 待讨论
明确初始化顺序、必需字段、默认值和“可正常使用”的判定条件。
## 04-04 初始化期间的访问与逃逸
- [ ] 待讨论
明确未完成初始化的值能否被读取、调用、共享或捕获,以及违规行为。
## 04-05 初始化失败
- [ ] 待讨论
明确失败如何传播、已初始化部分如何清理,以及外部可观察状态的保证。
## 04-06 别名与修改权限
- [ ] 待讨论
明确多个引用能否同时存在、谁能修改,以及修改对其他持有者是否可见。
## 04-07 复制规则
- [ ] 待讨论
明确哪些值可复制、浅复制与深复制的含义、内部共享关系及复制失败行为。
## 04-08 移动规则
- [ ] 待讨论
明确是否支持移动、何时发生、源值之后能做什么,以及禁止移动的情况。
## 04-09 跨函数与闭包传递
- [ ] 待讨论
明确传参、返回值、闭包捕获沿用哪些生命周期规则,局部值逃逸如何处理。依赖:函数与闭包相关规则。
## 04-10 所有权与借用
- [ ] 待讨论
明确是否存在所有者、借用及其期限;哪些约束由静态检查或运行时检查保证。依赖:类型系统。
## 04-11 引用失效与悬空访问
- [ ] 待讨论
明确销毁、容器变化、存储迁移等情况下引用何时失效,以及失效访问的处理。
## 04-12 自动回收机制
- [ ] 待讨论
明确采用作用域释放、引用计数、追踪式 GC 或混合机制,并区分语言保证与实现自由。
## 04-13 循环引用与弱引用
- [ ] 待讨论
明确循环如何处理,是否提供弱引用,以及弱引用失效后的访问结果。
## 04-14 销毁时机与顺序
- [ ] 待讨论
明确销毁是否及时、顺序是否保证,以及正常退出和提前退出时的行为。
## 04-15 清理中的异常行为
- [ ] 待讨论
明确清理失败、重复清理、清理期间重入,以及重新发布待销毁值的处理规则。
## 04-16 非内存资源
- [ ] 待讨论
明确文件、锁等资源如何及时释放,是否支持显式关闭或作用域管理,以及关闭后操作的结果。
## 04-17 用户自定义生命周期操作
- [ ] 待讨论
明确哪些创建、复制、移动、清理操作允许定制,其触发时机与限制。依赖:定制协议。
## 04-18 分配失败与手动存储能力
- [ ] 待讨论
明确内存不足如何表现,以及是否暴露手动分配、释放或分配器选择;不支持也须明确。
## 04-19 并发、异步与外部接口边界
- [ ] 待讨论
明确跨执行单元共享或转移、任务取消、外部代码持有值时的生命周期契约;不支持的能力明确排除。依赖:相应并发及外部接口规则。
## 04-20 跨分类一致性验收
- [ ] 待讨论
用创建、共享、传递、失败、退出的完整例子检查已定规则;解决与类型、对象、协议和运行时规则的冲突。
## 当前问题的方案与语言对照
以下沿用原讨论材料;建议和参考语言的做法均不自动成为本语言规则。
当前仍是 **04-01:`b = a` 默认意味着什么?** 重点看两个问题:修改 `b` 是否影响 `a`,赋值后还能否使用 `a`。
- **Python:赋值建立绑定,不复制对象。**
`a = [1]; b = a; b.append(2)` 后,`a` 也是 `[1, 2]`。但给 `b` 重新赋值不会改变 `a` 的绑定。共享很方便,代价是可变对象的修改会沿别名传播。对我们的启发是:必须分清“修改同一个值”和“变量改为指向另一个值”。这是 Python 的语义,不依赖 CPython 的引用计数实现。[官方说明](https://docs.python.org/3/library/copy.html)
- **JS:对象赋值保留同一对象;TS 沿用这一运行时行为。**
`const a = {n: 1}; const b = a; b.n = 2` 后,`a.n` 为 `2`。`const` 限制重新赋值,不会冻结对象;数字等原始值则没有可共享修改的内部状态。TS 的类型标注不会让赋值自动复制对象。好处是对象共享直接,代价是静态类型本身不消除别名影响。启发是:绑定是否可改、值是否可变、是否共享,需要分别规定。[JS 规范](https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-assignment-operators-runtime-semantics-evaluation)、[TS 文档](https://www.typescriptlang.org/docs/handbook/2/basic-types.html#erased-types)
- **Go:复制赋给变量的值,但不递归复制它引用的数据。**
`a := [1]int{1}; b := a; b[0] = 2` 后,`a[0]` 仍为 `1`;换成切片 `a := []int{1}`,同样操作后 `a[0]` 为 `2`,因为复制的切片描述信息仍引用同一底层数组。好处是支持直接的数据复制,也能方便共享;代价是必须理解类型内部是否含引用。启发是:**“赋值复制值”不保证整份数据相互独立。** [Go 规范](https://go.dev/ref/spec#Assignment_statements)
- **Rust:赋值根据类型发生复制或移动。**
`let a = 1; let b = a;` 后仍可读 `a`,因为整数实现了 `Copy`;`let a = String::from("hi"); let b = a;` 后再读取 `a` 会被编译器拒绝。好处是无需隐式复制字符串,也能明确资源归属;代价是使用者需要理解移动、借用和复制能力。启发是:按类型区分可行,但需要类型系统支撑;“放在栈上还是堆上”并非判断复制或移动的规则。[Rust 官方教程](https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html)
回到我们的待选方案:
| 方案 | 普通可变数据的 `b = a` |
|---|---|
| **A:默认独立值** | 修改 `b` 不影响 `a`;需要明确独立性覆盖到哪一层。 |
| **B:默认共享** | 修改共享内容会影响 `a`;需要独立副本时显式复制。 |
| **C:默认移动** | 转移使用资格;之后不能继续读取原来的 `a`。 |
| **D:按类别区分** | 为普通数据、共享引用、唯一资源分别规定行为;类别必须可识别。 |
**我的建议是先从 A 或 B 确定普通可变数据的使用体验**,再在复制、移动任务中检查其他类别是否需要不同规则。这样能先明确用户可观察的行为,不必提前决定完整类型系统或回收机制。这只是推进建议,尚未替你选择。
对于普通可变记录或序列,你更希望默认 **A:各自独立**,还是 **B:共享修改**?如果你希望所有权成为语言的基础,也可以直接选 C 或 D。
## 讨论来源
《语言设计 04:存储与生命周期》,整理日期 2026-10-28。章节编号沿用原清单。
@@ -1,144 +0,0 @@
# 05 · 对象与成员
0/18 项已确认。当前推进 05-01;本分类尚未完成选择。
本页整理原讨论的问题、验收要求和候选方案。勾选代表讨论中已确认的规则,不代表编译器或运行时已经实现。
## 05-01 对象的基本模型
- [ ] 待讨论
完成标准:确定对象以记录、类实例、原型委托或明确的组合模型为基础;说明对象怎样获得数据和行为,是否必须存在类。
## 05-02 成员种类与命名空间
- [ ] 待讨论
完成标准:确定支持哪些成员种类;明确字段、方法、计算属性等能否同名,同名时如何处理。
## 05-03 成员集合与结构变化
- [ ] 待讨论
完成标准:确定成员是否预先声明,能否在运行时增加、删除或替换;说明非法变更的行为。
## 05-04 字段状态与可写性
- [ ] 待讨论
完成标准:确定字段是否有默认值、能否未初始化、如何表达只读;说明读取未初始化字段、非法赋值的行为。值与引用的具体规则依赖【类型系统/语言语义】。
## 05-05 方法接收者与方法取出
- [ ] 待讨论
完成标准:确定接收者如何传入;说明 `f = p.move` 后调用 `f()` 的含义,以及接收者不匹配时的行为。
## 05-06 计算属性与访问器
- [ ] 待讨论
完成标准:决定是否支持 getter、setter;若支持,明确读写触发规则、只读属性赋值及访问器递归的处理。
## 05-07 成员查找与缺失成员
- [ ] 待讨论
完成标准:确定查找顺序、遮蔽规则,以及成员不存在时是报错、返回特殊结果还是允许自定义处理。错误的发现时机依赖【类型系统】。
## 05-08 成员可见性与封装
- [ ] 待讨论
完成标准:确定公开、私有等访问范围及默认值;说明跨对象、模块、继承关系中的访问边界和失败行为。
## 05-09 对象创建与初始化
- [ ] 待讨论
完成标准:确定创建入口、初始化顺序、必填字段要求;说明初始化失败及初始化期间访问对象的规则。资源释放交由相关【运行时实现/语言语义】议题处理。
## 05-10 继承与委托关系
- [ ] 待讨论
完成标准:决定支持哪些关系、是否允许多个来源及关系变更;说明循环和非法关系的处理。是否形成子类型关系依赖【类型系统】。
## 05-11 组合与成员转发
- [ ] 待讨论
完成标准:确定组合对象的成员必须显式访问,还是支持自动转发;若支持转发,明确冲突、接收者和可见性规则。
## 05-12 覆盖与冲突约束
- [ ] 待讨论
完成标准:确定是否允许覆盖、是否要求显式标记、能否禁止覆盖;明确多个来源提供同名成员时的处理。签名兼容依赖【类型系统】。
## 05-13 方法分派
- [ ] 待讨论
完成标准:确定调用实现由什么决定,例如接收者或多个参数;说明重载候选冲突、无匹配实现的行为。依赖【类型系统】的部分单独标记。
## 05-14 调用上层或指定来源的实现
- [ ] 待讨论
完成标准:决定是否提供 `super` 或类似能力;若支持,明确查找起点及实际接收者。若不支持相关对象关系,明确其后果后可关闭此项。
## 05-15 类型级成员与共享状态
- [ ] 待讨论
完成标准:决定是否存在类/类型级方法和字段;明确访问方式、共享范围,以及它们与实例成员的关系。
## 05-16 外部扩展与行为替换
- [ ] 待讨论
完成标准:决定能否在定义之外添加方法、扩展成员或替换既有行为;明确作用范围、冲突和已有对象受到的影响。
## 05-17 公共基础接口、反射与内部字段边界
- [ ] 待讨论
完成标准:决定是否需要共同顶级类或公共基础协议;明确哪些成员信息可以检查,哪些运行时元数据不可作为普通成员访问。依赖【定制协议/运行时实现/内置与标准库】。
## 05-18 跨分类一致性验收
- [ ] 待讨论
完成标准:用一组贯穿示例检查创建、读写、方法调用、封装和行为复用;核对相关分类的已定规则,解决冲突及阻塞依赖。
## 当前问题的方案与语言对照
以下沿用原讨论材料;建议和参考语言的做法均不自动成为本语言规则。
**05-01 当前要决定的是:对象的数据与行为,主要通过什么机制组织?** 这里先不决定继承、动态增删成员或类型检查。
下面是官方文档中的语言机制;“好处与代价”是供我们判断的取舍分析。
| 语言 | 规则与具体行为 | 好处、代价及对我们的启发 |
|---|---|---|
| **Python** | 用户通常通过类定义实例行为。例如类定义 `move(self, dx, dy)`,调用 `p.move(3, 4)` 时自动传入 `p`。类本身也是对象。[官方文档](https://docs.python.org/3/tutorial/classes.html) | 数据与行为容易集中组织;同时需要解释实例、类、方法绑定等关系。启发:选择类模型,仍需逐项决定这些规则,不能把 Python 的整套机制一并带入。 |
| **JS** | 可以直接创建对象,也可以通过原型共享行为。例如 `p = Object.create(proto)` 后,`p.move()` 可以找到 `proto` 上的方法。`class` 也建立在原型机制之上,普通实例方法通常位于原型上。[ECMAScript 规范](https://tc39.es/ecma262/multipage/overview.html#sec-objects) | 对象创建和行为共享灵活;查找涉及自身成员与原型链,需要处理遮蔽和共享成员变化。启发:类语法与底层对象模型是两个决定。 |
| **TS** | 在 JS 之上提供静态类型检查。例如 `const p: {x: number; y: number} = {x: 1, y: 2}` 不需要定义类。普通对象类型主要按结构检查;运行时对象仍遵循 JS 语义。[官方文档](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html) | 可以描述“需要哪些成员”,而不要求对象来自同一个类;但这种类型描述本身不提供运行时成员验证。启发:对象如何构成,与类型如何判断兼容,应分开决定。 |
| **Go** | 结构体定义字段,方法单独声明并关联到接收者类型。例如为 `Point` 声明接收者为 `*Point` 的 `Move` 方法,然后调用 `p.Move(3, 4)`。方法不是任意对象上随手添加的函数属性,接收者基础类型必须定义在同一包内。[语言规范](https://go.dev/ref/spec#Method_declarations) | 无须类也能组织数据与方法;扩展方法受到明确边界约束。启发:我们此前的 A 方案还应包含“记录/结构体+关联方法”这一变体,不能只考虑独立函数。 |
| **Rust** | `struct Point` 定义数据,`impl Point` 定义关联方法;例如 `move_by(&mut self, …)` 可修改接收者。没有传统的结构体继承,行为抽象还可通过 trait 表达。[方法文档](https://doc.rust-lang.org/book/ch05-03-method-syntax.html)、[继承与多态说明](https://doc.rust-lang.org/book/ch18-01-what-is-oo.html) | 数据、关联方法和行为抽象可以分别组织;需要明确它们之间的规则。启发:方法、封装和多态都不必以类继承为前提。所有权与借用属于其他分类,不随此方案自动引入。 |
因此,仍保留三个主要方向,并把 A 说明得更完整:
- **A.记录/结构体为基础**:数据由记录或结构体表达;行为可以是独立函数,也可以有明确关联的方法。选 A 后,再明确这两种组织方式的关系。
- **B.类与实例为基础**:类集中描述实例的数据与行为。是否继承、能否动态扩展,继续按后续任务决定。
- **C.对象与原型委托为基础**:对象可以直接构造,通过其他对象共享行为;必须明确委托查找规则。
**如果优先考虑基础机制较少、数据与行为关系显式,我建议从 A 中的“结构体+关联方法”开始。** 它能提供 `p.move(...)` 的使用方式,又不必同时引入类继承或原型链;代价是动态对象扩展需要另外设计。这只是建议,尚未定案。
你更倾向 **A、B,还是 C**?如果选 A,我们接着比较“独立函数为主”与“关联方法为主”。
## 讨论来源
《语言设计 05:对象与成员》,整理日期 2026-10-28。章节编号沿用原清单。
@@ -1,173 +0,0 @@
# 06 · 表达式与运算
0/20 项已确认。当前推进 06-01;本分类尚未完成选择。
本页整理原讨论的问题、验收要求和候选方案。勾选代表讨论中已确认的规则,不代表编译器或运行时已经实现。
## 06-01 普通操作数的求值顺序
- [ ] 待讨论
明确二元运算、嵌套表达式的求值顺序,并确定是否采用一条适用于调用参数等表达式的共同规则;用有副作用的例子验收。
## 06-02 求值失败与中断
- [ ] 待讨论
明确某一步发生错误或不终止时,后续哪些步骤还会执行;说明副作用是否保留。依赖【错误处理】。
## 06-03 运算符清单与分组规则
- [ ] 待讨论
列明本轮支持的运算符、优先级、结合性,以及是否允许连续比较;用存在歧义的表达式验收。与【语法】核对。
## 06-04 数值运算的类型组合与转换
- [ ] 待讨论
确定哪些数值组合可运算、是否隐式转换、结果类型,以及不允许组合的失败方式。依赖【类型系统】。
## 06-05 算术结果的具体含义
- [ ] 待讨论
明确支持的加减乘除、余数及其他算术规则,覆盖负数、零、除零、舍入和精度。
## 06-06 溢出与特殊数值
- [ ] 待讨论
确定整数越界、浮点溢出、无穷、NaN、正负零等情况是否存在,以及相应行为。依赖【数值类型】。
## 06-07 同类型值的相等
- [ ] 待讨论
确定相等比较的含义、结果要求,以及复合值按结构还是其他方式比较;说明是否允许违反自反性等性质。
## 06-08 跨类型相等
- [ ] 待讨论
确定不同类型的值能否相等、是否转换,以及无法比较时返回假还是失败;与数值转换规则核对。
## 06-09 身份比较
- [ ] 待讨论
决定是否提供身份判断、哪些值具有身份,以及复制、共享对身份的影响。依赖【对象与类型模型】【内存与生命周期】。
## 06-10 大小比较
- [ ] 待讨论
确定可比较的类型、全序或偏序、不可比较时的行为,以及大小比较与相等的关系。
## 06-11 哈希及其约束
- [ ] 待讨论
决定哪些值可哈希,明确相等与哈希一致性的要求,以及可变值和自定义相等的影响。与【集合与容器】核对。
## 06-12 条件位置接受什么值
- [ ] 待讨论
确定条件必须为布尔值还是允许其他值,并说明转换规则和失败行为。与【控制流】【类型系统】核对。
## 06-13 逻辑运算与短路
- [ ] 待讨论
确定逻辑运算符、是否跳过右侧、结果为布尔值还是操作数,以及能否自定义这些行为。
## 06-14 位运算与移位
- [ ] 待讨论
决定是否支持;若支持,明确适用类型、位宽、负数、右移和非法移位量的行为。
## 06-15 运算扩展的声明方式
- [ ] 待讨论
决定用户能否定义或重载运算,以及通过函数、协议、类型成员或其他机制声明。依赖【类型系统】【定制协议】。
## 06-16 运算实现的选择规则
- [ ] 待讨论
若允许扩展,确定左右操作数、类型匹配、转换和候选优先级,以及歧义的处理;若不允许扩展,记录内置选择规则。
## 06-17 不支持操作时的行为
- [ ] 待讨论
明确“不支持”、运算内部出错、存在歧义的区别,以及编译期和运行期分别如何处理。依赖【类型系统】【错误处理】。
## 06-18 复合赋值与修改语义
- [ ] 待讨论
决定是否支持 `+=` 等形式;若支持,明确目标求值次数、是否原地修改、失败后的状态。依赖【变量与赋值】【对象与类型模型】。
## 06-19 常量求值与优化边界
- [ ] 待讨论
明确常量表达式和普通表达式是否遵守相同结果、错误与精度规则,以及优化必须保留哪些可观察行为。依赖【执行模型】。
## 06-20 跨分类一致性验收
- [ ] 待讨论
核对上述规则与相关分类的最终决定;通过包含副作用、混合数值、不可比较值、哈希、自定义运算和失败路径的代表性例子,解决所有冲突。
## 当前问题的方案与语言对照
以下沿用原讨论材料;建议和参考语言的做法均不自动成为本语言规则。
当前仍是 **06-01:普通操作数的求值顺序**。要决定的是:一个表达式包含多个计算步骤时,语言保证怎样的先后顺序。
例如下面的伪代码,假设各函数都会记录自己被调用了:
```text
a() + b() * c()
```
“先乘后加”决定运算分组;它并不意味着必须先调用 `b()`。我们讨论的是这些函数以及普通变量读取的先后顺序。
相关语言的对照如下。这里说的是语言规则,后面的取舍分析不代表其历史设计动机。
| 语言 | 规则与具体行为 | 对我们设计的启发与取舍 |
|---|---|---|
| **Python 3** | 普通表达式从左到右求值。上例依次调用 `a、b、c`,再完成乘法、加法。赋值另有规则:先求右侧,再处理左侧目标。[语言参考](https://docs.python.org/3/reference/expressions.html#evaluation-order) | 顺序容易预测;实现需要保留可观察行为。也提醒我们应明确赋值等特殊结构的边界。 |
| **JavaScript / TypeScript** | JS 的普通算术表达式先求左操作数,再求右操作数;上例也是 `a、b、c`。TS 对这类表达式提供静态检查,运行时遵循生成的 JS 的规则,类型标注不会建立另一套求值顺序。[ECMAScript 规范](https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-evaluatestringornumericbinaryexpression)、[TS 文档](https://www.typescriptlang.org/docs/handbook/2/basic-types.html#erased-types) | 类型检查与求值顺序可以分别设计;无论选择静态还是动态类型,都可以保证从左到右。 |
| **Rust** | 普通算术、调用参数、元组元素等规定按源码从左到右求值,上例也是 `a、b、c`。赋值和复合赋值另有专门规则。[Rust Reference](https://doc.rust-lang.org/reference/expressions.html#evaluation-order-of-operands)、[赋值规则](https://doc.rust-lang.org/reference/expressions/operator-expr.html#assignment-expressions) | 明确顺序与编译优化可以并存;需要谨慎控制特殊规则,否则使用者记忆负担会上升。 |
| **Go** | 采用部分顺序保证:函数调用、方法调用、接收操作等按词法从左到右,但并非所有普通读取都固定顺序。所以上例调用仍是 `a、b、c`,但下面的例子可能有两个结果。[Go 规范](https://go.dev/ref/spec#Order_of_evaluation) | 可以只固定部分操作的顺序,给其他求值留下自由;代价是“从左到右”的直觉不足以解释所有表达式。 |
Go 的区别可以用这个例子看出来:
```go
a := 1
f := func() int { a++; return a }
x := []int{a, f()}
```
规范允许 `x` 是 `[1, 2]` 或 `[2, 2]`:普通读取 `a` 与调用 `f()` 之间的顺序没有固定。这说明,**仅保证函数调用从左到右,比保证所有普通操作数从左到右更弱**。[Go 规范中的示例](https://go.dev/ref/spec#Order_of_evaluation)
据此,我们有三个可选方案:
- **A:普通操作数统一从左到右。** 变量读取、函数调用等都遵守;逻辑短路、赋值等结构单独明确。最容易推理。
- **B:普通操作数顺序不作保证。** 必要的先后依赖仍须遵守;带副作用的表达式可能允许多个结果,实现选择空间更大。
- **C:部分操作保证从左到右。** 类似 Go,固定调用等操作的顺序,允许其他求值顺序不确定。折中,但规范和使用都更复杂。
**我建议 A**,原因是它能为后续错误传播、重载和复合表达式提供清晰基础;不能观察到差别的优化仍可进行。错误发生后的传播规则留在 06-02,短路留在 06-13,赋值与复合赋值的顺序另行核对。
你选择 **A、B,还是 C**?
## 讨论来源
《语言设计 06:表达式与运算》,整理日期 2026-10-28。章节编号沿用原清单。
@@ -1,182 +0,0 @@
# 07 · 函数与调用
0/22 项已确认。当前推进 07-01;本分类尚未完成选择。
本页整理原讨论的问题、验收要求和候选方案。勾选代表讨论中已确认的规则,不代表编译器或运行时已经实现。
## 07-01 函数作为值【进行中】
- [ ] 待讨论
明确能否赋值、存入数据、作为参数和返回结果,以及受限情形。
## 07-02 函数定义与创建
- [ ] 待讨论
明确具名函数、匿名函数是否支持;定义何时创建函数、何时可见,重复定义怎样处理。
## 07-03 可调用目标
- [ ] 待讨论
明确哪些值可以调用,调用非函数如何失败;方法、可调用对象若存在,明确接入边界。依赖:**对象与类型模型**。
## 07-04 调用的求值策略与顺序
- [ ] 待讨论
明确实参是立即求值还是延迟求值,调用目标与实参的求值顺序,以及求值中断后的行为。
## 07-05 参数传递与修改
- [ ] 待讨论
明确形参赋值、修改实参所指数据分别有什么效果;是否支持引用、借用或其他传递方式。依赖:**变量与内存模型、类型系统**。
## 07-06 位置参数
- [ ] 待讨论
明确按什么顺序绑定,是否支持仅限位置的参数;数量不足或过多怎样处理。
## 07-07 命名参数
- [ ] 待讨论
明确是否支持、参数名是否属于公开接口、与位置参数如何混用,以及未知名称与重复绑定的处理。
## 07-08 默认参数
- [ ] 待讨论
明确求值时机、可访问的名字、默认值之间的依赖、可变默认值的共享行为,以及省略与显式传空值的区别。
## 07-09 可变参数与展开
- [ ] 待讨论
明确是否支持剩余位置参数、剩余命名参数及调用展开;规定收集形式、顺序、重复名称与非法展开。
## 07-10 绑定失败的统一规则
- [ ] 待讨论
明确绑定失败何时发现、是否执行函数体、此前哪些表达式可能已执行,以及错误如何报告。依赖:**错误处理**。
## 07-11 返回与无结果
- [ ] 待讨论
明确显式返回、执行到末尾、无结果与空值、多结果及接收数量不符的行为。
## 07-12 递归与名字绑定
- [ ] 待讨论
明确自递归、相互递归、局部递归函数是否支持,以及重绑定函数名后递归调用指向什么。依赖:**作用域与模块**。
## 07-13 尾调用与调用深度
- [ ] 待讨论
明确是否保证尾调用、哪些位置算尾位置,以及调用深度耗尽怎样失败。依赖:**控制流与运行时**。
## 07-14 闭包的名字解析
- [ ] 待讨论
明确是否支持访问外层局部名字,采用词法作用域还是其他规则,以及遮蔽和未绑定名字的行为。依赖:**作用域**。
## 07-15 闭包捕获与共享状态
- [ ] 待讨论
明确捕获值还是变量、自动还是显式捕获,以及多个闭包、循环变量与外部修改之间的关系。依赖:**变量与内存模型**。
## 07-16 闭包逃逸与生命周期
- [ ] 待讨论
明确闭包能否比创建它的调用活得更久,捕获的资源如何保持有效,不能逃逸时如何拒绝。依赖:**资源管理与类型系统**。
## 07-17 函数的调用接口
- [ ] 待讨论
明确函数签名包含哪些信息、怎样检查兼容性;重载或泛型若存在,明确调用选择的接口。依赖:**类型系统**。
## 07-18 函数身份与比较
- [ ] 待讨论
明确函数是否可比较、复制后是否保持身份、重复创建的闭包有什么关系,以及能否作为键。依赖:**值与相等性模型**。
## 07-19 非普通控制流的调用边界
- [ ] 待讨论
明确异常、异步、生成器等若被采用,对调用与返回产生什么影响;只规定函数侧接口。依赖:**错误处理、并发与控制流**。
## 07-20 内置函数与外部函数
- [ ] 待讨论
明确它们是否遵守同样的参数、返回和错误规则,以及无法兼容时如何声明差异。依赖:**内置与标准库、外部互操作**。
## 07-21 语言调用语义与底层约定
- [ ] 待讨论
明确哪些行为属于语言保证,哪些由实现决定;外部调用需要哪些额外约定,避免把栈、寄存器布局误当语言规则。
## 07-22 跨分类一致性验收
- [ ] 待讨论
用代表性程序核对上述规则,并解决与其他分类的冲突;没有未解决的阻塞后完成。
## 当前问题的方案与语言对照
以下沿用原讨论材料;建议和参考语言的做法均不自动成为本语言规则。
当前仍是 **07-01:函数能否作为值**,即能否保存、传参、返回,以及通过变量调用。闭包捕获规则仍留在后续任务。
### 五种语言怎样做
下面的例子都假定已经定义了一个“输入整数、返回整数”的 `increment` 函数。取舍是针对我们设计的分析,不代表这些语言的历史设计动机。
| 语言 | 规则与具体行为 | 好处、代价及对我们的启发 |
|---|---|---|
| **Python** | 函数是对象。`f = increment` 保存函数,`f(3)` 调用它;也能放入列表、传参和返回。[官方教程](https://docs.python.org/3/tutorial/controlflow.html#defining-functions) | 行为组合直接;单靠语言运行时的动态检查,部分不合法调用到执行时才发现。**支持函数值,不要求我们采用动态类型或 Python 的对象模型。** |
| **JS** | 函数是可调用的对象。例如 `const f = increment; f(3)`,也能作为参数和结果。[ECMAScript 规范](https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-object-type) | 回调与组合方便;接口的正确使用需要额外约束。**函数具有对象属性,是可单独选择的能力,不必随函数值一起引入。** |
| **TS** | 延续 JS 的函数值行为,并增加静态函数类型,例如 `const f: (x: number) => number = increment`。类型标注被擦除,实际执行的是 JS,不会自动获得运行时参数校验。[函数类型](https://www.typescriptlang.org/docs/handbook/2/functions.html)、[类型擦除](https://www.typescriptlang.org/docs/handbook/2/basic-types.html#erased-types) | 能提前检查许多接口错误;需要设计类型兼容规则,且静态检查不能代替外部输入验证。**函数值能力和检查时机可以分开决定。** |
| **Go** | 支持有静态类型的函数值,例如 `var f func(int) int = increment`,随后 `f(3)`;函数也可传参、返回。函数类型变量的零值为 `nil`。[Go 规范](https://go.dev/ref/spec#Function_types) | 调用接口明确;需要处理空函数值等边界。**可以支持函数值,同时保留较直接的签名模型;我们不必继承 `nil`。** |
| **Rust** | 支持保存、传递和返回函数相关的值,但区分函数项、函数指针和闭包。例如 `let f: fn(i32) -> i32 = increment;`。捕获环境的闭包不能直接当作普通 `fn` 指针,通常通过 `Fn` 等接口使用。[Rust 官方书](https://doc.rust-lang.org/book/ch20-04-advanced-functions-and-closures.html)、[函数指针规则](https://doc.rust-lang.org/reference/types/function-pointer.html) | 能明确表达不同调用能力;类型与接口规则更多。**一等函数不要求所有可调用值具有同一种类型或底层表示。** |
这些语言都支持广泛的函数值能力,区别主要在类型、对象能力和环境捕获。**Rust 的类型区分并不意味着它只支持我们下面的 B。**
### 我们当前的三个选项
- **A:函数只作为调用目标。** 可以直接调用,不能作为普通数据保存、传递或返回。规则较少,但通用回调需要其他机制。
- **B:支持受限的函数引用。** 只开放明确的一部分能力,例如仅允许引用静态定义的函数。适合有具体限制目标的语言,需要说明每条限制的理由。
- **C:函数是一等值。** 允许保存、传参和返回,遵守一般的类型与生命周期规则。随后分别决定匿名函数、闭包和函数类型。
**我仍建议 C**:它让排序比较器、事件回调、可替换的处理步骤等接口容易表达,同时保留后续设计空间。
选择 C 后,下面的基础行为应成立:
```text
chosen = increment // 保存,不执行
apply(chosen, 3) // apply 内部调用 chosen,得到 4
return increment // 可以返回已有函数
```
调用非函数应被拒绝;拒绝发生在静态检查还是运行时,依赖类型系统。返回捕获局部变量的函数是否允许,仍待闭包任务决定。
**你选择 A、B,还是 C?**
## 讨论来源
《语言设计 07:函数与调用》,整理日期 2026-10-28。章节编号沿用原清单。
@@ -1,157 +0,0 @@
# 08 · 控制流
0/20 项已确认。当前推进 08-01;本分类尚未完成选择。
本页整理原讨论的问题、验收要求和候选方案。勾选代表讨论中已确认的规则,不代表编译器或运行时已经实现。
## 08-01 控制结构与值的关系
- [ ] 待讨论
明确条件、匹配、循环中哪些能作为表达式,以及结果能否被忽略;给出允许和不允许的使用位置。
## 08-02 条件分支的完整规则
- [ ] 待讨论
明确分支写法、结果来源、缺少 `else` 的行为,以及不同出口如何形成结果。结果兼容性依赖【类型系统】。
## 08-03 条件与真值判断
- [ ] 待讨论
明确条件接受哪些值、是否转换或允许定制,以及不合法条件如何失败。定制机制依赖【对象与类型模型/定制协议】。
## 08-04 短路与条件求值
- [ ] 待讨论
明确是否支持短路逻辑运算、返回布尔值还是操作数,以及哪些子表达式不会执行。
## 08-05 循环形式与正常结束
- [ ] 待讨论
明确支持哪些循环、条件何时检查、零次执行和正常耗尽的行为;若循环有值,定义正常结束的结果。
## 08-06 遍历与迭代协议
- [ ] 待讨论
明确遍历怎样取得下一项、如何识别结束,以及遍历失败和提前退出时的行为。协议形态依赖【定制协议】。
## 08-07 分支及循环的绑定与作用域
- [ ] 待讨论
明确局部绑定的可见范围、循环变量在各次迭代间的关系、循环后的可见性;检查闭包捕获的交互,依赖【名称与作用域/函数】。
## 08-08 `break`
- [ ] 待讨论
明确合法目标、是否携带值、嵌套时如何选择目标,以及目标不存在或值不合法时的行为。
## 08-09 `continue`
- [ ] 待讨论
明确合法目标、跳转后执行循环的哪个阶段、是否支持指定外层循环,以及非法位置的行为。
## 08-10 `return`
- [ ] 待讨论
明确返回目标、无返回值的行为、函数末尾是否隐式返回,以及在嵌套函数和生成器中的含义。依赖【函数】。
## 08-11 模式的种类与匹配含义
- [ ] 待讨论
明确支持哪些模式、每种模式如何判断成功,以及不支持的形式如何处理。结构读取与相等判断依赖【对象与类型模型/运算符】。
## 08-12 匹配顺序与守卫条件
- [ ] 待讨论
明确分支选择顺序、守卫何时执行、失败后是否继续,以及重叠模式和副作用的行为。
## 08-13 匹配覆盖与失败
- [ ] 待讨论
明确是否要求穷尽、是否允许默认分支、无分支命中的行为,以及不可达分支如何处理。静态检查能力依赖【类型系统】。
## 08-14 模式中的变量绑定
- [ ] 待讨论
明确绑定与已有值比较如何区分、重复变量与组合模式是否合法,以及失败匹配是否留下绑定。
## 08-15 生成器的基本控制模型
- [ ] 待讨论
决定是否支持生成器;若支持,明确创建、启动、暂停、恢复和结束,以及如何交付生成值和最终结果。函数调用形式依赖【函数】。
## 08-16 生成器的提前终止与生命周期
- [ ] 待讨论
若支持生成器,明确主动关闭、内部失败、恢复已结束生成器及重入的行为;明确遗弃生成器时对清理作何保证。依赖【资源管理/内存管理】。
## 08-17 异常与执行路径
- [ ] 待讨论
明确异常怎样穿过条件、循环、匹配和函数,在哪里可以被接住,以及捕获后从哪里继续。异常表示与匹配规则依赖【异常与错误处理】。
## 08-18 非正常退出与资源清理
- [ ] 待讨论
明确 `break`、`continue`、`return`、异常和生成器关闭触发哪些清理、执行顺序,以及清理自身再次退出或失败时谁生效。依赖【异常与错误处理/资源管理】。
## 08-19 控制流边界与非法跨越
- [ ] 待讨论
明确跳转能否跨越函数、闭包、生成器或清理区域,是否支持额外的非局部跳转;给出非法跨越的诊断原则。若其他分类引入异步边界,也在此核对。
## 08-20 跨分类一致性验收
- [ ] 待讨论
用一组涵盖嵌套分支、循环退出、匹配失败、生成器暂停及清理冲突的例子核对最终规则;所有相关依赖均已解决或明确不适用,没有相互矛盾的决定。
## 当前问题的方案与语言对照
以下沿用原讨论材料;建议和参考语言的做法均不自动成为本语言规则。
当前待选的 **08-01** 是:**条件、匹配和循环能否直接产生值?产生的值能否被忽略?**
相关语言提供了几种不同的安排:
| 语言 | 规则与小例子 | 取舍及对我们的启发 |
|---|---|---|
| **Python** | `if`、`match`、`for`、`while` 是语句;另有条件表达式:`x = a if ready else b`。[语句规则](https://docs.python.org/3/reference/compound_stmts.html)、[条件表达式](https://docs.python.org/3/reference/expressions.html#conditional-expressions) | 简短选择可以直接产生值;需要多步计算时,通常回到分支赋值或辅助函数。说明“语句为主”仍可以单独提供条件表达式。 |
| **JS/TS** | `if`、`switch` 和循环是语句;条件表达式写成 `const x = ready ? a : b`。TS 在此沿用 JS 的执行语义,并增加静态检查,不是另一套运行时。[ECMAScript 语句](https://tc39.es/ecma262/multipage/ecmascript-language-statements-and-declarations.html)、[条件表达式](https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-conditional-operator)、[TS 类型擦除](https://www.typescriptlang.org/docs/handbook/2/basic-types.html#erased-types) | 与 Python 类似:简单选择紧凑,多步分支不直接作为表达式组合。启发是要区分“单独的条件运算符”与“整个分支块都能产生值”。 |
| **Go** | `if`、`switch`、`for` 是语句,也没有 `?:`;通常先声明变量,再在分支中赋值。[Go 规范](https://go.dev/ref/spec)、[官方 FAQ](https://go.dev/doc/faq#Does_Go_have_a_ternary_form) | 选择结果与执行步骤使用同一种分支结构,规则较少;代价是简单初始化也可能需要多行。适合参考方案 A。 |
| **Rust** | `if`、`match` 是表达式,如 `let x = if ready { a } else { b };`。`loop` 可以用 `break value` 交付结果。[分支规则](https://doc.rust-lang.org/reference/expressions/if-expr.html)、[匹配规则](https://doc.rust-lang.org/reference/expressions/match-expr.html)、[循环规则](https://doc.rust-lang.org/reference/expressions/loop-expr.html) | 多步计算也能直接组合、初始化变量;需要处理各出口的结果兼容性。**但 Rust 的 `while`、`for` 正常结束得到 `()`,不能像 `loop` 那样直接 `break value`。** 表达式化仍然可以区分不同循环。 |
关于“忽略结果”,Rust 明确支持通过表达式语句求值并丢弃结果;这说明**能够产生值,不必意味着必须接住值**。[Rust 表达式语句](https://doc.rust-lang.org/reference/statements.html#expression-statements)
回到我们的三个候选方向:
- **A:控制结构主要是语句。** 简单条件是否另设表达式,需要明确;Python/JS 和 Go 在这里就有区别。
- **B:`if`、匹配可以产生值,循环是语句。** 分支计算容易组合,循环无需引入结果规则。
- **C:分支和循环都可以产生值。** 查找、重试可以直接交付结果,但需要定义哪些循环支持、正常耗尽时得到什么。
**我的建议是 B,并允许忽略控制表达式的结果。** 它能让多步分支直接初始化变量,又暂时不引入循环结果的额外规则。这只是建议,尚未记为决定;分支缺少 `else` 等细节仍在 `08-02` 处理。
你更倾向 **A、B、C** 哪一种?对于能够产生值的控制结构,是否允许直接执行并忽略结果?
## 讨论来源
《语言设计 08:控制流》,整理日期 2026-10-28。章节编号沿用原清单。
@@ -1,168 +0,0 @@
# 09 · 集合与访问协议
0/22 项已确认。当前推进 09-01;本分类尚未完成选择。
本页整理原讨论的问题、验收要求和候选方案。勾选代表讨论中已确认的规则,不代表编译器或运行时已经实现。
## 09-01 能力如何组织
- [ ] 待讨论
确定下标、迭代、成员判断等能力独立提供、按类别打包,还是允许相互推导;明确组合约束。
## 09-02 下标接受什么
- [ ] 待讨论
确定位置、键、多维下标及其他选择器的支持范围,以及不支持的下标如何失败。
## 09-03 读取失败与缺失
- [ ] 待讨论
确定键不存在、无法读取时的行为,以及如何区分“缺失”和“存储的空值”。依赖「错误处理与结果类型」。
## 09-04 下标写入与删除
- [ ] 待讨论
确定是否分别支持、键不存在时如何处理,以及只读容器上的行为。
## 09-05 位置索引规则
- [ ] 待讨论
确定索引起点、可接受的索引值、负索引、越界与空集合行为。
## 09-06 文本索引单位
- [ ] 待讨论
确定字符串是否支持位置访问,以及访问单位和边界。依赖「字符串与文本」。
## 09-07 切片范围规则
- [ ] 待讨论
确定端点包含关系、默认端点、步长、反向切片、越界与非法步长行为。
## 09-08 切片结果语义
- [ ] 待讨论
确定副本、共享视图或惰性结果的选择规则,以及原集合变化的影响。
## 09-09 切片写入与删除
- [ ] 待讨论
确定支持范围、替换长度要求、重叠操作及失败后的集合状态。
## 09-10 长度与有限性
- [ ] 待讨论
确定哪些对象提供长度,未知或无限长度如何表达,长度是否允许通过遍历计算。
## 09-11 获得迭代器与重复遍历
- [ ] 待讨论
确定如何开始遍历,区分可重复遍历对象与单次消费对象,以及多个遍历是否独立。
## 09-12 取下一项与迭代结束
- [ ] 待讨论
确定元素、正常结束和读取失败的区别,以及结束后再次读取的行为。依赖「错误处理与结果类型」。
## 09-13 遍历内容与顺序
- [ ] 待讨论
确定映射等容器产出什么,各类容器承诺哪些顺序,以及顺序是否稳定。
## 09-14 遍历期间修改集合
- [ ] 待讨论
确定允许的修改、观察到的结果,以及冲突检测和失败行为。
## 09-15 提前结束与资源释放
- [ ] 待讨论
确定退出循环、异常和部分消费时的迭代器处理。依赖「资源管理与控制流」。
## 09-16 成员判断的含义
- [ ] 待讨论
确定判断键、值还是元素,采用何种匹配规则,以及缺失时的结果。依赖「值与相等性」。
## 09-17 成员判断的默认实现
- [ ] 待讨论
确定是否允许退回遍历,以及单次流、无限数据和遍历失败时的行为。
## 09-18 解包的数据来源
- [ ] 待讨论
确定解包依赖迭代、位置访问还是独立协议,以及消费顺序。
## 09-19 解包数量与失败
- [ ] 待讨论
确定数量不足、数量过多、剩余项捕获及无限输入的行为;明确失败时已消费数据和已绑定变量的状态。依赖「绑定与赋值」。
## 09-20 自定义容器如何接入
- [ ] 待讨论
确定用户需要提供哪些操作、哪些有默认行为,以及缺失或冲突如何报告。依赖「类型系统与定制协议」。
## 09-21 异步遍历的支持边界
- [ ] 待讨论
确定本轮是否支持;若支持,明确与同步遍历、取消和清理的关系。依赖「并发与异步」。
## 09-22 跨协议一致性验收
- [ ] 待讨论
检查索引、长度、切片、迭代、成员判断和解包之间的约束;核对相关分类决定,解决冲突。
## 当前问题的方案与语言对照
以下沿用原讨论材料;建议和参考语言的做法均不自动成为本语言规则。
当前仍是 **09-01:下标、迭代、成员判断等能力如何组织**。核心问题是:支持一种能力,是否自动获得其他能力?
下面的规则来自语言或标准库文档;其中的优缺点是我们的取舍分析,不代表已经确认的历史设计动机。
| 语言 | 相关规则与具体行为 | 好处、代价及启发 |
|---|---|---|
| **Python** | 能力分别定制,但有回退:没有 `__contains__` 时,`x in obj` 会尝试迭代,再尝试旧式位置访问。因此 `2 in iter([1, 2, 3])` 为真,同时消费迭代器到 `2`。标准库也提供 `Sequence` 等组合契约。[数据模型](https://docs.python.org/3/reference/datamodel.html#object.__contains__)、[容器抽象](https://docs.python.org/3/library/collections.abc.html) | 定制操作较少也能获得便利;但成员判断可能隐含消费和长时间搜索。说明“独立能力”“组合契约”“默认推导”可以分层存在。 |
| **JS** | 迭代通过 `Symbol.iterator`,与下标访问分开。`{0: "a", length: 1}` 能读取 `[0]`,却不能直接用于 `for...of`。`Map` 通过 `.has(k)` 判断键是否存在;JS 的 `in` 检查对象属性,并非通用集合成员判断。[迭代协议](https://tc39.es/ecma262/2025/multipage/abstract-operations.html#sec-getiterator)、[属性判断](https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-relational-operators-runtime-semantics-evaluation)、[Map.has](https://tc39.es/ecma262/2025/multipage/keyed-collections.html#sec-map.prototype.has) | 能力边界较明确;不同容器的查询接口也可能不同。启发是:统一语法与统一能力是两个决定。 |
| **TS** | 用静态类型分别表达能力,例如 `ArrayLike<T>` 描述数字下标和长度,`Iterable<T>` 描述可迭代;只有前者不保证满足后者。执行仍依赖生成的 JS、运行环境及编译配置,TS 没有独立运行时。[对象类型](https://www.typescriptlang.org/docs/handbook/2/objects.html)、[迭代类型](https://www.typescriptlang.org/docs/handbook/iterators-and-generators.html) | 可以提前检查算法需要的能力;类型声明仍需与运行行为一致。我们可以先决定语义,再决定是否静态检查。 |
| **Go** | 内置下标适用于规定的类型,不能靠给任意结构体添加方法来重载 `[]`;`range` 有独立规则,Go 1.23 起支持规定签名的迭代函数。例如通道可以被 `range` 遍历,却不能下标访问。[语言规范](https://go.dev/ref/spec) | 内置规则集中明确;自定义容器接入下标语法的空间较小。说明访问与遍历也可以采用不同的扩展机制。 |
| **Rust** | `Index` 与 `IntoIterator` 是分开的 trait。自定义类型只实现 `Index`,不会因此获得 `for` 遍历。通用搜索可显式写成 `iter.any(|x| x == 42)`,会推进迭代器直到匹配或结束。[Index](https://doc.rust-lang.org/std/ops/trait.Index.html)、[IntoIterator](https://doc.rust-lang.org/std/iter/trait.IntoIterator.html)、[any](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.any) | 能力要求明确,搜索也能复用;自定义类型需要提供对应能力。说明“提供通用算法”不必意味着“自动赋予另一项协议”。 |
这些语言都混合使用了多种机制,不能简单把整门语言归为某一个方案。我们当前仍选这三个方向之一:
- **A.按类别打包**:序列、映射等各自规定必需能力;特殊容器可能需要适配。
- **B.能力独立**:可下标不自动意味着可迭代,可迭代不自动意味着支持成员判断;通用搜索可以显式调用。
- **C.能力独立并允许默认推导**:例如没有专门成员判断时,自动遍历;具体回退规则在后续任务明确。
**我建议 B**:目前语言用途尚未确定,先独立表达能力,可以容纳数组、映射和单次输入流;常用能力仍可组合成契约。代价是自定义容器可能要多提供几个操作。这只是建议,尚未成为决定。
你倾向 **A、B,还是 C**?
## 讨论来源
《语言设计 09:集合与访问协议》,整理日期 2026-10-28。章节编号沿用原清单。
@@ -1,136 +0,0 @@
# 10 · 错误与资源
0/16 项已确认。当前推进 10-01;本分类尚未完成选择。
本页整理原讨论的问题、验收要求和候选方案。勾选代表讨论中已确认的规则,不代表编译器或运行时已经实现。
## 10-01 失败的基本控制流
- [ ] 待讨论
确定使用错误结果、异常还是混合机制,以及失败后调用处如何继续。
## 10-02 错误的表示与分类
- [ ] 待讨论
确定错误携带哪些信息、如何区分类别和匹配处理器,是否允许自定义错误。依赖【对象与类型模型】。
## 10-03 失败是否属于接口契约
- [ ] 待讨论
确定函数是否声明可能的失败、调用者是否必须处理,以及遗漏处理如何检查。依赖【类型系统】。
## 10-04 向上传播与机制转换
- [ ] 待讨论
确定自动或显式传播规则,以及错误结果与异常之间是否允许转换、转换保留什么信息。
## 10-05 捕获与恢复
- [ ] 待讨论
确定处理器的匹配顺序、可见范围、重新传播,以及处理后从哪里继续;明确是否支持回到失败点继续执行。
## 10-06 退出作用域与栈展开
- [ ] 待讨论
确定错误跨越调用和作用域时的退出顺序,哪些清理必须执行,以及清理能访问哪些仍有效的数据。
## 10-07 清理结构与执行时机
- [ ] 待讨论
决定支持哪些 `finally`、`defer` 或资源作用域机制,明确注册时机、执行顺序,以及正常结束、提前返回和错误退出的行为。
## 10-08 资源责任与关闭规则
- [ ] 待讨论
确定谁负责关闭、责任能否转移,以及共享、重复关闭和关闭后使用如何处理。依赖【所有权与生命周期/类型系统】。
## 10-09 清理过程中再次失败
- [ ] 待讨论
确定原始错误与清理错误如何保留,后续清理是否继续,以及最终向外报告什么。
## 10-10 析构、终结与 GC 的边界
- [ ] 待讨论
确定是否提供析构或终结机制、执行保证及限制,明确内存回收能否承担资源关闭责任。依赖【内存管理与运行时】。
## 10-11 不可恢复失败与终止
- [ ] 待讨论
区分可处理失败、程序缺陷和强制终止;确定各类情况下是否展开和清理,以及未处理错误到达程序边界后的行为。
## 10-12 异步操作与取消中的清理
- [ ] 待讨论
明确是否支持异步清理、取消如何传播,以及清理期间再次取消的行为。依赖【并发与异步】。
## 10-13 并发中的多个失败
- [ ] 待讨论
确定任务失败由谁接收,多个失败如何组合,其他任务是否继续或取消。依赖【并发与异步】。
## 10-14 资源协议与外部接口边界
- [ ] 待讨论
明确语言内置保证与库协议的分工,以及外部调用的错误如何进入本语言、跨越外部调用时谁负责清理。依赖【定制协议、标准库与外部互操作】。
## 10-15 错误诊断与上下文保留
- [ ] 待讨论
确定是否保留调用位置、传播路径、原始原因与附加上下文,以及包装、重抛后保留什么。
## 10-16 跨分类一致性验收
- [ ] 待讨论
用正常返回、嵌套失败、清理失败、未处理失败及适用的取消场景核对全部规则,消除与其他分类决定的冲突和阻塞。
## 当前问题的方案与语言对照
以下沿用原讨论材料;建议和参考语言的做法均不自动成为本语言规则。
当前仍是 **10-01:常规操作失败时,返回失败结果,还是直接中断控制流?** 这里先讨论文件不存在、配置格式错误等情况,致命故障留到 10-11。
**相关语言怎样做**
| 语言 | 规则与小例子 | 好处、代价与启发 |
|---|---|---|
| **Python** | 广泛使用异常。例如 `int("abc")` 抛出 `ValueError`,跳过当前路径中的后续语句,寻找匹配的 `except`;中间函数不必逐层转发。也允许接口返回普通错误结果。[官方文档](https://docs.python.org/3/tutorial/errors.html) | 正常路径紧凑,便于外层集中处理;调用表达式本身通常看不出可能抛什么。启发:选择异常,就要接受并管理这种隐式退出路径。 |
| **JS** | 支持 `throw` 和 `try/catch`,可抛出任意值。例如 `throw "bad config"` 会中断当前路径,由外层捕获。[语言规范](https://tc39.es/ecma262/multipage/ecmascript-language-statements-and-declarations.html#sec-throw-statement) | 传播方便,但处理器不能天然假定拿到的是标准错误对象。启发:**采用异常**与**允许抛什么**是两个独立决定。 |
| **TS** | 运行时沿用 JS 的异常行为。静态检查可以约束错误的使用:启用 `useUnknownInCatchVariables` 时,捕获值默认为 `unknown`,访问其属性前需要缩小类型;这不等于静态列出函数可能抛出的错误。[官方文档](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-4.html#defaulting-to-the-unknown-type-in-catch-variables---useunknownincatchvariables) | 能提高处理代码的安全性,却不会自动消除隐式异常路径。启发:静态类型检查与失败控制流要分别设计。 |
| **Go** | 常规错误通常作为额外返回值,例如得到 `(文件, error)` 后检查 `err != nil`;返回错误本身不会自动退出调用者。另有 `panic/recover` 机制。[官方说明](https://go.dev/doc/effective_go#errors) | 检查与返回位置明确;逐层传播容易重复,也可能忽略错误。启发:显式错误值和另一种中断机制可以并存,但需要明确用途。 |
| **Rust** | 常规可恢复错误使用 `Result<T, E>`,区分 `Ok` 与 `Err`。例如 `File::open(path)?` 在失败时提前返回当前函数;不用 `?` 时,可检查或传递这个结果。[官方文档](https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html) | 结果结构清楚,传播简写减少重复;需要相应的类型与转换规则。启发:**显式结果并不要求每次手写完整分支**,传播语法可以另行设计。 |
这些实践提供参考,不替我们作决定;表中的好处与代价是取舍分析,不是对历史设计动机的断言。
**我们当前的三个候选方案**
沿用“配置文件不存在就使用默认配置,格式错误就停止启动”的例子:
- **A.常规失败统一返回显式结果**:调用者检查或显式传播。可以随后讨论传播简写,无须现在确定类型语法。
- **B.常规失败统一通过异常传播**:调用者捕获“文件不存在”来恢复,格式错误继续向外传播。
- **C.两者并存**:允许不同接口选择,但必须进一步确定分工,避免调用者不清楚该检查返回值还是捕获异常。
我的暂时建议是优先考虑 **A**:它让失败路径容易在调用处辨认,也能通过传播简写减少重复。如果你更重视多层调用后集中处理错误,**B** 更值得选。选择 **C** 则需要有明确的两类使用场景,收益才足以抵消复杂度。语言用途尚未确定,这个建议仍是候选。
你更倾向 **A、B,还是 C**?
## 讨论来源
《语言设计 10:错误与资源》,整理日期 2026-10-28。章节编号沿用原清单。
@@ -1,168 +0,0 @@
# 11 · 模块与程序执行
0/22 项已确认。当前推进 11-01;本分类尚未完成选择。
本页整理原讨论的问题、验收要求和候选方案。勾选代表讨论中已确认的规则,不代表编译器或运行时已经实现。
## 11-01 模块的职责与顶层内容
- [ ] 待讨论
明确模块主要承载声明、可执行初始化,还是可实例化的定义;说明导入与执行的关系。
## 11-02 模块的组织单位
- [ ] 待讨论
明确模块与文件、目录、包的对应关系,以及嵌套模块、多文件模块是否支持;说明不合法的组织形式。
## 11-03 模块身份
- [ ] 待讨论
明确判断“同一个模块”的依据;覆盖路径别名、重复引用及不同版本,说明身份冲突如何处理。
## 11-04 名称与路径解析
- [ ] 待讨论
明确绝对引用、相对引用、搜索位置及优先级;说明找不到目标、存在多个候选时的行为。
## 11-05 依赖与版本选择
- [ ] 待讨论
明确依赖由谁声明、版本由谁选择,能否同时使用多个版本;划清语言、构建工具和包管理器的职责。
## 11-06 导出与可见性
- [ ] 待讨论
明确默认公开范围、显式导出及内部成员的访问规则;说明越权引用如何失败。依赖【名称与作用域】。
## 11-07 导入形式与名称冲突
- [ ] 待讨论
明确整体导入、成员导入、别名、重新导出和批量导入的支持范围;说明重名与遮蔽规则。
## 11-08 跨模块绑定语义
- [ ] 待讨论
明确导入获得值、持续关联的绑定,还是其他形式;说明重新赋值和修改共享对象的区别。依赖【名称与作用域】【对象与类型模型】。
## 11-09 依赖检查与条件导入
- [ ] 待讨论
明确依赖必须何时可用,是否支持可选或条件导入;说明未使用但缺失的依赖是否导致失败。依赖【编译与执行方式】。
## 11-10 初始化触发与次数
- [ ] 待讨论
明确初始化何时发生、何种范围内执行几次,以及重复导入的行为。
## 11-11 初始化顺序与并发
- [ ] 待讨论
明确依赖之间的初始化顺序,是否允许并发或异步初始化;说明顺序不确定时有哪些保证。依赖【并发与异步】。
## 11-12 循环依赖
- [ ] 待讨论
明确允许哪些循环,何时检测;说明访问尚未准备好的定义或状态如何处理。
## 11-13 初始化失败
- [ ] 待讨论
明确失败后模块处于什么状态,能否重试,如何处理已发生的副作用,以及依赖方会得到什么结果。依赖【错误处理】。
## 11-14 模块状态与隔离
- [ ] 待讨论
明确是否允许可变模块状态、由谁持有、何时结束,以及多个程序环境能否相互隔离。依赖【内存与资源管理】。
## 11-15 程序入口的确定
- [ ] 待讨论
明确入口是指定函数、指定模块、顶层代码还是其他形式;说明缺失或存在多个入口时如何处理。
## 11-16 入口的输入输出契约
- [ ] 待讨论
明确参数如何传入、入口允许返回什么,以及如何形成退出结果;说明不合法的入口签名。依赖【类型系统】【标准库与宿主接口】。
## 11-17 启动流程
- [ ] 待讨论
明确依赖准备、模块初始化与入口执行的先后关系;说明启动中途失败时入口和清理是否执行。
## 11-18 动态装载与模块生命周期
- [ ] 待讨论
明确是否支持运行中加载、重新加载和卸载;如支持,说明旧引用、状态与资源的去向;如不支持,记录限制。
## 11-19 正常结束
- [ ] 待讨论
明确入口返回、显式退出与后台工作之间的关系;说明哪些条件代表程序真正结束。依赖【并发与异步】。
## 11-20 异常与外部终止
- [ ] 待讨论
明确未处理错误、取消、外部终止请求和强制终止分别有什么保证;说明错误报告与退出结果。依赖【错误处理】【并发与异步】【宿主接口】。
## 11-21 清理协议与顺序
- [ ] 待讨论
明确清理动作如何注册或归属、执行顺序、能否异步,以及清理自身失败或重复触发的处理。依赖【内存与资源管理】【错误处理】。
## 11-22 跨分类一致性验收
- [ ] 待讨论
用普通启动、共享依赖、循环依赖、初始化失败和退出清理等完整场景检查规则;相关分类决定已对齐,且没有未解决的阻塞,才可完成。
## 当前问题的方案与语言对照
以下沿用原讨论材料;建议和参考语言的做法均不自动成为本语言规则。
当前仍是 **11-01:模块的职责与顶层内容**。要决定的是:模块顶层能否执行读取配置、打开连接等动作,以及这些动作是否由导入自动触发。
相关语言的对照如下。下述好处与代价是设计取舍分析,不是在断言它们的历史设计动机。
| 语言 | 规则与具体行为 | 取舍及对我们的启发 |
|---|---|---|
| **Python** | 对普通源码模块,常规首次导入会执行模块代码。例如顶层 `current = read_config()` 会在导入时调用函数;后续命中模块缓存时通常不再执行。这里说的是 Python 导入规则,并非 CPython 独有的优化。[官方参考](https://docs.python.org/3/reference/import.html#loading) | 顶层写法直接,容易提供共享状态;但一次导入可能读文件、修改环境或抛出异常。对应我们的 **B**。 |
| **JS:ES Modules** | 模块允许顶层语句。例如模块中写 `console.log("loaded")`,该模块被求值时就会打印;普通静态导入会使依赖参与求值。导入声明的连接过程与模块代码求值是不同阶段。这里专指 ESM,CommonJS 另有规则。[ECMAScript 规范](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#sec-modules) | 能自动执行依赖的准备动作;也需要处理副作用、初始化顺序和失败。提醒我们区分“找到并连接定义”和“执行初始化”。 |
| **TS** | 运行时模块行为由实际采用的 JS 模块格式和宿主决定,TS 不提供独立运行时。`import type { Config } ...` 会从输出 JS 中消除,因此这条导入本身不会触发运行时加载。[官方文档](https://www.typescriptlang.org/docs/handbook/modules/reference#type-only-imports-and-exports) | 类型依赖可以与运行时依赖分开,但要明确两者的区别。启发是:需要一个类型,不一定需要运行其所属模块;是否采用该机制留给后续任务。 |
| **Go** | 包顶层不能任意写执行语句,但变量初始化可以调用函数,如 `var current = readConfig()`;还可以写自动执行的 `func init()`。程序启动时先初始化依赖包,再运行入口。[语言规范](https://go.dev/ref/spec#Package_initialization) | 顶层结构规整,仍可自动准备共享状态;副作用仍可能藏在变量初始化里。说明**“顶层只准声明”并不等于“没有自动执行”**。 |
| **Rust** | 模块是定义项的容器,没有普通的“导入即执行模块体”机制。直接在模块体中写 `read_config();` 不合法;普通 `static` 的初始化表达式要求能在编译期求值,不能直接读取运行时配置文件。可把读取动作放入函数,由调用者调用。[模块规则](https://doc.rust-lang.org/reference/items/modules.html)、[静态项规则](https://doc.rust-lang.org/reference/items/static-items.html) | 普通模块使用不会自动读取配置,启动动作更明确;调用者需要组织初始化。对应我们的 **A** 方向。 |
还有一点需要澄清:**A、B 两种模块都可以导出 `create(config)` 这样的普通函数,用它创建多个独立状态。** 只有当我们希望语言本身支持“创建模块实例、给实例连接依赖”时,才需要之前的 **C**;普通对象或函数已经能解决不少隔离需求。
因此当前三个选择保持不变:
- **A.显式调用**:模块提供定义;读取配置等运行时动作放入函数,由调用者启动。还需明确允许哪些顶层初始化表达式,才能保证声明中不会暗藏这些动作。
- **B.自动初始化**:模块可以带运行时初始化,使用模块时由系统安排执行;触发时机、次数和失败处理分别在后续任务决定。
- **C.显式模块实例**:导入定义后,由调用者创建带状态的模块实例;语言需要提供实例创建和依赖连接规则。
**我暂建议 A**:它让初始化失败发生在明确的调用处,也方便调用者决定状态如何共享。代价是程序需要多写一些启动与状态传递代码。如果你希望模块直接提供“准备好的共享服务”,B 会更顺手。这个建议尚未成为决定。
## 讨论来源
《语言设计 11:模块与程序执行》,整理日期 2026-10-28。章节编号沿用原清单。
@@ -1,163 +0,0 @@
# 12 · 并发与异步
0/21 项已确认。当前推进 12-01;本分类尚未完成选择。
本页整理原讨论的问题、验收要求和候选方案。勾选代表讨论中已确认的规则,不代表编译器或运行时已经实现。
## 12-01 可变状态的共享规则
- [ ] 待讨论
确定普通可变值默认能否跨执行单位共享,以及允许共享的条件、违规后果。
## 12-02 数据跨边界传递
- [ ] 待讨论
确定参数、闭包捕获和消息采用复制、共享还是转移;明确嵌套引用及发送后继续访问的规则。依赖:对象与类型模型。
## 12-03 支持哪些执行单位
- [ ] 待讨论
确定是否提供线程、任务、协程或其他单位,明确各自能力及相互关系;不支持的单位也明确记录。
## 12-04 并行能力与边界
- [ ] 待讨论
确定哪些执行单位允许同时执行,哪些必须串行,以及程序能否依赖特定并行度。
## 12-05 创建、启动与生命周期
- [ ] 待讨论
确定工作何时启动、由谁管理、能否脱离创建者,以及作用域退出时如何处理未结束工作。
## 12-06 等待结果与失败传播
- [ ] 待讨论
确定等待一次或多次的行为、结果保存方式、未观察错误,以及多个子任务失败时的处理。依赖:异常与错误处理。
## 12-07 异步暂停与调用规则
- [ ] 待讨论
确定暂停是否显式、哪些上下文允许等待、可等待值的协议,以及普通调用与异步调用如何组合。依赖:函数与定制协议。
## 12-08 阻塞操作与异步的衔接
- [ ] 待讨论
确定同步 I/O、阻塞调用进入任务后的行为,以及是否提供转交线程等机制;明确误用后果。
## 12-09 取消与清理
- [ ] 待讨论
确定谁能请求取消、何时生效、能否屏蔽、如何观察取消,以及清理代码能否继续等待。依赖:资源管理、异常与错误处理。
## 12-10 超时与竞争等待
- [ ] 待讨论
确定超时是否取消工作、多路等待如何选胜者、同时完成如何处理,以及落选工作由谁回收。
## 12-11 调度与进展保证
- [ ] 待讨论
确定切换时机、协作式或抢占式约束、饥饿与公平性保证,以及执行单位能否迁移线程。
## 12-12 显式同步工具
- [ ] 待讨论
确定支持哪些锁、条件变量或信号量;明确重入、持锁暂停、取消等待和死锁的处理。
## 12-13 消息与通道
- [ ] 待讨论
确定是否支持消息机制;若支持,明确顺序、容量、背压、关闭、发送失败与接收结束的行为。
## 12-14 原子操作
- [ ] 待讨论
确定是否提供原子值及操作;若支持,明确操作范围、内存顺序和不支持的值类型。依赖:类型系统、数值与基础类型。
## 12-15 内存可见性与数据竞争
- [ ] 待讨论
明确同步建立什么可见性保证,以及未同步冲突访问是被拒绝、报错,还是具有某种规定的后果。
## 12-16 运行时自身的并发保护
- [ ] 待讨论
明确 GIL、对象级锁、隔离堆等候选机制必须保障什么,以及哪些实现选择可以保留自由;区分运行时安全与用户操作的原子性。依赖:运行时实现。
## 12-17 复合操作与重入
- [ ] 待讨论
明确单次安全操作能否组合成事务;规定回调、用户代码重入及跨多个值维护不变量时的保证。依赖:对象与定制协议。
## 12-18 资源与对象生命周期的交互
- [ ] 待讨论
明确任务退出、共享对象存活、资源关闭、终结处理与并发访问发生冲突时的规则。依赖:内存管理、资源管理。
## 12-19 外部代码与外来线程
- [ ] 待讨论
确定是否支持外部阻塞调用、外来线程进入语言和跨边界回调;明确同步责任及违规后果。依赖:外部接口、运行时实现。
## 12-20 执行上下文与局部状态
- [ ] 待讨论
确定线程局部、任务局部或等价机制是否存在,以及创建、迁移、取消时如何继承和释放。
## 12-21 跨分类一致性与最终验收
- [ ] 待讨论
用代表性场景检查上述规则能否共同成立,解决跨分类冲突,并形成最终规则摘要。
## 当前问题的方案与语言对照
以下沿用原讨论材料;建议和参考语言的做法均不自动成为本语言规则。
当前仍是 **12-01:普通可变数据能否跨执行单位直接共享,以及谁负责保证安全。** 这里允许不同边界采用不同规则,例如同一线程内的任务与可能并行的工作之间,可以有不同限制。
现有语言提供了四种有用的参考:
| 语言 | 规则与具体行为 | 好处、代价及启发 |
|---|---|---|
| **Python** | 同一解释器中的线程可以持有同一个列表、字典等对象。程序员可以用同一把锁保护“检查库存+扣减”。GIL 是 CPython 的实现机制;CPython 也有自由线程构建,内置容器的内部保护不能替代业务同步。[官方文档](https://docs.python.org/3.14/howto/free-threading-python.html) | 共享方便,但同步责任主要由程序员承担。启发:必须分别说明“对象内部安全”和“整段业务操作正确”。 |
| **JS/TS** | 同一 JS agent 内的异步工作可以访问同一个普通对象;跨 `await` 的操作仍可能交错。浏览器 Worker 之间通常通过消息复制或转移数据,不能直接共享普通对象;共享内存可显式使用 `SharedArrayBuffer`,配合原子操作。Worker 属于宿主提供的机制。[ECMAScript](https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-agents)、[HTML 标准](https://html.spec.whatwg.org/multipage/workers.html) | 普通对象跨 Worker 的隔离减少直接共享风险,但有传递成本;显式共享内存增加复杂性。启发:可以采用“局部共享、跨并行边界默认隔离”。 |
| **Go** | goroutine 可以直接共享变量,但冲突访问必须同步。例如两个 goroutine 访问同一库存,可以用 `sync.Mutex` 保护;也可以让一个 goroutine 管理库存,其他工作通过 channel 请求。channel 不会自动剥夺发送者对所发送指针的访问权。[内存模型](https://go.dev/ref/mem)、[Effective Go](https://go.dev/doc/effective_go#sharing) | 共享内存和消息协作都方便;“发送后不再访问”通常需要程序员遵守。启发:提供消息工具与强制隔离是两个不同决定。 |
| **Rust** | 安全 Rust 通过所有权、借用和跨线程约束限制危险共享。共享修改库存可以使用 `Arc<Mutex<库存>>`;访问锁内数据需要取得锁守卫,普通 `Rc` 则不能直接跨线程传递。[官方教程](https://doc.rust-lang.org/book/ch16-03-shared-state.html) | 能提前拒绝部分危险访问,但规则和类型更复杂,也仍可能死锁或出现业务竞态。启发:可以把“满足条件才能共享”做成可检查的规则。 |
其中,**TS 没有独立的并发运行时**,实际行为依赖生成的 JS 和宿主环境。`readonly` 等静态限制也不会自动提供运行时冻结或同步保护。[TS 对象类型文档](https://www.typescriptlang.org/docs/handbook/2/objects.html#readonly-properties)
回到我们的三个候选方向:
- **A.默认允许共享,使用者负责同步。** 接近 Python 线程、Go 共享变量的使用方式。表达直接,但容易遗漏同步。
- **B.跨并行边界默认隔离,通过消息协作。** 可以参考浏览器 Worker 的普通对象边界。管理责任清楚,但必须明确复制、转移和响应规则。
- **C.只有满足检查规则才能共享或转移。** 可以参考 Rust。能更早发现危险用法,但依赖更完整的别名、所有权或运行期检查设计。
**我的暂定建议是优先考虑 B**:先让普通可变数据跨并行边界的归属清楚,再讨论是否提供显式共享机制。它也有成本:频繁交换大型数据时,需要认真设计传递方式。如果我们的目标更偏向细粒度共享和底层控制,C 值得优先考虑。这个建议尚未成为决定。
你更倾向 **A 的直接共享、B 的默认隔离,还是 C 的受检查共享**?如果你的偏好是“同一线程内共享、跨并行边界隔离”,也可以直接这样确定方向。
## 讨论来源
《语言设计 12:并发与异步》,整理日期 2026-10-28。章节编号沿用原清单。
@@ -1,175 +0,0 @@
# 13 · 外部交互
0/22 项已确认。当前推进 13-01;本分类尚未完成选择。
本页整理原讨论的问题、验收要求和候选方案。勾选代表讨论中已确认的规则,不代表编译器或运行时已经实现。
## 13-01 外部交互的默认安全边界
- [ ] 待讨论
明确普通代码默认能否接触外部地址、依靠谁保证安全,以及是否提供不安全入口。
## 13-02 I/O 的基础接口单位
- [ ] 待讨论
明确字节、文本、流、消息之间的基础关系,以及哪些能力由上层封装提供。
## 13-03 I/O 的执行方式
- [ ] 待讨论
明确阻塞、异步是否支持,默认方式,以及超时和取消对操作结果的影响。依赖【并发与执行模型】。
## 13-04 文件与路径接口
- [ ] 待讨论
明确路径如何表示,文件如何打开,以及创建、覆盖、追加和访问失败的规则。
## 13-05 网络接口范围
- [ ] 待讨论
明确本轮支持哪些通信抽象,以及连接、监听、半关闭和断开的基本行为;明确不支持的能力。
## 13-06 I/O 的部分成功与结束状态
- [ ] 待讨论
明确短读、短写、EOF、暂不可用、部分完成后失败如何区分和报告。
## 13-07 外部资源的关闭规则
- [ ] 待讨论
明确谁负责关闭、重复关闭和关闭后使用的行为,以及自动清理是否保证及时发生。依赖【资源管理】。
## 13-08 FFI 支持范围与调用约定
- [ ] 待讨论
明确支持哪些外部调用目标、是否支持向外导出函数,以及变参等特殊接口的支持边界。依赖【目标平台与执行方式】。
## 13-09 外部声明与符号解析
- [ ] 待讨论
明确外部函数如何声明、库和符号何时解析,以及缺失、重复或声明不匹配时的行为。依赖【模块系统】。
## 13-10 基础类型映射
- [ ] 待讨论
明确整数宽度与符号、浮点、布尔、空值、指针的对应关系,以及不能表示时的处理。
## 13-11 复合类型与数据布局
- [ ] 待讨论
明确结构体、数组、枚举等如何传递,哪些布局可以公开,以及大小、对齐、填充如何确定。依赖【类型与数据模型】。
## 13-12 字符串与编码
- [ ] 待讨论
明确编码、长度单位、终止符、内嵌零值和非法编码的处理,以及转换责任。
## 13-13 缓冲区访问契约
- [ ] 待讨论
明确只读或可写、连续性、复制或共享、借用期限,以及外部保存地址时的要求。依赖【内存管理】。
## 13-14 内存所有权与分配器配对
- [ ] 待讨论
明确跨边界分配、转移、释放的责任,以及由哪个分配器或释放函数回收。
## 13-15 不透明句柄与运行时值映射
- [ ] 待讨论
明确外部资源或复杂值如何表示,是否允许直接暴露语言内部值,以及无效句柄的处理。依赖【对象与类型模型】。
## 13-16 回调的注册与生命周期
- [ ] 待讨论
明确回调如何传递、保存、注销,如何保活其上下文,以及注销后迟到调用的处理。
## 13-17 外部线程与重入
- [ ] 待讨论
明确哪些线程能进入运行时、是否允许嵌套调用,以及违反约束时的行为。依赖【并发与运行时】。
## 13-18 跨边界错误传递
- [ ] 待讨论
明确错误码、异常和取消怎样转换,是否允许栈展开越过边界,以及失败时如何清理。依赖【错误机制】。
## 13-19 库卸载与运行时退出
- [ ] 待讨论
明确仍有句柄、回调或进行中的操作时,能否卸载库或退出运行时,以及处理顺序。
## 13-20 跨进程与跨运行时的数据交换
- [ ] 待讨论
明确是否提供通用交换格式或协议;若提供,规定支持的值、身份或引用关系及转换失败行为。
## 13-21 平台差异与接口版本兼容
- [ ] 待讨论
明确哪些保证跨平台成立,不兼容布局或库版本如何识别,以及无法验证时由谁承担责任。
## 13-22 跨分类一致性验收
- [ ] 待讨论
用文件读取、外部缓冲区、外部返回资源、异步回调四类完整场景,检查上述规则与相关分类是否一致,消除阻塞和冲突。
## 当前问题的方案与语言对照
以下沿用原讨论材料;建议和参考语言的做法均不自动成为本语言规则。
当前仍是 **13-01:外部交互的默认安全边界**。要决定的是:普通代码调用外部库时,内存有效性由谁保证,哪些责任交给底层绑定作者。
仍用同一个例子:把字节缓冲区交给外部库,外部库可能立即读取,也可能保存地址、稍后读取。
**Python:底层接口可以绕过通常的安全保护。**
以 CPython 提供的 `ctypes` 为例,可以创建缓冲区、传递指针、声明外部函数参数类型。但声明参数类型不等于证明外部访问安全;错误的地址或使用方式可能导致内存损坏、进程崩溃。这种方式方便直接接入本地库,代价是调用者或封装作者承担较多责任。对我们的启发是:**“能声明函数签名”与“能保证生命周期正确”需要分开设计。** [官方文档](https://docs.python.org/3/library/ctypes.html)
**JS:这里以 Node.js 的原生扩展机制为例,属于宿主提供的能力。**
Node-API 可以把外部内存包装成 JS 可访问的 `Buffer`,并关联回收时调用的清理回调。例如,扩展分配一块内存,再将它暴露给 JS;扩展仍需正确安排其有效期和释放。好处是普通 JS 使用者可以通过熟悉的值访问数据;代价是底层扩展仍然承担内存责任,不同宿主的支持也可能不同。启发是:**上层接口可以简单,但底层责任必须明确。** [Node-API 文档](https://nodejs.org/api/n-api.html#napi_create_external_buffer)
**TS:增加静态检查,不另提供一套运行时内存保证。**
例如,把接口声明成接受 `Uint8Array`,可以帮助检查调用处的类型,却不能证明原生扩展没有保存过期地址。类型注解会被擦除,运行时行为仍取决于 JS 宿主和扩展。好处是提前发现一部分接口误用;局限是**类型声明本身不能验证外部实现是否守约**。这也提醒我们,方案 B 不能只停留在“多写几个类型”。[TS 官方说明](https://www.typescriptlang.org/docs/handbook/2/basic-types.html#erased-types)
**Go:cgo 明确限制跨边界指针,并提供复制转换。**
例如,`C.CBytes(data)` 会复制字节到 C 堆,由调用者安排释放;直接向 C 传递 Go 内存则受指针和固定内存规则约束,不能默认让 C 无限期保存。好处是既支持复制也支持受约束共享;代价是绑定作者需要理解两边的内存规则。启发是:**复制与共享可以并存,但应该让使用者知道自己选择了哪种契约。** [cgo 官方文档](https://pkg.go.dev/cmd/cgo)
**Rust:用显式不安全边界承接无法自动检查的责任,再封装安全接口。**
例如,一个接受“地址+长度”的不安全外部函数,可以被包装成接受字节切片的安全函数;包装作者必须确保长度正确、调用期间内存有效,并确认外部不会在约定期限外使用地址。好处是普通调用者不必反复证明这些条件;代价是包装必须正确,否则安全接口也可能失去保证。启发是:**区分“使用接口的人”与“为接口安全性负责的人”。** [Rust 官方说明](https://doc.rust-lang.org/stable/book/ch20-01-unsafe-rust.html)
据此,我们的三个选项仍然是:
| 方案 | 普通使用者默认面对的接口 | 主要代价 |
|---|---|---|
| **A:直接控制地址** | 自己保证地址、长度和生命周期 | 调用方便直接,但内存责任分散到调用处 |
| **B:通过契约绑定** | 使用声明好借用、保留或所有权转移规则的接口 | 绑定设计更复杂,需要信任绑定和外部库守约 |
| **C:主要使用副本与句柄** | 交换独立数据,通过受管理操作访问资源 | 复制与转换开销,部分底层接口较难表达 |
**我仍建议 B,并保留显式不安全入口供底层绑定使用。** 这样可以把责任集中在绑定层,缓冲区采用复制还是共享则留给 13-13 决定。其保证以绑定和外部库遵守契约为前提;同进程原生代码违约时,不能承诺一定转成可捕获错误,也可能损坏或终止进程。
## 讨论来源
《语言设计 13:外部交互》,整理日期 2026-10-28。章节编号沿用原清单。
@@ -1,136 +0,0 @@
# 14 · 反射与表示
0/16 项已确认。当前推进 14-01;本分类尚未完成选择。
本页整理原讨论的问题、验收要求和候选方案。勾选代表讨论中已确认的规则,不代表编译器或运行时已经实现。
## 14-01 运行时反射的基本承诺
- [ ] 待讨论
确定普通程序默认可以观察哪些信息,哪些需要显式声明,以及元数据是否可以被裁剪。
## 14-02 类型查询与类型描述的身份
- [ ] 待讨论
确定能查询什么、返回什么,如何比较查询结果;明确静态类型、运行时实际类型与抽象接口之间的关系。依赖【类型系统】【对象与类型模型】。
## 14-03 元数据的内容与可用性
- [ ] 待讨论
确定字段名、参数、注解、文档、源码位置等分别是否可查询,以及信息缺失、不同构建配置下的行为。
## 14-04 动态读取成员
- [ ] 待讨论
确定按名称查找的对象范围、查找规则,以及成员不存在、不可访问、读取触发计算时的行为。依赖【对象与类型模型】【定制协议】。
## 14-05 动态修改成员
- [ ] 待讨论
确定是否允许新增、写入、删除成员,以及只读值、类型不匹配和修改失败的行为。依赖【类型系统】【对象与类型模型】。
## 14-06 动态调用
- [ ] 待讨论
确定是否支持通过反射调用函数或成员,以及参数匹配、默认参数和调用失败的规则。依赖【函数与调用】。
## 14-07 反射权限与封装
- [ ] 待讨论
确定查看描述、读取、修改、调用分别需要什么权限,以及跨模块和私有成员的边界。依赖【模块与封装】【安全模型,如有】。
## 14-08 调试表示
- [ ] 待讨论
确定默认调试文本、定制方式、信息隐藏要求,以及是否要求稳定、唯一或可重新解析。
## 14-09 用户字符串与格式化
- [ ] 待讨论
确定用户文本与调试表示的关系、默认回退、格式参数和转换失败行为。依赖【定制协议】【格式化语法】。
## 14-10 表示过程的执行边界
- [ ] 待讨论
确定生成文本能否执行用户代码、产生副作用或抛错,以及循环、递归深度、长度和并发修改的处理。
## 14-11 可序列化的值与定制入口
- [ ] 待讨论
确定哪些值可以保存,如何处理函数、类型描述、外部资源及不支持的值;明确是否依赖反射。依赖【值与资源模型】【定制协议】。
## 14-12 数据格式、模式与版本兼容
- [ ] 待讨论
确定语言承诺与格式库的分工,以及名称、缺失字段、未知字段和版本变化的处理。
## 14-13 反序列化与有效状态恢复
- [ ] 待讨论
确定如何选择目标类型、是否调用构造或校验逻辑,以及无效数据、部分恢复失败的行为。依赖【类型系统】【构造与不变量】【资源管理】。
## 14-14 身份、共享引用与循环的保存
- [ ] 待讨论
确定保存树还是对象图、何时保留共享关系,以及循环、不支持的引用和恢复后身份的语义。依赖【身份与相等性】【内存与引用模型】。
## 14-15 反序列化的权限与资源限制
- [ ] 待讨论
确定输入能否指定类型或执行定制代码,以及不可信数据、超限输入和错误报告的默认规则。依赖【错误处理】【安全模型,如有】。
## 14-16 跨分类一致性验收
- [ ] 待讨论
用一组完整例子核对反射、权限、表示、恢复与相关分类的规则;记录并解决冲突及阻塞。
## 当前问题的方案与语言对照
以下沿用原讨论材料;建议和参考语言的做法均不自动成为本语言规则。
当前仍在讨论 **14-01:普通程序拿到一个陌生值,默认能查询多少结构信息?** 例如,检查工具能否直接知道 `Account` 有 `name` 和 `token` 两个字段?
下面是相关语言的对照。取舍和启发是我们的分析,不代表这些语言的历史设计动机。
| 语言 | 规则与具体行为 | 好处、代价及对我们的启发 |
|---|---|---|
| **Python** | 许多普通实例可用 `vars(x)` 取得属性字典;没有 `__dict__` 的值会报 `TypeError`。`dir(x)` 可以被定制,也不保证列出完整、准确的属性集合。[官方文档](https://docs.python.org/3/library/functions.html#vars) · [dir](https://docs.python.org/3/library/functions.html#dir) | 通用检查很方便,但“列出名称”“实例存储”“所有可访问属性”并不相同。启发:不能只设计一个含糊的“查看所有字段”操作。 |
| **JS** | `Reflect.ownKeys({name: "林"})` 返回 `["name"]`,枚举自身属性键,包括不可枚举属性和 Symbol。`#token` 私有元素不属于普通属性,不会这样列出。[ECMAScript 规范](https://tc39.es/ecma262/multipage/reflection.html#sec-reflect.ownkeys) · [私有元素](https://tc39.es/ecma262/2024/#sec-privateelement-specification-type) | 对普通属性默认可检查,适合动态工具;代价是这些名称成为可观察行为。启发:可以对普通数据开放反射,同时为私有状态设置独立边界。 |
| **TS** | 类型注解、接口等静态信息通常被擦除。例如 `interface Account { name: string }` 不会自动产生可供运行时查询的接口描述;运行时仍使用 JS 的机制。[官方文档](https://www.typescriptlang.org/docs/handbook/2/basic-types.html#erased-types) | 静态检查不必带来完整运行时元数据成本;代价是运行时校验、按接口生成表单等功能需要额外描述。启发:查询对象现有属性与查询静态类型定义必须分开。 |
| **Go** | 标准库 `reflect` 能查询结构体字段,包括未导出字段的描述。但取得描述不等于取得全部操作权限:对来自未导出字段的值调用 `Interface()` 会 panic,修改还受 `CanSet()` 等条件限制。[官方文档](https://pkg.go.dev/reflect) | 通用工具能使用结构元数据,但必须处理权限和操作前提。启发:**知道字段存在、读取字段值、修改字段是不同能力**。 |
| **Rust** | 稳定版标准库的 `Any` 支持类型识别及向具体类型转换,例如 `x.is::<Account>()`;它不提供通用字段枚举。`#[derive(Debug)]` 可以生成调试输出代码,这本身不等于开放字段反射。实验性的编译期反射另当别论。[Any 文档](https://doc.rust-lang.org/std/any/index.html) · [derive 文档](https://doc.rust-lang.org/book/appendix-03-derivable-traits.html) | 可以按需提供能力,避免要求所有值都携带通用字段描述;代价是通用工具需要显式适配或生成代码。启发:**调试打印可以独立于运行时结构反射存在**。 |
这些实践不能直接一一对应我们的选项。一门语言可以同时采用不同规则,例如:普通属性默认可枚举,静态接口被擦除,调试表示又通过显式生成提供。
回到 **14-01**,我们的三个方向仍是:
- **A.不保证通用结构反射**:陌生值只能通过主动提供的接口接受检查。语言承诺较少,通用工具需要更多适配。
- **B.显式参与结构反射**:只有声明参与或提供描述的值,才能通过统一机制检查。暴露范围可控,但需要维护描述。
- **C.默认提供结构反射**:对适用的结构化值,默认保证可以查询结构;访问权限另外规定。通用工具方便,但元数据需要成为语言承诺的一部分。
我仍建议先考虑 **B**:它让我们能够分别决定反射、调试和序列化的能力。不过语言用途尚未确定,这只是建议,尚未采用。
如果选 B,我建议同时确定两个验收边界:**“没有反射描述”必须区别于“有描述但没有字段”;已经承诺可查询的描述,发布构建不能静默裁剪。** 权限规则留给 14-07。
你倾向 **A、B、C** 中哪一个?如果选 B,是否一并接受上面两个边界?
## 讨论来源
《语言设计 14:反射与表示》,整理日期 2026-10-28。章节编号沿用原清单。
-24
View File
@@ -1,24 +0,0 @@
# 从浏览器排版到 Pretext:高性能文本布局源码教程
这份教程回答一个问题:**Pretext 为什么能很快地算出一段文字的换行与高度?**
读完后,你应该能解释它的性能来源,沿着函数调用读懂核心源码,并知道如何在自己的界面中使用它。只需要了解 JavaScript 函数、数组和基本 DOM;字体、字素、回流等概念会逐步解释。
分析基于上游提交 [`0d102a5cd933611e86a7e60bda7b4baa5995c6b0`](https://github.com/chenglou/pretext/tree/0d102a5cd933611e86a7e60bda7b4baa5995c6b0),研究日期为 2026 年 10 月 28 日。本文描述这个版本,不把早期实现或未来版本的行为混在一起。源码链接指向本次研究的上游版本;实验脚本和记录随文保存在本站。
## 阅读路线
1. [第一章:为什么算文字高度也会慢](/docs/pretext/01-chapter)
2. [第二章:先看懂 prepare 与 layout 的分工](/docs/pretext/02-chapter)
3. [第三章:分析阶段如何把文字变成可布局的分段](/docs/pretext/03-chapter)
4. [第四章:测量缓存如何减少浏览器工作](/docs/pretext/04-chapter)
5. [第五章:换行算法为什么能变成数值循环](/docs/pretext/05-chapter)
6. [第六章:为什么只求高度比拿到每行文字便宜](/docs/pretext/06-chapter)
7. [第七章:如何避免长文本拖垮算法](/docs/pretext/07-chapter)
8. [第八章:准确性怎样影响性能设计](/docs/pretext/08-chapter)
9. [第九章:把它接进界面和虚拟列表](/docs/pretext/09-chapter)
10. [第十章:亲自验证性能机制](/docs/pretext/10-chapter)
11. [第十一章:按什么顺序读源码](/docs/pretext/11-chapter)
12. [第十二章:把这些设计迁移到自己的代码](/docs/pretext/12-chapter)
第一次阅读建议顺着读到第六章,再看第九章。第四、七、八章适合对照源码细读;第十章提供实验与复现方法。
-45
View File
@@ -1,45 +0,0 @@
# 第一章:为什么算文字高度也会慢
### 1.1 字符数不能直接决定高度
假设一段话放在宽 320px 的聊天气泡中。它会占几行,至少取决于文字内容、字体、字号、可用宽度、字间距和换行规则。
同样长度的 `iiii` 和 `WWWW` 宽度可能差很多。中文通常能在许多字之间断行,英文更常在空格处换行;标点又有自己的限制。所以“字符数乘平均字宽”只能估计,无法可靠地预测真正的行数。
传统方案是让浏览器先排一遍,再读取结果:
```js
element.textContent = text
element.style.width = '320px'
const height = element.getBoundingClientRect().height
```
写入文字或宽度后,之前的排版结果可能失效。此时立刻读取几何信息,浏览器必须先保证这个结果是最新的;如果有待处理的样式或布局工作,就可能同步完成这些工作。
这就是这里关心的“回流”或“布局”:确定元素的尺寸与位置。**一次几何读取不一定总触发昂贵回流,关键在于读取时是否存在需要更新的布局。**
### 1.2 写与读交替,会放大成本
```js
for (const row of rows) {
row.element.style.width = width + 'px'
heights.push(row.element.getBoundingClientRect().height)
}
```
这个循环可能不断要求浏览器“写一点、立即排好、读出来”。把写操作与读操作分批,通常能改善它。但虚拟列表还有另一个问题:大多数行根本没有挂载,无法直接读取那些行的 DOM 高度。
Pretext 的切入点是:**如果能在应用代码中算出文本行数,就能在创建那些 DOM 之前知道高度。** 浏览器最终仍然负责把字画出来。
### 1.3 区分布局和绘制
把一段文字显示到屏幕上,可以粗略拆成:
```text
文字与样式 → 确定字形和宽度 → 决定换行与位置 → 绘制文字
```
Pretext 借助 Canvas 的字体测量,自己完成一部分换行与布局计算。它不是一个自动让任意页面绘制都变快的开关,也不承担完整的浏览器 CSS 排版。
本章检查:如果宽度变化,文字本身的字体宽度一定要重新测量吗?通常不需要。变的是“每行能装多少”,这就是下一章的设计基础。
-61
View File
@@ -1,61 +0,0 @@
# 第二章:先看懂 prepare 与 layout 的分工
### 2.1 同一份输入,有不同的变化频率
聊天消息的文字和字体可能很久不变,但容器宽度会在拖动窗口时连续变化。把所有工作绑在一个函数里,每次 resize 都会重复做许多相同的事情。
Pretext 将它拆成两步:
```js
import { prepare, layout } from '@chenglou/pretext'
const prepared = prepare('Hello 世界,今天一起读源码。', '16px Arial')
const narrow = layout(prepared, 200, 24)
const wide = layout(prepared, 400, 24)
```
这里 `200`、`400` 是内容宽度,`24` 是调用者指定的每行高度,单位都是 CSS 像素。
| 阶段 | 输入 | 主要工作 | 什么时候重做 |
| --- | --- | --- | --- |
| `prepare()` | 文字、字体、选项及语言环境 | 分析断行机会、测量、记录修正数据 | 相关输入变化 |
| `layout()` | prepared、宽度、行高 | 根据已知宽度决定换行并计数 | 容器宽度或行高变化 |
prepared 可以理解为“已经整理好的排版材料”。它与容器宽度无关,但并不包含任意宽度下现成的布局答案。
### 2.2 沿源码走一遍
打开 [`src/layout.ts`](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/src/layout.ts),定位 `prepareInternal()`:
```text
读取引擎规则与语言
→ analyzeText(...)
→ measureAnalysis(...)
→ prepared handle
```
再定位 `layout()`,其核心逻辑是:
```js
const lineCount = countPreparedLines(internal, normalizedWidth)
return { lineCount, height: lineCount * lineHeight }
```
这是省略类型与内部转换的源码摘义。它没有逐行调用 Canvas,也没有先创建 DOM 再读取高度。
### 2.3 一段文字被布局多次,省下多少工作
用 `P` 表示准备成本,用 `L` 表示一次布局成本,一共布局 `k` 次:
```text
复用 prepared:P + k × L
每次重新 prepare:k × (P + L)
```
两种写法相差 `(k - 1) × P`。这解释了为什么使用方式会直接决定收益:在 resize 回调中重新 prepare,会让原本可以省掉的分析工作再次发生。
另外,重复 prepare 可能命中测量缓存,但仍要分析文本、组装新的 handle。它依然比直接复用已有 handle 做得更多。
本章检查:`layout()` 是 O(1) 吗?不是。它通常还要扫描分段。快来自减少每一步要做的工作,而不是完全不看文本。
-54
View File
@@ -1,54 +0,0 @@
# 第三章:分析阶段如何把文字变成可布局的分段
### 3.1 三个容易混淆的单位
| 名称 | 含义 | 例子 |
| --- | --- | --- |
| UTF-16 码元 | JavaScript 字符串索引使用的存储单位 | 一些 emoji 的 `length` 大于 1 |
| 字素簇,简称字素 | 用户通常感知的一个文字单位 | 字母加组合音标、由多个码点组成的 emoji |
| 分段 segment | Pretext 为测量与换行建立的单位 | 一个词、一段文本、空格或特殊控制段 |
分段并不总等于单词。字素也不总等于一个 JavaScript 字符。理解这两个区别,后面的超长单词断行和 emoji 处理才容易读懂。
### 3.2 先找哪里允许断行
对 `hello world`,可以先想象成:
```text
['hello', ' ', 'world']
```
这个示意足以解释普通英文,但真实的中文标点、日文、泰文、软连字符和不可断空格需要更多规则。
[`analysis.ts` 的 `analyzeText()`](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/src/analysis.ts)先处理空白与源文本,再按引擎选择扫描器,最后形成分段及标志。其中主要的引擎对应关系是:
| 浏览器引擎 | 主要实现入口 |
| --- | --- |
| Blink,例如 Chrome | `line-breaks.ts` 中的 `getBlinkLineBreaks()` |
| WebKit,例如 Safari | `line-breaks.ts` 中的 `getWebKitLineBreaks()` |
| Gecko,例如 Firefox | `gecko-line-breaks.ts` 中的 `getGeckoLineBreaks()` |
选择由 `measurement.ts` 的 `getEngineProfile()` 完成,并缓存 profile。未知引擎采用 Blink 的模型。
### 3.3 表驱动扫描是什么意思
可以把扫描器想象成一个不断读取字符的机器:
```text
当前状态 + 当前字符类别 → 新状态 + 是否出现边界
```
字符属于哪一类、某种状态遇到某一类应该怎么走,都放在表里。运行时主要是读字符、查类别、查转换表、更新状态。
规则数据来自对应引擎及其 Unicode/ICU 数据,保存在 [`src/generated/`](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/src/generated)。数据经过压缩;页面按需解开自己的引擎所用的表。这样能把复杂规则变成较直接的扫描,但也增加了包体积与初始化成本。这是有代价的工程选择。
[`graphemes.ts` 的 `findGraphemeEnds()`](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/src/graphemes.ts)也采用类似方式寻找字素边界。这个版本不是对所有文字都调用 `Intl.Segmenter`:词典分词主要留给泰语、老挝语、高棉语、缅甸语等特定文字的运行片段。
### 3.4 为什么空白处理要先做
`white-space: normal` 下,连续普通空格通常被折叠;`pre-wrap` 则保留空格、换行与制表符的相关行为。若测量的是原始字符串,绘制时浏览器却折叠了它,预测自然会错。
所以分析阶段必须建立与目标 CSS 相符的文本视图。这个版本对孤立 `\r` 有明确限制:`pre-wrap` 下绘制前也应把它规范成 `\n`,详见根目录 README。
本章检查:为什么不能直接 `text.split(' ')`?因为断行机会远不止空格,而且许多语言没有用空格分隔每一个词。
-56
View File
@@ -1,56 +0,0 @@
# 第四章:测量缓存如何减少浏览器工作
### 4.1 Canvas 提供的是字体测量能力
浏览器已经知道如何选择字体、塑造字形、计算宽度。Pretext 通过:
```js
context.font = '16px Arial'
const width = context.measureText('hello').width
```
获得字体事实。它优先创建 `OffscreenCanvas`;没有该能力时,创建不挂载到文档的普通 Canvas。
`OffscreenCanvas` 这个名字不表示测量会自动进入另一个线程。代码在哪里调用,工作就在哪里执行;想在 worker 中使用,需要应用自己安排,还有第八章的准确性限制。
### 4.2 两层复用各解决什么问题
第一层是应用保留 prepared。第二层在库内部,以字体等测量条件组织缓存,再按分段文本查找:
```text
字体 A 的缓存
'hello' → 测量结果
'world' → 测量结果
字体 B 的缓存
'hello' → 另一份测量结果
```
在 [`measurement.ts`](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/src/measurement.ts) 搜索 `getSegmentMetrics()`,会看到这一核心表达式:
```js
measurement.metrics.get(seg) ?? addMetrics(...)
```
命中就读取结果;未命中才调用 Canvas 并存入缓存。相同词语出现在不同消息里,也有机会复用,不要求整段消息完全相同。
实际缓存不只有一个数字。`SegmentMetrics` 还会保留拆词适配、emoji 统计或其他修正所需的信息。语言变化和字距造成的 shaping 模式变化也受到专门处理;不能把它简单描述成全局 `(text, font)` 永久不变的表。
### 4.3 命中缓存后,连字体设置都尽量省
`getFontMeasurement()` 先找到字体对应的数据;真正测量时才通过 `getContext()` 设置 Canvas 的字体和测量模式。
为什么把两者拆开?富文本可能反复查询许多 item 的字体,但这些 item 的文字全都命中缓存。既然无需测量,也就不必为了查询而触碰 Canvas。
源码同时照顾了字体后加载等浏览器行为,因此不是无条件地“字体字符串相同就永不赋值”。读这一段时应一起看注释,而不要只把它当成普通 setter 缓存。
### 4.4 prepared 与全局测量缓存的寿命不同
全局缓存为了复用测量,prepared 为了一段具体文本的后续布局。二者不应混为一谈。
`MAX_CACHED_SEGMENTS = 1 << 18`,即 262,144。所有字体缓存的分段数超过边界后,下一次准备开始时会清理测量缓存。它不是每次插入都逐出一个条目的 LRU,因此单次准备期间可能超过阈值。
已经创建的 handle 保留自己的布局数据,不会因为这次缓存清理突然失效。不过,如果实际字体或语言变了,旧 handle 也不会自动更新,仍需要重新准备。
本章检查:清理全局缓存会自动把旧 prepared 换成新字体宽度吗?不会。缓存清理和重新创建 handle 是两步。
-60
View File
@@ -1,60 +0,0 @@
# 第五章:换行算法为什么能变成数值循环
### 5.1 用一组虚构宽度手算
下面只是教学数据,不代表某种真实字体:
```text
分段:hello 空格 world 空格 again
宽度: 40 4 45 4 42
可用宽度:90
```
从左到右累计:
| 操作 | 当前宽度 | 决定 |
| --- | ---: | --- |
| 放入 hello | 40 | 留在第一行 |
| 放入空格 | 44 | 继续 |
| 放入 world | 89 | 继续 |
| 尝试后面的空格 | 93 | 第一行结束,行尾空格按规则处理 |
| 放入 again | 42 | 第二行 |
最终两行;若行高是 24px,高度就是 48px。
### 5.2 核心思路简单,实现处理边界
[`line-break.ts` 的 `countPreparedLines()`](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/src/line-break.ts)为普通文本专门实现数值循环。以下是教学版,不是可替代源码的完整算法:
```js
for (const segment of segments) {
if (currentWidth + segment.width <= availableWidth) {
currentWidth += segment.width
} else {
finishCurrentLine()
startNextLineWith(segment)
}
}
```
真实代码还处理行首空白、行尾修正、零宽空格、放不下一整个分段时的字素断行。关键是这些判断都基于提前准备的数据,不再重新问 Canvas 这个词有多宽。
### 5.3 为什么需要简单路径和完整路径
准备阶段设置两个相关标志:
- `simpleLineWalkFastPath`:适合简单逐行遍历。
- `simpleLineCountFastPath`:适合简单行数计算;某些边界仍可能回退处理。
普通文本、普通空格等可以走简单路径。保留空白、制表符和软连字符等内容需要更完整的规则,进入 `walkPreparedComplexLines()` 等路径。
源码在 `countPreparedLines()` 前的注释记录:完整 walker 每个分段的成本约为简单路径的 3~5 倍。**这是项目记录的局部测量依据,不是所有设备上的固定倍数。** 分开实现的目的,是让常见输入少支付复杂状态管理的成本。
### 5.4 高度计算为何不经过“每行对象”
仅求高度时,简单计数循环使用局部数值变量,直接累加行数,没有每行游标对象和逐行回调。`layout()` 最后仍会返回一个结果对象,所以准确说法是“避免每行分配”,而不是“零分配”。
这也是为什么一个看似更通用、更加抽象的 walker,未必适合最高频的行数路径。
本章检查:为什么文本支持更多规则,不一定让普通英文同样变慢?因为准备阶段能判断适用路径,复杂处理不必全部进入普通文本的热循环。
-52
View File
@@ -1,52 +0,0 @@
# 第六章:为什么只求高度比拿到每行文字便宜
### 6.1 计算结果越详细,需要的工作越多
假设一个段落有 50 行。高度只需要一个行数;返回每行文字则还要确定范围、切字符串、拼接内容并创建结果对象。这些临时对象最终还需要垃圾回收。
因此选择 API 时应先问:调用者实际要什么?
| 需求 | 准备方式 | 布局 API |
| --- | --- | --- |
| 高度、行数 | `prepare()` | `layout()` |
| 行数、最宽行 | `prepare()` | `measureLineStats()` |
| 每行范围与宽度 | `prepare()` | `walkLineRanges()` |
| 每行文字 | `prepareWithSegments()` | `layoutWithLines()` |
| 逐行取文字,可按需停止 | `prepareWithSegments()` | `layoutNextLine()` |
普通 `prepare()` 的最终 handle 不保留分段字符串;准备过程中仍会使用文字,不能因此说它完全没有字符串处理。
### 6.2 范围与文字分开生成
一行可以先表示为:
```text
从第几个 segment 的第几个字素开始
到第几个 segment 的第几个字素结束
这一行有多宽
```
这是 range。需要显示那一行时,再把 range 变成实际字符串,这一步叫 materialize,即生成具体内容。
`walkLineRanges()` 不生成行字符串,但公共回调接收的 range 及其游标仍然会分配对象。它比“每行完整文字”做得少,也不应被描述成零分配。
[`line-text.ts`](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/src/line-text.ts)还用 WeakMap 按 handle 缓存所需分段的字素结束位置。只有行边界切进一个分段内部、确实需要定位字素时,才会补上相关数据。
### 6.3 流式逐行适合什么情境
如果只显示前三行摘要,逐行 API 可以在取到所需行数时停止;如果每行可用宽度不同,也可以逐次传入不同宽度。
```js
let cursor
for (let i = 0; i < 3; i++) {
const line = layoutNextLine(preparedWithSegments, 320, cursor)
if (!line) break
console.log(line.text)
cursor = line.end
}
```
这个片段假设已经导入 `layoutNextLine` 并准备好 `preparedWithSegments`。它演示逐行读取,不包含省略号处理。高度用途直接选 `layout()` 会更合适。
本章检查:虚拟列表只想知道 10,000 条消息各有多高,是否需要为每条创建所有行字符串?不需要。
-38
View File
@@ -1,38 +0,0 @@
# 第七章:如何避免长文本拖垮算法
### 7.1 一次调用的成本也取决于字符串长度
假设为了知道单词的每个位置能否断开,依次测量:
```text
a
ab
abc
abcd
……
```
虽然只调用了 n 次 Canvas,总共提交的字符量却是:
```text
1 + 2 + … + n = n(n + 1) / 2
```
因此只数 API 调用次数还不够,也要数测量请求总共处理了多少文字。
### 7.2 96 个字素的边界
[`measurement.ts` 的 `getSegmentFit()`](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/src/measurement.ts)按引擎与输入情况使用字素求和、相邻组合、前缀等测量模型。
前缀测量限定在 `MAX_PREFIX_FIT_GRAPHEMES = 96` 以内。超过这一规模,相关路径改用相邻字素组合,限制提交给 Canvas 的文字量随长度增长的放大程度。
这并不等于严格证明浏览器内部 shaping 总是线性的。它限制的是库自己构造测量请求所造成的成本;浏览器字体引擎还有自己的工作。
### 7.3 逐行继续时,不从头重扫
逐行 API 将上一行结束的游标传给下一行,遍历从那里继续。若每取下一行都从全文开头重新定位,行数多时会累积出大量重复工作。
[`RESEARCH.md` 的 Keeping Work Bounded](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/RESEARCH.md#keeping-work-bounded)记录了许多实际陷阱,包括增长字符串的反复处理、从每行起点重扫到文本结尾、重复搜索断行片段。阅读性能代码时,除了看一次循环的内容,也要问:这个循环会被重复启动多少次?
本章检查:每个前缀只测一次,能否保证总成本是 O(n)?不能,因为每次测量的输入长度在增长。
-41
View File
@@ -1,41 +0,0 @@
# 第八章:准确性怎样影响性能设计
### 8.1 字符宽度不总能独立相加
字偶距 kerning 会让相邻字形距离发生变化;连字可能把多个字母组成一个字形;阿拉伯文字母还会随上下文改变形态。
所以 `width('A') + width('V')` 未必等于 `width('AV')`。一个词整体测得的宽度,也未必直接告诉你从词中间截断后每一行的正确宽度。
Pretext 的策略是测量完整分段,同时提前记录紧急断词所需的 advances,以及行首、行尾的修正信息。复杂性主要支付在准备阶段,布局阶段读取结果。
### 8.2 相同 Unicode 文本,各浏览器也可能不同
浏览器引擎在空白、标点、行边界与拆词等方面存在差异。Pretext 的 engine profile 选择对应扫描规则、拟合模式与容差,而不是仅靠一套理想化 Unicode 换行实现预测所有浏览器。
这份模型有已知缺口,尤其在特殊字体设置、窄到不常见的内容宽度、某些复杂文字与平台 bug 上。它的目标是匹配支持范围内的浏览器行为;“算法能给出一个结果”并不能证明所有浏览器都一定这样画。
### 8.3 “无 DOM”有需要说明的例外
`layout()` 本身不做 DOM 测量。准备阶段则可能:
- 读取 `<html lang>`,除非调用者通过 `setLocale()` 给出了语言。
- 在需要修正 emoji 宽度时,按字体使用 DOM span 探测。
- 缺少 OffscreenCanvas 时,创建不挂载的 Canvas 元素。
因此应理解成它避免了“每个文本块依赖 DOM 测量”的架构,而不是声称整个库绝对不会访问 DOM。worker 中缺少 emoji 探测条件,准确性也可能不同。
### 8.4 重新准备的条件
| 变化 | 通常怎样处理 |
| --- | --- |
| 只有宽度变化 | 复用 handle,重新 `layout()` |
| 只有调用者指定行高变化 | 复用 handle,传入新行高 |
| 文字、字体、字号、字距、whiteSpace、wordBreak 变化 | 重新 `prepare()` |
| 页面语言变化 | 重新准备;旧 handle 保留旧规则与宽度 |
| 先前测过后,目标字体才加载完成 | 清理缓存并重新准备,避免继续用回退字体数据 |
| macOS emoji 相关 DPR 变化 | 按 README 要求清理并重新准备含 emoji 文本 |
最终行为受字体、语言和 CSS 的共同约束。根目录 [README 的 Caveats](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/README.md#caveats)、[PLATFORM_BUGS.md](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/PLATFORM_BUGS.md)和 [ENGINE_FOLLOWUPS.md](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/ENGINE_FOLLOWUPS.md)是理解边界的重要入口。
本章检查:`height = lineCount × lineHeight` 是否能概括带任意大小图片的富文本?不能,图片或不同字体可能把某一行撑高,需要调用者处理行高模型。
-94
View File
@@ -1,94 +0,0 @@
# 第九章:把它接进界面和虚拟列表
### 9.1 一个完整的浏览器模块例子
先在你的前端项目中安装:
```sh
npm install @chenglou/pretext
```
下面是供 Vite 等支持 npm 模块导入的环境使用的 JavaScript 模块。直接打开一个 HTML 文件并不能解析这个包名。
```js
import { prepare, layout } from '@chenglou/pretext'
const text = 'Hello 世界,今天一起读源码。'.repeat(8)
const font = '16px "Helvetica Neue", "PingFang SC", Arial, sans-serif'
const lineHeight = 24
document.documentElement.lang = 'zh-CN'
await document.fonts.load(font, text)
const prepared = prepare(text, font)
const block = document.createElement('div')
block.textContent = text
Object.assign(block.style, {
font,
lineHeight: lineHeight + 'px',
overflowWrap: 'break-word',
wordBreak: 'normal',
whiteSpace: 'normal',
lineBreak: 'auto',
letterSpacing: '0px',
wordSpacing: '0px',
fontKerning: 'auto',
textWrap: 'wrap',
hyphens: 'manual',
textRendering: 'auto',
boxSizing: 'content-box',
padding: '0',
border: '0',
})
document.body.appendChild(block)
function setContentWidth(width) {
const predicted = layout(prepared, width, lineHeight)
block.style.width = width + 'px'
return predicted
}
const predicted = setContentWidth(320)
console.log('预测高度:', predicted.height)
console.log('实际高度:', block.getBoundingClientRect().height)
// 模拟容器变宽:复用相同的 prepared。
setContentWidth(480)
```
这个例子最后的 DOM 高度读取是开发期核对。生产中如果每次布局都再读一次 DOM,仍会承担那次读取可能触发的工作。示例也假设没有其他 CSS 规则覆盖这些值,所用字体在目标设备上可用;跨平台项目应配置自己的字体。
### 9.2 让模型与显示使用相同输入
`layout(prepared, 320, 24)` 中的 320 是内容宽度。如果外框总宽 320px、左右各有 12px padding,再加边框,真正给文字使用的宽度就小于 320px。
可以由应用维护一个尺寸模型:先算出内容宽度,把同一个数同时传给 Pretext 和渲染层。这样可以减少“CSS 用一个值,JS 猜另一个值”的误差。
准备前等待具体文字所需字体加载很重要。只有 `document.fonts.ready` 不一定覆盖尚未使用、由 unicode-range 分片加载的字体;因此例子把 text 也传给 `document.fonts.load()`。
### 9.3 虚拟列表从哪里获得收益
假设有 10,000 条消息,屏幕上只显示 20 条:
```text
每条消息保留 prepared
→ 按当前内容宽度求各条高度
→ 累计高度,确定每条消息顶部的位置
→ 按滚动位置找可见范围
→ 只挂载可见消息与少量缓冲区
```
Pretext 解决的是“没挂载的文字怎样求高度”。累计位置、查找可见范围、滚动锚定、图片高度、选择与无障碍行为,仍由应用或虚拟列表库负责。
窗口变宽时,每条消息通常仍需要重新布局。因此大量消息的整表 resize 成本不会凭空消失。准备与布局分离,让这次更新的单位成本更低。
### 9.4 编辑与富文本如何处理
长文本编辑时,可以按段落保留 prepared,只重新准备被编辑影响的段落;有跨段结构变化时,应用还要更新相应块。
混合字体、提及标签和行内图片使用 `@chenglou/pretext/rich-inline`。这个版本把富文本作为一个段落分析,保留 item 与分段的映射,然后复用布局基础设施,而不是把每个 span 完全独立换行。
富文本还要考虑垂直尺寸:字体大小、上升部与下降部、行内盒子高度都可能影响真实行高。不要直接把所有富文本的行数乘一个固定值,当成任意 CSS 的精确高度。可继续读项目自己的 [Markdown 聊天案例说明](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/pages/demos/markdown-chat.md)。
本章检查:Pretext 是否已经替你完成虚拟列表?没有,但它提供了此前很难在挂载前获得的文本几何信息。
-75
View File
@@ -1,75 +0,0 @@
# 第十章:亲自验证性能机制
### 10.1 先把问题变成可观察的证据
本教程关心四件事:
1. 首次准备是否调用 Canvas?
2. 相同文本再次准备是否复用测量?
3. 改变宽度做布局时是否继续调用 Canvas?
4. 这些样本的预测高度是否与 DOM 相符?
下载脚本后,将它放进研究版本的 Pretext 仓库 `tutorials/zh-CN/verify.cjs`(按需新建目录),因为它以该位置查找 `../../src`。
随文的 [`verify.cjs`](/materials/pretext/verify.cjs)用真实 Chromium Canvas,在 `measureText()` 外包一层计数器,同时统计提交字符串的 UTF-16 码元总数。然后测试英文、中文、保留空白的文本。
实验不使用伪造的平均字宽。源码通过 Node 的 TypeScript 转换功能转换为浏览器模块;没有修改库实现。
### 10.2 第一次研究保存的结果
原始记录在 [`results.chrome156.json`](/materials/pretext/results.chrome156.json)。环境为本机 macOS、Headless Chrome 156、DPR 1,设置 `<html lang="en">`,使用记录脚本中的命名字体及其回退链。三段文本都重复了 12 次,刻意让缓存效果可观察。
| 样本 | 字符串 length | 首次 Canvas 次数 | 首次提交码元数 | 再次准备 Canvas 次数 | 变宽布局 Canvas 次数 |
| --- | ---: | ---: | ---: | ---: | ---: |
| 英文 | 1,428 | 62 | 529 | 0 | 0 |
| 中文 | 408 | 53 | 67 | 0 | 0 |
| 保留空白、tab、软连字符 | 888 | 50 | 497 | 0 | 0 |
每段分别在 80、121、200、319、320、321、480、800px 下求高度,24 组结果与 DOM 高度一致。这只能证明这些样本的高度一致,不能证明所有语言与断行位置都完全一致。
### 10.3 怎样理解计时数据
预先准备后,每轮做 3,000 次宽度切换,热身后取 5 轮中位数。原始记录为:
| 样本 | Pretext 布局总时间 | DOM 改宽后立即测高总时间 |
| --- | ---: | ---: |
| 英文 | 1.7ms | 86.0ms |
| 中文 | 1.5ms | 57.8ms |
| 保留空白等 | 6.3ms | 93.9ms |
这些数字展示“用已测宽度计算”与“改变 DOM 宽度后同步求几何信息”的成本差异。它们不包含 Pretext 首次准备成本,也不包含完整页面绘制。
这不是严谨的产品性能排名:样本很少且重复,固定先测 Pretext 再测 DOM,没有记录硬件与电源条件,短样本受计时分辨率影响,也没有与批量 DOM 测量对照。结果不应被宣传成“任意页面快几十倍”。更可靠的机制证据是调用次数和这组样本的结果核对。
### 10.4 复现方法
要求 Node 23.10+ 或支持 `module.stripTypeScriptTypes(..., { mode: 'transform' })` 的较新 Node、Playwright,以及可用 Chromium。
为避免改动本仓库的依赖,在单独目录安装实验依赖:
```sh
mkdir -p /tmp/pretext-study-deps
npm install --prefix /tmp/pretext-study-deps playwright
/tmp/pretext-study-deps/node_modules/.bin/playwright install chromium
```
在本仓库根目录运行:
```sh
PLAYWRIGHT_MODULE=/tmp/pretext-study-deps/node_modules/playwright \
node tutorials/zh-CN/verify.cjs
```
默认打印 JSON,不覆盖已归档结果。若要保存本次输出:
```sh
PLAYWRIGHT_MODULE=/tmp/pretext-study-deps/node_modules/playwright \
node tutorials/zh-CN/verify.cjs --output /tmp/pretext-study-result.json
```
也可以通过 `CHROME_EXECUTABLE` 指定已安装 Chrome 的可执行文件路径;两种方式选一种即可。脚本只监听本机回环地址,结束后关闭服务器与浏览器。
脚本会检查 24 组高度、重复准备和布局的 Canvas 调用次数,以及行文本 API 与行数 API 的行数一致性。失败时返回非零状态。它不设置速度阈值,因为这种微型计时不适合承担性能回归门禁。
项目正式验证还要看 [`harness/README.md`](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/harness/README.md) 与 [`harness/bench/README.md`](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/harness/bench/README.md):区分新文本、已见文本、resize、生成行文本与最坏情况;同时处理浏览器自身缓存、JIT、GC 和跨轮噪声。本教程没有运行完整的固定浏览器 harness,也没有把 headless 结果当成其正式准确性基线。
-21
View File
@@ -1,21 +0,0 @@
# 第十一章:按什么顺序读源码
不要从庞大的规则数据开始。先建立主流程,再读会影响这个流程的细节。
| 顺序 | 文件与函数 | 阅读时要回答的问题 |
| --- | --- | --- |
| 1 | [`layout.ts`](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/src/layout.ts):`prepareInternal`、`prepare`、`layout` | 两个阶段分别接收什么、返回什么? |
| 2 | [`line-break.ts`](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/src/line-break.ts):`PreparedLineBreakData` | 布局真正需要哪些提前准备的数据? |
| 3 | 同文件:`countPreparedLines` | 普通文本怎样被一个循环计数? |
| 4 | [`analysis.ts`](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/src/analysis.ts):`analyzeText` | 空白、语言与引擎怎样影响分段? |
| 5 | [`prepare.ts`](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/src/prepare.ts):`measureAnalysis` | 宽度、适配数据与快路径标志在哪里建立? |
| 6 | [`measurement.ts`](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/src/measurement.ts):`getFontMeasurement`、`getSegmentMetrics`、`getContext` | 命中缓存时到底跳过哪些浏览器工作? |
| 7 | 同文件:`getSegmentFit`、`measureFitAdvances` | 一个放不下的词怎样预先准备拆分信息? |
| 8 | [`line-text.ts`](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/src/line-text.ts) 与 `layoutWithLines` | 哪些成本只在要文字时出现? |
| 9 | `stepPreparedSimpleLineGeometry`、`walkPreparedComplexLines` | 按行输出和复杂规则怎样衔接? |
| 10 | [`line-breaks.ts`](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/src/line-breaks.ts)、[`gecko-line-breaks.ts`](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/src/gecko-line-breaks.ts)、[`graphemes.ts`](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/src/graphemes.ts) | 扫描器怎样使用表和状态? |
| 11 | [`rich-inline.ts`](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/src/rich-inline.ts) | item 与整段文本的映射怎样建立? |
| 12 | [`RESEARCH.md`](https://github.com/chenglou/pretext/blob/0d102a5cd933611e86a7e60bda7b4baa5995c6b0/RESEARCH.md) 的 Keeping Work Bounded、Evaluation Traps 与 Decisions Log | 哪些看似更简单的实现被测过,又为何没保留? |
做一个小练习:对 `hello world` 写下你认为需要的 prepared 数据,然后依次改变宽度、字体和 whiteSpace,判断哪些数据能保留。能解释这些变化,就已经掌握主设计,不需要先记住所有 Unicode 规则。
-27
View File
@@ -1,27 +0,0 @@
# 第十二章:把这些设计迁移到自己的代码
### 12.1 先按变化频率拆工作
找出输入中很少变的部分与高频变化的部分。这里文字和字体较稳定,宽度频繁变化,于是准备与布局分离。类似思路也适用于图表几何、搜索索引和格式化数据。
预计算需要内存,也会增加第一次处理的成本。只有被复用、能减少更贵的工作时,它才值得。
### 12.2 让 API 允许调用者少做事
如果只要计数,就提供计数路径;只要范围,就不强迫构造字符串;只要可见几行,就允许提前停止。
这比在一个总会生成全部结果的 API 内微调循环,通常更能减少实际工作。
### 12.3 给常见输入一条清晰的快路径
先通过准备阶段建立可验证的前提,再让热循环使用这些前提。完整路径仍负责其余情况。快路径的正确性应来自明确条件,而不是凭输入“看起来简单”猜测。
### 12.4 衡量总工作量
除了总耗时,还观察测量次数、提交字符量、扫描范围、对象分配和重复启动次数。它们能解释为什么快,也能暴露短样本看不到的二次增长。
### 12.5 把结论说到证据能支持的位置
这次源码研究与实验支持的是:Pretext 用准备阶段的测量和规则数据,使后续宽度变化主要成为数值布局;缓存、专用计数循环和按需生成结果进一步减少工作。完整页面是否获益、获益多少,还取决于首次准备量、复用频率、字体与 CSS 是否匹配,以及应用剩余的 DOM 和绘制成本。
读完后可以用三个问题检验理解:哪些工作被移到准备阶段?哪些结果被跨调用复用?调用者只要高度时,哪些工作根本没有发生?把这三个问题顺着源码回答出来,就能解释 Pretext 的主要性能来源。
-51
View File
@@ -1,51 +0,0 @@
# Smalltalk-80:从消息到交互系统
Smalltalk 把语言、类库和开发工具放进同一个可探索的对象世界。本教程以“消息怎样找到行为”为起点,逐步走到集合、进程、图形与 MVC,帮助你阅读经典系统中的类与方法。
**资料范围**:A. Mével、T. Guéguen 著《Smalltalk-80》,Macmillan Education,1987;英文版编辑 Mario Wolczko,翻译 M. J. Stewart。书中说明以 Xerox Version 2 Virtual Image 为基础。用户提供的扫描 PDF 共 190 页,正文印刷页 1–174 对应 PDF 页 12–185;印刷页 p 的 PDF 页为 p + 11。
这里是按学习任务重组的中文研究导读,覆盖原书 21 章的主要主题,保留章节与页码定位,不是逐页译本或完整 API 手册。补充示例为本文编写,预期值来自语义推演,未在 Smalltalk 镜像运行。本文没有对某个 Smalltalk 实现仓库做逐文件源码核验;“源码研究”是站点分类,阅读原书后仍需进入对应镜像浏览实际方法。
## 选择阅读路线
| 目标 | 路线 | 读完能做什么 |
| --- | --- | --- |
| 初次接触 | 消息 → 语法 → 类与方法 → 集合 | 读懂表达式,分清返回值与接收者 |
| 研究语言机制 | 类与方法 → 对象协议 → 反射 | 追踪查找起点、对象身份和类侧行为 |
| 研究交互系统 | 开发环境 → 图形 → MVC | 追踪一次选择如何改变模型并刷新视图 |
| 对照原书 | 下方映射表 → 各篇来源说明 | 找到对应章、印刷页和扫描页 |
## 教程目录
1. [消息与对象:理解执行模型](/docs/source-studies/smalltalk/messages)
2. [语法与块:读懂一段 Smalltalk](/docs/source-studies/smalltalk/syntax-and-blocks)
3. [类、方法与元类:行为放在哪里](/docs/source-studies/smalltalk/classes-and-methods)
4. [对象协议:身份、复制与依赖](/docs/source-studies/smalltalk/object-protocol)
5. [类的架构与反射:系统怎样描述自己](/docs/source-studies/smalltalk/reflection)
6. [基础类库:数值、集合与流](/docs/source-studies/smalltalk/collections-and-streams)
7. [进程与同步:协作执行的对象](/docs/source-studies/smalltalk/processes)
8. [图形系统:坐标、位图与绘制](/docs/source-studies/smalltalk/graphics)
9. [开发环境:浏览、检查与调试](/docs/source-studies/smalltalk/live-environment)
10. [MVC 与曲线案例:连接数据和界面](/docs/source-studies/smalltalk/mvc)
11. [术语、练习与原书对照](/docs/source-studies/smalltalk/reference)
## 原书与新结构
| 原章 | 印刷页 | PDF 页 | 本教程 |
| --- | --- | --- | --- |
| 1 Introduction;2 Principles | 1–8 | 12–19 | 消息与对象 |
| 3 Classes;5 Methods | 9–12;21–25 | 20–23;32–36 | 类、方法与元类 |
| 4 Syntax | 13–20 | 24–31 | 语法与块 |
| 7 Object | 31–39 | 42–50 | 对象协议 |
| 8 Architecture of Classes | 40–52 | 51–63 | 类的架构与反射 |
| 9 Magnitude;10 Numeric;11 Collection;12 Stream | 53–90 | 64–101 | 基础类库 |
| 13 Processes | 91–95 | 102–106 | 进程与同步 |
| 14 Point/Rectangle;15 Graphics | 96–117 | 107–128 | 图形系统 |
| 6 Interface;16–19 Windows/Browsers | 26–30;118–149 | 37–41;129–160 | 开发环境 |
| 20 User Events;21 MVC | 150–174 | 161–185 | MVC 与曲线案例 |
## 阅读约定
代码使用 `:=` 表示赋值、`^` 表示方法返回,替代扫描版的箭头符号;字符串用单引号,注释用双引号。`smalltalk` 代码块可复制,但页面不执行它们。历史类名与选择子保留原样,所有平台相关操作均以原书镜像为范围。
“现代化”体现在清晰的阅读路线、可搜索文本、代码块、对照表、语义链接与练习。本文不把 1987 年的鼠标操作、字符集、进程调度或 MVC API 当作所有今天实现的共同规范。
-53
View File
@@ -1,53 +0,0 @@
# 消息与对象:理解执行模型
> 原书依据:第 1–2 章,印刷页 1–8,PDF 页 12–19。
Smalltalk 的基本动作是:向一个接收者发送消息,由接收者的类决定执行哪个方法。数字、字符串、集合、类和块都能成为接收者。程序由相互发送消息的对象组成。
## 把表达式拆成三个部分
```smalltalk
8 + 5
'hello' size
#(10 20 30) at: 2
```
| 表达式 | 接收者 | 选择子 | 参数 | 推演结果 |
| --- | --- | --- | --- | --- |
| `8 + 5` | 整数 8 | `+` | 5 | 13 |
| `'hello' size` | 字符串 | `size` | 无 | 5 |
| `#(10 20 30) at: 2` | 数组 | `at:` | 2 | 20 |
消息是请求;方法是为响应请求而执行的实现。选择子标识这次请求的名称。不同类可以实现同一个选择子,因此同一条消息能够触发不同的行为。
## 变量保存引用
```smalltalk
| first second |
first := OrderedCollection new.
second := first.
first add: 8.
second size
```
预期最后得到 1:两个变量指向同一个集合。赋值没有自动复制集合。理解引用关系,是之后理解浅复制、模型共享和对象检查器的基础。
对象的状态与它支持的消息要分开看。调用方通过协议与对象协作;读取某个内部槽位的调试能力,不等于应用程序应依赖那个槽位。
## 表达式可以继续发送消息
```smalltalk
(8 + 5) printString
```
先得到 13,再向结果发送 `printString`。与之不同,`collection add: item` 的结果不一定是集合;链式发送前必须确认方法返回值。[语法篇](/docs/source-studies/smalltalk/syntax-and-blocks)解释如何用级联表达“始终向原接收者发送”。
## 镜像为什么重要
原书描述的 virtual image 是包含对象和类的系统世界。开发时可以浏览类、编译方法、检查运行对象并继续执行。语言工具和应用共享对象体系,使重用既有类库成为日常开发方式。
这是历史系统的组织方式;不能据此推断任意镜像的持久化、发布或隔离策略。研究时要同时记录类的定义和当前镜像状态,避免把一次手工修改当成可复现的工程成果。
## 自检
为何两个对象都能接收 `size`,却不需要有相同的数据布局?因为调用方依赖消息协议,各自的类提供方法。下一篇从词法和优先级说明如何读出真正发送的消息。
@@ -1,81 +0,0 @@
# 语法与块:读懂一段 Smalltalk
> 原书依据:第 4 章,印刷页 13–20,PDF 页 24–31。赋值和返回符号按导读约定规范化。
## 先识别字面量
| 写法 | 含义 |
| --- | --- |
| `42`、`16rFF`、`2.5` | 整数、十六进制整数、浮点数 |
| `$A` | 字符 |
| `'Ada'` | 字符串 |
| `'it''s'` | 包含单引号的字符串 |
| `#size`、`#at:put:` | 符号,常用于表示选择子 |
| `#(2 4 6)` | 字面量数组 |
| `[ :x | x + 1 ]` | 接受参数的块 |
`nil`、`true`、`false`、`self`、`super` 是具有特殊语义的伪变量,不能按普通局部变量赋值。不要把字符串 `'size'` 与符号 `#size` 混用。
## 三种消息与优先级
一元消息没有参数;二元消息接受一个参数;关键词消息通过带冒号的片段标出参数位置。`at:put:` 是一个选择子。
优先级为:**括号内表达式 → 一元消息 → 二元消息 → 关键词消息**。同级按从左到右处理,二元选择子没有传统乘除优先的规则。
```smalltalk
2 + 3 * 4 "推演为 20"
2 + (3 * 4) "推演为 14"
10 raisedTo: 2 + 3 "等价于 10 raisedTo: (2 + 3)"
```
`a at: 1 put: 9` 是一条消息。两个关键词调用连续出现时,用括号明确哪一个结果作为另一个的参数。
## 句点与分号
句点分隔表达式;分号让消息继续发给同一个接收者。
```smalltalk
| values |
values := OrderedCollection new.
values add: 4; add: 9; add: 16.
values size
```
这里三次 `add:` 的接收者都是 `values`。若要在一段级联表达式末尾取得集合本身,可发送 `yourself`:
```smalltalk
OrderedCollection new
add: 4;
add: 9;
yourself
```
## 块把计算推迟
```smalltalk
| square |
square := [ :n | n * n ].
square value: 6
```
创建块不执行块体;`value:` 传入参数并求值,预期结果是 36。无参数块接收 `value`,两个参数可用 `value:value:`。
控制流也可以用消息与块组织:
```smalltalk
| count total |
count := 1.
total := 0.
[count <= 4] whileTrue: [
total := total + count.
count := count + 1].
total
```
推演结果为 10。条件块每轮重新求值;把条件提前算成单个布尔对象,会失去更新后的判断。`ifTrue:ifFalse:` 则把两段待执行的分支交给布尔对象选择。
## 返回的边界
块通常返回最后一个表达式的值。方法若没有 `^` 显式返回,则默认返回接收者。块中的 `^` 涉及从其所属方法返回,不能简单理解成普通函数局部 return。本文示例避免把含非局部返回的块保存到方法退出之后;需要这样的用法时,应核对目标镜像的块实现。
练习:`3 + 4 factorial` 先执行哪条消息?先向 4 发送 `factorial`,再向 3 发送 `+`;按数学结果推演为 27。
@@ -1,76 +0,0 @@
# 类、方法与元类:行为放在哪里
> 原书依据:第 3 章和第 5 章,印刷页 9–12、21–25,PDF 页 20–23、32–36。
## 状态的可见范围
| 名称 | 用途 | 阅读时应问 |
| --- | --- | --- |
| 实例变量 | 每个实例自己的状态槽位 | 是否继承自父类? |
| 类变量 | 由类及相关子类共享的绑定 | 是否存在跨实例影响? |
| 全局变量 | 经系统字典访问的绑定 | 是否形成隐含依赖? |
| 池变量 | 多个类共享的变量集合 | 类定义引用了哪个池? |
| 参数、临时变量 | 一次方法或块计算的局部名称 | 生命周期与可赋值规则是什么? |
原书第 5 章明确限制对方法参数再赋值。本教程沿用不修改参数的写法,不把其他实现的语法扩展套回原书。
## 用一个小类串起语法
以下为本文编写的历史风格定义。类定义在 Browser 或相应定义工具中提交;后续方法分别安装在实例侧,不是把整段当作 Workspace 表达式执行。
```smalltalk
Object subclass: #StudyCounter
instanceVariableNames: 'count'
classVariableNames: ''
poolDictionaries: ''
category: 'Study-Examples'
```
实例侧方法:
```smalltalk
initialize
count := 0
```
```smalltalk
increment
count := count + 1
```
```smalltalk
value
^count
```
工作区中的使用示意:
```smalltalk
| counter |
counter := StudyCounter new.
counter initialize.
counter increment.
counter value
```
这里显式初始化,不依赖 `new` 是否自动发送 `initialize`。这些安装步骤和类定义消息需要在目标镜像核对。`increment` 没有显式返回,所以返回 `counter`;`value` 返回槽位保存的数值。
## 方法查找
普通消息从接收者的类开始找选择子,找不到就沿父类向上查找。原书的根类为 `Object`。整个链中没有对应方法时,进入 `doesNotUnderstand:` 协议。
重写方法只改变行为的选择,不把接收者换成父类对象。`self` 始终指向当前接收者。
## super 改变查找起点
假设 B 继承 A,C 继承 B;方法 `describe` 定义在 B 中。如果该方法发送 `super name`,查找从 A 开始,即便当前接收者是 C 的实例。若发送 `self name`,查找从接收者实际的类 C 开始。
因此 `super` 不是“父对象”。它保留同一接收者,按方法定义位置确定查找起点。
## 实例侧与类侧
`StudyCounter new` 的接收者是类对象;`counter increment` 的接收者是它的实例。类自身也是对象,类侧方法由元类组织。
读类侧构造方法时,要检查它究竟分配实例、初始化状态还是返回缓存对象。`new` 只是发给类对象的消息,不能把它视为所有类都必须遵循的固定语法。
原书的 primitive 方法把部分操作交给虚拟机。追踪执行路径时,需区分 Smalltalk 方法体与 VM 提供的原语;本教程没有验证任何 VM 的原语源码。
@@ -1,51 +0,0 @@
# 对象协议:身份、复制与依赖
> 原书依据:第 7 章,印刷页 31–39,PDF 页 42–50。
`Object` 提供许多基础协议。阅读子类时,既要看它新增了什么,也要看它重写了什么。
## 身份与相等
`==` 判断是否为同一对象;`=` 表达由类定义的相等关系。两个内容相同的集合可以是两个不同对象。修改其中一个是否影响另一个,还取决于它们内部引用是否共享。
`class` 返回所属类。`isMemberOf:` 针对确切的类;`isKindOf:` 包含继承关系。检查 `nil` 通常使用 `isNil` 或 `notNil`。面向协议设计时,不必为了发送每条消息都先做具体类判断。
## 浅复制复制的是哪一层
```text
原容器 ──→ 可变子对象
副本 ──→ 同一个可变子对象
```
浅复制产生新的外层对象,内部引用仍可指向相同子对象。给副本的某个槽位重新赋值,与修改该槽位所指子对象,是两种不同操作:前者改变副本自己的引用,后者可能被原对象观察到。
原书还讨论 `deepCopy` 和可被子类重写的 `copy`。深复制不应简单等同于“任意对象图都可以安全递归复制”:循环、共享关系和外部资源需要实现策略。本文没有对这些情况做运行验证。
## 表示与动态发送
| 消息 | 主要意图 |
| --- | --- |
| `printString` | 得到便于人阅读的表示 |
| `storeString` | 得到用于重建对象的源文本表示 |
| `perform:` | 用符号指定要发送的消息 |
| `perform:with:` | 动态发送带一个参数的消息 |
可读表示不等于持久化格式。动态发送也不是解析任意源代码:`perform:` 选择一个消息,而编译器求值会解释完整表达式。
## 用依赖通知连接对象
原书的依赖协议包含 `addDependent:`、`removeDependent:`、`changed:` 和 `update:`。模型改变后通知依赖者,让视图自行读取需要显示的状态。
```text
视图注册到模型
↓
模型状态修改 → changed: 某方面
↓
依赖者收到 update: → 重新读取模型 → 更新显示
```
通知描述“需要更新”,并不天然携带完整的新状态。实现时要明确通知粒度、订阅解除和是否可能形成递归更新。原书的键盘互斥示例说明对象之间可以靠依赖关系协调状态;最后的 MVC 案例继续使用这套机制。
## 阅读检查点
对一个复制操作,画出复制前后的引用箭头;对一次通知,列出谁订阅、谁修改、谁通知、谁重绘。这两张简图往往比逐个记忆方法名更能暴露误解。
-44
View File
@@ -1,44 +0,0 @@
# 类的架构与反射:系统怎样描述自己
> 原书依据:第 8 章,印刷页 40–52,PDF 页 51–63;结合第 3 章元类介绍。
类既组织实例行为,又作为对象接受消息。把“谁是谁的实例”与“谁继承谁”分成两条关系,才能读懂元类。
## 三个层次的责任
| 类 | 原书讨论的主要责任 |
| --- | --- |
| `Behavior` | 方法字典、实例创建和类层级操作 |
| `ClassDescription` | 变量名、分类、源码组织与编译相关描述 |
| `Class` | 表示通常使用的类对象 |
| `Metaclass` | 表示类对象所对应的元类 |
`Class` 和 `Metaclass` 都属于 `ClassDescription` 的子类体系。对于具体类对象,可以用 `class` 访问其元类;实例侧与类侧的方法分别组织在相应行为对象中。
原书元类部分文字较简略。不能据此把所有具体元类都画成直接继承 `Class` 的平面结构;具体元类的父类关系应在目标镜像中检查。
## 研究一次消息查找
```smalltalk
Integer superclass
Number subclasses
Number allSubclasses
```
这是向类对象发消息,查询系统结构。阅读结果时区分“直接子类”和“全部后代”;枚举顺序不应成为应用逻辑依赖。
方法字典把选择子映射到方法。查找协议从当前类的字典开始,再沿 superclass 关系继续。Browser 的实现者查询、类层级浏览和方法安装,都建立在这类可访问的描述结构上。
## 编译是系统服务
原书把方法编译、分类管理、变量名访问和类的文件导出放进类描述协议。由此可以理解:Browser 中接受一段方法文本,是向运行中的系统安装行为。
应分别记录:源文本、编译后方法、当前类结构,以及镜像中的对象状态。只导出源文本,并不一定包含恢复某个实验状态所需的全部数据。
## 多重继承的边界
第 8 章最后讨论多重继承。阅读时将“讨论的组织方案”与前面实际使用的单一父类查找链分开,不能因为书中出现这一节就宣称标准 Smalltalk-80 类定义普遍支持多重继承。
## 可复现的探索记录
在镜像中选择一个类,记录其父类、实例变量、实例侧选择子与类侧选择子;再选择一个继承而来的方法,注明实际定义类。这个小表可以验证你是否把类对象、实例和元类混在了一起。本文只给出检查路线,尚未在镜像执行。
@@ -1,58 +0,0 @@
# 基础类库:数值、集合与流
> 原书依据:第 9–12 章,印刷页 53–90,PDF 页 64–101。
这些章节适合按“要保存什么、如何比较、如何遍历”阅读。具体消息是否存在仍以书中 Version 2 镜像为范围。
## 有序值与数值
`Magnitude` 抽象可比较对象;原书讨论其下的 `Date`、`Time`、`Character` 与数值体系。日期和时间章节的构造协议具有历史性,不能直接当作时区或国际化指南。字符章节基于当时的有限字符集合,不能扩展成 Unicode 结论。
`Number` 的主要子类包含 `Integer`、`Fraction` 和 `Float`。整数层级还区分小整数与大整数。算术消息的结果可以由不同数值类表示,不能仅根据左操作数认定返回类型。
```smalltalk
1 / 3
7 // 2
7 \\ 2
```
在原书数值模型中,第一条可以表示精确分数,后两条分别体现整除和余数协议。这里的余数选择子写作两个反斜杠;使用其他方言时务必核对拼写和负数规则。浮点计算则要区分显示值和实际精度。
## 选择集合
| 集合 | 关注点 | 典型选择理由 |
| --- | --- | --- |
| `Array` | 固定长度、按位置访问 | 已知元素数量 |
| `OrderedCollection` | 保留顺序、可增删 | 动态列表 |
| `SortedCollection` | 根据排序关系组织 | 持续维护顺序 |
| `Set` | 去重 | 关心是否出现 |
| `Bag` | 保留重复次数 | 计数、频次 |
| `Dictionary` | 键到值的映射 | 命名索引 |
| `IdentityDictionary` | 以身份区分键 | 相等值仍需视为不同键 |
| `Interval` | 规则数列 | 数字范围遍历 |
| `LinkedList` | 链式组织 | 研究链节点协议 |
有索引不代表所有集合都使用相同索引语义;数组和序列类的常见索引从 1 开始。
## 四种遍历意图
```smalltalk
#(2 3 4) do: [ :n | n squared ].
#(2 3 4) collect: [ :n | n squared ].
#(2 3 4) select: [ :n | n > 2 ].
#(2 3 4) inject: 0 into: [ :sum :n | sum + n ]
```
`do:` 强调逐个执行,不收集块结果;`collect:` 映射结果;`select:` 保留满足条件的元素;`inject:into:` 累积一个值。依次推演,后三条分别表达平方序列、筛出 3 和 4、求和 9。结果容器类型取决于接收者协议,不能一概写为 Array。
`add:` 的返回值可能是加入的元素;对 Dictionary,`at:put:` 表示设置键值;对固定数组则是替换槽位。共享选择子并不抹平各类的约束。
## 流把位置与内容分开
`ReadStream`、`WriteStream` 和 `ReadWriteStream` 在底层集合之上维护读取或写入位置。`next`、`nextPut:`、`atEnd` 表达逐项读、逐项写和结束判断。`contents` 用于取得流的内容,`position` 与 `reset` 处理位置。
文件流把同一思路扩展到外部文件;原书还介绍 `FileDirectory`。不要把内存流的生命周期直接套给文件:外部资源需关闭,并明确失败与部分写入的处理方式。本文没有运行文件写入示例。
## 练习
对“保留输入顺序并允许重复”“按名称找对象”“统计同一值出现几次”,依次考虑 OrderedCollection、Dictionary、Bag。再问一层:键相等采用什么语义,修改键会不会破坏查找?带着这些问题去浏览实际类的方法,能比背类名得到更多信息。
-35
View File
@@ -1,35 +0,0 @@
# 进程与同步:协作执行的对象
> 原书依据:第 13 章,印刷页 91–95,PDF 页 102–106。
原书的 `Process` 是 Smalltalk 镜像中的执行活动。不能把这个名字直接解释为操作系统进程,也不能据此推断多核并行。
## 四类对象的分工
| 类 | 作用 |
| --- | --- |
| `Process` | 表示可恢复、可挂起的执行活动 |
| `ProcessorScheduler` | 按调度策略管理执行活动 |
| `Delay` | 表示一定时间的等待 |
| `Semaphore` | 协调活动之间的等待与信号 |
块可以作为执行活动的计算内容。是否立即运行、以什么优先级运行,要看创建和启动协议;不要把“创建 Process”和“已经执行完成”混为一谈。
## 等待不是空转
Delay 表达经过一段时间后再继续。Semaphore 的 `wait` 与 `signal` 表达同步关系。等待活动暂停后,其他可运行活动可以继续推进;反复检查共享变量的循环则可能消耗执行时间。
```text
消费者:等待信号 → 检查共享状态 → 取出结果
生产者:准备结果 → 更新共享状态 → 发出信号
```
这是一张概念顺序图,不是可直接运行的队列实现。仅有信号量不代表共享集合访问自动安全,还要定义互斥、空队列、重复信号和退出条件。
## 阅读调度代码的顺序
先找创建与启动位置,再找可能挂起的位置,最后沿恢复路径检查共享状态。重点记录“此处继续执行时,其他活动可能改过什么”。原书的优先级说明属于该系统调度模型,不是跨实现的性能保证。
## 思考题
如果发送信号后才写入结果,消费者恢复时能否看见完整状态?这取决于调度与同步保证;设计时应先完成状态转换,再通过约定的同步协议通知。研究时也要核对目标 VM 的具体行为。本教程未运行并发或调度测试。
-49
View File
@@ -1,49 +0,0 @@
# 图形系统:坐标、位图与绘制
> 原书依据:第 14–15 章,印刷页 96–117,PDF 页 107–128。
图形对象不是外接的独立语言。坐标、矩形、位图、光标和绘图动作都通过消息协议组织。
## Point 与 Rectangle
```smalltalk
12 @ 18
(12 @ 18) x
(12 @ 18) y
```
`@` 是向数字发送的二元消息,产生一个 Point。后两条分别取 x 与 y。因为 `@` 和 `+` 同为二元消息,混用时最好加括号。
Rectangle 表示矩形区域。阅读构造调用时要分清传入的是角点还是尺寸;阅读几何操作时要区分包含、相交、平移和缩放。边界点是否包含应核对具体方法,不能只凭图形直觉。
## 坐标系必须写在图旁
原书图形环境的屏幕坐标通常从左上向右下增加;数学曲线常要求 y 轴向上。因此绘制之前要说明对象坐标与显示坐标之间的转换。
```text
模型坐标 → 视图变换 → 显示坐标 → 裁剪 → 像素绘制
```
相同点在不同视图中可以对应不同屏幕位置。移动视口时只改变显示映射,不应顺便改变领域模型中的坐标。
## 从存储到绘制
| 对象 | 阅读重点 |
| --- | --- |
| `Bitmap` | 像素位信息的底层存储 |
| `DisplayObject` | 可显示对象的公共概念 |
| `DisplayText` | 文本的图形表示 |
| `Form` | 位图图像、尺寸与显示属性 |
| `Cursor` | 光标图形 |
| `DisplayScreen` | 屏幕显示对象 |
| `Path` | 图形路径 |
| `BitBlt` | 源区域到目标区域的位图传输与组合 |
| `Pen` | 以位置和移动描述绘图 |
研究 BitBlt 时记录源、目标、传输区域、裁剪范围和组合规则。它涉及像素组合,不只是语言层面的对象复制。Form 的副本若共享底层位图,修改像素仍可能影响原图;这与对象协议篇的浅复制问题相连。
## 曲线绘制的最小思路
把函数保存为块,对一系列 x 求 y,转换成显示点,再用线段连接。采样步长越大,细节越容易丢失。固定采样不是精度保证;异常值、坐标越界和不连续曲线都需要额外策略。
这是对原书曲线案例的阅读提示,不是新增的绘图运行时。下一篇先介绍如何借助系统工具观察这些对象,再进入 MVC 的连接过程。
@@ -1,47 +0,0 @@
# 开发环境:浏览、检查与调试
> 原书依据:第 6 章、第 16–19 章,印刷页 26–30、118–149,PDF 页 37–41、129–160。
原书使用三键鼠标与特定窗口菜单。现代阅读时,保留动作的目的:求值、查找行为、检查对象、定位调用现场。具体菜单位置和快捷键以使用的镜像为准。
## 从工作区开始
Workspace 是编辑和求值表达式的空间。原书区分执行选中文本的 `do it` 与执行后显示返回值的 `print it`。检查器则让你观察得到的对象。
```smalltalk
OrderedCollection new inspect
```
这段表达式会请求打开对象检查器;它不是网页中的交互按钮。运行前应准备具有相应 GUI 工具的镜像。
## Browser 看行为,Inspector 看状态
| 工具 | 主要问题 | 需要记录 |
| --- | --- | --- |
| Browser | 方法定义在哪里? | 类侧或实例侧、协议分类、定义类 |
| Inspector | 这个对象现在引用了什么? | 对象身份、槽位、子对象 |
| Debugger | 执行为什么到这里? | 调用链、接收者、参数、临时变量 |
| 文件浏览工具 | 源码怎样导入导出? | 文件内容、导入动作与当前镜像变化 |
| Form/Bit Editor | 图形数据怎样变化? | 图像对象与像素编辑结果 |
Browser 常按类分类、类、协议、方法逐层选择。协议分类是组织方法的标签,不应与消息选择子或语言类型系统混同。
## 沿选择子阅读源码
1. 在 Browser 中找到一个方法,确认实例侧还是类侧。
2. 查看 implementors,比较相同选择子在不同类中的实现。
3. 查看 senders,找出哪些方法发送这条消息。
4. 查实例变量引用,理解状态由谁读写。
5. 对具体对象求值并 inspect,核对静态阅读与实际状态是否一致。
senders 反映可被工具识别的引用;使用 `perform:` 的动态调用可能需要另外追踪。只看到一个实现者,也不能直接推断所有调用都落到它。
## 调试的四个视角
原书 Debugger 把调用链、代码、接收者检查器和局部变量检查器放在一起。某个对象不理解消息时,先检查接收者实际是什么,再检查参数,最后追踪调用者为何给它发送这个选择子。
修改运行系统中的方法可能影响其他窗口。为了复现,单独记录实验类、方法文本、镜像版本和预期结果,不把手工修复后的镜像当作原始系统状态。
## 一个建议实验
创建本教程的 StudyCounter,求值并 inspect;调用 increment 后再看 count;随后在 Browser 中查 value 的实现。实验将“表达式结果、对象状态、方法定义”连成一条路线。此实验尚未在本次任务执行。
-61
View File
@@ -1,61 +0,0 @@
# MVC 与曲线案例:连接数据和界面
> 原书依据:第 20–21 章,印刷页 150–174,PDF 页 161–185。曲线应用位于印刷页 167–174。
Smalltalk-80 的 MVC 是桌面交互对象之间的协作。不要直接把 View、Controller 与 Web 请求处理器或某个现代框架组件一一等同。
## 输入与职责
原书先介绍 `InputState` 与 `InputSensor`,再讨论 View、Controller 和 ControlManager。输入状态、输入读取和交互控制是相邻但不同的责任。
| 角色 | 保存或处理什么 | 对曲线应用的意义 |
| --- | --- | --- |
| Model | 应用数据与状态转换 | 曲线文本列表及当前选择 |
| View | 读取模型并显示,管理视图几何 | 列表显示、曲线图形 |
| Controller | 输入处理与操作协调 | 选择、菜单、编辑动作 |
| ControlManager | 活动控制器的管理 | 协调窗口交互 |
一个模型可以有多个视图。视图和控制器组成显示与交互的配对关系;模型不需要直接包含像素绘制代码。
## window 与 viewport
原书的 View 既有自身坐标范围,也有放到上层视图中的区域。曲线区域将数学坐标映射到窗口子区域,以便 y 轴向上,而屏幕坐标保持其约定。
调试错误显示时,先核对数据,再核对 window、viewport 和显示变换。模型数值正确不等于变换正确。
## 曲线案例的对象结构
原书以 `CurveList` 继承 `OrderedCollection`,增加当前曲线选择;以 `CurveListView` 继承 `View`,重定义绘制行为。列表视图复用 SelectionInListView,并用选择子告诉它如何访问列表、读取选择、改变选择和取得菜单。
```text
窗口 StandardSystemView
├─ 列表子视图 ──读写──→ CurveList
└─ 曲线子视图 ──读取──→ 同一个 CurveList
│
状态变化通知
↓
更新依赖视图
```
类名在扫描 OCR 中存在大小写混淆,本文按语义统一写作 CurveList 和 CurveListView;安装原版代码前应以页面图像与镜像类名复核。
## 追踪一次选择
1. 用户在列表中选择一条曲线。
2. 交互对象通过配置的选择子更新模型的当前选择。
3. 模型发送改变通知。
4. 依赖视图接收更新并重新读取模型。
5. 曲线视图清理绘制区域,将选择的文本求值为块。
6. 对采样点执行块,通过显示变换绘出折线。
这里“文本 → 块”是编译求值,不是普通数据解析。原书在可信镜像中演示原型编程;把这一机制用于接收外部输入的服务,需要另行设计输入语言与隔离策略。本文页面不执行用户代码。
## 增删改也是模型转换
添加曲线会收集文本、追加列表、重置选择并通知;删除会移除当前项、清空选择并通知;修改会替换列表中的相应文本,再使显示更新。无选择时的菜单与有选择时不同。
这说明通知应该位于状态转换之后,让所有视图看到一致状态。曲线文本、当前选择与显示结果应作为三个不同层次检查。
## 迁移设计思想
可以借鉴的组织方式是:将领域状态集中在模型,让输入动作调用明确的状态转换,由订阅者负责刷新显示。原书具体的窗口类、编译器调用和采样算法仍属于历史实现。本教程没有复原完整 GUI 程序或声称它已在当代实现中运行。
-68
View File
@@ -1,68 +0,0 @@
# 术语、练习与原书对照
> 来源范围:全书主要章节;各主题的精确印刷页与 PDF 页见[总览](/docs/source-studies/smalltalk/overview)。本篇练习和术语释义为整理者编写。
## 术语速查
| 原词 | 本文译法 | 区分要点 |
| --- | --- | --- |
| object | 对象 | 有身份、状态和行为的实体 |
| receiver | 接收者 | 本次消息发给谁 |
| message | 消息 | 对接收者的行为请求 |
| selector | 选择子 | 消息的名称,如 at:put: |
| method | 方法 | 某类提供的响应实现 |
| class / instance | 类 / 实例 | 行为组织与由其描述的对象 |
| metaclass | 元类 | 类对象所属的类 |
| protocol | 协议 | 一组协作消息;Browser 中也用于分类 |
| block | 块 | 延迟求值的计算对象 |
| cascade | 级联 | 以分号向同一接收者继续发送 |
| primitive | 原语 | 由虚拟机支持的底层操作 |
| identity / equality | 身份 / 相等 | 同一对象与值意义上的相等 |
| shallow copy | 浅复制 | 复制外层但可能共享内部对象 |
| dependent | 依赖者 | 注册后接收模型变化通知的对象 |
| image | 镜像 | 保存并运行对象系统的状态载体 |
| workspace | 工作区 | 编辑与求值表达式的工具 |
| browser | 类浏览器 | 查看、编辑类与方法的工具 |
| inspector | 对象检查器 | 查看具体对象状态的工具 |
| window / viewport | 坐标窗口 / 视口 | 自身坐标范围与显示映射区域 |
## 预测结果
以下是语义练习,答案为推演,未运行。
| 问题 | 答案与原因 |
| --- | --- |
| `2 + 3 * 4` | 20,二元消息从左到右 |
| `2 + (3 * 4)` | 14,括号改变次序 |
| `[ :x | x + 2 ] value: 5` | 7,value: 触发块求值 |
| 方法只有 `count := 9` 时返回什么? | 接收者,方法无显式返回 |
| B 中的方法对 C 实例发送 super 消息,从哪查找? | 从 B 的父类开始,接收者仍是 C 实例 |
| 浅复制后替换副本某个槽位,是否替换原件槽位? | 不会;修改共享子对象才可能双方可见 |
| do: 能否代替 collect: 收集块结果? | 不能直接代替,两者协议目的不同 |
| 给模型发送 changed: 是否自动修改模型数据? | 不应这样理解;先完成修改,再通知依赖者 |
## 三次源码阅读练习
**消息路线**:选择一个集合的 `size`,找实际定义类,再查看一个发送者;记录“接收者类 → 继承链 → 方法”。
**状态路线**:创建两个指向同一集合的变量,画引用图;再复制外层容器,比较替换槽位与修改子对象的差别。
**界面路线**:对原书曲线案例画出选择、修改模型、通知、重绘与显示变换,注明每一步所在对象。
完成实验时另记镜像名称、版本、源代码、实际输出和差异。本次任务只完成文档整理,没有运行这些实验。
## 扫描校勘与适用边界
| 扫描问题 | 本文处理 |
| --- | --- |
| 箭头被识别成 `<-`、字母或感叹号 | 赋值统一为 :=,返回统一为 ^ |
| Smalltalk-80 被识别成 Smalltalk-SO | 依据书名与语境规范化 |
| 选择子被拆开、类名大小写混淆 | 行文合并;具体 API 安装前再核对原页 |
| 第 4 章个别科学计数法示例数位不一致 | 不沿用异常数值作为例题 |
| 后部 GUI 源码嵌在截图中 | 解释调用结构,不将 OCR 文本冒充可运行源码 |
原书是学习 Smalltalk-80 系统的资料,不是本文对所有 Smalltalk 方言兼容性的承诺。全文以语义重述和新增短例组织;没有把原始扫描件、OCR 全文或本机下载路径发布到网站。
## 后续核验清单
准备目标镜像后,可分别验证类定义消息、初始化约定、块返回边界、数值除法、集合返回类型、信号量时序,以及 MVC 选择子与坐标变换。核验结果应与本文的语义推演分别记录,避免把“读过”写成“运行通过”。
+3 -2
View File
@@ -13,8 +13,9 @@
"lint": "pnpm --filter @endor/docs run lint", "lint": "pnpm --filter @endor/docs run lint",
"typecheck": "pnpm --filter @endor/docs run typecheck", "typecheck": "pnpm --filter @endor/docs run typecheck",
"figures": "uv run --locked --project tools/figures endor-figures", "figures": "uv run --locked --project tools/figures endor-figures",
"check": "pnpm run lint && pnpm run typecheck && pnpm run test:auth && pnpm run test:research && pnpm --filter @endor/docs run images && pnpm --filter @endor/docs run test:images && pnpm run figures && pnpm run build", "check": "pnpm run lint && pnpm run typecheck && pnpm run test:auth && pnpm run test:research && pnpm run test:docs && 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", "test:auth": "pnpm --filter @endor/docs run test:auth",
"test:research": "pnpm --filter @endor/docs run test:research" "test:research": "pnpm --filter @endor/docs run test:research",
"test:docs": "pnpm --filter @endor/docs run test:docs"
} }
} }
+9 -3
View File
@@ -9,7 +9,8 @@ from urllib.parse import urlsplit
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
DATA = json.loads((ROOT / 'content/research/ai-companies.seed.json').read_text()) DATA = json.loads((ROOT / 'content/research/ai-companies.seed.json').read_text())
AUDIT = json.loads((ROOT / 'content/research/ai-companies.coverage.json').read_text()) AUDIT = json.loads((ROOT / 'content/research/ai-companies.coverage.json').read_text())
TARGET = ROOT / 'content/docs/ai-company-research/02-coverage-and-gaps.md' TARGET = ROOT / '.artifacts/reports/02-coverage-and-gaps.md'
SOURCE_SLUG = 'ai-company-research/02-coverage-and-gaps'
BASE = '/docs/other-research/ai-company-research/overview' BASE = '/docs/other-research/ai-company-research/overview'
@@ -95,13 +96,18 @@ def render():
def main(): def main():
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--check', action='store_true') parser.add_argument('--check', action='store_true')
parser.add_argument('--document-export', type=Path)
args = parser.parse_args() args = parser.parse_args()
content = render() content = render()
if args.check: if args.check:
if not TARGET.exists() or TARGET.read_text() != content: if args.document_export is not None:
bundle = json.loads(args.document_export.read_text())
stored = next((d['content'] for d in bundle['documents'] if d['sourceSlug'] == SOURCE_SLUG), None)
if stored != content:
sys.exit('Research coverage report differs; review and regenerate') sys.exit('Research coverage report differs; review and regenerate')
print('Research coverage accounting and report are consistent') print('Research coverage accounting verified; supplied document export compared when requested')
else: else:
TARGET.parent.mkdir(parents=True, exist_ok=True)
TARGET.write_text(content) TARGET.write_text(content)
print(TARGET.relative_to(ROOT)) print(TARGET.relative_to(ROOT))
+1
View File
@@ -7,6 +7,7 @@ pnpm run lint
pnpm run typecheck pnpm run typecheck
pnpm --filter @endor/docs run test:auth pnpm --filter @endor/docs run test:auth
pnpm --filter @endor/docs run test:research pnpm --filter @endor/docs run test:research
pnpm --filter @endor/docs run test:docs
pnpm --filter @endor/docs run images pnpm --filter @endor/docs run images
pnpm --filter @endor/docs run test:images pnpm --filter @endor/docs run test:images
cp -R apps/docs/public/figures /tmp/expected-figures cp -R apps/docs/public/figures /tmp/expected-figures
+14 -2
View File
@@ -15,7 +15,7 @@ APP = "endorphin-tabularium-docs"
INPUTS = ["Dockerfile", "Dockerfile.ci", ".dockerignore", ".npmrc", "package.json", INPUTS = ["Dockerfile", "Dockerfile.ci", ".dockerignore", ".npmrc", "package.json",
"pnpm-lock.yaml", "pnpm-workspace.yaml", "apps/docs", "content", "tools/figures", "pnpm-lock.yaml", "pnpm-workspace.yaml", "apps/docs", "content", "tools/figures",
"scripts/check-container.sh", "scripts/ci.py", "scripts/summarize-ai-income.py", "scripts/check-container.sh", "scripts/ci.py", "scripts/summarize-ai-income.py",
"scripts/audit-ai-research.py"] "scripts/audit-ai-research.py", "scripts/export-legacy-documents.py"]
def run(*args: str, capture: bool = False) -> str: def run(*args: str, capture: bool = False) -> str:
@@ -36,9 +36,17 @@ def fingerprint() -> str:
def smoke_image(image: str, name: str) -> None: def smoke_image(image: str, name: str) -> None:
run("docker", "run", "-d", "--name", name, "--mount", "type=volume,dst=/auth", "-p", "127.0.0.1::3000", image) run("docker", "run", "-d", "--name", name, "--mount", "type=volume,dst=/auth", "-e", "DOCUMENT_SMOKE_TEST=1", "-p", "127.0.0.1::3000", image)
try: try:
run("docker", "exec", name, "node", "apps/docs/auth-cli.cjs", "init") run("docker", "exec", name, "node", "apps/docs/auth-cli.cjs", "init")
subprocess.run(["docker", "exec", "-i", name, "node", "apps/docs/docs-cli.cjs", "import"],
input=(ROOT / ".artifacts/documents-migration.json").read_bytes(), check=True)
run("docker", "exec", name, "node", "apps/docs/docs-cli.cjs", "export", "/auth/migrated-documents.json")
migrated = json.loads(run("docker", "exec", name, "cat", "/auth/migrated-documents.json", capture=True))
expected = json.loads((ROOT / ".artifacts/documents-migration.json").read_text())
if migrated != expected:
raise RuntimeError("Migration changed document content or catalog metadata")
run("docker", "exec", name, "node", "apps/docs/docs-cli.cjs", "backup", "/auth/smoke-backup.sqlite")
for _ in range(60): for _ in range(60):
state = json.loads(run("docker", "inspect", name, capture=True))[0]["State"] state = json.loads(run("docker", "inspect", name, capture=True))[0]["State"]
if state.get("Health", {}).get("Status") == "healthy": if state.get("Health", {}).get("Status") == "healthy":
@@ -64,6 +72,10 @@ def smoke_image(image: str, name: str) -> None:
def main() -> None: def main() -> None:
os.chdir(ROOT) os.chdir(ROOT)
migration = ROOT / ".artifacts/documents-migration.json"
migration.parent.mkdir(exist_ok=True)
migration.unlink(missing_ok=True)
run(sys.executable, "scripts/export-legacy-documents.py", "--output", str(migration))
run(sys.executable, "scripts/summarize-ai-income.py", "--check") run(sys.executable, "scripts/summarize-ai-income.py", "--check")
run(sys.executable, "scripts/audit-ai-research.py", "--check") run(sys.executable, "scripts/audit-ai-research.py", "--check")
run(sys.executable, "-m", "unittest", "discover", "-s", "scripts", "-p", "test_*.py") run(sys.executable, "-m", "unittest", "discover", "-s", "scripts", "-p", "test_*.py")
+16
View File
@@ -7,6 +7,7 @@ import re
import shutil import shutil
import socket import socket
import subprocess import subprocess
import time
import urllib.request import urllib.request
from ci import APP, ROOT, fingerprint, run from ci import APP, ROOT, fingerprint, run
@@ -108,6 +109,21 @@ def main() -> None:
run("docker", "run", "--rm", "--network", "none", "--mount", run("docker", "run", "--rm", "--network", "none", "--mount",
f"type=bind,src={auth_dir},dst=/auth", actual, f"type=bind,src={auth_dir},dst=/auth", actual,
"node", "apps/docs/auth-cli.cjs", "init") "node", "apps/docs/auth-cli.cjs", "init")
# Back up the entire shared database, then bootstrap documents only when empty.
# A normal release never reimports, overwrites or resets authored DB content.
migration = ROOT / ".artifacts/documents-migration.json"
if not migration.is_file():
raise RuntimeError("Verified document migration export is missing")
backup = f"/auth/before-documents-{sha}-{time.time_ns()}.sqlite"
run("docker", "run", "--rm", "--network", "none", "--mount",
f"type=bind,src={auth_dir},dst=/auth", actual,
"node", "apps/docs/docs-cli.cjs", "backup", backup)
subprocess.run(["docker", "run", "--rm", "-i", "--network", "none", "--mount",
f"type=bind,src={auth_dir},dst=/auth", actual,
"node", "apps/docs/docs-cli.cjs", "import", "--if-empty"],
input=migration.read_bytes(), check=True)
shutil.copyfile(ROOT / "scripts/docs-admin.sh", DEPLOY_ROOT / "docs")
(DEPLOY_ROOT / "docs").chmod(0o750)
shutil.copyfile(ROOT / "scripts/auth-admin.sh", DEPLOY_ROOT / "auth") shutil.copyfile(ROOT / "scripts/auth-admin.sh", DEPLOY_ROOT / "auth")
(DEPLOY_ROOT / "auth").chmod(0o750) (DEPLOY_ROOT / "auth").chmod(0o750)
current = DEPLOY_ROOT / "current" current = DEPLOY_ROOT / "current"
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
# Run on ces-control. JSON content travels through stdin, never shell arguments.
set -eu
cd /opt/endorphin-tabularium-docs/current
case "${1:-}" in
list|get|save|history|export|backup|import)
docker compose -p endorphin-tabularium-docs --env-file /etc/endorphin-tabularium-docs/production.env --env-file release.env -f compose.yml exec -T docs node apps/docs/docs-cli.cjs "$@" ;;
*) echo 'Usage: docs list | get <slug> | save < JSON | history <sourceSlug> | export <private-file> | backup <private-file> | import < JSON' >&2; exit 1 ;;
esac
+48
View File
@@ -0,0 +1,48 @@
"""One-time export from an explicit historical Git revision, never a runtime seed."""
import argparse
import json
import os
from pathlib import Path
import re
import subprocess
ROOT = Path(__file__).resolve().parents[1]
BASELINE = '25c5e83d08eae4277fe429a692955bd3bb2e717f' # Last main revision before database-backed documents.
def export(ref):
commit = subprocess.check_output(['git', 'rev-parse', '--verify', ref + '^{commit}'], cwd=ROOT, text=True).strip()
def read(name):
return subprocess.check_output(['git', 'show', f'{commit}:{name}'], cwd=ROOT).decode()
catalog = json.loads(read('content/catalog.json'))
names = subprocess.check_output(['git', 'ls-tree', '-r', '--name-only', commit, '--', 'content/docs'], cwd=ROOT, text=True).splitlines()
documents = []
for name in sorted(n for n in names if n.endswith('.md')):
source = name.removeprefix('content/docs/').removesuffix('.md')
metadata = catalog['documents'][source]
collection = next(c for c in catalog['collections'] if not c['path'] or source.startswith(c['path'] + '/'))
content = read(name)
title = re.search(r'^# (.+)$', content, re.M)
documents.append(dict(slug=metadata['slug'], sourceSlug=source, category=metadata['category'],
title=title[1].strip() if title else source, content=content,
group=metadata.get('group', collection['title']), collection=metadata.get('collection', collection['title']),
description=metadata.get('description', collection['description']),
draft=metadata.get('draft', False), featured=metadata.get('featured', False), revision=1))
if not documents:
raise ValueError('Refusing empty migration')
return dict(format='tabularium-documents-v1', documents=documents)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--ref', default=BASELINE)
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
payload = export(args.ref)
args.output.parent.mkdir(parents=True, exist_ok=True)
descriptor = os.open(args.output, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(descriptor, 'w') as output:
json.dump(payload, output, ensure_ascii=False)
print(f"Exported {len(payload['documents'])} documents; source is immutable Git history")
+9 -3
View File
@@ -9,7 +9,8 @@ import sys
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
DATA = json.loads((ROOT / 'content/research/ai-companies.seed.json').read_text()) DATA = json.loads((ROOT / 'content/research/ai-companies.seed.json').read_text())
REFERENCE = json.loads((ROOT / 'content/research/ai-income-analysis.json').read_text()) REFERENCE = json.loads((ROOT / 'content/research/ai-income-analysis.json').read_text())
TARGET = ROOT / 'content/docs/ai-company-research/01-income-snapshot.md' TARGET = ROOT / '.artifacts/reports/01-income-snapshot.md'
SOURCE_SLUG = 'ai-company-research/01-income-snapshot'
BASE = '/docs/other-research/ai-company-research/overview' BASE = '/docs/other-research/ai-company-research/overview'
COMPANIES = {c['id']: c for c in DATA['companies']} COMPANIES = {c['id']: c for c in DATA['companies']}
CATEGORIES = {c['id']: c['name'] for c in DATA['categories']} CATEGORIES = {c['id']: c['name'] for c in DATA['categories']}
@@ -137,14 +138,19 @@ def render():
def main(): def main():
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--check', action='store_true') parser.add_argument('--check', action='store_true')
parser.add_argument('--document-export', type=Path)
args = parser.parse_args() args = parser.parse_args()
verify() verify()
content = render() content = render()
if args.check: if args.check:
if not TARGET.exists() or TARGET.read_text() != content: if args.document_export is not None:
bundle = json.loads(args.document_export.read_text())
stored = next((d['content'] for d in bundle['documents'] if d['sourceSlug'] == SOURCE_SLUG), None)
if stored != content:
sys.exit('Income snapshot differs; regenerate after reviewing data changes') sys.exit('Income snapshot differs; regenerate after reviewing data changes')
print('Income reference, samples and report are consistent') print('Income reference and samples verified; supplied document export compared when requested')
else: else:
TARGET.parent.mkdir(parents=True, exist_ok=True)
TARGET.write_text(content) TARGET.write_text(content)
print(TARGET.relative_to(ROOT)) print(TARGET.relative_to(ROOT))
+2 -2
View File
@@ -1,4 +1,4 @@
"""Guard cache invalidation against stale documents and needless metadata rebuilds.""" """Guard cache invalidation against stale application code and needless metadata rebuilds."""
from pathlib import Path from pathlib import Path
import subprocess import subprocess
import tempfile import tempfile
@@ -13,7 +13,7 @@ class FingerprintTests(unittest.TestCase):
root = Path(directory) root = Path(directory)
subprocess.run(["git", "init", "-q", str(root)], check=True) subprocess.run(["git", "init", "-q", str(root)], check=True)
(root / ".gitignore").write_text(".artifacts/\n**/.next/\n") (root / ".gitignore").write_text(".artifacts/\n**/.next/\n")
doc = root / "content/docs/concept.md" doc = root / "apps/docs/src/lib/docs.ts"
doc.parent.mkdir(parents=True) doc.parent.mkdir(parents=True)
doc.write_text("# Original\n") doc.write_text("# Original\n")
package = root / "package.json" package = root / "package.json"