diff --git a/.asf.yaml b/.asf.yaml index 453ff07f19..bd40ce071f 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -19,9 +19,8 @@ github: description: >- - Apache Maka (Incubating) is a local-first AI agent workspace. - Model messages, tool calls, tool results, permission decisions, - and termination events are recorded as an append-only log. + Apache Maka (Incubating) is a high-performance agent workspace + that keeps a complete record of everything it did. labels: - ai - ai-agent diff --git a/.github/assets/maka-hero.en.png b/.github/assets/maka-hero.en.png deleted file mode 100644 index af2f69556d..0000000000 Binary files a/.github/assets/maka-hero.en.png and /dev/null differ diff --git a/.github/assets/maka-hero.zh-CN.png b/.github/assets/maka-hero.zh-CN.png deleted file mode 100644 index d1ae67b378..0000000000 Binary files a/.github/assets/maka-hero.zh-CN.png and /dev/null differ diff --git a/.github/assets/readme-hero.en.dark.png b/.github/assets/readme-hero.en.dark.png new file mode 100644 index 0000000000..9b54ddf056 Binary files /dev/null and b/.github/assets/readme-hero.en.dark.png differ diff --git a/.github/assets/readme-hero.en.light.png b/.github/assets/readme-hero.en.light.png new file mode 100644 index 0000000000..d15ce83f35 Binary files /dev/null and b/.github/assets/readme-hero.en.light.png differ diff --git a/.github/assets/readme-hero.json b/.github/assets/readme-hero.json new file mode 100644 index 0000000000..2d1b5f6e53 --- /dev/null +++ b/.github/assets/readme-hero.json @@ -0,0 +1,4 @@ +{ + "en": "confirmed up to here Model says \"I'll rerun the failing test.\" Runs a command Bash · npm test Asks permission leaves the sandbox You approve written to the log Gets the result exit 1 · pruned, kept Edits a file resume.ts Turn ends run completed one turn · seven RuntimeEvents · append-only State(t) = Project(Log[0…t])", + "zh-CN": "到这里已确认 模型说 「我重新跑一下失败的测试。」 执行命令 Bash · npm test 请求权限 超出沙箱 你批准了 已写进日志 拿到结果 exit 1 · 裁剪展示,全量保留 编辑文件 resume.ts 本轮结束 运行完成 一轮交互 · 7 条运行时事件 · 只追加写入 State(t) = Project(Log[0…t])" +} diff --git a/.github/assets/readme-hero.zh-CN.dark.png b/.github/assets/readme-hero.zh-CN.dark.png new file mode 100644 index 0000000000..eb774086b2 Binary files /dev/null and b/.github/assets/readme-hero.zh-CN.dark.png differ diff --git a/.github/assets/readme-hero.zh-CN.light.png b/.github/assets/readme-hero.zh-CN.light.png new file mode 100644 index 0000000000..01a4ef521c Binary files /dev/null and b/.github/assets/readme-hero.zh-CN.light.png differ diff --git a/README.md b/README.md index 7399f1ac12..81c955da3a 100644 --- a/README.md +++ b/README.md @@ -21,87 +21,46 @@ Maka Apache Maka (Incubating) -

Incubating at The Apache Software Foundation

+

Apache Maka (Incubating) is a high-performance agent workspace that keeps a complete record of everything it did.

- GitHub stars - License: Apache 2.0 - macOS Apple Silicon and Intel - Windows unsigned preview - Linux unsigned preview - DeepWiki: third-party AI-generated docs - 中文文档 -

- -

- A local-first Agent workspace built for real work.
- Maka inspects projects, runs tools under a sandbox boundary, and records - model messages and tool calls as recoverable execution facts — on your - machine, through one Runtime Host. + Website · + Documentation · + Download · + 中文文档

- Download Desktop Nightly
- Daily builds from main for developers and testers. Not an ASF release, not intended for production use. + GitHub stars + License: Apache 2.0 + Platforms: macOS, Windows preview, Linux preview

