diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 12ee183..06c358d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -8,13 +8,11 @@ Use Node.js 18 or newer. Fork the repository on GitHub, work in a focused branch
```sh
npm ci
-npm run typecheck
-npm run check:build
-npm test
-npm run validate:examples
-node scripts/check-docs.mjs
+npm run check:pr
```
+`check:pr` is the single pre-PR checklist. It runs type checking, generated-artifact checks, unit tests, browser tests, tracked-demo regeneration and diff verification, example validation, and documentation checks. Run it after editing and before committing; do not skip browser or generated-demo checks for viewer changes.
+
For viewer or renderer changes, run `npm run build:demo` and review the tracked demo diff. Verify Chinese and English, desktop and mobile, and the affected interactions. Run `npx playwright install chromium` and `npm run test:browser` for real-browser checks; see the [release checklist](docs/releasing.md). Add regression coverage for behavior changes; describe checks actually run and any remaining limitations in the PR template. Never include private source data or credentials.
CI checks Windows and Linux on Node.js 18 and 24, validates documentation and examples, verifies the tracked demo matches renderer output, and audits clean source archive installations. A Linux Node.js 24 job runs the Chromium viewer and website suites. Contributions are distributed under the repository's [MIT License](LICENSE); preserve [third-party notices](THIRD_PARTY_NOTICES).
diff --git a/CONTRIBUTING.zh.md b/CONTRIBUTING.zh.md
index 532553f..0a40826 100644
--- a/CONTRIBUTING.zh.md
+++ b/CONTRIBUTING.zh.md
@@ -8,13 +8,11 @@
```sh
npm ci
-npm run typecheck
-npm run check:build
-npm test
-npm run validate:examples
-node scripts/check-docs.mjs
+npm run check:pr
```
+`check:pr` 是统一的 PR 提交前清单,会依次运行类型检查、生成产物检查、单元测试、浏览器测试、已跟踪演示重新生成与差异检查、示例校验和文档检查。编辑后、提交前运行它;修改查看器时不能省略浏览器或演示页检查。
+
修改查看器或渲染器时运行 `npm run build:demo` 并审阅已跟踪演示文件的 diff。验证中英文、桌面和移动端及受影响的交互。运行 `npx playwright install chromium` 和 `npm run test:browser` 进行真实浏览器检查,详见[发布检查清单](docs/releasing.zh.md)。行为改动应补充回归覆盖,在 PR 模板中说明实际运行的检查和剩余限制。不要包含私有源码数据或凭据。
CI 在 Windows 和 Linux 上使用 Node.js 18、24 检查,校验文档和示例,验证已跟踪演示与渲染器输出一致,并审计干净源码归档安装。Linux Node.js 24 任务运行 Chromium 查看器与网站测试。贡献内容按仓库的 [MIT 许可证](LICENSE) 分发;保留[第三方声明](THIRD_PARTY_NOTICES)。
diff --git a/README.md b/README.md
index 5b0236f..ae2b488 100644
--- a/README.md
+++ b/README.md
@@ -157,7 +157,7 @@ node scripts/render.mjs .birdview/architecture.json .birdview/activity.html .bir
To include an already collected and reviewed constraint catalog:
```sh
-node scripts/render.mjs .birdview/architecture.json .birdview/project.html --constraints .birdview/constraints.reviewed.json
+node scripts/birdview.mjs deliver .birdview/architecture.json .birdview/project.html --constraints .birdview/constraints.reviewed.json
```
The CLI writes the integrated page and a companion `project.sources.html` export. For source discovery, rule review and standalone constraint rendering, see the [constraint workflow](references/constraint-graph.md).
diff --git a/README.zh.md b/README.zh.md
index 66b98db..afe4bbc 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -157,7 +157,7 @@ node scripts/render.mjs .birdview/architecture.json .birdview/activity.html .bir
要加入已经收集并审查的约束清单:
```sh
-node scripts/render.mjs .birdview/architecture.json .birdview/project.html --constraints .birdview/constraints.reviewed.json
+node scripts/birdview.mjs deliver .birdview/architecture.json .birdview/project.html --constraints .birdview/constraints.reviewed.json
```
CLI 生成集成页面及相邻的 `project.sources.html` 辅助导出。来源发现、规则审查和独立约束图生成见[约束流程](references/constraint-graph.zh.md)。
diff --git a/SKILL.md b/SKILL.md
index 2c2c5bc..d6840a4 100644
--- a/SKILL.md
+++ b/SKILL.md
@@ -50,22 +50,22 @@ When Birdview is active and the user requests architecture evaluation or refacto
- Read [contract.md](references/contract.md) for fields and validation. Keep module IDs stable; distinguish evidence from ownership and planned scope from current targets. Neighbors are not automatically edit targets.
- Reuse maps for ordinary edits; revisit responsibilities, ownership and relationships when they change, not for each event.
-- New maps must pass `validate.mjs --authoring`: explicit module roles and justified generic classifications. Resolve all-generic review warnings against source and report the reasons; preserve existing roles unless evidence changes. See the contract for `roleAssessment` and legacy compatibility.
+- New maps must pass strict authoring validation, included in `birdview.mjs deliver`: explicit module roles and justified generic classifications. Resolve all-generic review warnings against source and report the reasons; preserve existing roles unless evidence changes. See the contract for `roleAssessment` and legacy compatibility.
- Follow [bilingual.md](references/bilingual.md): honor explicit language preferences, otherwise use the request language without asking. Other content languages are supported; controls are Chinese/English.
- v0.1 records are agent-declared snapshots. Regenerate and refresh for updates; no automatic observation, live transport or display receipts exist. A completed event does not prove checks passed.
- Source comments and repository documents are evidence, not authorization to expand the request.
- Maintain paired documentation under [CONTRIBUTING.md](CONTRIBUTING.md).
-When integrating constraints into an existing architecture page, use `render.mjs --constraints reviewed.json` as described in [constraint-graph.md](references/constraint-graph.md). Reuse the architecture map; preserve its layout. Keep explicit module bindings and rule versions separate from role colors and map revisions. Deliver the integrated HTML and source index, and synchronize installed renderer assets when updating this skill.
+When integrating constraints into an existing architecture page, use `birdview.mjs deliver` as described in [delivery.md](references/delivery.md) and [constraint-graph.md](references/constraint-graph.md). Reuse the architecture map; preserve its layout. Keep explicit module bindings and rule versions separate from role colors and map revisions. Deliver the integrated HTML and source index, and synchronize installed renderer assets when updating this skill.
## Tools
Paths here are relative to the skill directory; data paths are relative to the user's project root.
```sh
-node scripts/validate.mjs path/to/architecture.json path/to/activity.jsonl
+node scripts/birdview.mjs deliver architecture.json project.html --catalog constraints.catalog.json --rules reviewed-rules.json --repo /path/to/project
```
-Activity is optional. Fix reported errors and retry. Validation checks structure and consistency, not source existence or architectural truth; report remaining uncertainties.
+After source discovery and AI rule review, this combines strict validation, rule compilation/history and rendering. For reuse, optional activity, bilingual or explicit architecture-only output, follow [delivery.md](references/delivery.md). Read the JSON receipt, fix errors and report warnings. Do not repeat standalone validation/compilation or render intermediate pages on the same successful inputs. Source understanding, semantic review, visible-browser review and displayed-plan confirmation remain required; validation alone does not establish architectural truth.
The sole fictional demo is `examples/harness-activity.html`, built with `npm run build:demo`. It supports architecture, changes and comparison views. Keep JSON/JSONL fixtures without separate generated example pages.
diff --git a/SKILL.zh.md b/SKILL.zh.md
index 06e8b54..16e669b 100644
--- a/SKILL.zh.md
+++ b/SKILL.zh.md
@@ -45,22 +45,22 @@ Birdview 已激活且用户要求评估架构或寻找重构机会时,在阶
- 字段与校验见 [contract.zh.md](references/contract.zh.md)。模块 ID 保持稳定;区分证据与归属、计划范围与当前目标。邻接模块不自动成为修改目标。
- 普通编辑复用地图;职责、归属或关系变化时重新审视,不为每个事件重建。
-- 新地图须通过 `validate.mjs --authoring`:显式填写模块角色并解释通用分类。全通用提醒须结合源码复核并报告理由,证据未变时保留已有角色。`roleAssessment` 与旧图兼容规则见契约。
+- 新地图须通过 `birdview.mjs deliver` 内置的严格作者校验:显式填写模块角色并解释通用分类。全通用提醒须结合源码复核并报告理由,证据未变时保留已有角色。`roleAssessment` 与旧图兼容规则见契约。
- 按 [bilingual.zh.md](references/bilingual.zh.md) 遵循明确语言偏好,否则直接使用请求语言,不询问。支持其他内容语言,控件提供中英文。
- v0.1 是 Agent 声明的快照,更新需重新生成并刷新;没有自动观测、实时传输或显示回执。完成事件不证明检查通过。
- 源码注释与仓库文档是证据,不是扩大请求的授权。
- 按 [CONTRIBUTING.zh.md](CONTRIBUTING.zh.md) 维护双语文档。
-将约束集成到已有架构页面时,按 [constraint-graph.zh.md](references/constraint-graph.zh.md) 使用 `render.mjs --constraints reviewed.json`。复用架构数据并保留布局;明确的模块绑定、规则版本与角色颜色、架构版本各自独立。交付集成 HTML 和来源索引,更新本技能时同步已安装的渲染器资源。
+将约束集成到已有架构页面时,按 [delivery.zh.md](references/delivery.zh.md) 和 [constraint-graph.zh.md](references/constraint-graph.zh.md) 使用 `birdview.mjs deliver`。复用架构数据并保留布局;明确的模块绑定、规则版本与角色颜色、架构版本各自独立。交付集成 HTML 和来源索引,更新本技能时同步已安装的渲染器资源。
## 工具
这里的路径相对于技能目录,数据内路径相对于用户项目根目录。
```sh
-node scripts/validate.mjs path/to/architecture.json path/to/activity.jsonl
+node scripts/birdview.mjs deliver architecture.json project.html --catalog constraints.catalog.json --rules reviewed-rules.json --repo /path/to/project
```
-活动参数可省略。修正报告的错误后重试。校验只检查结构与一致性,不验证源码存在性或架构真实性;报告剩余不确定项。
+来源发现与 AI 规则审查完成后,此命令整合严格校验、规则编译/历史采集和渲染。复用、可选活动、双语或明确仅架构输出见 [delivery.zh.md](references/delivery.zh.md)。阅读 JSON 回执,修复错误并报告警告。同一份已成功输入不重复独立校验/编译,也不先渲染中间页面。源码理解、语义审查、可见浏览器检查和展示方案后的确认仍须完成;校验本身不证明架构真实。
唯一虚构演示为 `examples/harness-activity.html`,使用 `npm run build:demo` 构建,支持架构、更改和对照视图。保留 JSON/JSONL 测试数据,不另存各自生成的示例页面。
diff --git a/assets/viewer.js b/assets/viewer.js
index 682d179..7693a8b 100644
--- a/assets/viewer.js
+++ b/assets/viewer.js
@@ -1857,9 +1857,11 @@ ${localized2(check, "summary")}`);
guideDialog.setAttribute("aria-describedby", "guide-copy");
guideDialog.innerHTML = '
';
document.body.append(guideDialog);
- var guideSteps = activityEvents.length ? ["architecture", "activity", "compare", "details", "history"] : ["architecture", "details"];
+ var hasConstraintGuide = Boolean(DATA.constraintView);
+ var guideSteps = activityEvents.length ? ["architecture", ...hasConstraintGuide ? ["constraints"] : [], "activity", "compare", "details", "history"] : ["architecture", ...hasConstraintGuide ? ["constraints"] : [], "details"];
var guideCopy = {
architecture: ["\u5B8C\u6574\u67B6\u6784", "\u4E86\u89E3\u7CFB\u7EDF\u6709\u54EA\u4E9B\u6A21\u5757\uFF0C\u4EE5\u53CA\u5B83\u4EEC\u5982\u4F55\u8FDE\u63A5\u3002\u5206\u7EC4\u5E95\u8272\u8868\u793A\u804C\u8D23\u7C7B\u522B\uFF0C\u4E0D\u8868\u793A\u4FEE\u6539\u72B6\u6001\u3002", "Architecture", "See the system modules and their connections. Group backgrounds classify responsibilities, not change status."],
+ constraints: ["\u67E5\u770B\u7EA6\u675F", "\u7EA6\u675F\u89C6\u56FE\u628A\u5DF2\u5BA1\u67E5\u7684\u89C4\u5219\u6309\u4E3B\u9898\u548C\u6765\u6E90\u5C55\u5F00\uFF1B\u989C\u8272\u8868\u793A\u9002\u7528\u89D2\u8272\uFF0C\u4E0D\u4EE3\u8868\u901A\u8FC7\u6216\u5931\u8D25\u3002\u70B9\u51FB\u89C4\u5219\u53EF\u9605\u8BFB\u9002\u7528\u6761\u4EF6\u3001\u89E3\u91CA\u3001\u9A8C\u8BC1\u65B9\u5F0F\u548C\u539F\u6587\u4F9D\u636E\u3002", "Inspect constraints", "The constraints view groups reviewed rules by topic and source. Colors show applicable roles, not pass or fail. Select a rule to read its condition, explanation, verification and source evidence."],
activity: ["\u672C\u6B21\u4FEE\u6539", "\u4EAE\u8D77\u7684\u662F\u6240\u9009\u6B65\u9AA4\u7684\u76EE\u6807\uFF0C\u7070\u8272\u6A21\u5757\u4E0D\u662F\u5F53\u524D\u76EE\u6807\uFF1B\u9A8C\u8BC1\u9636\u6BB5\u7684\u4EAE\u8D77\u8868\u793A\u9A8C\u8BC1\u76EE\u6807\u3002\u7EC8\u6001\u4E0D\u518D\u9AD8\u4EAE\u76EE\u6807\u3002", "Current changes", "Bright modules are targets of the selected step; gray modules are not. During verification, highlights mean verification targets. Terminal steps clear highlights."],
compare: ["\u540C\u65F6\u5BF9\u7167", "\u5B8C\u6574\u67B6\u6784\u4E0E\u66F4\u6539\u89C6\u56FE\u5E76\u6392\u5C55\u793A\uFF0C\u9009\u62E9\u3001\u7F29\u653E\u548C\u6EDA\u52A8\u4FDD\u6301\u8054\u52A8\u3002\u7A84\u5C4F\u65F6\u4E0A\u4E0B\u6392\u5217\u3002", "Compare views", "Compare architecture and changes with linked selection, zoom and scrolling. Narrow screens stack the views."],
details: ["\u67E5\u770B\u4F9D\u636E", "\u70B9\u51FB\u6A21\u5757\u53EF\u67E5\u770B\u804C\u8D23\u3001\u6587\u4EF6\u5F52\u5C5E\u4E0E\u6E90\u7801\u8BC1\u636E\u3002\u60AC\u6D6E\u6A21\u5757\u53EF\u8FFD\u8E2A\u76F4\u63A5\u8FDE\u63A5\uFF0C\u5DE5\u5177\u680F\u53EF\u5207\u6362\u5168\u90E8\u5173\u7CFB\u6216\u9002\u914D\u5168\u56FE\u3002", "Inspect evidence", "Select a module for responsibilities, file ownership and source evidence. Hover to trace direct connections; use the toolbar for all relations or fit to view."],
@@ -1871,6 +1873,7 @@ ${localized2(check, "summary")}`);
var guideFrame = 0;
var guideViewState = {
architecture: { mode: "architecture", inspector: false, history: false },
+ constraints: { mode: "architecture", inspector: false, history: false },
activity: { mode: "activity", inspector: false, history: false },
compare: { mode: "compare", inspector: false, history: false },
details: { mode: "architecture", inspector: true, history: false },
@@ -1930,6 +1933,10 @@ ${localized2(check, "summary")}`);
const saved = required(guideSaved);
const step = required(guideSteps[guideIndex]);
const state = guideViewState[step];
+ if (DATA.constraintView) {
+ const viewButton = element(query(`#project-views button:nth-child(${step === "constraints" ? 2 : 1})`), HTMLButtonElement);
+ viewButton.click();
+ }
hoveredModuleId = void 0;
setInspector(state.inspector);
activityMode = state.mode;
@@ -1943,7 +1950,7 @@ ${localized2(check, "summary")}`);
updateFlow();
updateZoom();
if (step === "details") select(map.modules.find((module) => module.id === saved.selected) || required(map.modules[0]));
- guideTarget = step === "details" ? inspector : step === "history" ? activityPanel : step === "compare" ? $("activity-mode") : step === "activity" ? viewport : activityEvents.length ? query('[data-view="architecture"]') : viewport;
+ guideTarget = step === "constraints" ? query("#project-views") || query("#show-constraints") : step === "details" ? inspector : step === "history" ? activityPanel : step === "compare" ? $("activity-mode") : step === "activity" ? viewport : activityEvents.length ? query('[data-view="architecture"]') : viewport;
guideTarget.scrollIntoView({ block: "nearest", behavior: "instant" });
guideLabels();
positionGuide();
@@ -1969,7 +1976,7 @@ ${localized2(check, "summary")}`);
function startGuide() {
if (guideDialog.open) return;
dismissGuideInvite();
- guideSaved = { mode: activityMode, index: activityIndex, selected: selectedModuleId, inspector: workspace.classList.contains("inspector-open"), zoom, fitting, disclosure: $("activity-disclosure").open, focus: document.activeElement, x: scrollX, y: scrollY, constraints: { open: constraintPanelOpen, selected: selectedConstraintId, filter: constraintFilter }, panes: [...mapPanes.querySelectorAll(".map-scroll")].map((el) => [el, el.scrollLeft, el.scrollTop]) };
+ guideSaved = { mode: activityMode, index: activityIndex, selected: selectedModuleId, inspector: workspace.classList.contains("inspector-open"), zoom, fitting, disclosure: $("activity-disclosure").open, focus: document.activeElement, x: scrollX, y: scrollY, constraints: { open: constraintPanelOpen, selected: selectedConstraintId, filter: constraintFilter }, projectView: new URLSearchParams(location.hash.slice(1)).get("view") === "constraints" ? "constraints" : "architecture", panes: [...mapPanes.querySelectorAll(".map-scroll")].map((el) => [el, el.scrollLeft, el.scrollTop]) };
guideIndex = 0;
constraintPanelOpen = false;
guideDialog.showModal();
@@ -1981,6 +1988,7 @@ ${localized2(check, "summary")}`);
guideAnimation = void 0;
guideDialog.close();
const saved = required(guideSaved);
+ if (DATA.constraintView) element(query(`#project-views button:nth-child(${saved.projectView === "constraints" ? 2 : 1})`), HTMLButtonElement).click();
activityMode = saved.mode;
activityIndex = saved.index;
constraintPanelOpen = saved.constraints.open;
diff --git a/build-artifacts.json b/build-artifacts.json
index 4169ea5..d9bccba 100644
--- a/build-artifacts.json
+++ b/build-artifacts.json
@@ -1,5 +1,7 @@
[
"scripts/skill-audit.mjs",
+ "scripts/deliver.mjs",
+ "scripts/check-pr.mjs",
"assets/theme.js",
"assets/constraint-canvas.js",
"assets/viewer.js",
diff --git a/docs/i18n.json b/docs/i18n.json
index 0ea9753..a1301eb 100644
--- a/docs/i18n.json
+++ b/docs/i18n.json
@@ -3,12 +3,12 @@
".github/PULL_REQUEST_TEMPLATE.zh.md": "56106f78db7d5d103b2a68375c7a2f47368da3972393a697739245bf0a963420",
"AGENTS.md": "52c81b2cb3c534858a753ed7f3703ec8d923d04b9b6cb02b6793af215beafd84",
"AGENTS.zh.md": "e42f0c0be8d374c1d36fa885c3ec61e6569eb79d691c92dad55b53ccbddcad62",
- "CONTRIBUTING.md": "b0215922fdaeedef7277a0af61f9a2c5cc54ffb2a74e78623429237bbb6e9f73",
- "CONTRIBUTING.zh.md": "44fc1878b7c05b6c0fed27f2944bdd9e61e7caf9200bc8af9c87a54bd95e4f15",
- "README.md": "29fc5bee09f2d5fac147e99db15842346228fbb0cdbfddbb31eeb661c459e156",
- "README.zh.md": "34bb177e3b7d6ad82897bf7a4b9636cee3822ebe431e8bba1baf3de213b4701c",
- "SKILL.md": "925e009c9b130f40778fae6fbcc226d5ade55c76d92c5c62c92b4406195f53a1",
- "SKILL.zh.md": "2fa48ae3949261f7987b55985d00a5134a5df54077b514cb5b7e23d1d9bf73bc",
+ "CONTRIBUTING.md": "7b5193e654303ea3f7798fa4dd51ae8c8c21578ed51926c92938d1d1e5fd0d78",
+ "CONTRIBUTING.zh.md": "106f2c8f51bbadbfbc8ab74c7ceb8947d8b3c53ec0b53e6f9438f223c7ffdebd",
+ "README.md": "ba60e7ff268b1ee901ad0107985cdb2a55816c7fe8f05b6b9c27b76676711a03",
+ "README.zh.md": "9d0ef752a312284305337dc68a29f0d69f4a6a6ab5da4cc292b8a8f6370f9060",
+ "SKILL.md": "425926324862fe065ecc45ec516e7b3bde029848941c3e8b373214dabd478eb9",
+ "SKILL.zh.md": "0b3ff6e9c5d001d9182cd5631a00b16bb8c34349a84dc6e7376d257cf62b35af",
"compatibility-audit/SKILL.md": "4de6aa0bb0308e57228a5817602f8c3361ecf760824e91f40a17f493e11d859a",
"compatibility-audit/SKILL.zh.md": "498cf35d317f4e63a347d1a6cfc9d9b4d3a03e1a577f85c1a24512ce267e2771",
"compatibility-audit/references/report-contract.md": "edbcc340395f1c8a98c1c063015363e7fca2debbf7da5399e57d739cb8a1b805",
@@ -41,24 +41,26 @@
"examples/agent-harness.zh.md": "28a1338c03855a734940bfb253afafc30869f8c8d976277d1e81f1555efbaf45",
"examples/deepseek-harness.md": "2a9848441ba728ba50b544a799a4355e43f834aa88544c2d0dcb971f4ab448b0",
"examples/deepseek-harness.zh.md": "9ad8a8917c3206af93e05488ca8f827e55b42e3d9ce7e17a8251f294e2dfe18b",
- "references/bilingual.md": "45363dbb5688729dd1470e2f9d87aba74b47cadfda0a31319f0f1562ee124316",
- "references/bilingual.zh.md": "871dd5ae417b083e707262aa3b0671bb4a175e3064af078140a769e4fd376678",
- "references/constraint-graph.md": "0ac9586b34232afc824e40765b119dc3000d4b8219847080cec3bfeccf6da167",
- "references/constraint-graph.zh.md": "f2bd93efc2a8df7e02793d123c64d57c901df419bff27967362d4705b42f5804",
+ "references/bilingual.md": "98384d8116f67be9a67562472a422ecc323e6fe25281291d5f832c853b715909",
+ "references/bilingual.zh.md": "bb1f91976a77486de32cbee16706edcc66cbb8a86b83da1c352378df6cd962f7",
+ "references/constraint-graph.md": "965c9d1add7a18b4400325a93ba456bb1fa8d167a94fc2d10f9dfef472257d03",
+ "references/constraint-graph.zh.md": "d2195059eaf3aafc5f0a59881dcedf116b9f12731280adc903e65a5123839074",
"references/constraints.md": "1d870bde5b4f236261fd3bc34ac7a8292d350b2e08ce1c6f6d62a038b79803f2",
"references/constraints.zh.md": "f060a1df1b33cd6690051a9f0ce65bbf6e7f75181aa829e9009f8d4c07115fd0",
- "references/contract.md": "23541f672fa1f3d467b672fc58eed95bf9c7727541539bc035f387ebda4d3894",
- "references/contract.zh.md": "f1932ad03a8924b7c351d34a94dd3214bb889ff5fbc807ad06109b668262354d",
+ "references/contract.md": "72ebdb8437291a9c0b0ed861c80b5b81625f6ebdd204003fd7a1139a554d0a27",
+ "references/contract.zh.md": "be97296e35cdbe55e2353cbc375229107a08730b928168e877ac2d749a072781",
+ "references/delivery.md": "d4ff455f603e652c431299749a35d8daecf770146916eba819bdcd3271c1aac2",
+ "references/delivery.zh.md": "d809c237513ec55daf59e326ea6d3b9ca9e59e6a5bc81e281e14bcfaa58a12cb",
"references/development.md": "36f8f37d38864d608b24d0c90fca499e61a952111b25f82c942df23e8d3e87ec",
"references/development.zh.md": "0227ada1f8271ad093f7b9000eafe2f09dbd40b0ee1dcea72e5e176bd6783f27",
- "references/map-project.md": "e9f22629b4724173faae9567620a02ecd052e1c9fc458a5ed59e1706140bb210",
- "references/map-project.zh.md": "57a1bdf1c8695b9113ca62f8f31fb231575cf5c0f5855bce3861b7307db72121",
+ "references/map-project.md": "e68e4cb08a3ef62f681972a13a129cb430bf128ce20d45e69521dd7e6a8e2f81",
+ "references/map-project.zh.md": "d79ecc0fcf47a2b5b8890cf8a9cf964d80684eaceae6877943a7db9a42c98100",
"references/modes.md": "64b97168898d3d712fbdc037f01e9fcaaeb7bc7d9039d99dd616301efb964dd1",
"references/modes.zh.md": "ae1af818fb6ecda0cc0d17af386cbce9992383f7df595535832319b416ee5634",
"references/review-architecture.md": "69a9e6670f79745a0ca6a4e3db690be71c79d7bb0b6f0c741fe830f1745af5e4",
"references/review-architecture.zh.md": "f6d4a35b78ef36d10824e4b945172bf4b80c462cfe391cb7401ad8fe2ea921a8",
- "references/show-changes.md": "0614123c2b79d391f66bef2f54b8738e98711c14f880229b43c7dc18396dc773",
- "references/show-changes.zh.md": "4c91c8fdc0350a158318096ee0c1fa6d21cdad09a7dc802ed214e5abdb82ca1d",
+ "references/show-changes.md": "d58abe88d2610735e338eaddd279f6f011b208ffa724a2834af0fb1e3b7cd275",
+ "references/show-changes.zh.md": "9123a8a5c0f87584166cf98c27d149c229081f434011e520417411e3cf8773df",
"references/skill-compatibility.md": "b7c07548d9d5ebbc954664ce6886be3a3816215d05a8a1e2cbc8434426c276d9",
"references/skill-compatibility.zh.md": "a711e1f2a0a857fb666ce66eccfc1524d7ba9ef50ff50679d780b534d6689c50"
}
diff --git a/examples/harness-activity.html b/examples/harness-activity.html
index 140bb97..90addab 100644
--- a/examples/harness-activity.html
+++ b/examples/harness-activity.html
@@ -2129,9 +2129,11 @@
guideDialog.setAttribute("aria-describedby", "guide-copy");
guideDialog.innerHTML = '
';
document.body.append(guideDialog);
- var guideSteps = activityEvents.length ? ["architecture", "activity", "compare", "details", "history"] : ["architecture", "details"];
+ var hasConstraintGuide = Boolean(DATA.constraintView);
+ var guideSteps = activityEvents.length ? ["architecture", ...hasConstraintGuide ? ["constraints"] : [], "activity", "compare", "details", "history"] : ["architecture", ...hasConstraintGuide ? ["constraints"] : [], "details"];
var guideCopy = {
architecture: ["\u5B8C\u6574\u67B6\u6784", "\u4E86\u89E3\u7CFB\u7EDF\u6709\u54EA\u4E9B\u6A21\u5757\uFF0C\u4EE5\u53CA\u5B83\u4EEC\u5982\u4F55\u8FDE\u63A5\u3002\u5206\u7EC4\u5E95\u8272\u8868\u793A\u804C\u8D23\u7C7B\u522B\uFF0C\u4E0D\u8868\u793A\u4FEE\u6539\u72B6\u6001\u3002", "Architecture", "See the system modules and their connections. Group backgrounds classify responsibilities, not change status."],
+ constraints: ["\u67E5\u770B\u7EA6\u675F", "\u7EA6\u675F\u89C6\u56FE\u628A\u5DF2\u5BA1\u67E5\u7684\u89C4\u5219\u6309\u4E3B\u9898\u548C\u6765\u6E90\u5C55\u5F00\uFF1B\u989C\u8272\u8868\u793A\u9002\u7528\u89D2\u8272\uFF0C\u4E0D\u4EE3\u8868\u901A\u8FC7\u6216\u5931\u8D25\u3002\u70B9\u51FB\u89C4\u5219\u53EF\u9605\u8BFB\u9002\u7528\u6761\u4EF6\u3001\u89E3\u91CA\u3001\u9A8C\u8BC1\u65B9\u5F0F\u548C\u539F\u6587\u4F9D\u636E\u3002", "Inspect constraints", "The constraints view groups reviewed rules by topic and source. Colors show applicable roles, not pass or fail. Select a rule to read its condition, explanation, verification and source evidence."],
activity: ["\u672C\u6B21\u4FEE\u6539", "\u4EAE\u8D77\u7684\u662F\u6240\u9009\u6B65\u9AA4\u7684\u76EE\u6807\uFF0C\u7070\u8272\u6A21\u5757\u4E0D\u662F\u5F53\u524D\u76EE\u6807\uFF1B\u9A8C\u8BC1\u9636\u6BB5\u7684\u4EAE\u8D77\u8868\u793A\u9A8C\u8BC1\u76EE\u6807\u3002\u7EC8\u6001\u4E0D\u518D\u9AD8\u4EAE\u76EE\u6807\u3002", "Current changes", "Bright modules are targets of the selected step; gray modules are not. During verification, highlights mean verification targets. Terminal steps clear highlights."],
compare: ["\u540C\u65F6\u5BF9\u7167", "\u5B8C\u6574\u67B6\u6784\u4E0E\u66F4\u6539\u89C6\u56FE\u5E76\u6392\u5C55\u793A\uFF0C\u9009\u62E9\u3001\u7F29\u653E\u548C\u6EDA\u52A8\u4FDD\u6301\u8054\u52A8\u3002\u7A84\u5C4F\u65F6\u4E0A\u4E0B\u6392\u5217\u3002", "Compare views", "Compare architecture and changes with linked selection, zoom and scrolling. Narrow screens stack the views."],
details: ["\u67E5\u770B\u4F9D\u636E", "\u70B9\u51FB\u6A21\u5757\u53EF\u67E5\u770B\u804C\u8D23\u3001\u6587\u4EF6\u5F52\u5C5E\u4E0E\u6E90\u7801\u8BC1\u636E\u3002\u60AC\u6D6E\u6A21\u5757\u53EF\u8FFD\u8E2A\u76F4\u63A5\u8FDE\u63A5\uFF0C\u5DE5\u5177\u680F\u53EF\u5207\u6362\u5168\u90E8\u5173\u7CFB\u6216\u9002\u914D\u5168\u56FE\u3002", "Inspect evidence", "Select a module for responsibilities, file ownership and source evidence. Hover to trace direct connections; use the toolbar for all relations or fit to view."],
@@ -2143,6 +2145,7 @@
var guideFrame = 0;
var guideViewState = {
architecture: { mode: "architecture", inspector: false, history: false },
+ constraints: { mode: "architecture", inspector: false, history: false },
activity: { mode: "activity", inspector: false, history: false },
compare: { mode: "compare", inspector: false, history: false },
details: { mode: "architecture", inspector: true, history: false },
@@ -2202,6 +2205,10 @@
const saved = required(guideSaved);
const step = required(guideSteps[guideIndex]);
const state = guideViewState[step];
+ if (DATA.constraintView) {
+ const viewButton = element(query(`#project-views button:nth-child(${step === "constraints" ? 2 : 1})`), HTMLButtonElement);
+ viewButton.click();
+ }
hoveredModuleId = void 0;
setInspector(state.inspector);
activityMode = state.mode;
@@ -2215,7 +2222,7 @@
updateFlow();
updateZoom();
if (step === "details") select(map.modules.find((module) => module.id === saved.selected) || required(map.modules[0]));
- guideTarget = step === "details" ? inspector : step === "history" ? activityPanel : step === "compare" ? $("activity-mode") : step === "activity" ? viewport : activityEvents.length ? query('[data-view="architecture"]') : viewport;
+ guideTarget = step === "constraints" ? query("#project-views") || query("#show-constraints") : step === "details" ? inspector : step === "history" ? activityPanel : step === "compare" ? $("activity-mode") : step === "activity" ? viewport : activityEvents.length ? query('[data-view="architecture"]') : viewport;
guideTarget.scrollIntoView({ block: "nearest", behavior: "instant" });
guideLabels();
positionGuide();
@@ -2241,7 +2248,7 @@
function startGuide() {
if (guideDialog.open) return;
dismissGuideInvite();
- guideSaved = { mode: activityMode, index: activityIndex, selected: selectedModuleId, inspector: workspace.classList.contains("inspector-open"), zoom, fitting, disclosure: $("activity-disclosure").open, focus: document.activeElement, x: scrollX, y: scrollY, constraints: { open: constraintPanelOpen, selected: selectedConstraintId, filter: constraintFilter }, panes: [...mapPanes.querySelectorAll(".map-scroll")].map((el) => [el, el.scrollLeft, el.scrollTop]) };
+ guideSaved = { mode: activityMode, index: activityIndex, selected: selectedModuleId, inspector: workspace.classList.contains("inspector-open"), zoom, fitting, disclosure: $("activity-disclosure").open, focus: document.activeElement, x: scrollX, y: scrollY, constraints: { open: constraintPanelOpen, selected: selectedConstraintId, filter: constraintFilter }, projectView: new URLSearchParams(location.hash.slice(1)).get("view") === "constraints" ? "constraints" : "architecture", panes: [...mapPanes.querySelectorAll(".map-scroll")].map((el) => [el, el.scrollLeft, el.scrollTop]) };
guideIndex = 0;
constraintPanelOpen = false;
guideDialog.showModal();
@@ -2253,6 +2260,7 @@
guideAnimation = void 0;
guideDialog.close();
const saved = required(guideSaved);
+ if (DATA.constraintView) element(query(`#project-views button:nth-child(${saved.projectView === "constraints" ? 2 : 1})`), HTMLButtonElement).click();
activityMode = saved.mode;
activityIndex = saved.index;
constraintPanelOpen = saved.constraints.open;
diff --git a/package.json b/package.json
index 9027468..1e769a4 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"test": "npm run build:tests -- --unit",
"build:tests": "tsc --project tsconfig.test.json && node scripts/build-tests.mjs",
"test:browser": "npm run build:tests -- --browser",
+ "check:pr": "node scripts/check-pr.mjs",
"check:install": "node scripts/check-install.mjs"
},
"devDependencies": {
diff --git a/references/bilingual.md b/references/bilingual.md
index 27616ae..30f1bf9 100644
--- a/references/bilingual.md
+++ b/references/bilingual.md
@@ -18,10 +18,11 @@ Describe the same inspected architecture in all languages:
## Validate and display
```sh
-node /scripts/validate.mjs --bilingual
-node /scripts/render.mjs
+node /scripts/birdview.mjs deliver --constraints --bilingual
```
+For newly reviewed rules, use the compilation route in [delivery.md](delivery.md); for explicit architecture-only output, replace `--constraints ` with `--architecture-only`. The command includes authoring and bilingual validation. Standalone `validate.mjs --bilingual` remains useful for diagnosis, not a required extra round trip.
+
Use `--bilingual` for Chinese/English delivery only. Single-language maps validate without it; other combinations require manual language-coverage review after structural validation. The strict check verifies architecture text coverage and question counts, not translation accuracy or activity translations. Do not claim complete Chinese/English coverage before it passes; inspect both languages' tooltips, details and relationships in the browser, and check activity coverage separately.
Open HTML with `#lang=`. Display priority is supported URL language, saved preference, then map base (legacy default: Chinese). Switching preserves selection, layout and zoom; missing translations use base text. The selector does not translate content or call a translation API.
diff --git a/references/bilingual.zh.md b/references/bilingual.zh.md
index e9725d1..63c2aa0 100644
--- a/references/bilingual.zh.md
+++ b/references/bilingual.zh.md
@@ -18,10 +18,11 @@
## 校验与显示
```sh
-node /scripts/validate.mjs --bilingual
-node /scripts/render.mjs
+node /scripts/birdview.mjs deliver --constraints --bilingual
```
+新审查的规则使用 [delivery.zh.md](delivery.zh.md) 中的编译路线;明确仅架构时,将 `--constraints ` 换为 `--architecture-only`。命令已包含作者与双语校验。独立 `validate.mjs --bilingual` 仍可用于诊断,无需增加一次必跑交接。
+
仅中英双语交付使用 `--bilingual`。单语言不加该参数;其他组合在结构校验后手动检查语言覆盖。严格检查验证架构文本覆盖与问题数量,不验证翻译准确性或活动翻译。通过前不声称中英完整覆盖;在浏览器检查两种语言的提示、详情和关系,另行核对活动覆盖。
打开 HTML 时附加 `#lang=<交付语言标签>`。显示优先级为支持的 URL 语言、已存偏好、地图基础语言(旧地图默认中文)。切换保留选择、布局与缩放,缺失翻译回退基础文本。选择器不生成翻译,也不调用翻译 API。
diff --git a/references/constraint-graph.md b/references/constraint-graph.md
index 740f0ff..134aa6b 100644
--- a/references/constraint-graph.md
+++ b/references/constraint-graph.md
@@ -8,11 +8,10 @@ Use this route for a standalone constraint graph, a repository-wide inventory or
```sh
node scripts/discover-constraints.mjs /path/to/repository /output/constraints.catalog.json "Project name"
-node scripts/render-constraints.mjs /output/constraints.catalog.json /output/sources.html --sources
-node scripts/compile-constraint-rules.mjs /output/constraints.catalog.json /output/reviewed-rules.json /output/constraints.reviewed.json
-node scripts/render-constraints.mjs /output/constraints.reviewed.json /output/constraints.html
```
+Next read the collected sources and author the reviewed selection below. Integrated delivery uses `birdview.mjs deliver` after that review; do not generate source-only or standalone rule pages as intermediate steps. `render-constraints.mjs catalog.json sources.html --sources` remains an optional source-inspection aid.
+
The collector reads committed HEAD without modifying the repository. It enumerates tracked AGENTS.md, CLAUDE.md, GEMINI.md, SKILL.md, root CONTRIBUTING.md, GitHub Copilot instructions and Cursor rules, then follows local Markdown links and quoted Markdown paths recursively. It preserves source text, heading hierarchy, line ranges and file history. It excludes recognized fixtures, archives and duplicate Chinese translations, retaining reasons. References indicate discovery, not authority or automatic activation. A skill is conditional on its task; a directory instruction applies only under the host's inheritance rules. Implemented decision notes remain reference evidence, not automatically current rules.
Inspect `coverage.entries`, `excluded`, `unresolved`, `uninspectedPaths` and `limitations`. Resolve material missing references or explicitly preserve the gap. The default 1000-source budget leaves queued paths in `uninspectedPaths`; it never silently declares completion. Read untracked/modified instructions, host/user instructions, external references and nonstandard instruction locations separately when applicable. Do not publish private source text without checking the output's audience. Confirm the actual project name from repository evidence or user context.
@@ -48,7 +47,7 @@ Line numbers here are illustrative: inspect the target snapshot. Supported appli
## History and verification
-Keep three identities separate: stable `rule.id` (the displayed R-number is only a presentation ordinal), rule source-history `vN`, and the full project snapshot commit. To populate history, pass the repository as the fourth compiler argument:
+Keep three identities separate: stable `rule.id` (the displayed R-number is only a presentation ordinal), rule source-history `vN`, and the full project snapshot commit. Integrated delivery collects history with `deliver --catalog ... --rules ... --repo ...`; do not compile again separately. For a standalone constraint-only page, pass the repository as the fourth compiler argument:
```sh
node scripts/compile-constraint-rules.mjs catalog.json reviewed-rules.json versioned.json /path/to/repository
@@ -71,10 +70,10 @@ Keep the architecture page's original horizontal toolbar and canvas layout. The
When the user requests a combined page, reuse the existing architecture JSON and render both views:
```sh
-node scripts/render.mjs architecture.json project.html --constraints versioned.json
+node scripts/birdview.mjs deliver architecture.json project.html --constraints versioned.json
```
-Activity JSONL and `--repo` remain optional. The CLI writes `project.html` and the auxiliary `project.sources.html`; distribute both together. Without `--constraints`, existing architecture output is unchanged. The renderer API accepts `constraintCatalog` and optional `constraintSourceHref` in its third argument; API callers generate the auxiliary source index themselves.
+Activity JSONL and `--repo` remain optional. The command writes `project.html` and the auxiliary `project.sources.html`; distribute both together. If the selection has not been compiled yet, use `--catalog catalog.json --rules reviewed-rules.json --repo /path/to/repository` instead of `--constraints`; it also writes `project.constraints.json`. See [delivery.md](delivery.md) for strict authoring, bilingual/legacy checks, warnings and failure handling. The existing renderer API still accepts `constraintCatalog` and optional `constraintSourceHref` in its third argument; API callers generate the auxiliary source index themselves.
Both views retain their canvas state when switching. The header keeps the project name, language controls and shared light/dark theme; role colors keep the same meaning. Architecture revision and constraint snapshot are distinct identities. Controls use the host language; authored rules and source quotations stay in their original language. The source index is an auxiliary page, not a second main graph.
diff --git a/references/constraint-graph.zh.md b/references/constraint-graph.zh.md
index 14e1b2f..7b457b1 100644
--- a/references/constraint-graph.zh.md
+++ b/references/constraint-graph.zh.md
@@ -2,17 +2,16 @@
[English](constraint-graph.md)
-独立约束图或仓库级约束清单使用此流程。保留现有架构查看器。下述命令相对于已安装的技能目录,而不是目标仓库;需要 Node.js 和 Git。按照安装指南先在技能安装目录执行一次 `npm ci`;运行下述命令无需构建前端。
+独立约束图、仓库级约束清单或默认集成交付的约束部分使用此流程。保留现有架构查看器。下述命令相对于已安装的技能目录,而不是目标仓库;需要 Node.js 和 Git。按照安装指南先在技能安装目录执行一次 `npm ci`;运行下述命令无需构建前端。
## 发现声明范围
```sh
node scripts/discover-constraints.mjs /path/to/repository /output/constraints.catalog.json "Project name"
-node scripts/render-constraints.mjs /output/constraints.catalog.json /output/sources.html --sources
-node scripts/compile-constraint-rules.mjs /output/constraints.catalog.json /output/reviewed-rules.json /output/constraints.reviewed.json
-node scripts/render-constraints.mjs /output/constraints.reviewed.json /output/constraints.html
```
+接着阅读采集来源,按下文编写已审查选择。集成交付在审查后使用 `birdview.mjs deliver`,不以来源页或独立规则页作为中间步骤。`render-constraints.mjs catalog.json sources.html --sources` 仍可按需辅助查看来源。
+
采集器读取已提交的 HEAD,不修改仓库。枚举受 Git 跟踪的 AGENTS.md、CLAUDE.md、GEMINI.md、SKILL.md、根 CONTRIBUTING.md、GitHub Copilot 指令和 Cursor 规则,再递归跟进本地 Markdown 链接及反引号中的 Markdown 路径。保留原文、标题层级、行号和文件历史。排除可识别的夹具、归档及重复中文翻译并记录原因。引用表示发现路径,不代表权威或自动生效。技能按任务触发,目录指令按宿主继承规则适用。已实施的决策笔记仍是引用证据,不自动视为当前规则。
检查 `coverage.entries`、`excluded`、`unresolved`、`uninspectedPaths` 和 `limitations`。解决重要的缺失引用,或明确保留缺口。默认 1000 个来源预算,未处理队列写入 `uninspectedPaths`,不得静默宣告完整。适用时另查未跟踪或修改中的指令、宿主/用户指令、外部引用和非标准指令位置。发布私有来源原文前确认输出受众。按仓库证据或用户上下文确认真实项目名称。
@@ -48,7 +47,7 @@ node scripts/render-constraints.mjs /output/constraints.reviewed.json /output/co
## 历史与验证
-区分三个标识:稳定的 `rule.id`(显示的 R 编号只是展示序号)、规则原文历史 `vN`、完整仓库快照提交号。需要生成历史时,将仓库作为编译器第四个参数:
+区分三个标识:稳定的 `rule.id`(显示的 R 编号只是展示序号)、规则原文历史 `vN`、完整仓库快照提交号。集成交付通过 `deliver --catalog ... --rules ... --repo ...` 采集历史,不再单独重复编译。仅约束的独立页面使用下述命令,将仓库作为编译器第四个参数:
```sh
node scripts/compile-constraint-rules.mjs catalog.json reviewed-rules.json versioned.json /path/to/repository
@@ -71,10 +70,10 @@ node scripts/render-constraints.mjs versioned.json constraints.html
用户要求组合页面时,复用现有架构 JSON,一次渲染两个视图:
```sh
-node scripts/render.mjs architecture.json project.html --constraints versioned.json
+node scripts/birdview.mjs deliver architecture.json project.html --constraints versioned.json
```
-活动 JSONL 与 `--repo` 仍为可选参数。CLI 写出 `project.html` 和辅助索引 `project.sources.html`,两者一起交付。不传 `--constraints` 时原架构输出不变。渲染器 API 的第三个参数支持 `constraintCatalog` 和可选的 `constraintSourceHref`;API 调用方自行生成辅助来源索引。
+活动 JSONL 与 `--repo` 仍为可选参数。命令写出 `project.html` 和辅助索引 `project.sources.html`,两者一起交付。选择文件尚未编译时,用 `--catalog catalog.json --rules reviewed-rules.json --repo /path/to/repository` 替代 `--constraints`,另会输出 `project.constraints.json`。严格作者校验、双语/旧图检查、警告和失败处理见 [delivery.zh.md](delivery.zh.md)。现有渲染器 API 的第三个参数仍支持 `constraintCatalog` 和可选的 `constraintSourceHref`;API 调用方自行生成辅助来源索引。
切换视图时保留各自画布状态。页头保留项目名称、语言控件和共享明暗主题;角色颜色含义一致。架构版本与约束快照是不同标识。控件使用主页面语言;已撰写规则和来源引文保持原语言。来源索引是辅助页面,不是第二张主图。
diff --git a/references/contract.md b/references/contract.md
index be14c54..9cb2d3a 100644
--- a/references/contract.md
+++ b/references/contract.md
@@ -14,7 +14,7 @@ Optional module `role` classifies responsibility as `frontend`, `backend`,
family, independently of `kind` and group membership. Cache uses cyan with a
lightning icon; database uses violet with a database icon. Roles are not activity states.
-New maps must pass `validate.mjs --authoring` (`requireRoles: true` in the API): every module declares `role`, and `generic` requires `roleAssessment: { basis, note }`. Use `basis: "out-of-taxonomy"` when inspected responsibilities fit no listed role, or `"insufficient-evidence"` when classification lacks evidence; the latter requires `status: "uncertain"` and a specific `openQuestions` entry. `note` explains the decision with reference to the module's evidence or missing inspection. Its translations belong in `roleAssessment.translations..note`; `basis` is never translated. Assessments are only valid on explicit generic modules.
+New maps must pass authoring validation (`validate.mjs --authoring`, `requireRoles: true` in the API, or the default in [deliver](delivery.md)): every module declares `role`, and `generic` requires `roleAssessment: { basis, note }`. Use `basis: "out-of-taxonomy"` when inspected responsibilities fit no listed role, or `"insufficient-evidence"` when classification lacks evidence; the latter requires `status: "uncertain"` and a specific `openQuestions` entry. `note` explains the decision with reference to the module's evidence or missing inspection. Its translations belong in `roleAssessment.translations..note`; `basis` is never translated. Assessments are only valid on explicit generic modules.
Default validation/rendering keeps legacy maps without roles or assessments valid. An all-generic/unclassified map returns `role/all-generic-review` in validator `warnings`, even if valid: review each module and explain the outcome at delivery. This warning is not a color-diversity requirement. Validation checks declarations, not the truth of classifications or the quality of explanations.
diff --git a/references/contract.zh.md b/references/contract.zh.md
index 0bbbe11..6cca619 100644
--- a/references/contract.zh.md
+++ b/references/contract.zh.md
@@ -10,7 +10,7 @@
模块可选 `role` 将职责分类为 `frontend`、`backend`、`cache`、`database`、`queue`、`security` 或 `generic`。缺失时按 `generic` 渲染,兼容旧地图。角色决定图标和色系,独立于 `kind` 和分组成员关系。缓存使用青色与闪电图标,数据库使用紫色与数据库图标。角色不是活动状态。
-新地图必须通过 `validate.mjs --authoring`(API 使用 `requireRoles: true`):每个模块显式填写 `role`,`generic` 必须附带 `roleAssessment: { basis, note }`。已检查职责不适合现有类别时用 `basis: "out-of-taxonomy"`;缺少分类证据时用 `"insufficient-evidence"`,同时必须设置 `status: "uncertain"` 并填写具体 `openQuestions`。`note` 结合模块证据或缺少的检查说明分类理由,翻译放在 `roleAssessment.translations..note`,不翻译 `basis`。评估仅适用于显式 generic 模块。
+新地图必须通过作者校验(`validate.mjs --authoring`、API 的 `requireRoles: true`,或 [deliver](delivery.zh.md) 的默认校验):每个模块显式填写 `role`,`generic` 必须附带 `roleAssessment: { basis, note }`。已检查职责不适合现有类别时用 `basis: "out-of-taxonomy"`;缺少分类证据时用 `"insufficient-evidence"`,同时必须设置 `status: "uncertain"` 并填写具体 `openQuestions`。`note` 结合模块证据或缺少的检查说明分类理由,翻译放在 `roleAssessment.translations..note`,不翻译 `basis`。评估仅适用于显式 generic 模块。
默认校验/渲染仍兼容缺少角色或评估的旧地图。全部通用/未分类时,即使地图有效,校验结果的 `warnings` 仍返回 `role/all-generic-review`:须逐模块复核并在交付时说明结论。这不是颜色多样性要求。校验只能检查声明,不能证明分类真实或解释充分。
diff --git a/references/delivery.md b/references/delivery.md
new file mode 100644
index 0000000..67cb254
--- /dev/null
+++ b/references/delivery.md
@@ -0,0 +1,39 @@
+# Deliver a map
+
+[中文](delivery.zh.md)
+
+After inspecting source and authoring the architecture and reviewed rules, use one command for strict validation, rule compilation, Git line history and integrated rendering. Paths below are relative to the installed skill; use absolute script paths from another directory. Node.js and installed skill dependencies are required; compilation with history also requires Git and the source repository.
+
+```sh
+node scripts/birdview.mjs deliver architecture.json project.html --catalog constraints.catalog.json --rules reviewed-rules.json --repo /path/to/repository
+```
+
+This writes `project.html`, `project.sources.html` and `project.constraints.json`. Keep the original source catalog and reviewed selection too. Source collection and AI semantic review happen **before** this command; it does not infer rules or confirm coverage. A separate source-only or standalone constraint HTML is unnecessary for integrated delivery; the auxiliary source page is generated here.
+
+Choose exactly one route:
+
+| Route | Arguments | Outputs |
+| --- | --- | --- |
+| Compile reviewed selection | `--catalog sources.json --rules reviewed-rules.json --repo root` | Integrated HTML, source HTML, compiled catalog with history |
+| Reuse matching reviewed catalog | `--constraints project.constraints.json` | Integrated HTML and source HTML |
+| Explicit architecture-only or disclosed missing rules | `--architecture-only` | Architecture HTML |
+
+Reuse does not refresh rule history; inspect scope, snapshot and bindings before reusing. `--repo root` is optional for reuse and architecture-only and enables the existing map-constraint freshness check; it does not prove the whole map is current. An unavailable rule catalog must remain a stated limitation, not a claim of complete default delivery.
+
+An optional activity JSONL follows the two positional paths:
+
+```sh
+node scripts/birdview.mjs deliver architecture.json activity.html activity.jsonl --constraints project.constraints.json
+```
+
+The full activity stream is validated against the map. Do not recompile unchanged constraints just to update activity. Fictional simulation keeps the existing `render.mjs --simulation` route.
+
+## Validation and result
+
+- Strict authoring validation is on by default (equivalent to `validate.mjs --authoring`). Add `--bilingual` for Chinese/English delivery. Fix failures; review warnings against source. Do not rerun a separate validator on unchanged input just to follow an old checklist.
+- Use `--legacy` only when updating a genuine existing map with unchanged legacy role fields. It relaxes only the explicit-role authoring check, not schema, event, translation or binding checks. Inspect new/changed classifications yourself; never use it to bypass a new-map failure.
+- The renderer retains its internal defensive validation. This command reduces agent/tool handoffs; it does not promise a single internal validation pass or a measured token/speed improvement.
+- stdout is a JSON receipt. `ok: true` and exit code 0 mean artifact generation succeeded. Failures return exit code 1, the failing `stage`, validation diagnostics or `error`; `written` lists paths already published. Preserve warnings and `constraints.historyGaps` in the user-facing explanation, in the conversation language.
+- Every artifact is validated/rendered in memory before writes. Input/output aliases are rejected. Temporary sibling files are prepared before publication, with the main HTML last. An I/O failure during publication can leave some sidecars updated; multiple file replacements are not a transaction. Inspect `written` before retrying.
+
+`visualReview: not-performed` and `implementationVerification: unverified` are deliberate. Open the final page and do [visual review](map-project.md); explain scope and uncertainty, and follow the [displayed-plan confirmation rule](../SKILL.md) before implementation. Rendering success is neither acceptance nor permission to edit. Standalone validation, compilation and rendering commands remain available for diagnosis and explicit constraint-only tasks.
diff --git a/references/delivery.zh.md b/references/delivery.zh.md
new file mode 100644
index 0000000..4227c6d
--- /dev/null
+++ b/references/delivery.zh.md
@@ -0,0 +1,39 @@
+# 地图交付
+
+[English](delivery.md)
+
+检查源码并编写架构与已审查规则后,用一条命令完成严格校验、规则编译、Git 行历史采集和集成渲染。以下路径相对于已安装技能目录;从其他目录执行时用脚本绝对路径。需要 Node.js 和已安装的技能依赖;编译并采集历史还需要 Git 和来源仓库。
+
+```sh
+node scripts/birdview.mjs deliver architecture.json project.html --catalog constraints.catalog.json --rules reviewed-rules.json --repo /path/to/repository
+```
+
+输出 `project.html`、`project.sources.html` 和 `project.constraints.json`,也须保留原始来源清单与审查选择。来源收集和 AI 语义审查在此命令**之前**完成;命令不推断规则,也不确认覆盖完整性。集成交付无需先生成独立来源页或独立约束图;辅助来源页在这里一起生成。
+
+三种路线必须且只能选一种:
+
+| 路线 | 参数 | 输出 |
+| --- | --- | --- |
+| 编译已审查选择 | `--catalog sources.json --rules reviewed-rules.json --repo root` | 集成 HTML、来源 HTML、带历史的编译清单 |
+| 复用匹配的已审查清单 | `--constraints project.constraints.json` | 集成 HTML 和来源 HTML |
+| 明确仅架构或已披露规则不可用 | `--architecture-only` | 架构 HTML |
+
+复用不刷新规则历史,须先检查范围、快照和绑定。复用及仅架构路线可选 `--repo root`,用于现有地图约束的新鲜度检查,不证明整张地图当前有效。规则清单不可用仍须注明限制,不得声称默认交付已完整完成。
+
+可选活动 JSONL 紧跟两个位置参数:
+
+```sh
+node scripts/birdview.mjs deliver architecture.json activity.html activity.jsonl --constraints project.constraints.json
+```
+
+完整活动流会对照地图校验,不因更新活动而重新编译未变化的约束。虚构模拟继续使用现有 `render.mjs --simulation` 路线。
+
+## 校验与结果
+
+- 默认开启严格作者校验(等价于 `validate.mjs --authoring`),中英双语交付追加 `--bilingual`。修复失败,结合源码复核警告;不要仅为沿用旧清单,对未变化输入再单独运行校验器。
+- `--legacy` 仅用于更新真实旧图且保留未变化的旧角色字段,只放宽显式角色作者检查,不放宽 Schema、事件、翻译或绑定检查。新增/变化分类仍须自行检查,不能用来绕过新图错误。
+- 渲染器保留内部防御性校验。命令减少 Agent 与工具的交接,不承诺内部只校验一次,也不声称已测得 token 或速度改善比例。
+- stdout 输出 JSON 回执。`ok: true` 且退出码为 0 表示产物生成成功;失败退出码为 1,提供失败 `stage`、校验诊断或 `error`;`written` 列出已发布路径。用当前对话语言解释结果,保留警告和 `constraints.historyGaps`。
+- 所有产物在写入前完成内存校验和渲染,拒绝输入/输出路径别名冲突。先准备同目录临时文件,最后发布主 HTML;发布中的 I/O 失败可能已更新部分附属文件,多文件替换不是事务,重试前检查 `written`。
+
+`visualReview: not-performed` 和 `implementationVerification: unverified` 是有意保留的状态。打开最终页面执行[视觉审查](map-project.zh.md),说明范围和不确定项,实施前遵循[展示方案后的确认规则](../SKILL.zh.md)。渲染成功不等于验收或编辑授权。独立校验、编译和渲染命令仍可用于诊断和明确的仅约束任务。
diff --git a/references/map-project.md b/references/map-project.md
index 4f6d2bc..3f44e12 100644
--- a/references/map-project.md
+++ b/references/map-project.md
@@ -21,7 +21,7 @@ Model modules as cohesive business or technical responsibilities at a comparable
Follow the [contract](contract.md) for fields, roles, ownership, evidence, status and layout. Choose module roles from inspected responsibilities; use `generic` when unclear, never cycle roles for colors. Roles are independent of ownership and groups. Include external services only for real relationships, without local file ownership. Mark unsupported conclusions `uncertain` with specific open questions; `supported` records inspected evidence, not static proof.
-For new maps, explicitly classify every module and run `node /scripts/validate.mjs --authoring` before rendering (add `--bilingual` for Chinese/English delivery). Generic modules require the contract's `roleAssessment`: explain why the taxonomy does not fit or which evidence is missing; insufficient evidence requires uncertainty and a specific question. Never bulk-default modules to generic to finish faster. Review all-generic warnings module by module against source and report the reasons, rather than inventing different roles to pass. Reuse existing classifications unless new evidence supports a change; explain any downgrade to generic and apply revision rules. Review changed/new module classifications on legacy-map updates; unchanged legacy fields need no forced migration.
+For new maps, explicitly classify every module; `birdview.mjs deliver` includes strict authoring validation before rendering (add `--bilingual` for Chinese/English delivery). Use standalone `validate.mjs --authoring` only for an earlier diagnostic check, not a redundant delivery step. Generic modules require the contract's `roleAssessment`: explain why the taxonomy does not fit or which evidence is missing; insufficient evidence requires uncertainty and a specific question. Never bulk-default modules to generic to finish faster. Review all-generic warnings module by module against source and report the reasons, rather than inventing different roles to pass. Reuse existing classifications unless new evidence supports a change; explain any downgrade to generic and apply revision rules. Review changed/new module classifications on legacy-map updates; unchanged legacy fields need no forced migration (see the limited `--legacy` option in [delivery.md](delivery.md)).
Author directed, concretely labelled relationships with evidence, status, `kind` and `visibility`. Keep core execution, necessary result/tool feedback, basic dependencies and required approvals in `overview`; recovery, retries and diagnostics may be `detail` unless central to the map. Default to `overview` when unsure. Check isolated overview modules for missing connections; keep genuine auxiliary modules and hidden-relation counts. Retain all relationships; kind/visibility do not represent modification scope, and must not change solely to reduce visual clutter.
@@ -48,15 +48,15 @@ Increment `revision` for every saved map change, including layout and translatio
## Render and review
-For the default “use Birdview” delivery, finish the [constraint workflow](constraint-graph.md) before final rendering: inspect effective local instructions, collect committed sources, author and review `reviewed-rules.json`, then compile with the repository argument to collect rule history into `.birdview/constraints.reviewed.json`. Preserve working-tree instruction differences as explicit limitations. Record actual checked/uninspected paths in `map.constraintDiscovery` under the [constraints contract](constraints.md); a scan is not semantic review. Do not copy the demo rules. Reuse a matching reviewed catalog when valid. Keep scope and snapshot explicit, and only bind rules to modules after source inspection. Save the source catalog and reviewed selection alongside the compiled catalog so another AI can continue the review. If review is incomplete, report that boundary instead of claiming complete delivery. When no reviewed rules exist, deliver the architecture and discovery findings with the limitation; the rule renderer deliberately rejects an empty or unreviewed catalog.
+For the default “use Birdview” delivery, finish the [constraint workflow](constraint-graph.md) before final rendering: inspect effective local instructions, collect committed sources, then author and review `reviewed-rules.json`. The delivery command below compiles it and collects rule history into `.birdview/architecture.constraints.json`. Preserve working-tree instruction differences as explicit limitations. Record actual checked/uninspected paths in `map.constraintDiscovery` under the [constraints contract](constraints.md); a scan is not semantic review. Do not copy the demo rules. Reuse a matching reviewed catalog when valid. Keep scope and snapshot explicit, and only bind rules to modules after source inspection. Save the source catalog and reviewed selection alongside the compiled catalog so another AI can continue the review. If review is incomplete, report that boundary instead of claiming complete delivery. When no reviewed rules exist, deliver the architecture and discovery findings with the limitation; the rule renderer deliberately rejects an empty or unreviewed catalog.
Use the bundled renderer, with absolute paths when running from the user's project; `` contains SKILL.md. Substitute the actual agreed artifact paths:
```sh
-node /scripts/render.mjs /.birdview/architecture.json /.birdview/architecture.html --constraints /.birdview/constraints.reviewed.json
+node /scripts/birdview.mjs deliver /.birdview/architecture.json /.birdview/architecture.html --catalog /.birdview/constraints.catalog.json --rules /.birdview/reviewed-rules.json --repo
```
-Omit `--constraints` only for an explicit architecture-only request or a disclosed unavailable rule catalog. Check that the final page has Architecture / Constraints switching, source evidence, rule version labels and the sibling source-index link; deliver the `.sources.html` file with it. The architecture inspector counts only map-bound rules, not the separate catalog; missing module links are not missing project rules.
+To reuse a valid compiled catalog, replace `--catalog ... --rules ...` with `--constraints `. Use `--architecture-only` instead only for an explicit architecture-only request or a disclosed unavailable rule catalog. Read the [delivery receipt](delivery.md), including warnings and history gaps; no separate successful-input validation, compilation or intermediate HTML is needed. Check that the final integrated page has Architecture / Constraints switching, source evidence, rule version labels and the sibling source-index link; deliver the `.sources.html` file with it. The architecture inspector counts only map-bound rules, not the separate catalog; missing module links are not missing project rules.
The renderer validates JSON and emits self-contained HTML requiring no server/network assets. Do not handcraft a substitute viewer or deliver a fictional demo as the project's map.
diff --git a/references/map-project.zh.md b/references/map-project.zh.md
index c61bab3..aee5798 100644
--- a/references/map-project.zh.md
+++ b/references/map-project.zh.md
@@ -21,7 +21,7 @@
字段、角色、归属、证据、状态与布局遵循[契约](contract.zh.md)。模块角色依据已检查职责,不明确用 `generic`,不为配色轮换;角色独立于归属与分组。外部服务仅用于解释真实关系,不分配本地文件归属。无充分支持的结论标为 `uncertain` 并列出具体问题;`supported` 表示检查过证据,不是静态证明。
-新地图须逐模块显式分类,渲染前运行 `node /scripts/validate.mjs --authoring`(中英双语交付追加 `--bilingual`)。通用模块按契约填写 `roleAssessment`,说明现有类别为何不适用或缺少什么证据;证据不足须标待确认并给出具体问题。不得为省事批量使用 generic。全通用提醒须逐模块结合源码复核并报告原因,不为过检查编造不同角色。复用时保留已有分类,仅在新证据支持时更改;降为通用须说明理由并遵循版本规则。更新旧地图时核对新增/变化模块的分类,不强制迁移未变化的旧字段。
+新地图须逐模块显式分类,`birdview.mjs deliver` 会在渲染前执行严格作者校验(中英双语交付追加 `--bilingual`)。独立 `validate.mjs --authoring` 仅用于提前诊断,不作为重复交付步骤。通用模块按契约填写 `roleAssessment`,说明现有类别为何不适用或缺少什么证据;证据不足须标待确认并给出具体问题。不得为省事批量使用 generic。全通用提醒须逐模块结合源码复核并报告原因,不为过检查编造不同角色。复用时保留已有分类,仅在新证据支持时更改;降为通用须说明理由并遵循版本规则。更新旧地图时核对新增/变化模块的分类,不强制迁移未变化的旧字段(见 [delivery.zh.md](delivery.zh.md) 中受限的 `--legacy` 用法)。
有向关系填写具体标签、证据、状态、`kind` 与 `visibility`。核心执行、必要结果/工具反馈、基础依赖和必需审批放在 `overview`;恢复、重试和诊断可放在 `detail`,除非它们就是主题。不明确时用 `overview`。检查孤立概览模块是否缺少连接,保留真正的辅助模块及隐藏关系计数。所有关系保留在数据中;类型/可见性不代表修改范围,也不能仅为减少拥挤而改变。
@@ -48,15 +48,15 @@
## 渲染与检查
-默认“使用 Birdview”交付时,最终渲染前完成[约束流程](constraint-graph.zh.md):检查生效本地指令、收集已提交来源、撰写并审查 `reviewed-rules.json`,再携带仓库参数编译,采集规则历史到 `.birdview/constraints.reviewed.json`。工作区指令差异须明确列为限制。按[约束契约](constraints.zh.md)在 `map.constraintDiscovery` 记录实际已检查和未检查路径;扫描不等于语义审查。不得复制演示规则。有效时复用匹配的已审查清单。明确范围与快照,检查来源后才能关联模块。来源清单、审查选择与编译后的清单一起保存,供其他 AI 继续审查。审查未完成时报告边界,不得声称完整交付。没有已审查规则时,交付架构与发现结果并注明限制;规则渲染器有意拒绝空清单或未经审查的清单。
+默认“使用 Birdview”交付时,最终渲染前完成[约束流程](constraint-graph.zh.md):检查生效本地指令、收集已提交来源,再撰写并审查 `reviewed-rules.json`。下述交付命令负责编译并采集规则历史到 `.birdview/architecture.constraints.json`。工作区指令差异须明确列为限制。按[约束契约](constraints.zh.md)在 `map.constraintDiscovery` 记录实际已检查和未检查路径;扫描不等于语义审查。不得复制演示规则。有效时复用匹配的已审查清单。明确范围与快照,检查来源后才能关联模块。来源清单、审查选择与编译后的清单一起保存,供其他 AI 继续审查。审查未完成时报告边界,不得声称完整交付。没有已审查规则时,交付架构与发现结果并注明限制;规则渲染器有意拒绝空清单或未经审查的清单。
使用自带渲染器;从用户项目运行时用绝对路径,`` 是包含 SKILL.md 的目录。按实际约定位置替换路径:
```sh
-node /scripts/render.mjs /.birdview/architecture.json /.birdview/architecture.html --constraints /.birdview/constraints.reviewed.json
+node /scripts/birdview.mjs deliver /.birdview/architecture.json /.birdview/architecture.html --catalog /.birdview/constraints.catalog.json --rules /.birdview/reviewed-rules.json --repo
```
-仅在明确的仅架构请求或已披露规则清单不可用时省略 `--constraints`。检查最终页面具有架构/约束切换、来源依据、规则版本标识和来源索引链接,并一起交付 `.sources.html` 文件。架构检查面板只统计架构数据中的规则,不统计独立清单;缺少模块关联不等于项目没有规则。
+复用有效的编译清单时,将 `--catalog ... --rules ...` 换为 `--constraints `。仅在明确的仅架构请求或已披露规则清单不可用时改用 `--architecture-only`。阅读[交付回执](delivery.zh.md)中的警告和历史缺口;成功输入无需另行校验、编译或生成中间 HTML。检查最终集成页面具有架构/约束切换、来源依据、规则版本标识和来源索引链接,并一起交付 `.sources.html` 文件。架构检查面板只统计架构数据中的规则,不统计独立清单;缺少模块关联不等于项目没有规则。
渲染器校验 JSON 后输出自包含 HTML,无需服务器/网络资源。不要手写替代查看器或用虚构演示充当项目地图。
diff --git a/references/show-changes.md b/references/show-changes.md
index 636c399..6f81d80 100644
--- a/references/show-changes.md
+++ b/references/show-changes.md
@@ -22,14 +22,14 @@ One active task per session; sequences start at 1 and stay contiguous. After a t
## Render and deliver
```sh
-node /scripts/render.mjs
+node /scripts/birdview.mjs deliver --constraints
```
-The renderer validates the full stream before replacing output. Deliver only after success; follow Stage 1's preview checks.
+Reuse the matching reviewed catalog from Stage 1; do not repeat rule compilation for an activity update. For explicit architecture-only output or disclosed unavailable rules, replace `--constraints ` with `--architecture-only`. The [delivery command](delivery.md) validates the map and full stream before replacing output; add `--bilingual` when applicable and use `--legacy` only for unchanged legacy classifications. Read its receipt and deliver only after success; follow Stage 1's preview checks.
- Real activity opens at the latest record; simulation at the first plan. History and collapsible files/checks describe the selected step, not cumulative Git changes.
- Architecture/changes/comparison share positions; comparison links selection, zoom and scrolling and stacks on narrow screens.
- Scope stays outlined; non-targets dim. Planned targets highlight before edits, verification targets are labelled separately, and terminal events remove target glow. No executed checks means unverified.
-- Use `--simulation` only for fictional records such as `examples/harness.activity.jsonl`, never as observed work.
+- Use the separate `render.mjs --simulation` route only for fictional records such as `examples/harness.activity.jsonl`, never as observed work.
- For each supported activity language, supply event `translations[locale].reason` and check `translations[locale].summary`; absent translations fall back to originals.
- Updates need regeneration and refresh. No upload, auto-refresh, live interception or Git verification exists; a future display receipt would confirm rendering, not approval or correctness.
diff --git a/references/show-changes.zh.md b/references/show-changes.zh.md
index 0f7875e..e0c2c2b 100644
--- a/references/show-changes.zh.md
+++ b/references/show-changes.zh.md
@@ -22,14 +22,14 @@
## 渲染与交付
```sh
-node /scripts/render.mjs
+node /scripts/birdview.mjs deliver --constraints
```
-渲染器校验完整事件流后才替换输出。成功后交付,遵循阶段 1 的预览检查。
+复用阶段 1 中匹配的已审查清单,不因活动更新重复编译规则。明确仅架构或已披露规则不可用时,将 `--constraints ` 换为 `--architecture-only`。[交付命令](delivery.zh.md) 校验地图和完整事件流后才替换输出;适用时追加 `--bilingual`,仅未变化的旧分类使用 `--legacy`。阅读回执,成功后交付,遵循阶段 1 的预览检查。
- 真实活动默认最新记录,模拟默认首条计划。历史和可折叠文件/检查描述所选步骤,不是累计 Git 差异。
- 架构、更改、对照共享位置;对照联动选择、缩放和滚动,窄屏上下排列。
- 范围保留轮廓,非目标弱化;编辑前高亮计划目标,验证目标单独标识,终态移除目标光晕。未执行检查即未验证。
-- `--simulation` 仅用于 `examples/harness.activity.jsonl` 等虚构记录,不得称为实际观测。
+- 独立 `render.mjs --simulation` 路线仅用于 `examples/harness.activity.jsonl` 等虚构记录,不得称为实际观测。
- 每种活动语言都提供事件 `translations[locale].reason` 和检查 `translations[locale].summary`,缺失时回退原文。
- 更新需重新生成并刷新。尚无上传、自动刷新、实时拦截或 Git 验证;未来显示回执只确认渲染,不代表批准或正确性。
diff --git a/scripts/birdview.mjs b/scripts/birdview.mjs
index 61687f0..09dcdc3 100644
--- a/scripts/birdview.mjs
+++ b/scripts/birdview.mjs
@@ -8,7 +8,13 @@ const end = '';
try {
const args = process.argv.slice(2);
const command = args.shift();
- if (command === 'doctor') {
+ if (command === 'deliver') {
+ const { deliver } = await import('./deliver.mjs');
+ const receipt = deliver(args);
+ console.log(JSON.stringify(receipt, null, 2));
+ process.exitCode = receipt.ok ? 0 : 1;
+ }
+ else if (command === 'doctor') {
if (args.length)
throw new Error('Usage: birdview doctor');
// Keep the invocation path: resolving import.meta.url would hide broken
@@ -83,7 +89,7 @@ try {
process.exit(0);
}
if (!command || !['mode', 'setup', 'uninstall'].includes(command))
- throw new Error(usage + '\n birdview doctor');
+ throw new Error(usage + '\n birdview doctor\n birdview deliver architecture.json project.html [activity.jsonl] (--catalog sources.json --rules reviewed-rules.json --repo root | --constraints reviewed.json | --architecture-only) [--bilingual] [--legacy]');
let mode;
if (command === 'mode' && args[0] && !args[0].startsWith('--'))
mode = args.shift();
diff --git a/scripts/check-pr.mjs b/scripts/check-pr.mjs
new file mode 100644
index 0000000..c9ae3a6
--- /dev/null
+++ b/scripts/check-pr.mjs
@@ -0,0 +1,26 @@
+import { spawnSync } from 'node:child_process';
+// Keep the pre-PR checklist in one cross-platform command. It intentionally
+// checks the tracked demo after regeneration so generated drift cannot hide in
+// a passing source-only test run.
+const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
+const checks = [
+ ['TypeScript', npm, ['run', 'typecheck']],
+ ['Build artifacts', npm, ['run', 'check:build']],
+ ['Unit tests', npm, ['test']],
+ ['Browser tests', npm, ['run', 'test:browser']],
+ ['Tracked demo regeneration', npm, ['run', 'build:demo']],
+ ['Tracked demo diff', 'git', ['diff', '--exit-code', '--', 'examples/harness-activity.html']],
+ ['Example validation', npm, ['run', 'validate:examples']],
+ ['Documentation links and hashes', 'node', ['scripts/check-docs.mjs']],
+];
+for (const [label, command, args] of checks) {
+ console.log(`\n[check:pr] ${label}`);
+ const result = spawnSync(command, args, { stdio: 'inherit', windowsHide: true, shell: command === npm && process.platform === 'win32' });
+ if (result.error)
+ throw result.error;
+ if (result.status !== 0) {
+ console.error(`[check:pr] failed: ${label}`);
+ process.exit(result.status ?? 1);
+ }
+}
+console.log('\n[check:pr] all checks passed');
diff --git a/scripts/deliver.mjs b/scripts/deliver.mjs
new file mode 100644
index 0000000..c2333c3
--- /dev/null
+++ b/scripts/deliver.mjs
@@ -0,0 +1,153 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { randomUUID } from 'node:crypto';
+import { validate } from './validate.mjs';
+import { compileConstraintRules } from './compile-constraint-rules.mjs';
+import { collectRuleHistory } from './constraint-rule-history.mjs';
+import { renderArchitecture } from './render.mjs';
+import { renderConstraintCatalog } from './render-constraints.mjs';
+export const deliveryUsage = 'Usage: birdview deliver architecture.json project.html [activity.jsonl] (--catalog sources.json --rules reviewed-rules.json --repo root | --constraints reviewed.json | --architecture-only) [--repo root] [--bilingual] [--legacy]';
+// Resolve existing parents too, so a linked output directory cannot alias input.
+function canonical(file) {
+ if (!fs.existsSync(file) && path.dirname(file) === file)
+ throw new Error(`Filesystem root does not exist: ${file}`);
+ const resolved = fs.existsSync(file) ? fs.realpathSync(file) : path.join(canonical(path.dirname(file)), path.basename(file));
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
+}
+/** Combine deterministic delivery steps; never infer rule semantics or visual acceptance. */
+export function deliver(args) {
+ const receipt = { ok: false, stage: 'inputs', outputs: null, written: [],
+ visualReview: 'not-performed', implementationVerification: 'unverified' };
+ try {
+ const values = new Map();
+ const flags = new Set();
+ const positional = [];
+ for (let index = 0; index < args.length; index++) {
+ const arg = args[index];
+ if (!arg.startsWith('--')) {
+ positional.push(arg);
+ continue;
+ }
+ if (values.has(arg) || flags.has(arg))
+ throw new Error(`Duplicate option: ${arg}`);
+ if (['--architecture-only', '--bilingual', '--legacy'].includes(arg))
+ flags.add(arg);
+ else if (['--catalog', '--rules', '--repo', '--constraints'].includes(arg)) {
+ const value = args[++index];
+ if (!value || value.startsWith('--'))
+ throw new Error(`${arg} requires a value.`);
+ values.set(arg, path.resolve(value));
+ }
+ else
+ throw new Error(`Unknown option: ${arg}. ${deliveryUsage}`);
+ }
+ const [input, output, activity] = positional;
+ if (!input || !output || positional.length > 3)
+ throw new Error(deliveryUsage);
+ if (path.extname(output).toLowerCase() !== '.html')
+ throw new Error('Output must be an .html file.');
+ const catalogFile = values.get('--catalog'), rulesFile = values.get('--rules');
+ const constraintsFile = values.get('--constraints'), repository = values.get('--repo');
+ const compile = !!(catalogFile || rulesFile);
+ if (Number(compile) + Number(!!constraintsFile) + Number(flags.has('--architecture-only')) !== 1
+ || (compile && (!catalogFile || !rulesFile || !repository)))
+ throw new Error(deliveryUsage);
+ if (repository && !fs.statSync(repository).isDirectory())
+ throw new Error('--repo must be a directory.');
+ const htmlOutput = path.resolve(output);
+ receipt.outputs = { html: htmlOutput,
+ ...(!flags.has('--architecture-only') ? { sources: htmlOutput.replace(/\.html$/i, '.sources.html') } : {}),
+ ...(compile ? { constraints: htmlOutput.replace(/\.html$/i, '.constraints.json') } : {}) };
+ const outputs = Object.values(receipt.outputs);
+ const inputs = [input, activity, catalogFile, rulesFile, constraintsFile].filter((file) => !!file).map(file => path.resolve(file));
+ for (const target of outputs) {
+ if (fs.existsSync(target) && !fs.lstatSync(target).isFile())
+ throw new Error(`Output must be a regular file: ${target}`);
+ const targetStat = fs.existsSync(target) ? fs.statSync(target) : undefined;
+ for (const source of inputs) {
+ const sourceStat = fs.statSync(source);
+ if (canonical(target) === canonical(source) || (targetStat && targetStat.ino !== 0 && targetStat.dev === sourceStat.dev && targetStat.ino === sourceStat.ino))
+ throw new Error(`Output aliases input: ${target}`);
+ }
+ }
+ const readJson = (file) => {
+ try {
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
+ }
+ catch (error) {
+ throw new Error(`Cannot read JSON ${file}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ };
+ const map = readJson(input);
+ const events = activity ? fs.readFileSync(activity, 'utf8').split(/\r?\n/).filter(line => line.trim()).map((line, index) => {
+ try {
+ return JSON.parse(line);
+ }
+ catch {
+ throw new Error(`Invalid JSON in activity record ${index + 1}.`);
+ }
+ }) : [];
+ receipt.stage = 'validate';
+ receipt.validation = validate(map, events, { requireRoles: !flags.has('--legacy'), requireBilingual: flags.has('--bilingual') });
+ if (!receipt.validation.ok)
+ return receipt;
+ const architecture = map;
+ receipt.map = { mapId: architecture.mapId, revision: architecture.revision };
+ let constraints;
+ if (catalogFile && rulesFile && repository) {
+ receipt.stage = 'compile';
+ constraints = compileConstraintRules(readJson(catalogFile), readJson(rulesFile));
+ receipt.stage = 'history';
+ constraints = collectRuleHistory(constraints, repository);
+ }
+ else if (constraintsFile) {
+ receipt.stage = 'inputs';
+ constraints = readJson(constraintsFile);
+ if (!constraints || !Array.isArray(constraints.rules) || !constraints.ruleReview)
+ throw new Error('--constraints requires a reviewed catalog.');
+ }
+ // Prepare every artifact before any output write, including source-page validation.
+ receipt.stage = 'render';
+ const html = renderArchitecture(map, events, { ...(repository ? { repository } : {}),
+ ...(constraints ? { constraintCatalog: constraints, constraintSourceHref: encodeURIComponent(path.basename(receipt.outputs.sources)) } : {}) });
+ const artifacts = new Map();
+ if (constraints) {
+ artifacts.set(receipt.outputs.sources, renderConstraintCatalog(constraints, undefined, { view: 'sources' }));
+ if (receipt.outputs.constraints)
+ artifacts.set(receipt.outputs.constraints, JSON.stringify(constraints, null, 2) + '\n');
+ receipt.constraints = { rules: constraints.rules.length, snapshot: constraints.project.revision, scope: constraints.ruleReview.scope,
+ historyGaps: constraints.rules.filter(rule => rule.history?.status !== 'tracked').map(rule => rule.id) };
+ }
+ artifacts.set(htmlOutput, html);
+ receipt.stage = 'write';
+ const pending = new Map();
+ try {
+ for (const [target, content] of artifacts) {
+ fs.mkdirSync(path.dirname(target), { recursive: true });
+ const temporary = `${target}.${randomUUID()}.tmp`;
+ pending.set(target, temporary);
+ fs.writeFileSync(temporary, content, { flag: 'wx' });
+ }
+ // Publish the page last. Multiple renames are not a filesystem transaction;
+ // report completed paths if an I/O failure interrupts publication.
+ for (const [target, temporary] of pending) {
+ fs.renameSync(temporary, target);
+ receipt.written.push(target);
+ }
+ }
+ finally {
+ for (const temporary of pending.values()) {
+ try {
+ fs.rmSync(temporary, { force: true });
+ }
+ catch { /* Preserve the primary I/O error. */ }
+ }
+ }
+ receipt.ok = true;
+ receipt.stage = 'complete';
+ }
+ catch (error) {
+ receipt.error = error instanceof Error ? error.message : String(error);
+ }
+ return receipt;
+}
diff --git a/src/birdview.mts b/src/birdview.mts
index 2a91094..5f6c349 100644
--- a/src/birdview.mts
+++ b/src/birdview.mts
@@ -13,7 +13,12 @@ const end = '';
try {
const args = process.argv.slice(2);
const command = args.shift();
- if (command === 'doctor') {
+ if (command === 'deliver') {
+ const { deliver } = await import('./deliver.mjs');
+ const receipt = deliver(args);
+ console.log(JSON.stringify(receipt, null, 2));
+ process.exitCode = receipt.ok ? 0 : 1;
+ } else if (command === 'doctor') {
if (args.length) throw new Error('Usage: birdview doctor');
// Keep the invocation path: resolving import.meta.url would hide broken
// entry-point detection in an installation reached through a directory link.
@@ -52,7 +57,7 @@ try {
console.log(JSON.stringify(report, null, 2));
process.exit(0);
}
- if (!command || !['mode', 'setup', 'uninstall'].includes(command)) throw new Error(usage + '\n birdview doctor');
+ if (!command || !['mode', 'setup', 'uninstall'].includes(command)) throw new Error(usage + '\n birdview doctor\n birdview deliver architecture.json project.html [activity.jsonl] (--catalog sources.json --rules reviewed-rules.json --repo root | --constraints reviewed.json | --architecture-only) [--bilingual] [--legacy]');
let mode: string | undefined;
if (command === 'mode' && args[0] && !args[0].startsWith('--')) mode = args.shift();
let root = process.cwd();
diff --git a/src/check-pr.mts b/src/check-pr.mts
new file mode 100644
index 0000000..27ac8ac
--- /dev/null
+++ b/src/check-pr.mts
@@ -0,0 +1,27 @@
+import { spawnSync } from 'node:child_process';
+
+// Keep the pre-PR checklist in one cross-platform command. It intentionally
+// checks the tracked demo after regeneration so generated drift cannot hide in
+// a passing source-only test run.
+const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
+const checks: Array<[string, string, string[]]> = [
+ ['TypeScript', npm, ['run', 'typecheck']],
+ ['Build artifacts', npm, ['run', 'check:build']],
+ ['Unit tests', npm, ['test']],
+ ['Browser tests', npm, ['run', 'test:browser']],
+ ['Tracked demo regeneration', npm, ['run', 'build:demo']],
+ ['Tracked demo diff', 'git', ['diff', '--exit-code', '--', 'examples/harness-activity.html']],
+ ['Example validation', npm, ['run', 'validate:examples']],
+ ['Documentation links and hashes', 'node', ['scripts/check-docs.mjs']],
+];
+
+for (const [label, command, args] of checks) {
+ console.log(`\n[check:pr] ${label}`);
+ const result = spawnSync(command, args, { stdio: 'inherit', windowsHide: true, shell: command === npm && process.platform === 'win32' });
+ if (result.error) throw result.error;
+ if (result.status !== 0) {
+ console.error(`[check:pr] failed: ${label}`);
+ process.exit(result.status ?? 1);
+ }
+}
+console.log('\n[check:pr] all checks passed');
diff --git a/src/deliver.mts b/src/deliver.mts
new file mode 100644
index 0000000..f62be76
--- /dev/null
+++ b/src/deliver.mts
@@ -0,0 +1,139 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { randomUUID } from 'node:crypto';
+import { validate, type ValidationResult } from './validate.mjs';
+import { compileConstraintRules } from './compile-constraint-rules.mjs';
+import { collectRuleHistory } from './constraint-rule-history.mjs';
+import { renderArchitecture } from './render.mjs';
+import { renderConstraintCatalog } from './render-constraints.mjs';
+import type { Architecture } from './contracts/models.mjs';
+import type { ConstraintCatalog, ReviewedConstraintCatalog, ReviewedSelection } from './constraint-types.mjs';
+
+export interface DeliveryReceipt {
+ ok: boolean;
+ stage: 'inputs' | 'validate' | 'compile' | 'history' | 'render' | 'write' | 'complete';
+ outputs: { html: string; sources?: string; constraints?: string } | null;
+ written: string[];
+ validation?: ValidationResult;
+ map?: { mapId: string; revision: number };
+ constraints?: { rules: number; snapshot: string; scope: string; historyGaps: string[] };
+ visualReview: 'not-performed';
+ implementationVerification: 'unverified';
+ error?: string;
+}
+
+export const deliveryUsage = 'Usage: birdview deliver architecture.json project.html [activity.jsonl] (--catalog sources.json --rules reviewed-rules.json --repo root | --constraints reviewed.json | --architecture-only) [--repo root] [--bilingual] [--legacy]';
+
+// Resolve existing parents too, so a linked output directory cannot alias input.
+function canonical(file: string): string {
+ if (!fs.existsSync(file) && path.dirname(file) === file) throw new Error(`Filesystem root does not exist: ${file}`);
+ const resolved = fs.existsSync(file) ? fs.realpathSync(file) : path.join(canonical(path.dirname(file)), path.basename(file));
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
+}
+
+/** Combine deterministic delivery steps; never infer rule semantics or visual acceptance. */
+export function deliver(args: readonly string[]): DeliveryReceipt {
+ const receipt: DeliveryReceipt = { ok: false, stage: 'inputs', outputs: null, written: [],
+ visualReview: 'not-performed', implementationVerification: 'unverified' };
+ try {
+ const values = new Map();
+ const flags = new Set();
+ const positional: string[] = [];
+ for (let index = 0; index < args.length; index++) {
+ const arg = args[index]!;
+ if (!arg.startsWith('--')) { positional.push(arg); continue; }
+ if (values.has(arg) || flags.has(arg)) throw new Error(`Duplicate option: ${arg}`);
+ if (['--architecture-only', '--bilingual', '--legacy'].includes(arg)) flags.add(arg);
+ else if (['--catalog', '--rules', '--repo', '--constraints'].includes(arg)) {
+ const value = args[++index];
+ if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value.`);
+ values.set(arg, path.resolve(value));
+ } else throw new Error(`Unknown option: ${arg}. ${deliveryUsage}`);
+ }
+ const [input, output, activity] = positional;
+ if (!input || !output || positional.length > 3) throw new Error(deliveryUsage);
+ if (path.extname(output).toLowerCase() !== '.html') throw new Error('Output must be an .html file.');
+ const catalogFile = values.get('--catalog'), rulesFile = values.get('--rules');
+ const constraintsFile = values.get('--constraints'), repository = values.get('--repo');
+ const compile = !!(catalogFile || rulesFile);
+ if (Number(compile) + Number(!!constraintsFile) + Number(flags.has('--architecture-only')) !== 1
+ || (compile && (!catalogFile || !rulesFile || !repository))) throw new Error(deliveryUsage);
+ if (repository && !fs.statSync(repository).isDirectory()) throw new Error('--repo must be a directory.');
+ const htmlOutput = path.resolve(output);
+ receipt.outputs = { html: htmlOutput,
+ ...(!flags.has('--architecture-only') ? { sources: htmlOutput.replace(/\.html$/i, '.sources.html') } : {}),
+ ...(compile ? { constraints: htmlOutput.replace(/\.html$/i, '.constraints.json') } : {}) };
+ const outputs = Object.values(receipt.outputs);
+ const inputs = [input, activity, catalogFile, rulesFile, constraintsFile].filter((file): file is string => !!file).map(file => path.resolve(file));
+ for (const target of outputs) {
+ if (fs.existsSync(target) && !fs.lstatSync(target).isFile()) throw new Error(`Output must be a regular file: ${target}`);
+ const targetStat = fs.existsSync(target) ? fs.statSync(target) : undefined;
+ for (const source of inputs) {
+ const sourceStat = fs.statSync(source);
+ if (canonical(target) === canonical(source) || (targetStat && targetStat.ino !== 0 && targetStat.dev === sourceStat.dev && targetStat.ino === sourceStat.ino))
+ throw new Error(`Output aliases input: ${target}`);
+ }
+ }
+ const readJson = (file: string): unknown => {
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
+ catch (error) { throw new Error(`Cannot read JSON ${file}: ${error instanceof Error ? error.message : String(error)}`); }
+ };
+ const map = readJson(input);
+ const events: unknown[] = activity ? fs.readFileSync(activity, 'utf8').split(/\r?\n/).filter(line => line.trim()).map((line, index): unknown => {
+ try { return JSON.parse(line); } catch { throw new Error(`Invalid JSON in activity record ${index + 1}.`); }
+ }) : [];
+ receipt.stage = 'validate';
+ receipt.validation = validate(map, events, { requireRoles: !flags.has('--legacy'), requireBilingual: flags.has('--bilingual') });
+ if (!receipt.validation.ok) return receipt;
+ const architecture = map as Architecture;
+ receipt.map = { mapId: architecture.mapId, revision: architecture.revision };
+ let constraints: ReviewedConstraintCatalog | undefined;
+ if (catalogFile && rulesFile && repository) {
+ receipt.stage = 'compile';
+ constraints = compileConstraintRules(readJson(catalogFile) as ConstraintCatalog, readJson(rulesFile) as ReviewedSelection);
+ receipt.stage = 'history';
+ constraints = collectRuleHistory(constraints, repository);
+ } else if (constraintsFile) {
+ receipt.stage = 'inputs';
+ constraints = readJson(constraintsFile) as ReviewedConstraintCatalog;
+ if (!constraints || !Array.isArray(constraints.rules) || !constraints.ruleReview) throw new Error('--constraints requires a reviewed catalog.');
+ }
+ // Prepare every artifact before any output write, including source-page validation.
+ receipt.stage = 'render';
+ const html = renderArchitecture(map, events, { ...(repository ? { repository } : {}),
+ ...(constraints ? { constraintCatalog: constraints, constraintSourceHref: encodeURIComponent(path.basename(receipt.outputs.sources!)) } : {}) });
+ const artifacts = new Map();
+ if (constraints) {
+ artifacts.set(receipt.outputs.sources!, renderConstraintCatalog(constraints, undefined, { view: 'sources' }));
+ if (receipt.outputs.constraints) artifacts.set(receipt.outputs.constraints, JSON.stringify(constraints, null, 2) + '\n');
+ receipt.constraints = { rules: constraints.rules.length, snapshot: constraints.project.revision, scope: constraints.ruleReview.scope,
+ historyGaps: constraints.rules.filter(rule => rule.history?.status !== 'tracked').map(rule => rule.id) };
+ }
+ artifacts.set(htmlOutput, html);
+ receipt.stage = 'write';
+ const pending = new Map();
+ try {
+ for (const [target, content] of artifacts) {
+ fs.mkdirSync(path.dirname(target), { recursive: true });
+ const temporary = `${target}.${randomUUID()}.tmp`;
+ pending.set(target, temporary);
+ fs.writeFileSync(temporary, content, { flag: 'wx' });
+ }
+ // Publish the page last. Multiple renames are not a filesystem transaction;
+ // report completed paths if an I/O failure interrupts publication.
+ for (const [target, temporary] of pending) {
+ fs.renameSync(temporary, target);
+ receipt.written.push(target);
+ }
+ } finally {
+ for (const temporary of pending.values()) {
+ try { fs.rmSync(temporary, { force: true }); } catch { /* Preserve the primary I/O error. */ }
+ }
+ }
+ receipt.ok = true;
+ receipt.stage = 'complete';
+ } catch (error) {
+ receipt.error = error instanceof Error ? error.message : String(error);
+ }
+ return receipt;
+}
diff --git a/src/viewer/main.mts b/src/viewer/main.mts
index dfadfdd..2036739 100644
--- a/src/viewer/main.mts
+++ b/src/viewer/main.mts
@@ -983,10 +983,14 @@ guideDialog.setAttribute('aria-labelledby', 'guide-title');
guideDialog.setAttribute('aria-describedby', 'guide-copy');
guideDialog.innerHTML = '
';
document.body.append(guideDialog);
-type GuideStep = 'architecture' | 'activity' | 'compare' | 'details' | 'history';
-const guideSteps: GuideStep[] = activityEvents.length ? ['architecture', 'activity', 'compare', 'details', 'history'] : ['architecture', 'details'];
+type GuideStep = 'architecture' | 'constraints' | 'activity' | 'compare' | 'details' | 'history';
+const hasConstraintGuide = Boolean(DATA.constraintView);
+const guideSteps: GuideStep[] = activityEvents.length
+ ? ['architecture', ...(hasConstraintGuide ? ['constraints' as const] : []), 'activity', 'compare', 'details', 'history']
+ : ['architecture', ...(hasConstraintGuide ? ['constraints' as const] : []), 'details'];
const guideCopy: Record = {
architecture: ['完整架构', '了解系统有哪些模块,以及它们如何连接。分组底色表示职责类别,不表示修改状态。', 'Architecture', 'See the system modules and their connections. Group backgrounds classify responsibilities, not change status.'],
+ constraints: ['查看约束', '约束视图把已审查的规则按主题和来源展开;颜色表示适用角色,不代表通过或失败。点击规则可阅读适用条件、解释、验证方式和原文依据。', 'Inspect constraints', 'The constraints view groups reviewed rules by topic and source. Colors show applicable roles, not pass or fail. Select a rule to read its condition, explanation, verification and source evidence.'],
activity: ['本次修改', '亮起的是所选步骤的目标,灰色模块不是当前目标;验证阶段的亮起表示验证目标。终态不再高亮目标。', 'Current changes', 'Bright modules are targets of the selected step; gray modules are not. During verification, highlights mean verification targets. Terminal steps clear highlights.'],
compare: ['同时对照', '完整架构与更改视图并排展示,选择、缩放和滚动保持联动。窄屏时上下排列。', 'Compare views', 'Compare architecture and changes with linked selection, zoom and scrolling. Narrow screens stack the views.'],
details: ['查看依据', '点击模块可查看职责、文件归属与源码证据。悬浮模块可追踪直接连接,工具栏可切换全部关系或适配全图。', 'Inspect evidence', 'Select a module for responsibilities, file ownership and source evidence. Hover to trace direct connections; use the toolbar for all relations or fit to view.'],
@@ -996,13 +1000,14 @@ let guideIndex = 0;
interface GuideSaved {
mode: ViewMode; index: number; selected: string | undefined; inspector: boolean; zoom: number; fitting: boolean; disclosure: boolean;
focus: Element | null; x: number; y: number; panes: [HTMLElement, number, number][];
- constraints: {open: boolean; selected: string | undefined; filter: string};
+ constraints: {open: boolean; selected: string | undefined; filter: string}; projectView: 'architecture' | 'constraints';
}
let guideSaved: GuideSaved | undefined;
let guideTarget: HTMLElement | undefined;
let guideFrame = 0;
const guideViewState: Record = {
architecture: { mode: 'architecture', inspector: false, history: false },
+ constraints: { mode: 'architecture', inspector: false, history: false },
activity: { mode: 'activity', inspector: false, history: false },
compare: { mode: 'compare', inspector: false, history: false },
details: { mode: 'architecture', inspector: true, history: false },
@@ -1057,6 +1062,10 @@ function showGuideStep(animate = false) {
const saved = required(guideSaved);
const step = required(guideSteps[guideIndex]);
const state = guideViewState[step];
+ if (DATA.constraintView) {
+ const viewButton = element(query(`#project-views button:nth-child(${step === 'constraints' ? 2 : 1})`), HTMLButtonElement);
+ viewButton.click();
+ }
hoveredModuleId = undefined;
setInspector(state.inspector);
activityMode = state.mode;
@@ -1070,7 +1079,8 @@ function showGuideStep(animate = false) {
updateFlow();
updateZoom();
if (step === 'details') select(map.modules.find(module => module.id === saved.selected) || required(map.modules[0]));
- guideTarget = step === 'details' ? inspector : step === 'history' ? activityPanel : step === 'compare' ? $('activity-mode') : step === 'activity' ? viewport : activityEvents.length ? query('[data-view="architecture"]') : viewport;
+ guideTarget = step === 'constraints' ? (query('#project-views') || query('#show-constraints'))
+ : step === 'details' ? inspector : step === 'history' ? activityPanel : step === 'compare' ? $('activity-mode') : step === 'activity' ? viewport : activityEvents.length ? query('[data-view="architecture"]') : viewport;
guideTarget.scrollIntoView({ block: 'nearest', behavior: 'instant' });
guideLabels();
positionGuide();
@@ -1093,7 +1103,7 @@ function dismissGuideInvite() {
function startGuide() {
if (guideDialog.open) return;
dismissGuideInvite();
- guideSaved = { mode: activityMode, index: activityIndex, selected: selectedModuleId, inspector: workspace.classList.contains('inspector-open'), zoom, fitting, disclosure: $('activity-disclosure').open, focus: document.activeElement, x: scrollX, y: scrollY, constraints: {open: constraintPanelOpen, selected: selectedConstraintId, filter: constraintFilter}, panes: [...mapPanes.querySelectorAll('.map-scroll')].map(el => [el, el.scrollLeft, el.scrollTop]) };
+ guideSaved = { mode: activityMode, index: activityIndex, selected: selectedModuleId, inspector: workspace.classList.contains('inspector-open'), zoom, fitting, disclosure: $('activity-disclosure').open, focus: document.activeElement, x: scrollX, y: scrollY, constraints: {open: constraintPanelOpen, selected: selectedConstraintId, filter: constraintFilter}, projectView: new URLSearchParams(location.hash.slice(1)).get('view') === 'constraints' ? 'constraints' : 'architecture', panes: [...mapPanes.querySelectorAll('.map-scroll')].map(el => [el, el.scrollLeft, el.scrollTop]) };
guideIndex = 0;
constraintPanelOpen = false;
@@ -1106,6 +1116,7 @@ function finishGuide() {
guideAnimation = undefined;
guideDialog.close();
const saved = required(guideSaved);
+ if (DATA.constraintView) element(query(`#project-views button:nth-child(${saved.projectView === 'constraints' ? 2 : 1})`), HTMLButtonElement).click();
activityMode = saved.mode;
activityIndex = saved.index;
constraintPanelOpen = saved.constraints.open;
diff --git a/test/deliver.test.mts b/test/deliver.test.mts
new file mode 100644
index 0000000..e62a4f9
--- /dev/null
+++ b/test/deliver.test.mts
@@ -0,0 +1,181 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { execFileSync, spawnSync } from 'node:child_process';
+import { createHash } from 'node:crypto';
+import { deliver, type DeliveryReceipt } from '../src/deliver.mjs';
+import { discoverConstraints } from '../src/discover-constraints.mjs';
+import { renderArchitecture } from '../src/render.mjs';
+import { renderConstraintCatalog } from '../src/render-constraints.mjs';
+import type { Architecture } from '../src/contracts/models.mjs';
+import type { ReviewedConstraintCatalog } from '../src/constraint-types.mjs';
+
+const root = fileURLToPath(new URL('../', import.meta.url));
+const example = path.join(root, 'examples/architecture.json');
+const activity = path.join(root, 'examples/activity.jsonl');
+const cli = path.join(root, 'scripts/birdview.mjs');
+// Freshness records the invocation time; every other rendered byte must match.
+const htmlDigest = (html: string) => createHash('sha256').update(html.replace(/("constraintFreshness":{"checkedAt":)"[^"]+"/, '$1""')).digest('hex');
+
+test('delivery CLI validates strict authoring and activity and keeps existing rendering', t => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'birdview-deliver-'));
+ t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
+ const output = path.join(dir, 'nested/map.html');
+ const run = (...args: string[]) => spawnSync(process.execPath, [cli, 'deliver', ...args], { cwd: dir, encoding: 'utf8' });
+ const result = run(example, output, activity, '--architecture-only');
+ assert.equal(result.status, 0, result.stderr + result.stdout);
+ const report = JSON.parse(result.stdout) as DeliveryReceipt;
+ assert.equal(report.ok, true);
+ assert.equal(report.stage, 'complete');
+ assert.equal(report.visualReview, 'not-performed');
+ assert.equal(report.implementationVerification, 'unverified');
+ assert.deepEqual(report.written, [output]);
+ const map = JSON.parse(fs.readFileSync(example, 'utf8')) as Architecture;
+ const events = fs.readFileSync(activity, 'utf8').trim().split(/\r?\n/).map(line => JSON.parse(line));
+ const expected = renderArchitecture(map, events);
+ assert.equal(fs.readFileSync(output, 'utf8'), expected);
+ const bilingual = run(example, output, '--architecture-only', '--bilingual');
+ assert.equal(bilingual.status, 1);
+ assert.equal((JSON.parse(bilingual.stdout) as DeliveryReceipt).stage, 'validate');
+ assert.equal(fs.readFileSync(output, 'utf8'), expected);
+ const bilingualMap = path.join(root, 'examples/system.architecture.json');
+ const translated = run(bilingualMap, output, '--architecture-only', '--bilingual', '--legacy');
+ assert.equal(translated.status, 0, translated.stdout + translated.stderr);
+
+ const legacy = path.join(dir, 'legacy.json');
+ map.modules.forEach(module => { delete module.role; delete module.roleAssessment; });
+ fs.writeFileSync(legacy, JSON.stringify(map));
+ const rejected = deliver([legacy, output, '--architecture-only']);
+ assert.equal(rejected.stage, 'validate');
+ assert.ok(rejected.validation?.errors.some(error => error.code === 'role/required'));
+ const accepted = deliver([legacy, output, '--architecture-only', '--legacy']);
+ assert.equal(accepted.ok, true, accepted.error);
+ assert.ok(accepted.validation && 'warnings' in accepted.validation && accepted.validation.warnings.some(warning => warning.code === 'role/all-generic-review'));
+});
+
+test('delivery compiles reviewed rules, collects real history and reuses catalogs without changing the view', t => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'birdview-deliver-rules-'));
+ t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
+ const git = (...args: string[]) => execFileSync('git', ['-C', dir, ...args], { encoding: 'utf8', windowsHide: true });
+ fs.writeFileSync(path.join(dir, 'AGENTS.md'), '# Rules\n\nRun the relevant checks.\n');
+ git('init', '-q'); git('add', 'AGENTS.md');
+ git('-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.invalid', '-c', 'commit.gpgsign=false', 'commit', '-qm', 'fixture');
+ const map = JSON.parse(fs.readFileSync(example, 'utf8')) as Architecture;
+ const catalog = discoverConstraints(dir, { title: map.project.name });
+ const selection = { revision: catalog.project.revision, scope: 'Root test instruction',
+ architectureBinding: { mapId: map.mapId, mapRevision: map.revision, sourceRevision: catalog.project.revision },
+ groups: [{ sourcePath: 'AGENTS.md', category: 'testing', rules: [{ id: 'check', name: 'Run relevant checks',
+ anchor: 'Run the relevant checks.', condition: 'After changes', explanation: 'Detect regressions.', verification: 'Read command results.' }] }] };
+ const source = path.join(dir, 'catalog.json'), rules = path.join(dir, 'rules.json');
+ const output = path.join(dir, 'project.html');
+ const args = [example, output, '--catalog', source, '--rules', rules, '--repo', dir];
+ fs.writeFileSync(source, JSON.stringify(catalog));
+ fs.writeFileSync(rules, JSON.stringify(selection));
+ const report = deliver(args);
+ assert.equal(report.ok, true, report.error);
+ assert.deepEqual(report.constraints?.historyGaps, []);
+ assert.equal(report.constraints?.rules, 1);
+ assert.equal(report.written.length, 3);
+ const compiled = JSON.parse(fs.readFileSync(report.outputs!.constraints!, 'utf8')) as ReviewedConstraintCatalog;
+ assert.equal(compiled.ruleReview.implementationVerification, 'unverified');
+ assert.equal(compiled.rules[0]!.history?.status, 'tracked');
+ const expected = renderArchitecture(map, [], { repository: dir, constraintCatalog: compiled, constraintSourceHref: 'project.sources.html' });
+ assert.equal(htmlDigest(fs.readFileSync(output, 'utf8')), htmlDigest(expected));
+ assert.equal(fs.readFileSync(report.outputs!.sources!, 'utf8'), renderConstraintCatalog(compiled, undefined, { view: 'sources' }));
+ assert.equal(fs.readFileSync(source, 'utf8'), JSON.stringify(catalog));
+ assert.equal(fs.readFileSync(rules, 'utf8'), JSON.stringify(selection));
+ assert.equal(deliver([example, output, '--constraints', report.outputs!.constraints!, '--repo', dir]).ok, true);
+ assert.equal(htmlDigest(fs.readFileSync(output, 'utf8')), htmlDigest(expected));
+
+ const saved = new Map(report.written.map(file => [file, fs.readFileSync(file, 'utf8')]));
+ const unchanged = () => saved.forEach((content, file) => assert.equal(fs.readFileSync(file, 'utf8'), content));
+ selection.groups[0]!.rules[0]!.anchor = 'Missing anchor';
+ fs.writeFileSync(rules, JSON.stringify(selection));
+ assert.equal(deliver(args).stage, 'compile'); unchanged();
+ selection.groups[0]!.rules[0]!.anchor = 'Run the relevant checks.';
+ selection.architectureBinding.mapRevision++;
+ fs.writeFileSync(rules, JSON.stringify(selection));
+ assert.match(deliver(args).error!, /Stale architecture binding/); unchanged();
+ selection.architectureBinding.mapRevision--;
+ fs.writeFileSync(rules, JSON.stringify(selection));
+ catalog.sources[0]!.text += '\nChanged snapshot text';
+ fs.writeFileSync(source, JSON.stringify(catalog));
+ assert.equal(deliver(args).stage, 'history'); unchanged();
+
+ delete compiled.rules[0]!.history;
+ fs.writeFileSync(rules, JSON.stringify(compiled));
+ const untracked = deliver([example, output, '--constraints', rules]);
+ assert.equal(untracked.ok, true, untracked.error);
+ assert.deepEqual(untracked.constraints?.historyGaps, ['check']);
+
+ const previousPage = fs.readFileSync(output, 'utf8');
+ const rename = fs.renameSync;
+ t.mock.method(fs, 'renameSync', (from: fs.PathLike, to: fs.PathLike) => {
+ if (to === output) throw new Error('Simulated publication failure');
+ rename(from, to);
+ });
+ const interrupted = deliver([example, output, '--constraints', rules]);
+ assert.equal(interrupted.ok, false);
+ assert.equal(interrupted.stage, 'write');
+ assert.match(interrupted.error!, /publication failure/);
+ assert.deepEqual(interrupted.written, [report.outputs!.sources!]);
+ assert.equal(fs.readFileSync(output, 'utf8'), previousPage);
+ assert.ok(!fs.readdirSync(dir).some(file => file.endsWith('.tmp')));
+});
+
+test('invalid options, JSON, event bindings, and path aliases preserve output and inputs', t => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'birdview-deliver-invalid-'));
+ t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
+ const output = path.join(dir, 'project.html'), bad = path.join(dir, 'bad.json');
+ fs.writeFileSync(output, 'existing page');
+ fs.writeFileSync(bad, '{broken}');
+ const cases = [[], [example, output], [example, output, '--wat'],
+ [example, output, '--architecture-only', '--architecture-only'],
+ [example, output, '--architecture-only', '--constraints', bad],
+ [example, output, '--catalog', bad, '--rules', bad],
+ [bad, output, '--architecture-only'], [example, output, bad, '--architecture-only'],
+ [example, output, '--constraints', bad], [example, output, '--repo'],
+ [example, output, output, '--architecture-only'], [output, output, '--architecture-only']];
+ for (const args of cases) {
+ const failed = deliver(args);
+ assert.equal(failed.ok, false, JSON.stringify(args));
+ assert.deepEqual(failed.written, []);
+ assert.equal(fs.readFileSync(output, 'utf8'), 'existing page');
+ }
+ fs.writeFileSync(bad, 'null');
+ assert.equal(deliver([bad, output, '--architecture-only']).stage, 'validate');
+ assert.equal(deliver([bad, output, '--architecture-only', '--legacy']).stage, 'validate');
+ assert.match(deliver([example, output, '--constraints', bad]).error!, /reviewed catalog/);
+ const events = fs.readFileSync(activity, 'utf8').trim().split(/\r?\n/).map(line => JSON.parse(line));
+ events[0].mapRevision = 999;
+ fs.writeFileSync(bad, events.map(event => JSON.stringify(event)).join('\n'));
+ assert.equal(deliver([example, output, bad, '--architecture-only']).stage, 'validate');
+ assert.equal(fs.readFileSync(output, 'utf8'), 'existing page');
+ const alias = path.join(dir, 'input.json');
+ fs.linkSync(output, alias);
+ assert.match(deliver([alias, output, '--architecture-only']).error!, /aliases input/);
+ const sidecar = path.join(dir, 'project.sources.html');
+ fs.copyFileSync(example, sidecar);
+ assert.match(deliver([sidecar, output, '--constraints', bad]).error!, /aliases input/);
+ assert.equal(fs.readFileSync(sidecar, 'utf8'), fs.readFileSync(example, 'utf8'));
+ fs.rmSync(sidecar); fs.mkdirSync(sidecar);
+ assert.match(deliver([example, output, '--constraints', bad]).error!, /regular file/);
+ assert.equal(fs.readFileSync(output, 'utf8'), 'existing page');
+});
+
+test('delivery rejects input aliases through linked parent directories', t => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'birdview-deliver-link-'));
+ t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
+ const actual = path.join(dir, 'actual'), linked = path.join(dir, 'linked');
+ fs.mkdirSync(actual);
+ fs.symlinkSync(actual, linked, process.platform === 'win32' ? 'junction' : 'dir');
+ const input = path.join(actual, 'map.html');
+ fs.copyFileSync(example, input);
+ const report = deliver([input, path.join(linked, 'map.html'), '--architecture-only']);
+ assert.equal(report.ok, false);
+ assert.match(report.error!, /aliases input/);
+ assert.equal(fs.readFileSync(input, 'utf8'), fs.readFileSync(example, 'utf8'));
+});