dsh-pictor 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/NOTICE.md ADDED
@@ -0,0 +1,18 @@
1
+ # NOTICE — 第三方内容声明
2
+
3
+ 本仓库包含以下源自第三方的内容,请在分发或再使用时留意相应许可:
4
+
5
+ ## references/paper/
6
+
7
+ - `2601.23265v1.md`:arXiv 预印本 2601.23265v1 的正文提取(仅文本,不含 PDF),
8
+ 作为研究背景资料随源码仓库分发。
9
+ - `paperbanana-prompts.md`:同一 arXiv 预印本附录 G、H 中提示词原文的忠实提取与中文翻译。
10
+
11
+ 两者均**不随发布包分发**(package.json 的 files 仅含 `references/domain`),
12
+ 只存在于源码仓库。再分发请以 arXiv 原文及其作者声明的许可为准。
13
+
14
+ ## 免责声明
15
+
16
+ 除上述文件外,本仓库其余内容(agents/ 蓝图、references/domain/ 模板与规范、
17
+ src/、fixtures/ 示例文档)均为作者原创。样式与布局模板的早期版本
18
+ 曾参考 PaperBanana(arXiv:2601.23265v1)的设计思路,但经重写已不包含其原文。
package/README.md CHANGED
@@ -1,81 +1,87 @@
1
- # Pictor — 文档转信息图工作台
1
+ # Pictor — Document-to-Infographic Workbench
2
2
 
3
- <p align="right"><i>English below.</i></p>
3
+ [ EN | [中文](./README.zh-cn.md) ]
4
4
 