-![Maka — Your work. Your agent.](./.github/assets/maka-hero.en.png) - -> [!NOTE] -> Apache Maka (Incubating) is an effort undergoing incubation at The Apache Software Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is required of all newly accepted projects until a further review indicates that the infrastructure, communications, and decision-making process have stabilized in a manner consistent with other successful ASF projects. While incubation status is not necessarily a reflection of the completeness or stability of the code, it does indicate that the project has yet to be fully endorsed by the ASF. [DISCLAIMER-WIP](./DISCLAIMER-WIP) records the issues the project is currently aware of. - -> [!IMPORTANT] -> Maka is under active development. Data formats, CLI commands, and experimental capabilities may still change. - -## Why Maka - -- **Your machine, your data.** Sessions, settings, and run records stay local by default. You bring the model: a cloud API, a local model, or a compatible gateway. -- **The record is kept.** Model messages, tool calls, tool results, and how a turn ended are written down. The UI and the next model call are views of that record, not the only copy. -- **Shorter context is not deleted history.** Maka can omit old tool output from the next prompt without throwing away the saved evidence. -- **One place runs the agent.** Desktop, the terminal, and Maka evaluation all go through Runtime Host. Eval only owns the experiment and its scores. + + + One turn of RuntimeEvents: the model speaks, runs a command, asks permission, you approve, it gets the result, edits a file, the turn ends. + -Read [Maka Backend Architecture](./ARCHITECTURE.md) for the design. +## What Maka is -## Surfaces +An agent harness exists to finish tasks. We hold it to one measure: how many it completes and at what cost. We publish every run: same model, same official verifier, full per-task record. -| Entry point | Best for | Current capability | -|---|---|---| -| **Desktop** | Daily interaction, file and Artifact workflows, model and permission setup | Electron + React with streaming sessions, tool timelines, branching, search, and recovery | -| **TUI / CLI** | Using Maka in the current project directory or running one non-interactive Turn | `maka`, `maka run`; shares workspace and model connections with Desktop | -| **Eval** | Reproducible benchmark experiments across Maka and external subjects | `maka eval run --out ` | +- **Measured, not claimed.** Maka is benchmarked against other harnesses on the same model with the official verifier, and the per-task results ship with every report in [`docs/eval/`](./docs/eval). +- **The log is the runtime.** Every model message, tool call, permission decision and termination is an append-only RuntimeEvent. The UI, the next prompt and crash recovery are projections of that log, never the only copy. Old tool output can leave the next prompt without leaving the log. +- **Your machine, your model.** Sessions, settings and run records stay local. You bring the model: a cloud API, a local model or a compatible gateway. +- **One Runtime Host.** Desktop, the TUI and CLI, and Eval are thin clients of one execution authority; Eval owns only the experiment and its scores. -## Current capabilities +The [website](https://maka.apache.org/en/) walks through one turn of the log and links the published runs. [ARCHITECTURE.md](./ARCHITECTURE.md) has the system map. -### Agent Runtime +## Get Maka -- Multiple model connections, streaming output, thinking, usage, and clearer provider errors; -- Built-in tools: `Read`, `Write`, `Edit`, `Bash`, `Glob`, `Grep`. Computer Use and catalog skills are optional and not on by default; -- Tools that leave the sandbox must be approved; runs can be aborted; failures are classified; -- A durable execution record, crash recovery, and optional resume of an interrupted turn. +**Apache Releases**: Maka has not made an Apache release yet. When one exists, the signed source archive will be the official release; packages distributed elsewhere are convenience artifacts. See the [downloads page](https://maka.apache.org/en/downloads/) and [`.github/ASF_SOURCE_RELEASE.md`](./.github/ASF_SOURCE_RELEASE.md) for candidate criteria, signing procedures, and verification steps. -### Desktop workspace +**Desktop Nightly**: Built daily from `main` for developers and testers, for macOS on Apple Silicon and Intel, Windows x64 and Linux x64 and arm64; the Windows and Linux builds are unsigned previews. It is not an ASF release and is not intended for production use. The [downloads page](https://maka.apache.org/en/downloads/) has the installers and the platform status. -- Create, archive, search, rename, retry, regenerate, and branch sessions from a Turn; -- Artifact lists and previews, workspace instructions, model settings, and sandbox settings; -- Local memory and web search when configured; -- Chat apps (IM bots) are experimental. See [IM onboarding](./docs/architecture/bot-onboarding-runtime.zh-CN.md). +**Build from source**: To compile and run Desktop, the TUI, or the CLI directly from a source checkout, see the [Build from source](#build-from-source) section below. -### Evaluation - -- Declarative multi-arm experiments expanded into task × repetition × subject cells; -- Immutable per-cell attempts with targeted infrastructure replacement and earliest-valid selection; -- A small result kernel for score, normalized usage, attributable cost, duration, status, failure reason, and artifacts; -- Maka subjects execute only through Runtime Host; external subjects use generic external subject adapters. - -## Quick start - -### Releases and downloads - -Apache Maka has not made an Apache release yet. Everything currently published from this repository or from a package registry was produced before or during incubation, is not an Apache Software Foundation release, and has not been reviewed or voted on by the Incubator PMC. - -Once Apache releases exist, the official release is the source release published by the ASF and approved by the podling PPMC and the Incubator PMC. A package built from that source and distributed elsewhere, for example through a package registry or as a Desktop installer, is a convenience artifact rather than the release itself, and it is valid only when it is built from an approved source release. [`.github/ASF_SOURCE_RELEASE.md`](./.github/ASF_SOURCE_RELEASE.md) holds the candidate contract, signing path, and verification steps. - -[Desktop Nightly](https://github.com/apache/maka/releases) is built daily from `main` for developers and testers. Choose the newest **Maka Desktop Nightly** prerelease; after installation, the app updates automatically on the Nightly channel. It is not an ASF release and is not intended for production use. Desktop currently targets Apple Silicon Macs (`arm64`). Intel Macs and Linux are not supported yet. [Windows](docs/windows-support.md) is an unsigned preview, not a supported release tier. +## Build from source ### Requirements @@ -196,7 +155,7 @@ Experiment → Cells → Attempts → Results Runtime Host executes Maka subjects ``` -Start with [ARCHITECTURE.md](./ARCHITECTURE.md). It provides the system map, code boundaries, problem-oriented reading paths, and six bilingual deep dives. +Start with [ARCHITECTURE.md](./ARCHITECTURE.md). It provides the system map, code boundaries, problem-oriented reading paths, and links to the deep dives under `docs/architecture/`. ## Repository layout @@ -212,9 +171,14 @@ packages/eval/ Experiment cells, attempts, results, and executor/subject packages/computer-use/ Computer-use backend selection, host lifecycle, and protocol adapters packages/cli/ TUI and non-interactive CLI packages/ui/ Shared conversation, Markdown, Artifact, and UI primitives +native/ Rust: the direct-peer addon for Runtime Host and the gitoxide helper +website/ Astro source for maka.apache.org docs/ Architecture, product, security, privacy, and test contracts scripts/ Build hygiene, visual checks, smoke tests, and release helpers +skills/ Agent skills shipped with the repository +patches/ Patches applied to npm dependencies at install +experiments/ Platform experiments, currently the Windows sandbox smoke scripts ``` ## Local data and recovery @@ -276,11 +240,13 @@ Before submitting code, run typecheck, build, and focused tests proportionate to ## Documentation +- [Website](https://maka.apache.org/en/) - [Documentation index and authority map](./docs/README.md) - [Backend architecture](./ARCHITECTURE.md) - [Product design](./DESIGN.md) - [Contributing guide](./CONTRIBUTING.md) - [Security policy](./SECURITY.md) +- [DeepWiki](https://deepwiki.com/apache/maka), third-party AI-generated documentation the project does not maintain ## License @@ -289,3 +255,9 @@ Maka is licensed under the [Apache License 2.0](./LICENSE). See subject to their respective licenses and notices. Apache Maka, Maka, Apache, the Apache feather, and the Apache Maka project logo are either registered trademarks or trademarks of The Apache Software Foundation. + +> [!NOTE] +> Apache Maka (Incubating) is an effort undergoing incubation at The Apache Software Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is required of all newly accepted projects until a further review indicates that the infrastructure, communications, and decision-making process have stabilized in a manner consistent with other successful ASF projects. While incubation status is not necessarily a reflection of the completeness or stability of the code, it does indicate that the project has yet to be fully endorsed by the ASF. [DISCLAIMER-WIP](./DISCLAIMER-WIP) records the issues the project is currently aware of. + +> [!IMPORTANT] +> Maka is under active development. Data formats, CLI commands, and experimental capabilities may still change. diff --git a/README.zh-CN.md b/README.zh-CN.md index 4ec446b5c5..473f9b86ed 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -21,85 +21,46 @@ Maka Apache Maka (Incubating) -

正在 Apache 软件基金会孵化

+

Apache Maka(孵化中)是一个高性能的 Agent 工作台,并完整记录它做过的每一件事。

- GitHub stars - License: Apache 2.0 - macOS Apple Silicon 与 Intel - Windows 未签名预览 - Linux 未签名预览 - DeepWiki:第三方 AI 生成文档 - English -

- -

- 一个为真实工作而生的本地优先 Agent 工作台。
- Maka 在沙箱边界下阅读项目、执行工具,并把模型消息和工具调用保存为可恢复的运行事实——数据在本机,执行走同一个 Runtime Host。 + 官网 · + 文档 · + 下载 · + English

- 下载 Desktop Nightly
- 每天从 main 构建,面向开发者和测试者。不是 ASF release,也不适合生产使用。 + GitHub stars + License: Apache 2.0 + 平台:macOS、Windows 预览、Linux 预览

-![Maka——你的工作,你的 Agent。](./.github/assets/maka-hero.zh-CN.png) - -> [!NOTE] -> Apache Maka (Incubating) 是一个正在 Apache 软件基金会(ASF)孵化的项目,由 Apache Incubator PMC 提供 sponsor。所有新接受的项目都必须经过孵化,直到进一步审查表明其基础设施、沟通方式和决策流程已经稳定到与其他成功的 ASF 项目一致的程度。孵化状态并不必然反映代码的完成度或稳定性,但它确实表明该项目尚未得到 ASF 的完全认可。项目当前已知的问题记录在 [DISCLAIMER-WIP](./DISCLAIMER-WIP)(以英文原文为准)。 - -> [!IMPORTANT] -> Maka 仍在活跃开发中。数据格式、CLI 和实验能力仍可能变化。 - -## 为什么是 Maka - -- **数据在你的机器上。** 会话、设置和运行记录默认保存在本机。模型由你接:云 API、本地模型或兼容网关。 -- **做过的事会留下来。** 模型消息、工具调用、工具结果、这一轮怎么结束,都会记下来。界面和下一次模型请求只是这份记录的视图,不是唯一副本。 -- **缩短上下文不等于删掉历史。** Maka 可以不把旧的工具输出送进下一轮提示,但不会扔掉已保存的证据。 -- **Agent 只在一处跑。** 桌面、终端和 Maka 评测都走 Runtime Host。Eval 只负责实验和分数。 + + + 一轮交互的运行时事件:模型说、执行命令、请求权限、你批准了、拿到结果、编辑文件、本轮结束。 + -完整设计见 [Maka Backend Architecture](./ARCHITECTURE.zh-CN.md)。 +## 什么是 Maka -## 运行形态 +Agent harness 的本职就是把任务做完。衡量它的标准只有一条:完成了多少,花了多少。我们公开每一次运行:同一个模型,同一个官方验证器,逐任务的完整记录。 -| 入口 | 适合什么 | 当前能力 | -|---|---|---| -| **Desktop** | 日常交互、文件与 Artifact 工作流、模型和权限配置 | Electron + React,支持流式会话、工具时间线、分支、搜索和恢复 | -| **TUI / CLI** | 在当前工程目录中使用 Maka,或执行单次非交互 Turn | `maka`、`maka run`,复用 Desktop 的 workspace 和模型连接 | -| **Eval** | Maka 与外部 subject 的可复现实验 | `maka eval run --out ` | +- **靠测量,不靠宣称。** Maka 与其他 harness 在同一个模型、同一个官方验证器下对比跑分,每份报告都附逐任务结果,见 [`docs/eval/`](./docs/eval)。 +- **日志就是运行时。** 每条模型消息、工具调用、权限决定和终止都是一条只追加的 RuntimeEvent。界面、下一轮 prompt 和崩溃恢复都是这份日志的投影,从不是唯一副本。旧的工具输出可以不进下一轮 prompt,但不会从日志里消失。 +- **数据在你的机器上,模型由你接。** 会话、设置和运行记录保存在本机;云 API、本地模型或兼容网关都可以。 +- **一个 Runtime Host。** Desktop、TUI 和 CLI、Eval 都是瘦客户端,执行只由这一个 Runtime Host 说了算;Eval 只负责实验和分数。 -## 当前能力 +[官网](https://maka.apache.org/zh-CN/)演示了日志中的一轮,并链接到公开的运行结果。系统地图见 [ARCHITECTURE.zh-CN.md](./ARCHITECTURE.zh-CN.md)。 -### Agent Runtime +## 获取 Maka -- 多模型连接、流式输出、thinking、用量统计,以及更清楚的 provider 错误; -- 内置工具:`Read`、`Write`、`Edit`、`Bash`、`Glob`、`Grep`。Computer Use 和目录里的 skill 是可选的,默认不开; -- 越出沙箱的工具需要批准;运行可以中止;失败会被分类; -- 有一份可恢复的执行记录,进程崩溃后可以收敛状态,中断的回合可以按需续跑。 +**Apache Releases**:Maka 尚未发布过 Apache release。发布之后,带签名的源码包才是正式 release,其他渠道分发的包属于便利构建。候选版本的准入标准、签名路径与验包步骤见[下载页面](https://maka.apache.org/zh-CN/downloads/)与 [`.github/ASF_SOURCE_RELEASE.md`](./.github/ASF_SOURCE_RELEASE.md)。 -### Desktop Workspace +**Desktop Nightly**:每天从 `main` 构建,面向开发者和测试者,覆盖 macOS 的 Apple Silicon 与 Intel、Windows x64、Linux x64 与 arm64;Windows 和 Linux 构建是未签名预览。它不是 ASF release,不适合生产使用。安装包与平台状态见[下载页面](https://maka.apache.org/zh-CN/downloads/)。 -- 会话创建、归档、搜索、重命名、重试、重新生成和从 Turn 分支; -- Artifact 列表与预览、工作区说明、模型和沙箱设置; -- 配置后可使用本地记忆和联网搜索; -- 聊天应用(IM bot)仍是实验能力,见 [IM 接入](./docs/architecture/bot-onboarding-runtime.zh-CN.md)。 +**从源码构建**:要从源码 checkout 直接构建并运行 Desktop、TUI 或 CLI,见下方的[从源码构建](#从源码构建)一节。 -### Evaluation - -- 声明式多臂 Experiment 展开为 task × repetition × subject cell; -- 每个 cell 使用 immutable attempt,基础设施失败只替换该 cell,并选择最早有效 attempt; -- 通用结果只包含 score、normalized usage、可归因 cost、duration、status/failure reason 与 artifacts; -- Maka subject 只通过 Runtime Host 执行,外部 subject 使用 generic external subject adapter。 - -## 快速开始 - -### Release 与下载 - -Apache Maka 目前还没有发布过 Apache release。当前从本仓库或包管理器分发的一切内容,都是在进入孵化器之前或孵化期间产生的,不是 Apache 软件基金会的 release,也没有经过 Incubator PMC 审查和投票。 - -在 Apache release 出现之后,官方 release 指的是由 ASF 发布、并经 podling PPMC 和 Incubator PMC 批准的源码 release。由该源码构建并通过其他渠道分发的包,例如包管理器中的包或 Desktop 安装程序,属于 convenience artifact,本身不是 release,并且只有在由获批源码 release 构建时才有效。候选契约、签名路径和验包步骤见 [`.github/ASF_SOURCE_RELEASE.md`](./.github/ASF_SOURCE_RELEASE.md)。 - -[Desktop Nightly](https://github.com/apache/maka/releases) 面向开发者和测试者,每天从 `main` 构建。请选择最新的 **Maka Desktop Nightly** prerelease;安装后,应用会在 Nightly 渠道自动更新。它不是 ASF release,不适合生产使用。Desktop 目前面向 Apple Silicon Mac(`arm64`)。暂不支持 Intel Mac 和 Linux。[Windows](docs/windows-support.md) 是未签名预览,不是正式支持的发布层级。 +## 从源码构建 ### 环境要求 @@ -193,7 +154,7 @@ Experiment → Cells → Attempts → Results Runtime Host 执行 Maka subjects ``` -从 [ARCHITECTURE.zh-CN.md](./ARCHITECTURE.zh-CN.md) 开始阅读。它提供总体架构图、代码边界、按问题组织的阅读路径,以及六篇中英双语深度文章。 +从 [ARCHITECTURE.zh-CN.md](./ARCHITECTURE.zh-CN.md) 开始阅读。它提供总体架构图、代码边界、按问题组织的阅读路径,以及 `docs/architecture/` 下深度文章的链接。 ## 仓库结构 @@ -209,9 +170,14 @@ packages/eval/ Experiment cell、attempt、result 与 executor/subject a packages/computer-use/ Computer Use 后端选择、Host 生命周期和协议适配 packages/cli/ TUI 和非交互 CLI packages/ui/ 共享对话、Markdown、Artifact 与 UI primitives +native/ Rust:Runtime Host 的 direct-peer addon 与 gitoxide helper +website/ maka.apache.org 的 Astro 源码 docs/ 架构、产品、安全、隐私和测试契约 scripts/ Build hygiene、视觉检查、smoke 和 release helpers +skills/ 随仓库分发的 agent skill +patches/ 安装时应用到 npm 依赖的补丁 +experiments/ 平台实验,目前是 Windows 沙箱 smoke 脚本 ``` ## 本地数据与恢复 @@ -227,10 +193,10 @@ Workspace 数据默认放在 Electron `userData` 下: artifacts/ ``` -- API key 一类的秘密是本地明文文件(`credential-vault.json`),只有你的系统账号能读。界面进程拿不到明文。 +- API key 一类的机密存在本地明文文件(`credential-vault.json`),只有你的系统账号能读。界面进程拿不到明文。 - 写文件、跑 Shell 的工具必须先过沙箱边界。 -- `runtime.sqlite` 是当前活记录。更早的 JSONL transcript 和 Electron `safeStorage` 凭据不会导入;升级后会话可能是空的,那些凭据需要重新填写。 -- 中断回合的续跑默认关闭。只有设置 `MAKA_RUNTIME_SAFE_BOUNDARY_RESUME=1` 才会打开 Desktop **安全恢复**、CLI `/resume` 和启动时自动续跑——这些路径会打模型、消耗 token。 +- `runtime.sqlite` 是当前生效的那份记录。更早的 JSONL transcript 和 Electron `safeStorage` 凭据不会导入;升级后会话可能是空的,那些凭据需要重新填写。 +- 中断回合的续跑默认关闭。只有设置 `MAKA_RUNTIME_SAFE_BOUNDARY_RESUME=1` 才会打开 Desktop **安全恢复**、CLI `/resume` 和启动时自动续跑——这些路径会真的请求模型、消耗 token。 细节见 [SECURITY.md](./SECURITY.md)、[隐私](./docs/workspace-privacy-context.md)、[续跑](./docs/architecture/runtime-resume-architecture.zh-CN.md)。 @@ -273,11 +239,13 @@ npm --workspace @maka/desktop run smoke:real-window ## 文档入口 +- [官网](https://maka.apache.org/zh-CN/) - [文档索引与权威来源说明](./docs/README.md) - [后端架构总览](./ARCHITECTURE.zh-CN.md) - [产品设计](./DESIGN.md) - [贡献指南](./CONTRIBUTING.zh-CN.md) - [安全政策](./SECURITY.md) +- [DeepWiki](https://deepwiki.com/apache/maka),第三方 AI 生成文档,不由项目维护 ## 开源协议 @@ -285,3 +253,9 @@ Maka 使用 [Apache License 2.0](./LICENSE) 开源,归属信息见 [NOTICE](./NOTICE)。第三方组件仍分别适用其自身的许可证与声明。 Apache Maka、Maka、Apache、Apache 羽毛标志和 Apache Maka 项目标志是 Apache 软件基金会的注册商标或商标。 + +> [!NOTE] +> Apache Maka (Incubating) 是一个正在 Apache 软件基金会(ASF)孵化的项目,由 Apache Incubator PMC 主办。所有新接受的项目都必须经过孵化,直到进一步审查表明其基础设施、沟通方式和决策流程已经稳定到与其他成功的 ASF 项目一致的程度。孵化状态未必反映代码的完成度或稳定性,但它确实表明该项目尚未得到 ASF 的完全认可。项目当前已知的问题记录在 [DISCLAIMER-WIP](./DISCLAIMER-WIP)(以英文原文为准)。 + +> [!IMPORTANT] +> Maka 仍在活跃开发中。数据格式、CLI 和实验能力仍可能变化。 diff --git a/docs/code-origin-audit.md b/docs/code-origin-audit.md index 60a3ad1147..973fa43641 100644 --- a/docs/code-origin-audit.md +++ b/docs/code-origin-audit.md @@ -220,13 +220,13 @@ The exact boundary, locally authored divergences, and artifact reproducibility l A maintainer confirmed that the following assets were AI-generated and that no third-party image, logo, or artwork was uploaded as input: -- `.github/assets/maka-hero.en.png` -- `.github/assets/maka-hero.zh-CN.png` - `apps/desktop/assets/icon.png` - `apps/desktop/resources/status/cu-status.png` - `apps/desktop/resources/status/cu-status@2x.png` -The first three used ChatGPT Image. The status PNGs were exported from an AI-generated SVG. Exact prompts were not retained and may have requested a visual style reference, so this confirmation is provenance evidence rather than a guarantee that no style or IP concern exists. +`icon.png` used ChatGPT Image, as did the former README heroes `.github/assets/maka-hero.en.png` and `maka-hero.zh-CN.png`, removed on 2026-09-04. The status PNGs were exported from an AI-generated SVG. Exact prompts were not retained and may have requested a visual style reference, so this confirmation is provenance evidence rather than a guarantee that no style or IP concern exists. + +The README heroes now in `.github/assets/readme-hero.*.png` are not generated art: `website/scripts/readme-hero.mjs` renders them from the built website, so they carry only the site's own copy, styles and fonts, whose provenance is recorded with `website/`. ### Source archive non-text inventory @@ -234,7 +234,7 @@ The ASF source verifier reads this inventory from the candidate itself. A non-text image must match one of these paths; executable and archive magic is rejected even if a path is listed here. -- `.github/assets/*.png`: the AI-generated hero images recorded above. +- `.github/assets/*.png`: the README hero images rendered from the website, recorded above. - `apps/desktop/assets/icon.png`: the AI-generated application mark recorded above. - `apps/desktop/assets/app-icons/*.png`: `mono.png` is the contributor-submitted grayscale derivative of the application mark from pull request #3431; the remaining variants are reproducibly rendered from the Apache-licensed geometry and palette in `scripts/generate-app-icons.py` and byte-checked by `scripts/generate-app-icons.test.mjs`. - `apps/desktop/build/*.png`: contributor-submitted DMG artwork from pull request #3817; that contribution records Codex as review and verification assistance, not as the source of the artwork. diff --git a/package-lock.json b/package-lock.json index cbbd7c34b8..096a003ab3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17094,6 +17094,7 @@ "devDependencies": { "@fontsource-variable/geist": "^5.3.0", "@fontsource-variable/geist-mono": "^5.3.0", + "@playwright/test": "^1.62.1", "astro": "^7.2.10" } }, diff --git a/scripts/biome-staged-check.mjs b/scripts/biome-staged-check.mjs index 96c222882a..cf51bbbc40 100644 --- a/scripts/biome-staged-check.mjs +++ b/scripts/biome-staged-check.mjs @@ -31,9 +31,48 @@ const defaultBiomePath = join( process.platform === 'win32' ? 'biome.cmd' : 'biome', ); +const maxBlobBytes = 16 * 1024 * 1024; + +const decodes = (buffer) => { + try { + new TextDecoder('utf-8', { fatal: true }).decode(buffer); + return true; + } catch { + return false; + } +}; + +// git's own text/binary rule, asked for once and only when something is too +// large to read: it decides which oversized blob is an asset and which is a +// file the formatter would have owned. +function binaryStagedPaths(root) { + const records = execFileSync( + 'git', + ['diff', '--cached', '--numstat', '--diff-filter=ACMR', '-z'], + { cwd: root }, + ) + .toString('utf8') + .split('\0'); + const binary = new Set(); + for (let index = 0; index < records.length; index += 1) { + const record = records[index]; + if (!record) continue; + const [added, deleted, ...rest] = record.split('\t'); + // A rename writes its counts alone, then the old and the new path. + let path = rest.join('\t'); + if (path === '') { + index += 2; + path = records[index]; + } + if (added === '-' && deleted === '-') binary.add(path); + } + return binary; +} + export function checkStagedWithBiome({ root = defaultRepoRoot, biomePath = defaultBiomePath, + maxBytes = maxBlobBytes, } = {}) { const output = execFileSync( 'git', @@ -42,8 +81,34 @@ export function checkStagedWithBiome({ ); const paths = output.toString('utf8').split('\0').filter(Boolean); + let binary; for (const path of paths) { - const contents = execFileSync('git', ['show', `:${path}`], { cwd: root }); + // Every staged blob passes through here, images included, so ask for the + // size before reading it: Node's default 1 MiB buffer used to kill the + // commit for anything larger. An asset over the ceiling is left alone, but + // a text file over it is one the formatter owns and this cannot read, so + // it stops the commit rather than passing silently. + const size = Number( + execFileSync('git', ['cat-file', '-s', `:${path}`], { cwd: root }) + .toString('utf8') + .trim(), + ); + if (size > maxBytes) { + binary ??= binaryStagedPaths(root); + if (binary.has(path)) continue; + process.stderr.write( + `${path}: staged text is ${size} bytes, over the ${maxBytes} this check reads\n`, + ); + return false; + } + const contents = execFileSync('git', ['show', `:${path}`], { + cwd: root, + maxBuffer: maxBytes, + }); + // Biome reads stdin as UTF-8 and errors on anything else, so skip a blob + // it cannot decode. Deciding on that rather than on a NUL byte keeps a + // source file that legitimately contains one inside the check. + if (!decodes(contents)) continue; const result = spawnSync( biomePath, [ @@ -53,7 +118,8 @@ export function checkStagedWithBiome({ '--files-ignore-unknown=true', '--no-errors-on-unmatched', ], - { cwd: root, input: contents }, + // Biome echoes the file back, with room for a rewrite that grows it. + { cwd: root, input: contents, maxBuffer: 2 * maxBytes }, ); if (result.error) throw result.error; if (result.status !== 0) { @@ -61,6 +127,9 @@ export function checkStagedWithBiome({ if (result.stderr.length > 0) process.stderr.write(result.stderr); return false; } + // Biome echoes the input for files it formats or ignores, but prints + // nothing for a language it parses without formatting, such as Markdown. + if (result.stdout.length === 0) continue; if (!result.stdout.equals(contents)) { process.stderr.write(`${path}: staged content is not formatted by Biome\n`); return false; diff --git a/scripts/biome-staged-check.test.mjs b/scripts/biome-staged-check.test.mjs index 9c53d6e98e..283fc81360 100644 --- a/scripts/biome-staged-check.test.mjs +++ b/scripts/biome-staged-check.test.mjs @@ -58,6 +58,98 @@ test('checks staged bytes when the working tree was formatted afterward', () => } }); +test('skips a staged binary larger than the default child-process buffer', () => { + const root = fixture(); + try { + const binary = Buffer.alloc(2 * 1024 * 1024); + binary.write('\x89PNG\r\n\x1a\n', 'latin1'); + binary.fill(0xff, 64, 96); + writeFileSync(join(root, 'large.png'), binary); + execFileSync('git', ['add', 'large.png'], { cwd: root }); + + assert.equal(checkStagedWithBiome({ root, biomePath }), true); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test('checks a staged source file that holds a NUL byte', () => { + const root = fixture(); + try { + writeFileSync(join(root, 'valid.js'), Buffer.from('const value={answer:"\0"};\n', 'utf8')); + execFileSync('git', ['add', 'valid.js'], { cwd: root }); + + assert.equal(checkStagedWithBiome({ root, biomePath }), false); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test('checks a staged source file larger than the default child-process buffer', () => { + const root = fixture(); + try { + const padding = `// ${'a'.repeat(2 * 1024 * 1024)}\n`; + writeFileSync(join(root, 'large.js'), `${padding}const value={answer:42};\n`); + execFileSync('git', ['add', 'large.js'], { cwd: root }); + + assert.equal(checkStagedWithBiome({ root, biomePath }), false); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test('refuses a staged source file over the ceiling instead of skipping it', () => { + const root = fixture(); + try { + // Formatted, so only the ceiling can reject it. + writeFileSync(join(root, 'huge.js'), `// ${'a'.repeat(2048)}\nconst value = { answer: 42 };\n`); + execFileSync('git', ['add', 'huge.js'], { cwd: root }); + + assert.equal(checkStagedWithBiome({ root, biomePath, maxBytes: 1024 }), false); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test('skips a staged binary over the ceiling', () => { + const root = fixture(); + try { + const binary = Buffer.alloc(2048); + binary.write('\x89PNG\r\n\x1a\n', 'latin1'); + writeFileSync(join(root, 'huge.png'), binary); + execFileSync('git', ['add', 'huge.png'], { cwd: root }); + + assert.equal(checkStagedWithBiome({ root, biomePath, maxBytes: 1024 }), true); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test('skips a small staged binary Biome cannot decode', () => { + const root = fixture(); + try { + const binary = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0xff, 0xd8, 0xff]); + writeFileSync(join(root, 'icon.png'), binary); + execFileSync('git', ['add', 'icon.png'], { cwd: root }); + + assert.equal(checkStagedWithBiome({ root, biomePath }), true); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test('passes a language Biome parses but does not format', () => { + const root = fixture(); + try { + writeFileSync(join(root, 'notes.md'), '# Notes\n\n* loosely formatted\n'); + execFileSync('git', ['add', 'notes.md'], { cwd: root }); + + assert.equal(checkStagedWithBiome({ root, biomePath }), true); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + test('ignores unstaged formatting drift when staged bytes are formatted', () => { const root = fixture(); try { diff --git a/scripts/ci-test-plan.mjs b/scripts/ci-test-plan.mjs index a1acf8d6fe..ca8e4427d1 100644 --- a/scripts/ci-test-plan.mjs +++ b/scripts/ci-test-plan.mjs @@ -197,6 +197,8 @@ function isReleaseContractPath(path) { const DEDICATED_WORKSPACE_LANES = new Set(['packages/runtime-host']); +const SITE_SENTENCE_FILES = new Set(['README.md', 'README.zh-CN.md']); + // Scripts the Electron e2e job runs. Editing one of these changes what that // job verifies, so it has to re-run — a unit test on the runner is not // evidence that the run it drives still works. @@ -431,6 +433,14 @@ export function planTests(changedFiles, options = {}) { let code = false; let unknownCode = false; for (const path of files) { + // The READMEs must open with the sentence the website uses, and the + // website's test is what checks that, so a README change runs that + // workspace even though it is documentation. + if (SITE_SENTENCE_FILES.has(path)) { + code = true; + directWorkspaces.add('website'); + continue; + } // Documentation can live inside a workspace. Classify it before generic // workspace and product-surface membership; dedicated legal, release, and // generated-authority gates still inspect the complete file list below. diff --git a/scripts/ci-test-plan.test.mjs b/scripts/ci-test-plan.test.mjs index 8bb342981c..15af9bfc7d 100644 --- a/scripts/ci-test-plan.test.mjs +++ b/scripts/ci-test-plan.test.mjs @@ -55,6 +55,7 @@ const dirs = [ 'packages/cli', 'packages/ui', 'apps/desktop', + 'website', ]; const graph = { @@ -67,6 +68,7 @@ const graph = { ['packages/cli', new Set()], ['packages/ui', new Set(['apps/desktop'])], ['apps/desktop', new Set()], + ['website', new Set()], ]), testDirs: new Set(dirs), }; @@ -87,6 +89,15 @@ test('documentation inside workspaces selects nothing at all', () => { } }); +test('the READMEs run the website tests that check their opening sentence', () => { + for (const path of ['README.md', 'README.zh-CN.md']) { + const plan = planTests([path], { graph }); + + assert.equal(plan.code, true, path); + assert.deepEqual(plan.workspaces, ['website'], path); + } +}); + test('mixed documentation and code changes still select code validation', () => { const plan = planTests(['README.md', 'packages/core/src/index.ts'], { graph }); diff --git a/website/package.json b/website/package.json index 34eca22376..c46764295d 100644 --- a/website/package.json +++ b/website/package.json @@ -7,9 +7,11 @@ "dev": "astro dev", "build": "astro build", "preview": "astro preview", - "test:dist": "astro build && node --test test/site.test.mjs" + "test:dist": "astro build && node --test test/site.test.mjs", + "readme-hero": "astro build && node scripts/readme-hero.mjs" }, "devDependencies": { + "@playwright/test": "^1.62.1", "@fontsource-variable/geist": "^5.3.0", "@fontsource-variable/geist-mono": "^5.3.0", "astro": "^7.2.10" diff --git a/website/scripts/hero-text.mjs b/website/scripts/hero-text.mjs new file mode 100644 index 0000000000..c4fc6f9c8d --- /dev/null +++ b/website/scripts/hero-text.mjs @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * The text a README hero bakes in: the hero header of a built page, without + * the parts the render hides. The render records it beside the images and the + * site test recomputes it, so committed heroes cannot outlive their copy. + */ +const hidden = + /