5
- [源码仓库](https://github.com/bandung-circuits/pictor) · [问题与讨论](https://github.com/bandung-circuits/pictor/issues)
5
+ Pictor turns a document into a set of infographics. It is a [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) plugin published to npm as [`dsh-pictor`](https://www.npmjs.com/package/dsh-pictor), with source at [github.com/bandung-circuits/pictor](https://github.com/bandung-circuits/pictor). Each project is one coherent agent session that carries the whole extract → advise → render flow; the GUI is a thin layer that only shows file-fact state and shapes your input. Design decisions live in [docs/DESIGN.zh.md](docs/DESIGN.zh.md) (Chinese).
6
6
 
7
- Pictor 把一份文档变成一组信息图。它是 DeepSeek Harness(DSH)的插件:每新建一个项目,宿主创建一个连贯的会话作为项目会话,整个「提取 → 方案 → 渲染」流程都在这个会话里推进,GUI 只做两件事:查看信息、规整输入。设计文档见 [docs/DESIGN.zh.md](docs/DESIGN.zh.md)。
7
+ ![Pictor empty workbench](docs/screenshot-empty.png)
8
8
 
9
- ## 形态
9
+ ## Installation
10
10
 
11
- - 安装后首次使用自动生成 `~/.pictor` 工作区(注册为 dsh 工作区,侧栏归组见「Pictor」)。
12
- - 唯一入口是 dsh 左下角 footer 按钮「Pictor」,打开 shell.overlay 工作台:左栏列项目,右栏显示当前项目。
13
- - 项目步骤固定三步:提取结构 → 方案设计 → 渲染出图。每步由你做决定后,会话在同一会话里接着执行(agent-loop 的 resume 语义),不重跑已有阶段。
14
- - 项目名取文档首行,信息条内随时可改名,只动 index.json。
15
- - 状态只由文件事实驱动;运行中判定以 dsh agent 注册表为权威;把你对 orchestration 的关心都留给会话。
11
+ Pictor is published to npm as `dsh-pictor`:
16
12
 
17
- ## 两个模型
13
+ ```bash
14
+ dsh plugin --profile <profile> add dsh-pictor
15
+ ```
18
16
 
19
- - 推理模型:extract 与 advise 用 dsh 当前默认模型,无需在 Pictor 配置。
20
- - 画图模型:在工作台「设置」里配置(seedream / gemini / openai-compatible / mock)。密钥经 dsh 凭据子系统保存,配置只留引用。
17
+ Replace `<profile>` with the target profile name (e.g. `desktop`, `web`) and start that profile. A **Pictor** button appears at the bottom-left of the dsh footer. On first use a `~/.pictor` home is provisioned and registered as a DSH workspace; project sessions group under "Pictor" in the sidebar. For development installs (local link, live-reload while editing source) see [docs/DESIGN.zh.md](docs/DESIGN.zh.md).
21
18
 
22
- ## 安装
19
+ **For non-technical users**: install [DSH Desktop](https://dshdesktop.com/en/) (a community project, not an official DeepSeek product; it bundles the DeepSeek Harness runtime as a ready-to-run desktop app) and let the agent install the plugin for you — just send this one message in dsh:
23
20
 
24
- Pictor 以 npm 包 `dsh-pictor` 发布:
21
+ > Install the Pictor plugin from the npm package `dsh-pictor` into the current profile, restart if needed, and confirm when a Pictor button appears at the bottom left.
25
22
 
26
- ```bash
27
- dsh plugin --profile <profile> add dsh-pictor
28
- ```
23
+ ## Supported dsh version
24
+
25
+ Pictor is built and tested against **DSH Desktop 0.7.2** (bundled Harness **0.1.2-alpha.1**). The DeepSeek Harness client API moves quickly — the workspace/session services it exposes keep changing — so this plugin intentionally tracks one harness generation. Other versions (older or newer) may work or may break without notice, and are outside our support scope. If something stops working after a DSH update, update dsh to the version above rather than debugging the mismatch.
26
+
27
+ ## Usage
29
28
 
30
- 重启 profile 后,footer 左下角出现「Pictor」按钮。开发安装(改动源码实时联调)见 [docs/DESIGN.zh.md](docs/DESIGN.zh.md)。
29
+ 1. Open the workbench via the **Pictor** footer button and click **New project**: upload a file (`md` / `txt` / `docx` / `pdf` / image) or paste content (from Word or the web; formatting is kept, stored as sanitised HTML).
30
+ 2. The session reads and normalises the source (`docx` / `pdf` / image / HTML → `document.md` via DSH tooling) and proposes candidate structures.
31
+ 3. Tick the structures and confirm; the session moves to proposal design. Tick a proposal, pick an aspect ratio, and click **Render selected**.
32
+ 4. Preview and download from the result grid; open the discussion panel to talk to the session at any point, or use the step bar to go back and redo existing artifacts.
31
33
 
32
- ## 使用
34
+ ![Pictor workbench with a project in progress](docs/screenshot-workbench.png)
33
35
 
34
- 1. 打开工作台,点「新建项目」:上传文件(md/txt/docx/pdf/图片)或粘贴内容(从 Word/网页粘贴,格式保留,落盘为消毒 HTML)。
35
- 2. 会话读取并规整来源(docx/pdf/图片/HTML → document.md 由会话用 dsh 工具完成),提取候选结构。
36
- 3. 勾选结构,确认,会话进入方案设计;勾选方案,设画面比例,点「渲染所选方案」。
37
- 4. 结果网格里预览、下载;随时可展开讨论面板对会话说话,或点步骤条回到已有产物的步骤重做。
36
+ ## Form
38
37
 
39
- ## 开发与验证
38
+ - A `~/.pictor` home is created on first use (registered as a DSH workspace; sidebar shows "Pictor").
39
+ - The only entry point is the bottom-left **Pictor** footer button, which toggles a `shell.overlay` workbench: projects on the left, the selected project on the right.
40
+ - Every project has three fixed steps: extract structures → design proposals → render images. After each human decision the **same session resumes** (agent-loop resume semantics); existing artifacts are never re-derived.
41
+ - The project name is taken from the document's first line and can be renamed any time in the info bar (touches only `index.json`).
42
+ - State is driven purely by file facts; running status is authoritative via the DSH agent registry; orchestration concerns are left to the session.
43
+
44
+ ## Two models
45
+
46
+ - **Reasoner**: extract and advise run on DSH's current default model — no Pictor configuration needed.
47
+ - **Image model**: configured in the workbench Settings (`seedream` / `gemini` / `openai-compatible` / `mock`). API keys go through DSH's credential subsystem; the configuration stores only a reference.
48
+
49
+ ## Development & verification
40
50
 
41
51
  ```bash
42
- npm run build # esbuild 构建 host + 拼接 client
43
- npm run verify # L1 单元 + L2 host 集成(mock ctx,15 项)
44
- npm run verify:integration # L3 transport 冒烟(真实 dsh web)
45
- npm run test:e2e # L4a 浏览器 e2e(fixture 数据,确定性)
52
+ npm run build # esbuild build of host + bundled client
53
+ npm run verify # L1 unit + L2 host integration (mock ctx, 15 items)
54
+ npm run verify:integration # L3 transport smoke against a real dsh web (hermetic temp home)
55
+ npm run test:pack # package integrity: tarball carries lib/, agents/, references/, assets/
56
+ npm run test:install # L5: README install path (dsh plugin add) → real dsh web boot
57
+ npm run test:e2e # L4a browser e2e (fixture data, deterministic)
58
+ npm run hooks:install # one-time: enable the pre-push hook (git config core.hooksPath .githooks)
46
59
  ```
47
60
 
48
- 测试分层的定义与理由见 [docs/DESIGN.zh.md](docs/DESIGN.zh.md) 第 7 节。
61
+ The pre-push hook runs `npm run verify` + `npm run test:install` before every push; all smoke layers are hermetic (temp `DSH_HOME`, never the live `~/.dsh`).
62
+
63
+ Test-layer definitions and rationale: [docs/DESIGN.zh.md](docs/DESIGN.zh.md) §7.
49
64
 
50
- ## 目录
65
+ ### Layout
51
66
 
52
67
  ```
53
68
  pictor/
54
- ├── agents/ # 声明式蓝图(orchestrator/extractor/advisor/renderer)
69
+ ├── agents/ # declarative blueprints (orchestrator/extractor/advisor/renderer)
55
70
  ├── references/domain/ # base-prompt + layouts/ + styles/ + diagram-types/ + visual-principles
56
- ├── src/host/ # 插件宿主:~/.pictor、项目会话、/pictor RPC、生图
57
- ├── src/client/ # 工作台 UI(React.createElement + 主题变量)
71
+ ├── src/host/ # plugin host: ~/.pictor, project sessions, /pictor RPC, image generation
72
+ ├── src/client/ # workbench UI (React.createElement + theme variables)
58
73
  ├── e2e/ # L4a Playwright
59
- ├── verify.mjs # L1+L2 离线冒烟
74
+ ├── verify.mjs # L1+L2 offline smoke
60
75
  └── scripts/ # build + transport-smoke
61
76
  ```
62
77
 
63
- 数据目录 `~/.pictor/`:`index.json`(项目索引)、`pictor-config.json`(画图模型配置)、`<项目 id>/`(自包含:agents/references 快照 + 10.input + 11.extraction + 12.advice + output)。
64
-
65
- ## 许可证
66
-
67
- MIT,见 [LICENSE](LICENSE)。
68
-
69
- ---
78
+ Data home `~/.pictor/`: `index.json` (project index), `pictor-config.json` (image-model configuration), `<project-id>/` (self-contained: agents/references snapshot + `10.input` + `11.extraction` + `12.advice` + `output`).
70
79
 
71
- ## English
80
+ ## Documents
72
81
 
73
- Pictor turns a document into a set of infographics. It is a DeepSeek Harness (DSH) plugin published on npm as [`dsh-pictor`](https://www.npmjs.com/package/dsh-pictor); the source lives at [github.com/bandung-circuits/pictor](https://github.com/bandung-circuits/pictor). Each project is one coherent agent session that carries the whole extract → advise → render flow, and the GUI is a thin layer that only shows file-fact state and shapes your input. Design decisions live in [docs/DESIGN.zh.md](docs/DESIGN.zh.md).
82
+ - Design decisions: [docs/DESIGN.zh.md](docs/DESIGN.zh.md) (Chinese)
83
+ - Verification checklist: [docs/verification-checklist.zh.md](docs/verification-checklist.zh.md) (Chinese)
74
84
 
75
- - A `~/.pictor` home is provisioned on first use and registered as a DSH workspace; all project sessions group under "Pictor" in the sidebar.
76
- - The only entry point is the bottom-left "Pictor" footer button, which toggles a `shell.overlay` workbench: projects on the left, the selected project on the right.
77
- - Every project has three fixed steps: extract structures → design proposals → render images. After each human decision the **same session resumes** to keep working; existing artifacts are never re-derived (file facts are the source of truth).
78
- - Reasoner: DSH's default model (no Pictor config). Image model: configured in the workbench Settings (seedream / gemini / openai-compatible / mock); the API key goes through DSH's credentials subsystem.
79
- - Tests: L1 unit + L2 host integration (`npm run verify`), L3 real-dsh transport smoke (`npm run verify:integration`), L4a browser e2e against a seeded fixture (`npm run test:e2e`).
85
+ ## License
80
86
 
81
- MIT licensed.
87
+ MIT — see [LICENSE](LICENSE). Third-party content notices: [NOTICE.md](NOTICE.md).
@@ -0,0 +1,87 @@
1
+ # Pictor — 文档转信息图工作台
2
+
3
+ [ [English](./README.md) | 中文 ]
4
+
5
+ Pictor 把一份文档变成一组信息图。它是 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)的插件,已发布到 npm 包 [`dsh-pictor`](https://www.npmjs.com/package/dsh-pictor),源码仓库在 [github.com/bandung-circuits/pictor](https://github.com/bandung-circuits/pictor)。每新建一个项目,宿主创建一个连贯的会话作为项目会话,整个「提取 → 方案 → 渲染」流程都在这个会话里推进,GUI 只做两件事:查看信息、规整输入。设计文档见 [docs/DESIGN.zh.md](docs/DESIGN.zh.md)。
6
+
7
+ ![Pictor 初始工作台](docs/screenshot-empty.png)
8
+
9
+ ## 安装
10
+
11
+ 已发布到 npm,安装即一行命令:
12
+
13
+ ```bash
14
+ dsh plugin --profile <profile> add dsh-pictor
15
+ ```
16
+
17
+ 把 `<profile>` 换成目标 profile 名(如 `desktop`、`web`),然后启动该 profile,dsh 左下角会出现「Pictor」按钮。首次使用自动生成 `~/.pictor` 工作区(注册为 dsh 工作区,侧栏归组见「Pictor」)。开发安装(改动源码实时联调)见 [docs/DESIGN.zh.md](docs/DESIGN.zh.md)。
18
+
19
+ 非技术用户可安装 [DSH Desktop](https://dshdesktop.com/en/)(社区维护项目,非 DeepSeek 官方产品,把 DeepSeek Harness 运行封装成可直接打开的桌面应用),然后让 dsh 里的 AI 帮装,发这一句即可:
20
+
21
+ > 帮我把 Pictor 插件装上,从 npm 包 dsh-pictor 装到当前 profile,需要的话重启,装好告诉我左下角有没有 Pictor 入口。
22
+
23
+ ## 适配的 DSH 版本
24
+
25
+ Pictor 以 **DSH Desktop 0.7.2**(内置 Harness **0.1.2-alpha.1**)为适配与测试基准。DeepSeek Harness 的客户端 API(工作区/会话服务)演进很快,本月两个版本之间可能就面目全非,所以本插件只跟住一个 harness 世代。其他版本——无论更旧更新——可能能跑、也可能无预告地坏掉,都不在我们的支持范围内。DSH 升级后插件不工作,请先回到上面这个版本,而不是去排查版本差异。
26
+
27
+ ## 使用
28
+
29
+ 1. 打开工作台,点「新建项目」:上传文件(md/txt/docx/pdf/图片)或粘贴内容(从 Word/网页粘贴,格式保留,落盘为消毒 HTML)。
30
+ 2. 会话读取并规整来源(docx/pdf/图片/HTML → document.md 由会话用 dsh 工具完成),提取候选结构。
31
+ 3. 勾选结构,确认,会话进入方案设计;勾选方案,设画面比例,点「渲染所选方案」。
32
+ 4. 结果网格里预览、下载;随时可展开讨论面板对会话说话,或点步骤条回到已有产物的步骤重做。
33
+
34
+ ![Pictor 项目工作中](docs/screenshot-workbench.png)
35
+
36
+ ## 形态
37
+
38
+ - 安装后首次使用自动生成 `~/.pictor` 工作区(注册为 dsh 工作区,侧栏归组见「Pictor」)。
39
+ - 唯一入口是 dsh 左下角 footer 按钮「Pictor」,打开 shell.overlay 工作台:左栏列项目,右栏显示当前项目。
40
+ - 项目步骤固定三步:提取结构 → 方案设计 → 渲染出图。每步由你做决定后,会话在同一会话里接着执行(agent-loop 的 resume 语义),不重跑已有阶段。
41
+ - 项目名取文档首行,信息条内随时可改名,只动 index.json。
42
+ - 状态只由文件事实驱动;运行中判定以 dsh agent 注册表为权威;把你对 orchestration 的关心都留给会话。
43
+
44
+ ## 两个模型
45
+
46
+ - 推理模型:extract 与 advise 用 dsh 当前默认模型,无需在 Pictor 配置。
47
+ - 画图模型:在工作台「设置」里配置(seedream / gemini / openai-compatible / mock)。密钥经 dsh 凭据子系统保存,配置只留引用。
48
+
49
+ ## 开发与验证
50
+
51
+ ```bash
52
+ npm run build # esbuild 构建 host + 拼接 client
53
+ npm run verify # L1 单元 + L2 host 集成(mock ctx,15 项)
54
+ npm run verify:integration # L3 transport 冒烟(真实 dsh web,封闭临时 DSH_HOME)
55
+ npm run test:pack # 打包完整性:tarball 必带 lib/、agents/、references/、assets/
56
+ npm run test:install # L5:README 安装路径(dsh plugin add)→ 真实 dsh web 启动
57
+ npm run test:e2e # L4a 浏览器 e2e(fixture 数据,确定性)
58
+ npm run hooks:install # 一次性启用 pre-push 钩子(git config core.hooksPath .githooks)
59
+ ```
60
+
61
+ pre-push 钩子在每次 push 前依次跑 `npm run verify` 与 `npm run test:install`;所有冒烟层都是封闭式(临时 `DSH_HOME`,不碰真实 `~/.dsh`)。
62
+
63
+ 测试分层的定义与理由见 [docs/DESIGN.zh.md](docs/DESIGN.zh.md) 第 7 节。
64
+
65
+ ## 目录
66
+
67
+ ```
68
+ pictor/
69
+ ├── agents/ # 声明式蓝图(orchestrator/extractor/advisor/renderer)
70
+ ├── references/domain/ # base-prompt + layouts/ + styles/ + diagram-types/ + visual-principles
71
+ ├── src/host/ # 插件宿主:~/.pictor、项目会话、/pictor RPC、生图
72
+ ├── src/client/ # 工作台 UI(React.createElement + 主题变量)
73
+ ├── e2e/ # L4a Playwright
74
+ ├── verify.mjs # L1+L2 离线冒烟
75
+ └── scripts/ # build + transport-smoke
76
+ ```
77
+
78
+ 数据目录 `~/.pictor/`:`index.json`(项目索引)、`pictor-config.json`(画图模型配置)、`<项目 id>/`(自包含:agents/references 快照 + 10.input + 11.extraction + 12.advice + output)。
79
+
80
+ ## 文档
81
+
82
+ - 设计与决策:[docs/DESIGN.zh.md](docs/DESIGN.zh.md)
83
+ - 验证清单:[docs/verification-checklist.zh.md](docs/verification-checklist.zh.md)
84
+
85
+ ## 许可证
86
+
87
+ MIT,见 [LICENSE](LICENSE)。第三方内容声明见 [NOTICE.md](NOTICE.md)。
Binary file
Binary file
package/lib/client.js CHANGED
@@ -303,6 +303,7 @@ const I18N = {
303
303
  layoutLabel: '布局', styleLabel: '风格', metaConfirmedHint: '确认后作为项目名与提取依据,可编辑。',
304
304
  chooseLayout: '选择布局', chooseStyle: '选择风格', resetToAi: '回到 AI 推荐',
305
305
  layoutChanged: '·已改', aiSuggestion: 'AI 推荐',
306
+ batchStyleLabel: '批量设置风格', batchStyleHint: '为所选方案套用同一风格(未选则全部)',
306
307
  close: '关闭',
307
308
  discussion: '讨论', discussHint: '对项目会话自由说话:补充参考、要求搜索、调整方向。你的决定会在同一会话里接着执行。',
308
309
  discussPlaceholder: '例如:先不要继续,帮我查一下这篇文章提到的背景…',
@@ -360,6 +361,7 @@ const I18N = {
360
361
  layoutLabel: 'Layout', styleLabel: 'Style', metaConfirmedHint: 'Confirmed as the project name and extraction basis; editable.',
361
362
  chooseLayout: 'Choose layout', chooseStyle: 'Choose style', resetToAi: 'Reset to AI',
362
363
  layoutChanged: '· edited', aiSuggestion: 'AI',
364
+ batchStyleLabel: 'Batch style', batchStyleHint: 'Apply one style to all selected proposals (all if none selected)',
363
365
  close: 'Close',
364
366
  discussion: 'Discussion', discussHint: 'Talk to the project session: add context, ask for research, adjust direction. Your decisions continue it in the same session.',
365
367
  discussPlaceholder: 'e.g. Hold on — research the background this article mentions…',
@@ -485,10 +487,23 @@ function sanitizePaste(html) {
485
487
  // 会话创建与驱动走 dsh 的 workspace 流程(照 pomasa 的 driveSession):
486
488
  // 宿主只出 prompt,客户端经 workspaces.connectWorkspace 建会话(自然归入
487
489
  // 「Pictor」工作区、继承 profile 默认模型),再经 sessions.prompt 驱动。
490
+ // dsh ≥ 0.1.2-alpha.1(DSH Desktop 0.7.2)把 connectWorkspace 从
491
+ // `workspaces` 服务移到了 `uiWorkspace` 服务上:这里两个形态都尝试,哪个
492
+ // 在就调哪个,老版本前端不受影响。
493
+ function sessionConnectFn(ctx, ws) {
494
+ let uiWs = null
495
+ try { uiWs = ctx.uiWorkspace || ((ctx.get && ctx.get('uiWorkspace')) || null) } catch { /* ignore */ }
496
+ return (uiWs && typeof uiWs.connectWorkspace === 'function')
497
+ ? uiWs.connectWorkspace.bind(uiWs)
498
+ : (ws && typeof ws.connectWorkspace === 'function') ? ws.connectWorkspace.bind(ws) : null
499
+ }
500
+
488
501
  async function pictorWorkspace(ctx) {
489
502
  const ws = ctx.workspaces || (ctx.get && ctx.get('workspaces'))
490
503
  const base = await rpc(ctx, 'config.get').then((c) => (c && c.dataRoot) || '')
491
- if (!ws || typeof ws.connectWorkspace !== 'function') throw new Error('会话服务不可用(workspaces)')
504
+ // 这里只用 list/create/delete/rename,新旧 services 都有;connectWorkspace
505
+ // 的可用性由 driveProjectSession 检查。
506
+ if (!ws) throw new Error('会话服务不可用(workspaces)')
492
507
  const items = () => {
493
508
  try {
494
509
  const snap = ws.list && typeof ws.list.getSnapshot === 'function' ? ws.list.getSnapshot() : null
@@ -529,14 +544,29 @@ async function pictorWorkspace(ctx) {
529
544
  async function driveProjectSession(ctx, prompt) {
530
545
  const ws = ctx.workspaces || (ctx.get && ctx.get('workspaces'))
531
546
  const sessionsSvc = ctx.sessions || (ctx.get && ctx.get('sessions'))
547
+ const connect = sessionConnectFn(ctx, ws)
548
+ const canCreate = !!(sessionsSvc && typeof sessionsSvc.create === 'function')
549
+ if (!(connect || canCreate) || !(sessionsSvc && typeof sessionsSvc.binding === 'function')) {
550
+ let uiWs = null
551
+ try { uiWs = ctx.uiWorkspace || ((ctx.get && ctx.get('uiWorkspace')) || null) } catch { /* ignore */ }
552
+ const diag = { ws: !!ws, ses: !!sessionsSvc && typeof sessionsSvc.binding === 'function', uiws: !!uiWs, conn: !!connect, create: canCreate }
553
+ console.warn('[pictor] session services unavailable', diag)
554
+ try { window.__pictor_diag = diag } catch { /* ignore */ }
555
+ try { pictorDiag(ctx, 'drive:fail') } catch { /* ignore */ }
556
+ throw new Error(`会话服务不可用(workspaces) [ws:${diag.ws},ses:${diag.ses},uiws:${diag.uiws},conn:${diag.conn},create:${diag.create}]`)
557
+ }
532
558
  const wid = await pictorWorkspace(ctx)
533
559
  let sessionId
534
- try { sessionId = await ws.connectWorkspace(wid) }
560
+ try {
561
+ const created = connect ? await connect(wid) : await sessionsSvc.create({ workspaceId: wid })
562
+ sessionId = created && typeof created === 'object' && created.id ? created.id : created
563
+ }
535
564
  catch (e) { throw new Error('创建会话失败:' + String((e && e.message) || e)) }
536
565
  const bound = sessionsSvc && typeof sessionsSvc.binding === 'function' ? sessionsSvc.binding(sessionId) : null
537
566
  const sess = bound && bound.session
538
567
  if (!sess || typeof sess.prompt !== 'function') throw new Error('会话无 prompt 通道')
539
568
  await sess.prompt([{ type: 'text', text: String(prompt || '') }], 'queue')
569
+ try { pictorDiag(ctx, 'drive:ok') } catch { /* ignore */ }
540
570
  return sessionId
541
571
  }
542
572
 
@@ -793,9 +823,17 @@ function ProposalsPanel({ stage, proposals, suggestedStyle, steps, selected, onT
793
823
  const ratioFor = (id) => ratios[id] || '16:9'
794
824
  const activePrompt = promptId ? proposals.find((x) => x.id === promptId) || null : null
795
825
 
826
+ function batchTargetIds() {
827
+ return selected.length ? selected : proposals.map((p) => p.id)
828
+ }
796
829
  function pickOption(name) {
797
830
  const cur = pick
798
831
  if (!cur) return
832
+ if (cur.batch) {
833
+ batchTargetIds().forEach((id) => setSty(id, name))
834
+ setPick(null)
835
+ return
836
+ }
799
837
  if (cur.kind === 'layout') setLay(cur.id, name)
800
838
  else setSty(cur.id, name)
801
839
  setPick(null)
@@ -803,6 +841,11 @@ function ProposalsPanel({ stage, proposals, suggestedStyle, steps, selected, onT
803
841
  function resetCurrent() {
804
842
  const cur = pick
805
843
  if (!cur) return
844
+ if (cur.batch) {
845
+ batchTargetIds().forEach((id) => setSty(id, undefined))
846
+ setPick(null)
847
+ return
848
+ }
806
849
  if (cur.kind === 'layout') setLay(cur.id, undefined)
807
850
  else setSty(cur.id, undefined)
808
851
  setPick(null)
@@ -859,34 +902,60 @@ function ProposalsPanel({ stage, proposals, suggestedStyle, steps, selected, onT
859
902
  : null
860
903
 
861
904
  const chooserModal = pick ? (function chooser() {
862
- const p = proposals.find((x) => x.id === pick.id)
863
- if (!p) return null
864
905
  const kind = pick.kind
906
+ const batch = Boolean(pick.batch)
907
+ const p = batch ? null : proposals.find((x) => x.id === pick.id)
908
+ if (!batch && !p) return null
865
909
  const list = kind === 'layout'
866
- ? (layouts && layouts.length ? layouts : [p.suggested_layout])
867
- : (styles && styles.length ? styles : [p.suggested_style || suggestedStyle])
868
- const base = kind === 'layout' ? p.suggested_layout : (p.suggested_style || suggestedStyle)
869
- const current = kind === 'layout' ? (lay[pick.id] || p.suggested_layout) : (sty[pick.id] || p.suggested_style || suggestedStyle)
870
- const changed = kind === 'layout'
871
- ? Boolean(lay[pick.id] && lay[pick.id] !== base)
872
- : Boolean(sty[pick.id] && sty[pick.id] !== base)
910
+ ? (layouts && layouts.length ? layouts : (p ? [p.suggested_layout] : []))
911
+ : (styles && styles.length ? styles : (p ? [p.suggested_style || suggestedStyle] : (suggestedStyle ? [suggestedStyle] : [])))
912
+ const base = batch ? '' : (kind === 'layout' ? p.suggested_layout : (p.suggested_style || suggestedStyle))
913
+ // batch:高亮所有目标方案共享的同一风格(若有),否则不高亮
914
+ let current
915
+ if (batch) {
916
+ const ids = batchTargetIds()
917
+ const eff = ids.map((id) => {
918
+ const pp = proposals.find((x) => x.id === id)
919
+ return (pp ? (sty[id] || pp.suggested_style || suggestedStyle || '') : (sty[id] || ''))
920
+ })
921
+ current = eff.length && eff.every((s) => s === eff[0]) ? eff[0] : ''
922
+ } else {
923
+ current = kind === 'layout' ? (lay[pick.id] || p.suggested_layout) : (sty[pick.id] || p.suggested_style || suggestedStyle)
924
+ }
925
+ const changed = batch
926
+ ? ids_haveAnyOverrideStyle()
927
+ : (kind === 'layout' ? Boolean(lay[pick.id] && lay[pick.id] !== base) : Boolean(sty[pick.id] && sty[pick.id] !== base))
928
+ function ids_haveAnyOverrideStyle() {
929
+ return batchTargetIds().some((id) => Boolean(sty[id]))
930
+ }
873
931
  const items = list.map((n) => h('div', { key: n, className: 'pt-preview-item' + (n === current ? ' sel' : ''), onClick: () => pickOption(n) },
874
- n === base ? h('span', { className: 'ai-chip' }, t('aiSuggestion')) : null,
932
+ (!batch && n === base) ? h('span', { className: 'ai-chip' }, t('aiSuggestion')) : null,
875
933
  h('img', { src: '/pictor/preview/' + (kind === 'layout' ? 'layouts' : 'styles') + '/' + encodeURIComponent(n) + '.webp', alt: kind === 'layout' ? layoutLabel(n) : n }),
876
934
  h('div', { className: 'nm' }, kind === 'layout' ? layoutLabel(n) : n)))
935
+ const title = batch ? t('batchStyleLabel') : (kind === 'layout' ? t('chooseLayout') : t('chooseStyle'))
936
+ const batchSub = batch ? h('p', { className: 'pt-meta', style: { margin: '0 0 10px' }, key: 'sub' },
937
+ t('batchStyleHint') + ' · ' + t('selectCount', { s: selected.length, t: proposals.length })) : null
877
938
  return h('div', { className: 'pt-modal-backdrop', onClick: () => setPick(null) },
878
939
  h('div', { className: 'pt-modal pt-modal-wide', onClick: (e) => e.stopPropagation() },
879
940
  h('div', { className: 'pt-modal-head' },
880
- h('h3', { className: 'pt-option-title', style: { margin: 0 } }, kind === 'layout' ? t('chooseLayout') : t('chooseStyle')),
941
+ h('h3', { className: 'pt-option-title', style: { margin: 0 } }, title),
881
942
  h('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
882
943
  changed ? h('button', { className: 'pt-btn', style: { padding: '6px 12px', fontSize: 13 }, onClick: resetCurrent }, t('resetToAi')) : null,
883
944
  h('button', { className: 'pt-modal-close', onClick: () => setPick(null), title: t('close'), 'aria-label': t('close') }, '×'))),
884
- h('div', { className: 'pt-modal-body' }, h('div', { className: 'pt-preview-grid' }, items))))
945
+ h('div', { className: 'pt-modal-body' }, batchSub, h('div', { className: 'pt-preview-grid' }, items))))
885
946
  })() : null
886
947
 
948
+ const styleList = (styles && styles.length ? styles : (suggestedStyle ? [suggestedStyle] : []))
949
+ const batchStyleBtn = styleList.length ? h('button', {
950
+ className: 'pt-btn', style: { padding: '4px 10px', fontSize: 13 }, disabled: proposals.length === 0,
951
+ onClick: () => setPick({ id: null, kind: 'style', batch: true }),
952
+ title: t('batchStyleHint'),
953
+ }, t('batchStyleLabel')) : null
954
+
887
955
  const actionRow = h('div', { className: 'pt-card-row', style: { marginTop: 18 } },
888
- h('div', { style: { display: 'flex', gap: 10, alignItems: 'center' } },
956
+ h('div', { style: { display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' } },
889
957
  h('button', { className: 'pt-btn', onClick: onToggleAll }, allSelected ? t('deselectAll') : t('selectAll')),
958
+ batchStyleBtn,
890
959
  h('span', { className: 'pt-meta' }, t('selectCount', { s: selected.length, t: proposals.length }))),
891
960
  hasImageKey === false
892
961
  ? h('p', { className: 'pt-warn', style: { margin: 0 } }, t('imgKeyHint'))
@@ -904,7 +973,17 @@ function fileSlug(s) {
904
973
  return String(s || '').toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) || 'infographic'
905
974
  }
906
975
 
907
- function ResultsPanel({ outputs, images, renderErrors, proposals, lay, renderBatches }) {
976
+ // 结果图直出走同源 HTTP(data URL 在新版 DSH Desktop WebView 可能被拦),
977
+ // 失败时回退到 RPC 返回的 data URL。
978
+ const imgUrlFor = (projectId, name) => {
979
+ const base = '/pictor/asset/project/' + encodeURIComponent(projectId) + '/' + encodeURIComponent(name)
980
+ try {
981
+ const tok = new URLSearchParams(window.location.search || '').get('token')
982
+ return tok ? base + '?token=' + encodeURIComponent(tok) : base
983
+ } catch { return base }
984
+ }
985
+
986
+ function ResultsPanel({ projectId, outputs, images, renderErrors, proposals, lay, renderBatches }) {
908
987
  const [big, setBig] = React.useState(null)
909
988
  const [collapsed, setCollapsed] = React.useState({})
910
989
  if (!outputs.length && !renderErrors.length) {
@@ -926,7 +1005,7 @@ function ResultsPanel({ outputs, images, renderErrors, proposals, lay, renderBat
926
1005
  h('div', null, h('h3', { className: 'pt-option-title', style: { margin: 0 } }, bigInfo.title)),
927
1006
  h('button', { className: 'pt-modal-close', onClick: () => setBig(null), title: t('close'), 'aria-label': t('close') }, '×')),
928
1007
  h('div', { className: 'pt-modal-body', style: { textAlign: 'center' } },
929
- h('img', { src: images[big], alt: bigInfo.title, style: { maxWidth: '100%', maxHeight: '70vh', borderRadius: 10 } }),
1008
+ h('img', { src: imgUrlFor(projectId, big), alt: bigInfo.title, style: { maxWidth: '100%', maxHeight: '70vh', borderRadius: 10 }, onError: (e) => { if (e && e.currentTarget) e.currentTarget.src = images[big] } }),
930
1009
  bigInfo.actualLayout ? h('p', { className: 'pt-meta', style: { marginTop: 10 } }, t('layoutLabel') + ': ' + layoutLabel(bigInfo.actualLayout)) : null)))
931
1010
  : null
932
1011
 
@@ -950,7 +1029,7 @@ function ResultsPanel({ outputs, images, renderErrors, proposals, lay, renderBat
950
1029
  const dlName = info.prop ? (fileSlug(info.prop.title || layoutLabel(info.actualLayout || info.prop.suggested_layout)) + '-' + info.pid + '.png') : name
951
1030
  return h('figure', { key: name, className: 'pt-img-card' },
952
1031
  images[name]
953
- ? h('img', { className: 'pt-img', src: images[name], alt: info.title, style: { cursor: 'zoom-in' }, onClick: () => setBig(name) })
1032
+ ? h('img', { className: 'pt-img', src: imgUrlFor(projectId, name), alt: info.title, style: { cursor: 'zoom-in' }, onClick: () => setBig(name), onError: (e) => { if (e && e.currentTarget) e.currentTarget.src = images[name] } })
954
1033
  : h('div', { className: 'thumb', style: { width: '100%', height: 140 } }),
955
1034
  h('figcaption', null,
956
1035
  h('div', { className: 'pt-fig-title' }, info.title),
@@ -1364,7 +1443,7 @@ function DetailPane(props) {
1364
1443
  ratios, setRatio, onRender: renderSelected, renderBusy,
1365
1444
  hasImageKey: Boolean(imageCfg && imageCfg.hasKey), running: Boolean(data.running),
1366
1445
  renderState: data.renderState, renderRunning: Boolean(data.renderRunning) }),
1367
- render: h(ResultsPanel, { outputs: data.outputs, images, renderErrors, proposals, lay: layOv, renderBatches: data.renderBatches }),
1446
+ render: h(ResultsPanel, { projectId, outputs: data.outputs, images, renderErrors, proposals, lay: layOv, renderBatches: data.renderBatches }),
1368
1447
  }[step]
1369
1448
 
1370
1449
  return h('div', null,
@@ -1629,7 +1708,30 @@ function selectedLabel(ids) {
1629
1708
  return ids.join('、')
1630
1709
  }
1631
1710
 
1711
+ // 最佳努力诊断:上报此 ctx 上工作区/会话服务的暴露情况(DSH Desktop 0.7.2
1712
+ // 调整了客户端 API),host 落盘 ~/.pictor/diag.jsonl,无需 DevTools 即可排查。
1713
+ function pictorDiag(ctx, phase) {
1714
+ let ws = null, ses = null, ui = null
1715
+ try { ws = ctx && (ctx.workspaces || (ctx.get && ctx.get('workspaces'))) } catch { /* ignore */ }
1716
+ try { ses = ctx && (ctx.sessions || (ctx.get && ctx.get('sessions'))) } catch { /* ignore */ }
1717
+ try { ui = ctx && (ctx.uiWorkspace || (ctx.get && ctx.get('uiWorkspace'))) } catch { /* ignore */ }
1718
+ const body = {
1719
+ phase,
1720
+ ws: !!ws, ses: !!ses, ui: !!ui,
1721
+ wsMethods: ws ? Object.keys(ws).slice(0, 40) : null,
1722
+ sesMethods: ses ? Object.keys(ses).slice(0, 40) : null,
1723
+ uiMethods: ui ? Object.keys(ui).slice(0, 40) : null,
1724
+ connectWs: !!(ws && typeof ws.connectWorkspace === 'function'),
1725
+ connectUi: !!(ui && typeof ui.connectWorkspace === 'function'),
1726
+ bind: !!(ses && typeof ses.binding === 'function'),
1727
+ href: typeof location !== 'undefined' ? location.href.slice(0, 80) : '',
1728
+ }
1729
+ try { fetch('/pictor/diag', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }).catch(() => {}) } catch { /* ignore */ }
1730
+ console.warn('[pictor] diag', body)
1731
+ }
1732
+
1632
1733
  function apply(ctx) {
1734
+ try { pictorDiag(ctx, 'apply') } catch { /* ignore */ }
1633
1735
  console.info('[dsh-pictor] apply 被调用', { hasSlots: Boolean(ctx && ctx.slots) })
1634
1736
  if (typeof document !== 'undefined') {
1635
1737
  const styleId = 'dsh-pictor-style'
@@ -124,7 +124,7 @@ class DashScopeProvider {
124
124
  const base = this.cfg.baseUrl || "https://dashscope.aliyuncs.com/api/v1";
125
125
  const size = DASHSCOPE_SIZE_MAP[aspectRatio] || "1024*1024";
126
126
  const url = base.replace(/\/$/, "") + "/services/aigc/multimodal-generation/generation";
127
- const parameters = { size, prompt_extend: true, watermark: false };
127
+ const parameters = { size, watermark: false };
128
128
  const body = {
129
129
  model,
130
130
  input: { messages: [{ role: "user", content: [{ type: "text", text: prompt }] }] },
package/lib/index.js CHANGED
@@ -702,6 +702,52 @@ function apply(ctx, config = {}) {
702
702
  } });
703
703
  }
704
704
  }
705
+ const PREFIX = "/pictor/asset/project";
706
+ wsA.register({ kind: "prefix", path: PREFIX, handler: (req, res) => {
707
+ try {
708
+ const pathname = new URL(req.url || "/", "http://x").pathname;
709
+ const rest = pathname.slice(PREFIX.length + 1);
710
+ const slash = rest.indexOf("/");
711
+ if (slash <= 0) {
712
+ res.writeHead(404);
713
+ res.end("not found");
714
+ return;
715
+ }
716
+ const id = rest.slice(0, slash);
717
+ const file = rest.slice(slash + 1);
718
+ if (id.includes("..") || /[\\/]/.test(id) || !/^[0-9a-zA-Z_.-]+$/.test(file) || !file.endsWith(".png")) {
719
+ res.writeHead(400);
720
+ res.end("bad request");
721
+ return;
722
+ }
723
+ const buf = readFileSync(join(projectDir(id), "output", file));
724
+ res.writeHead(200, { "content-type": "image/png", "cache-control": "public, max-age=300" });
725
+ res.end(buf);
726
+ } catch {
727
+ res.writeHead(404);
728
+ res.end("not found");
729
+ }
730
+ } });
731
+ wsA.register({ kind: "exact", path: "/pictor/diag", handler: (req, res) => {
732
+ let body = "";
733
+ try {
734
+ req.on("data", (c) => {
735
+ body += c;
736
+ });
737
+ req.on("end", () => {
738
+ try {
739
+ mkdirSync(base, { recursive: true });
740
+ appendFileSync(join(base, "diag.jsonl"), JSON.stringify({ t: Date.now(), ...JSON.parse(body || "{}") }) + "\n");
741
+ } catch {
742
+ }
743
+ res.writeHead(204);
744
+ res.end();
745
+ });
746
+ } catch {
747
+ res.writeHead(204);
748
+ res.end();
749
+ }
750
+ } });
705
751
  }
706
752
  return { dataRoot: base };
707
753
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-pictor",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Pictor:文档转信息图工作台。每项目一个连贯 dsh 会话,GUI 为 shell.overlay 工作台(左栏项目、右栏三步:提取/方案/渲染),行为全部交给 DSH 智能运行时。",
5
5
  "type": "module",
6
6
  "repository": {
@@ -46,12 +46,16 @@
46
46
  "docs",
47
47
  "README.md",
48
48
  "LICENSE",
49
- "assets"
49
+ "assets",
50
+ "NOTICE.md"
50
51
  ],
51
52
  "scripts": {
52
53
  "build": "node scripts/build.mjs",
53
54
  "verify": "node scripts/build.mjs && node verify.mjs",
54
55
  "verify:integration": "bash scripts/transport-smoke.sh",
56
+ "test:pack": "bash scripts/package-integrity.sh",
57
+ "test:install": "bash scripts/install-smoke.sh",
58
+ "hooks:install": "git config core.hooksPath .githooks",
55
59
  "test:e2e": "playwright test --config e2e/playwright.config.ts --project chromium",
56
60
  "verify:live": "playwright test --config e2e/playwright.config.ts --project live",
57
61
  "prepare": "node scripts/build.mjs"
@@ -1,4 +1,4 @@
1
- Create a professional infographic following these specifications:
1
+ Create a professional infographic. This prompt separates what to render from how to render it: the Content section is the subject matter, data, and labels to put in the infographic; the Layout and Style sections are visual specifications (structure and aesthetic — they describe the look, not the subject). Put the content in first, then apply the layout and style to present it.
2
2
 
3
3
  ## Image Specifications
4
4
 
@@ -8,6 +8,13 @@ Create a professional infographic following these specifications:
8
8
  - **Aspect Ratio**: {{ASPECT_RATIO}}
9
9
  - **Language**: {{LANGUAGE}}
10
10
 
11
+ ## Content — the subject matter, data, and labels to render
12
+
13
+ {{CONTENT}}
14
+
15
+ Text labels (in {{LANGUAGE}}):
16
+ {{TEXT_LABELS}}
17
+
11
18
  ## Core Principles
12
19
 
13
20
  - Follow the layout structure precisely for information architecture
@@ -26,19 +33,10 @@ Create a professional infographic following these specifications:
26
33
  - Labels should be clear and appropriately sized
27
34
  - Use the specified language for all text content
28
35
 
29
- ## Layout Guidelines
36
+ ## Layout Guidelines — visual structure (how to arrange; not content)
30
37
 
31
38
  {{LAYOUT_GUIDELINES}}
32
39
 
33
- ## Style Guidelines
34
-
35
- {{STYLE_GUIDELINES}}
40
+ ## Style Guidelines — visual aesthetic (how it looks; not subject matter)
36
41
 
37
- ---
38
-
39
- Generate the infographic based on the content below:
40
-
41
- {{CONTENT}}
42
-
43
- Text labels (in {{LANGUAGE}}):
44
- {{TEXT_LABELS}}
42
+ {{STYLE_GUIDELINES}}
@@ -1,6 +1,6 @@
1
1
  # bandung-circuit
2
2
 
3
- Editorial illustration style of the Bandung Circuit column — a muted, paper-printed aesthetic in the lineage of left publishing-house interior pages (Tricontinental, Verso, *New Left Review*). Cream stock, duotone restraint, thin-line hand-drawn icons, and typography-led hierarchy. Reads as the inside of a thoughtful journal, not a marketing poster.
3
+ Editorial illustration style — a muted, paper-printed aesthetic. Cream stock, duotone restraint, thin-line hand-drawn icons, and typography-led hierarchy. Reads as an interior print spread, not a marketing poster.
4
4
 
5
5
  ## Color Palette
6
6
 
@@ -25,10 +25,10 @@ Editorial illustration style of the Bandung Circuit column — a muted, paper-pr
25
25
 
26
26
  ## Typography
27
27
 
28
- - **Font family: Poppins** — used as the single typeface across the entire composition (headlines, body, labels, captions, numerals). Poppins is a geometric sans with humanist warmth, which suits Bandung Circuit's editorial-but-warm register; do not pair it with a second family.
28
+ - **Font family: Poppins** — used as the single typeface across the entire composition (headlines, body, labels, captions, numerals). Poppins is a geometric sans with humanist warmth, which suits an editorial-but-warm register; do not pair it with a second family.
29
29
  - Headlines: Poppins SemiBold or Bold — uppercase or title case acceptable, tracking slightly opened
30
30
  - Body text and labels: Poppins Regular or Medium — comfortable reading weight
31
- - All-caps short labels for block headers (ONE CORE, FOUR PILLARS, etc.) — Poppins Medium/SemiBold, set in cream on solid duotone bar
31
+ - All-caps short labels for block headers — Poppins Medium/SemiBold, set in cream on solid duotone bar
32
32
  - Sub-captions and explanatory phrases beneath block titles: Poppins Italic or Light
33
33
  - Numerals match the body face (Poppins lining figures) — no oversized statement numerals (this style trusts the layout, not hero stats)
34
34
 
@@ -49,13 +49,9 @@ Editorial illustration style of the Bandung Circuit column — a muted, paper-pr
49
49
  - Use filled / solid icons (Material-style) — icons are line-art only
50
50
  - Introduce a fourth or fifth color — composition must feel like a duotone with a single accent
51
51
  - Use glossy or "tech product" iconography — keep icons modest and editorial
52
- - Use heavy textures, parchment overlays, ink splatter, or cross-hatching (that is `aged-academia`, not Bandung)
52
+ - Use heavy textures, parchment overlays, ink splatter, or cross-hatching (that is `aged-academia`, not this style)
53
53
  - Use bold red blocks or full-bleed red panels (that is `tricon-infographic`, the poster sibling — not this)
54
- - Crowd the canvas — Bandung Circuit pages always breathe
55
-
56
- ## Best For
57
-
58
- Editorial illustrations for left-publishing journal articles, conceptual frameworks, multi-pillar architectures, theoretical model diagrams, methodology overviews, Global South research publications, long-form essay infographics, in-text figures (not standalone posters)
54
+ - Crowd the canvas — these pages always breathe
59
55
 
60
56
  ## Recommended Pairings
61
57
 
@@ -67,16 +63,13 @@ Editorial illustrations for left-publishing journal articles, conceptual framewo
67
63
 
68
64
  ## Distinction from Sibling Styles
69
65
 
70
- - **vs `tricon-infographic`**: That style is the *cover/poster* voice of the same publisher — bold red on white, authoritative, full-bleed. Bandung Circuit is the *interior/essay* voice — cream stock, duotone, quiet.
71
- - **vs `aged-academia`**: That style is sepia parchment with cross-hatched scientific illustration. Bandung Circuit is contemporary editorial print, not historical specimen plate.
72
- - **vs `mckinsey-report`**: That style is white-background institutional consulting with serif/sans dual typography. Bandung Circuit is warm-paper publishing-house aesthetic with single sans family and political-intellectual register.
66
+ - **vs `tricon-infographic`**: That style is a poster voice — bold red on white, authoritative, full-bleed. `bandung-circuit` is an interior/essay voice — cream stock, duotone, quiet.
67
+ - **vs `aged-academia`**: That style is sepia parchment with cross-hatched scientific illustration. `bandung-circuit` is contemporary editorial print, not historical specimen plate.
68
+ - **vs `mckinsey-report`**: That style is white-background institutional consulting with serif/sans dual typography. `bandung-circuit` is a warm-paper aesthetic with a single sans family.
73
69
 
74
70
  ## Register
75
71
 
76
72
  - formal-casual: formal but warm
77
73
  - abstract-representational: slightly abstract
78
74
  - bright-dark: bright (warm-bright, not white-bright)
79
- - digital-handcrafted: leans handcrafted (digital execution with hand-drawn iconography)
80
- - cultural-anchor: left publishing-house interior page design (Tricontinental, Verso, *New Left Review*, *Monthly Review*) — editorial register of the international left intellectual tradition
81
- - best-for: in-text editorial illustrations for journal articles, conceptual diagrams in long-form essays, Global South research publications
82
- - avoid-for: marketing posters, product UI, children's content, entertainment, corporate consulting decks, social-media graphics requiring stopping power
75
+ - digital-handcrafted: leans handcrafted (digital execution with hand-drawn iconography)