.*?<\/h1>|

.*?<\/p>|

.*?<\/div>|
.*?<\/div>/gsu; + +const entities = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': "'", + ' ': ' ', +}; + +export function heroText(html) { + const start = html.indexOf('
'); + if (start === -1) throw new Error('no hero header in the page'); + const end = html.indexOf('
', start); + return html + .slice(start, end) + .replace(hidden, ' ') + .replace(/<[^>]+>/gu, ' ') + .replace(/&[a-z#0-9]+;/gu, (entity) => entities[entity] ?? entity) + .replace(/\s+/gu, ' ') + .trim(); +} diff --git a/website/scripts/readme-hero.mjs b/website/scripts/readme-hero.mjs new file mode 100644 index 0000000000..5407fbe5b6 --- /dev/null +++ b/website/scripts/readme-hero.mjs @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Renders the README hero images from the built site, so the README shows + * the same headline and RuntimeEvents scene as maka.apache.org. Run + * `npm --workspace @maka/website run readme-hero` after changing the hero + * copy or styles and commit the PNGs it writes to `.github/assets/`. + */ +import { execFileSync } from 'node:child_process'; +import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs'; +import http from 'node:http'; +import { extname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { chromium } from '@playwright/test'; + +import { heroText } from './hero-text.mjs'; + +const dist = fileURLToPath(new URL('../dist/', import.meta.url)); +const assets = fileURLToPath(new URL('../../.github/assets/', import.meta.url)); +const types = { + '.html': 'text/html', + '.css': 'text/css', + '.js': 'text/javascript', + '.woff2': 'font/woff2', + '.png': 'image/png', + '.svg': 'image/svg+xml', +}; + +// The built pages reference /_astro/... absolutely, so serve dist over HTTP. +const server = http.createServer((request, response) => { + let path = join(dist, decodeURIComponent(new URL(request.url, 'http://x').pathname)); + if (existsSync(path) && statSync(path).isDirectory()) path = join(path, 'index.html'); + if (!existsSync(path)) { + response.writeHead(404); + response.end(); + return; + } + response.writeHead(200, { 'content-type': types[extname(path)] ?? 'application/octet-stream' }); + response.end(readFileSync(path)); +}); +await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); +const origin = `http://127.0.0.1:${server.address().port}`; + +// The scene only: the README carries the headline, the lede and the links as +// its own text, and reduced motion shows every event of the turn at once. +const readmeOnly = ` + .display, .cta, .fine, .lede { display: none !important; } + .hero { padding-top: 32px !important; padding-bottom: 32px !important; } + .scene { margin-top: 0 !important; } +`; + +// npm ci installs the Playwright package but not a browser, so a clean +// checkout has to be able to fetch one before this command can run. +const executable = (() => { + try { + return chromium.executablePath(); + } catch { + return undefined; + } +})(); +if (!executable || !existsSync(executable)) { + execFileSync( + process.platform === 'win32' ? 'npx.cmd' : 'npx', + ['playwright', 'install', 'chromium'], + { + stdio: 'inherit', + }, + ); +} + +const manifest = {}; +const browser = await chromium.launch(); +try { + for (const locale of ['en', 'zh-CN']) { + manifest[locale] = heroText(readFileSync(join(dist, locale, 'index.html'), 'utf8')); + for (const colorScheme of ['light', 'dark']) { + const page = await browser.newPage({ + viewport: { width: 1600, height: 1000 }, + deviceScaleFactor: 2, + colorScheme, + reducedMotion: 'reduce', + }); + await page.goto(`${origin}/${locale}/`); + await page.addStyleTag({ content: readmeOnly }); + await page.evaluate(() => document.fonts.ready); + const path = join(assets, `readme-hero.${locale}.${colorScheme}.png`); + await page.locator('.hero').screenshot({ path }); + console.log(path); + await page.close(); + } + } + // The copy these images were made from, so the site test can tell when the + // pages have moved on and the committed images have not. + const path = join(assets, 'readme-hero.json'); + writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\n`); + console.log(path); +} finally { + await browser.close(); + server.close(); +} diff --git a/website/src/copy/en.ts b/website/src/copy/en.ts index d017a11cb3..aa5d17b508 100644 --- a/website/src/copy/en.ts +++ b/website/src/copy/en.ts @@ -19,10 +19,11 @@ import type { Copy } from './types'; -// The Incubator disclaimer is quoted verbatim from the Incubator branding -// guide and appears in English on every page, whatever the page language. +// The disclaimer is the first paragraph of DISCLAIMER-WIP word for word, so +// the site, the READMEs and the release file all state it the same way. It +// appears in English on every page, whatever the page language. export const incubatorDisclaimer = - 'Apache Maka is an effort undergoing incubation at The Apache Software Foundation (ASF), sponsored by the Apache Incubator. Incubation is required of all newly accepted projects until a further review indicates that the infrastructure, communications, and decision making process have stabilized in a manner consistent with other successful ASF projects. While incubation status is not necessarily a reflection of the completeness or stability of the code, it does indicate that the project has yet to be fully endorsed by the ASF.'; + 'Apache Maka is an effort undergoing incubation at The Apache Software Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is required of all newly accepted projects until a further review indicates that the infrastructure, communications, and decision-making process have stabilized in a manner consistent with other successful ASF projects. While incubation status is not necessarily a reflection of the completeness or stability of the code, it does indicate that the project has yet to be fully endorsed by the ASF.'; export const en: Copy = { locale: 'en', @@ -117,7 +118,7 @@ export const en: Copy = { p: 'Three paths, kept separate on purpose.', nightly: { title: 'Try Desktop Nightly', - body: 'Daily builds from main for developers and testers, published on GitHub Releases. Apple Silicon Macs now; Windows is an unsigned preview.', + body: 'Daily builds from main for developers and testers, published on GitHub Releases. macOS on Apple Silicon and Intel; Windows and Linux are unsigned previews.', note: 'NOT AN ASF RELEASE · MAY BE UNSTABLE', }, source: { @@ -188,7 +189,8 @@ export const en: Copy = { }, nightly: { label: 'Desktop Nightly', - value: 'Daily from main, Apple Silicon Macs, Windows unsigned preview.', + value: + 'Daily from main. macOS arm64 and x64; Windows x64 and Linux x64 and arm64 as unsigned previews.', note: 'NOT AN ASF RELEASE', }, source: { @@ -213,8 +215,8 @@ export const en: Copy = { nightly: { h2: 'Desktop Nightly', note: 'NOT AN ASF RELEASE', - p: 'Desktop Nightly is built daily from main for developers and testers and published as a GitHub prerelease. Choose the newest Maka Desktop Nightly; after installation the app updates itself on the Nightly channel. It is not an ASF release and is not intended for production use. It targets Apple Silicon Macs.', - windows: 'Windows is an unsigned preview, not a supported release tier.', + p: 'Desktop Nightly is built daily from main for developers and testers and published as a GitHub prerelease. Choose the newest Maka Desktop Nightly; after installation the app updates itself on the Nightly channel. It is not an ASF release and is not intended for production use. It ships for macOS on Apple Silicon and Intel, Windows x64, and Linux x64 and arm64.', + windows: 'The Windows and Linux builds are unsigned previews, not a supported release tier.', }, source: { h2: 'Build from source', diff --git a/website/src/copy/zh-CN.ts b/website/src/copy/zh-CN.ts index 9c1cbc54b6..6773ce82db 100644 --- a/website/src/copy/zh-CN.ts +++ b/website/src/copy/zh-CN.ts @@ -98,7 +98,7 @@ export const zhCN: Copy = { p: '三条路径,边界分明。', nightly: { title: '体验 Desktop Nightly', - body: '每天基于 main 构建,面向开发者和测试者,发布在 GitHub Releases。目前支持 Apple Silicon Mac;Windows 是未签名预览。', + body: '每天基于 main 构建,面向开发者和测试者,发布在 GitHub Releases。macOS 覆盖 Apple Silicon 与 Intel;Windows 和 Linux 是未签名预览。', note: '不是 ASF RELEASE · 可能不稳定', }, source: { @@ -168,7 +168,8 @@ export const zhCN: Copy = { }, nightly: { label: 'Desktop Nightly', - value: '每天基于 main 构建,支持 Apple Silicon Mac,Windows 为未签名预览。', + value: + '每天基于 main 构建。macOS arm64 与 x64;Windows x64、Linux x64 与 arm64 为未签名预览。', note: '不是 ASF RELEASE', }, source: { @@ -193,8 +194,8 @@ export const zhCN: Copy = { nightly: { h2: 'Desktop Nightly', note: '不是 ASF RELEASE', - p: 'Desktop Nightly 每天基于 main 构建,面向开发者和测试者,以 GitHub prerelease 形式发布。选择最新的 Maka Desktop Nightly;安装后应用会在 Nightly 渠道自动更新。它不是 ASF release,不适合生产环境。目前仅支持 Apple Silicon Mac。', - windows: 'Windows 是未签名预览,不属于受支持的发布层级。', + p: 'Desktop Nightly 每天基于 main 构建,面向开发者和测试者,以 GitHub prerelease 形式发布。选择最新的 Maka Desktop Nightly;安装后应用会在 Nightly 渠道自动更新。它不是 ASF release,不适合生产环境。提供 macOS 的 Apple Silicon 与 Intel、Windows x64、Linux x64 与 arm64 构建。', + windows: 'Windows 和 Linux 构建是未签名预览,不属于受支持的发布层级。', }, source: { h2: '从源码构建', diff --git a/website/test/site.test.mjs b/website/test/site.test.mjs index 210b16e472..b9c6a8bd26 100644 --- a/website/test/site.test.mjs +++ b/website/test/site.test.mjs @@ -25,6 +25,8 @@ import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import test from 'node:test'; +import { heroText } from '../scripts/hero-text.mjs'; + const dist = new URL('../dist/', import.meta.url); const page = (path) => readFileSync(new URL(path, dist), 'utf8'); const locales = ['en', 'zh-CN']; @@ -32,8 +34,12 @@ const pages = ['index.html', 'downloads/index.html']; const positioning = 'Apache Maka (Incubating) is a high-performance agent workspace that keeps a complete record of everything it did.'; -const disclaimer = - 'Apache Maka is an effort undergoing incubation at The Apache Software Foundation (ASF), sponsored by the Apache Incubator.'; +// DISCLAIMER-WIP is the project's copy of the Incubator disclaimer, so the +// site has to carry that paragraph rather than a wording of its own. +const disclaimer = readFileSync(new URL('../../DISCLAIMER-WIP', import.meta.url), 'utf8') + .split('\n\n')[0] + .replace(/\s+/gu, ' ') + .trim(); // The copyright and trademark line stays in English on every page, like the disclaimer. const trademark = 'Copyright © 2026 The Apache Software Foundation, licensed under the Apache License, Version 2.0. Apache Maka, Apache Incubator, Apache and the Apache feather logo are trademarks of The Apache Software Foundation.'; @@ -103,6 +109,38 @@ test('the English homepage uses the positioning sentence unchanged', () => { assert.ok(page('en/index.html').includes(positioning)); }); +// #4307 settled one sentence for the homepage, the READMEs and the repository +// description. The description is a folded YAML scalar, so compare it unfolded. +test('the READMEs and the repository description open with the same sentence', () => { + const root = new URL('../../', import.meta.url); + const read = (path) => readFileSync(new URL(path, root), 'utf8'); + assert.ok(read('README.md').includes(positioning), 'README.md'); + assert.ok( + read('README.zh-CN.md').includes( + 'Apache Maka(孵化中)是一个高性能的 Agent 工作台,并完整记录它做过的每一件事。', + ), + 'README.zh-CN.md', + ); + const [, description] = read('.asf.yaml').match(/description: >-\n((?: {4}.*\n)+)/u); + assert.equal(description.replace(/\s+/gu, ' ').trim(), positioning); +}); + +// The README heroes are screenshots of these pages, so the copy the render +// baked in has to be the copy the pages carry now. Compare through the +// manifest the render writes, which needs no browser and no pixels. +test('the committed README heroes were rendered from the current hero copy', () => { + const manifest = JSON.parse( + readFileSync(new URL('../../.github/assets/readme-hero.json', import.meta.url), 'utf8'), + ); + for (const locale of locales) { + assert.equal( + heroText(page(`${locale}/index.html`)), + manifest[locale], + `${locale}: run \`npm --workspace @maka/website run readme-hero\` and commit the images`, + ); + } +}); + test('both languages link the same documents', () => { for (const path of pages) { const [en, zh] = locales.map((locale) =>