projectkeeper 0.0.0-stage → 1.0.0
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/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/README.md +283 -2
- package/README.zh-CN.md +285 -0
- package/THIRD_PARTY_NOTICES.md +53 -0
- package/bin/pk.mjs +6 -0
- package/bin/postinstall.mjs +5 -0
- package/bin/strip-types.mjs +28 -0
- package/package.json +68 -4
- package/patches/README.md +111 -0
- package/patches/pi-ai-0.85.0.ts +122 -0
- package/patches/pi-ai-0.87.1.ts +122 -0
- package/scripts/apply-pi-streaming-patch.ts +86 -0
- package/skills/clerk/coverage/SKILL.md +35 -0
- package/skills/clerk/cross-check/SKILL.md +118 -0
- package/skills/clerk/deepen-dispatch/SKILL.md +69 -0
- package/skills/clerk/lane-follow-up/SKILL.md +94 -0
- package/skills/clerk/lane-plan/SKILL.md +107 -0
- package/skills/clerk/lane-slot/SKILL.md +113 -0
- package/skills/clerk/lane-topic/SKILL.md +116 -0
- package/skills/clerk/orientation/SKILL.md +160 -0
- package/skills/clerk/reconcile/SKILL.md +77 -0
- package/skills/clerk/skeleton-dispatch/SKILL.md +43 -0
- package/skills/clerk/spot-check/SKILL.md +52 -0
- package/skills/clerk/synthesis/SKILL.md +65 -0
- package/skills/table-follow-up/SKILL.md +42 -0
- package/src/cli.ts +319 -0
- package/src/codemap/README.md +13 -0
- package/src/codemap/engines.ts +102 -0
- package/src/codemap/facts.ts +504 -0
- package/src/context/agent-entry.ts +232 -0
- package/src/context/ask.ts +97 -0
- package/src/context/assemble.ts +1058 -0
- package/src/context/code-source.ts +103 -0
- package/src/context/k-briefs.ts +114 -0
- package/src/context/lineage.ts +100 -0
- package/src/context/object-brief.ts +300 -0
- package/src/context/owner-words.ts +191 -0
- package/src/context/pack-fixture.ts +342 -0
- package/src/context/project-rules.ts +160 -0
- package/src/intake/intake.ts +392 -0
- package/src/intake/material-rules.ts +166 -0
- package/src/intake/removal.ts +316 -0
- package/src/intake/skipped.ts +72 -0
- package/src/keeper/adjustment.ts +565 -0
- package/src/keeper/bounds/boundary.ts +509 -0
- package/src/keeper/bounds/command.ts +1665 -0
- package/src/keeper/bounds/paths.ts +164 -0
- package/src/keeper/bounds/reads.ts +718 -0
- package/src/keeper/bounds/shell-write-guard.ts +521 -0
- package/src/keeper/capabilities.ts +136 -0
- package/src/keeper/clerk-steps.ts +151 -0
- package/src/keeper/clerk-tools.ts +1424 -0
- package/src/keeper/conversation.ts +253 -0
- package/src/keeper/evidence.ts +662 -0
- package/src/keeper/fake-provider.ts +132 -0
- package/src/keeper/held.ts +15 -0
- package/src/keeper/history-tools.ts +163 -0
- package/src/keeper/jobs/answering.ts +53 -0
- package/src/keeper/key-check.ts +105 -0
- package/src/keeper/ledger-tools.ts +295 -0
- package/src/keeper/link-check.ts +185 -0
- package/src/keeper/merge.ts +260 -0
- package/src/keeper/note-origin.ts +56 -0
- package/src/keeper/numbers-check.ts +102 -0
- package/src/keeper/open-in-pi.ts +57 -0
- package/src/keeper/organize/absence.ts +266 -0
- package/src/keeper/organize/carried-on.ts +214 -0
- package/src/keeper/organize/clerk-coverage.ts +293 -0
- package/src/keeper/organize/clerk-prompts.ts +132 -0
- package/src/keeper/organize/clerk.ts +1395 -0
- package/src/keeper/organize/coverage-gate.ts +19 -0
- package/src/keeper/organize/coverage-tools.ts +765 -0
- package/src/keeper/organize/entries.ts +153 -0
- package/src/keeper/organize/entry-gate.ts +289 -0
- package/src/keeper/organize/fill-tools.ts +718 -0
- package/src/keeper/organize/follow-up.ts +280 -0
- package/src/keeper/organize/generation-check.ts +504 -0
- package/src/keeper/organize/graph.ts +230 -0
- package/src/keeper/organize/lane-tools.ts +289 -0
- package/src/keeper/organize/looked-tools.ts +54 -0
- package/src/keeper/organize/materials.ts +243 -0
- package/src/keeper/organize/owner-lines.ts +493 -0
- package/src/keeper/organize/place-tools.ts +216 -0
- package/src/keeper/organize/placement-inference.ts +1045 -0
- package/src/keeper/organize/placement-time.ts +320 -0
- package/src/keeper/organize/placing.ts +821 -0
- package/src/keeper/organize/positioning.ts +33 -0
- package/src/keeper/organize/readiness.ts +47 -0
- package/src/keeper/organize/reading.ts +401 -0
- package/src/keeper/organize/round-blocks.ts +65 -0
- package/src/keeper/organize/round-news.ts +164 -0
- package/src/keeper/organize/round-open.ts +586 -0
- package/src/keeper/organize/schedule-state.ts +39 -0
- package/src/keeper/organize/schedule.ts +137 -0
- package/src/keeper/organize/service.ts +409 -0
- package/src/keeper/organize/skills.ts +116 -0
- package/src/keeper/organize/slots.ts +85 -0
- package/src/keeper/organize/stage-gate.ts +175 -0
- package/src/keeper/organize/stage-tools.ts +471 -0
- package/src/keeper/organize/standing-notes.ts +141 -0
- package/src/keeper/organize/table-follow-up.ts +330 -0
- package/src/keeper/organize/takeover-clerk.ts +832 -0
- package/src/keeper/organize/takeover-state.ts +103 -0
- package/src/keeper/organize/takeover.ts +216 -0
- package/src/keeper/organize/update-pending.ts +535 -0
- package/src/keeper/organize/workbench-placement.ts +92 -0
- package/src/keeper/own-keys.ts +114 -0
- package/src/keeper/owner-text.ts +165 -0
- package/src/keeper/project-folder.ts +334 -0
- package/src/keeper/prompts.ts +101 -0
- package/src/keeper/references.ts +177 -0
- package/src/keeper/relook/compose.ts +120 -0
- package/src/keeper/relook/prompts.ts +36 -0
- package/src/keeper/roles.ts +259 -0
- package/src/keeper/round-tools.ts +227 -0
- package/src/keeper/route.ts +148 -0
- package/src/keeper/rules.ts +97 -0
- package/src/keeper/runtime.ts +1858 -0
- package/src/keeper/scope-tools.ts +194 -0
- package/src/keeper/sendback-text.ts +83 -0
- package/src/keeper/step-timing.ts +207 -0
- package/src/keeper/testing/clerk-script.ts +212 -0
- package/src/keeper/testing/streaming-arguments.ts +57 -0
- package/src/keeper/tools.ts +2340 -0
- package/src/keeper/utterance-tools.ts +64 -0
- package/src/keeper/verbatim.ts +397 -0
- package/src/ledger/adapters.ts +186 -0
- package/src/ledger/arrangements.ts +257 -0
- package/src/ledger/blame.ts +58 -0
- package/src/ledger/code-engine-child.ts +69 -0
- package/src/ledger/code-engine.ts +548 -0
- package/src/ledger/code.ts +607 -0
- package/src/ledger/docs.ts +294 -0
- package/src/ledger/git-read.ts +321 -0
- package/src/ledger/index.ts +1850 -0
- package/src/ledger/item-range.ts +16 -0
- package/src/ledger/lines.ts +328 -0
- package/src/ledger/numbering.ts +269 -0
- package/src/ledger/ranges.ts +74 -0
- package/src/ledger/rebuild-worker.ts +15 -0
- package/src/ledger/rebuild.ts +387 -0
- package/src/ledger/repo-scan.ts +181 -0
- package/src/ledger/schema.ts +450 -0
- package/src/ledger/sessions.ts +134 -0
- package/src/ledger/text-scan.ts +466 -0
- package/src/ledger/time.ts +185 -0
- package/src/ledger/ts-symbols.ts +180 -0
- package/src/ledger/views.ts +377 -0
- package/src/mcp/server.ts +127 -0
- package/src/mcp/tools.ts +187 -0
- package/src/model/ids.ts +23 -0
- package/src/model/k-types.ts +912 -0
- package/src/model/time.ts +85 -0
- package/src/model/types.ts +1382 -0
- package/src/model/views-k.ts +707 -0
- package/src/model/vocab.ts +269 -0
- package/src/process/analysis.ts +79 -0
- package/src/process/breakpoint-candidates.ts +383 -0
- package/src/process/breakpoints.ts +746 -0
- package/src/process/claim-check.ts +295 -0
- package/src/process/delivery-links.ts +52 -0
- package/src/process/facts.ts +487 -0
- package/src/process/generations.ts +138 -0
- package/src/process/index.ts +243 -0
- package/src/process/integration.ts +128 -0
- package/src/process/placement.ts +107 -0
- package/src/process/plan.ts +276 -0
- package/src/process/sendbacks.ts +244 -0
- package/src/process/text.ts +185 -0
- package/src/process/units.ts +543 -0
- package/src/process/unnumbered.ts +431 -0
- package/src/process/work.ts +884 -0
- package/src/scope/candidates.ts +86 -0
- package/src/scope/decisions.ts +229 -0
- package/src/scope/discover.ts +741 -0
- package/src/scope/ignore.ts +126 -0
- package/src/scope/ignored-place.ts +93 -0
- package/src/scope/skip.ts +161 -0
- package/src/scope/toolchain.ts +180 -0
- package/src/scope/worktree.ts +187 -0
- package/src/server/api.ts +549 -0
- package/src/server/app.ts +595 -0
- package/src/server/drafts-api.ts +170 -0
- package/src/server/folders-api.ts +243 -0
- package/src/server/graph-view.ts +639 -0
- package/src/server/http.ts +224 -0
- package/src/server/k-agent-api.ts +56 -0
- package/src/server/k-rounds-d99.ts +91 -0
- package/src/server/k-views.ts +405 -0
- package/src/server/keeper-page.ts +446 -0
- package/src/server/keys-api.ts +239 -0
- package/src/server/project-folder-api.ts +104 -0
- package/src/server/vendor.ts +14 -0
- package/src/server/workbench-content.ts +561 -0
- package/src/sources/anchor.ts +129 -0
- package/src/sources/files.ts +267 -0
- package/src/sources/gitobs.ts +41 -0
- package/src/sources/history.ts +231 -0
- package/src/sources/scan-worker.ts +10 -0
- package/src/sources/sessions/locate.ts +292 -0
- package/src/sources/sessions/read.ts +346 -0
- package/src/sources/sessions/scope.ts +42 -0
- package/src/sources/sessions/utterances.ts +223 -0
- package/src/sources/watch.ts +215 -0
- package/src/store/json-file.ts +50 -0
- package/src/store/paths.ts +39 -0
- package/src/store/project-store.ts +295 -0
- package/src/store/versions.ts +172 -0
- package/src/store/workspace.ts +216 -0
- package/src/table/check.ts +333 -0
- package/src/table/follow-up-tools.ts +30 -0
- package/src/table/follow-up.ts +344 -0
- package/src/table/import.ts +610 -0
- package/src/table/project-home.ts +61 -0
- package/src/table/sessions.ts +168 -0
- package/src/table/takeover.ts +483 -0
- package/src/util/git-check.ts +29 -0
- package/src/util/git.ts +411 -0
- package/src/util/own-ts.ts +15 -0
- package/src/util/paths.ts +225 -0
- package/template/FILLING-IN.md +148 -0
- package/template/examples/papertrail.json +747 -0
- package/template/projectkeeper.schema.json +1001 -0
- package/ui/app.js +1135 -0
- package/ui/failures.js +82 -0
- package/ui/flyout.js +120 -0
- package/ui/folder-chooser.js +264 -0
- package/ui/glossary.js +156 -0
- package/ui/graph-fit.js +157 -0
- package/ui/graph-material.js +61 -0
- package/ui/graph-palette.js +109 -0
- package/ui/graph-tools.js +62 -0
- package/ui/graph.js +1712 -0
- package/ui/index.html +31 -0
- package/ui/k/code-view.js +230 -0
- package/ui/k/draft-view.js +93 -0
- package/ui/k/k-views.css +224 -0
- package/ui/k/round-tree.js +450 -0
- package/ui/k/scope-k.js +159 -0
- package/ui/k-data.js +118 -0
- package/ui/k-fold.js +106 -0
- package/ui/k-process.css +302 -0
- package/ui/k-process.js +934 -0
- package/ui/keeper-activity.js +175 -0
- package/ui/keeper-view.js +801 -0
- package/ui/keeper.css +101 -0
- package/ui/key-order.js +62 -0
- package/ui/list-fold.js +67 -0
- package/ui/markdown.d.ts +21 -0
- package/ui/markdown.js +472 -0
- package/ui/placement.d.ts +92 -0
- package/ui/placement.js +445 -0
- package/ui/popover-place.d.ts +16 -0
- package/ui/popover-place.js +89 -0
- package/ui/popover.js +198 -0
- package/ui/project-folder.js +37 -0
- package/ui/shell-state.js +40 -0
- package/ui/strip-rows.js +171 -0
- package/ui/styles.css +492 -0
- package/ui/theme-pick.js +36 -0
- package/ui/theme.js +74 -0
- package/ui/themes/LICENSE.md +13 -0
- package/ui/themes/README.md +44 -0
- package/ui/themes/TOKENS.md +291 -0
- package/ui/themes/a1/board.css +1557 -0
- package/ui/themes/a1/pk.css +392 -0
- package/ui/themes/b1/board.css +1163 -0
- package/ui/themes/b1/pk.css +218 -0
- package/ui/themes/d2/board.css +518 -0
- package/ui/themes/d2/pk.css +243 -0
- package/ui/themes/guard.css +73 -0
- package/ui/themes/i1/board.css +776 -0
- package/ui/themes/i1/pk.css +223 -0
- package/ui/themes/manifest.json +13 -0
- package/ui/views.js +2796 -0
package/ui/glossary.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Vocabulary window (owner 2026-09-17): the fixed vocabulary of Spec §1.5, §1.6 and §2 with a formal
|
|
2
|
+
// definition, the names the industry uses for the same thing, and what the Keeper does to obtain each
|
|
3
|
+
// one. Shown in English or Chinese, switchable in the window (the Spec it comes from is Chinese; the
|
|
4
|
+
// interface's fixed text is English). Counts are computed from the current project's graph.
|
|
5
|
+
import { api, h, openDialog, state } from './app.js';
|
|
6
|
+
import { CATEGORY_COLOR, CATEGORY_COLUMN } from './graph.js';
|
|
7
|
+
|
|
8
|
+
const T = {
|
|
9
|
+
en: { title: 'Vocabulary', intro: 'A fixed vocabulary, the same for every project. Nodes keep the project\'s own names and numbers; only the category is shared, so legend, filters and layout work the same everywhere. In the graph, shape and colour tell the kind of object: a document for Product intent, a long hexagon for Work & plan (Plan and Work item in two colours), and for Observed reality a folder at the foot of each area’s column (an oval when drawn one by one). The category is written on each object. Marks sit on the corner: a star for what is new or changed, a flag, ❓ for a note (lit while it needs you), Update pending.',
|
|
10
|
+
categories: 'Categories', category: 'Category', definition: 'Definition', also: 'Also called in the industry', keeper: 'What the Keeper does to obtain it', inProject: (n) => `${n} in this project`,
|
|
11
|
+
relations: 'Relations', relIntro: 'Every relation carries a Claim (from the material, or the Keeper\'s summary), a basis (Explicit or Inferred), Evidence (sources, fact records, how far the facts go) and the Keeper\'s assessment.', type: 'Type', meaning: 'Meaning', example: 'Example',
|
|
12
|
+
status: 'Status words', word: 'Word', values: 'Values', rule: 'Rule', column: { 'Product intent': 'Product intent', 'Work & plan': 'Work & plan', 'Observed reality': 'Observed reality', 'Change': 'Change' } },
|
|
13
|
+
zh: { title: '词表', intro: '固定词表,对所有项目一样。节点显示项目自己的名字和编号,只有类别是通用的,图例、筛选和布局靠它。图上形状和颜色按对象的三类分:产品意图是文档形;工作与计划是拉长的六边形,Plan 和 Work item 各一种颜色;观察到的结果收在每列底部的文件夹里,逐个画出时是椭圆。类别写在对象上。标记在角上:★ 表示新增或有变动,另有 ⚑、❓ note(要你处理的醒目)、⟳ Update pending。',
|
|
14
|
+
categories: '类别', category: '类别', definition: '定义', also: '业界常叫', keeper: 'Keeper 怎么得到它', inProject: (n) => `本项目 ${n} 个`,
|
|
15
|
+
relations: '关系', relIntro: '每条关系带 Claim(材料原话或 Keeper 概括)、依据(Explicit / Inferred)、Evidence(来源、事实底稿、事实到了哪里)和 Keeper 的评估。', type: '类型', meaning: '含义', example: '例子',
|
|
16
|
+
status: '状态词', word: '词', values: '取值', rule: '规则', column: { 'Product intent': '产品意图', 'Work & plan': '工作与计划', 'Observed reality': '观察到的现实', 'Change': '变化' } },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const CATEGORIES = [
|
|
20
|
+
{ name: "Owner's words",
|
|
21
|
+
def: { en: 'What the owner said, or explicitly confirmed, in a session: one point per item, each with the owner\'s own words and where they were said. The top layer of the product reference; the product, goals, requirements and designs refine it.', zh: 'owner 在会话里直接说的、明确确认的意思,逐条提炼,每条带原话和出处。产品参照的最上层;产品、目标、需求和设计细化它。' },
|
|
22
|
+
also: { en: 'stakeholder statement · founder\'s brief · voice of the customer · verbatim requirement · the "as the owner put it" of a PRD', zh: '利益相关者陈述 · 创始人口述 · 客户原声(voice of the customer)· 原话需求 · PRD 里的「owner 原话」' },
|
|
23
|
+
keeper: { en: 'Taken first, from the owner\'s own messages in the sessions (not whole sessions) and from decision records that copy the owner\'s words; a project that already keeps this layer is used as it is. An agent\'s restatement is not one unless the owner confirmed it. Drift is judged by how far a description has moved from these words. On the graph they are one group on top, with its count, folded until opened.', zh: '最先整理:从会话里 owner 自己的发言(不读整段会话),以及照录 owner 原话的决定记录里取;项目已经保存了这一层的直接用。agent 的复述不算,除非 owner 确认过。漂移按一份描述离这些原话有多远来判。图上它们收成最上面的一组,写明条数,默认收起。' } },
|
|
24
|
+
{ name: 'Product',
|
|
25
|
+
def: { en: 'The one document that describes the whole product: what it is, for whom, and which parts it has. The root of the graph; goals refine it.', zh: '描述整个产品的那一份文档:它是什么、给谁、由哪几块组成。graph 的根节点,Goal 细化它。' },
|
|
26
|
+
also: { en: 'product.md · product overview · product brief · one-pager · product definition · charter', zh: 'product.md · 产品全貌 · 产品简述(product brief)· 一页纸 · 产品定义 · 立项书(charter)' },
|
|
27
|
+
keeper: { en: 'Taken from product.md when the project has one, otherwise from the document the project designates as its product overview (a product-and-modules design, for example). At most one Current item per project; the Product reference job writes it first and lets each Goal refine it. A project without such a document gets no Product node.', zh: '项目有 product.md 就用它;没有就用项目指定为产品全貌的文档(例如「产品与 Module」设计)。每个项目至多一项 Current;产品参照 job 先写它,再让每个 Goal 细化它。没有这种文档的项目不补空节点。' } },
|
|
28
|
+
{ name: 'Goal',
|
|
29
|
+
def: { en: 'A product purpose or user effect: why the product exists and what a user should get.', zh: '产品目的或用户效果:这个产品为什么存在、用户应当得到什么。' },
|
|
30
|
+
also: { en: 'product goal · objective (the O of OKR) · vision statement · north-star outcome · user outcome · the "why" of a PRD', zh: '产品目标 · objective(OKR 的 O)· 愿景(vision)· 北极星结果 · 用户效果 · PRD 里的「为什么」' },
|
|
31
|
+
keeper: { en: 'Read from intent material: product.md, the PRD\'s goals, a design\'s "effects" section, a README\'s purpose paragraph, and owner statements in the Keeper chat. Established by the Product reference job. Explicit when the material states it; Inferred, with a project-level note, when only README or issues exist.', zh: '从意图类材料提取:product.md、PRD 的目标章节、设计文档的「效果」节、README 的目的段,以及 owner 在 Keeper 对话里的陈述。由产品参照 job 建立;材料写明为 Explicit,只有 README/Issue 可依时为 Inferred 并在项目级 note 说明。' } },
|
|
32
|
+
{ name: 'Area',
|
|
33
|
+
def: { en: 'A part of the product: a Module, a feature, or a theme the Keeper grouped work under.', zh: '产品的一块:Module、功能,或 Keeper 归纳出的主题。' },
|
|
34
|
+
also: { en: 'module · component · subsystem · feature area · domain / bounded context · epic · workstream · product area', zh: '模块(module)· 组件(component)· 子系统 · 功能域(feature area)· 领域 / bounded context · epic · 工作流(workstream)· 产品区域' },
|
|
35
|
+
keeper: { en: 'Taken from the project\'s formal Modules when it has them; otherwise the Keeper groups work by user effect or capability (Inferred). Added, merged, split or retired only with material behind it. Each area later receives an area understanding: the effect reached now, remaining gaps, and what each work thread contributes.', zh: '项目有正式 Module 就用 Module;没有时 Keeper 按用户效果或能力归纳(Inferred)。有材料支撑才新增、合并、拆分或退役。之后每个区域形成区域理解:当前达到的效果、剩余缺口、各工作脉络的贡献。' } },
|
|
36
|
+
{ name: 'Requirement',
|
|
37
|
+
def: { en: 'What the product must do or provide, as stated by the project.', zh: '项目写明的需求或功能描述:产品必须做到什么。' },
|
|
38
|
+
also: { en: 'requirement · PRD item · user story · acceptance criteria · functional spec item · feature request · task contract', zh: '需求 · PRD 条目 · 用户故事(user story)· 验收条件(acceptance criteria)· 功能规格条目 · 功能请求 · task contract' },
|
|
39
|
+
keeper: { en: 'Extracted from intent material (PRD, requirement lists, task contracts) and owner statements, with validity, basis, sources and the quote. Suggestions from workers, QC or agents enter only as Proposed until the owner approves or the project adopts them. Re-established when intent material changes.', zh: '从意图类材料(PRD、需求清单、task contract)和 owner 陈述提取,带有效性、依据、来源和原话。worker、QC、agent 的建议只能以 Proposed 进入,直到 owner 批准或项目采纳。意图材料变化时重新建立。' } },
|
|
40
|
+
{ name: 'Design',
|
|
41
|
+
def: { en: 'How the product is meant to be built or behave: a design, a solution, a Spec.', zh: '打算怎么做到:设计、方案、Spec。' },
|
|
42
|
+
also: { en: 'design doc · technical spec · architecture doc · RFC · API spec · schema · wireframe / mockup', zh: '设计文档 · 技术规格(spec)· 架构文档 · RFC · API 规格 · 数据模型(schema)· 线框图 / mockup' },
|
|
43
|
+
keeper: { en: 'Extracted from design documents and specs in the intent material; each version is one item, and a newer version of the same document marks the older one Replaced. Sections the Keeper suspects are outdated get a Suspected stale mark instead of being rewritten.', zh: '从意图材料里的设计文档和 Spec 提取;每个版本一项,同一文档的新版本把旧版本标为 Replaced。疑似过期的段落挂 Suspected stale,不改写。' } },
|
|
44
|
+
{ name: 'Decision',
|
|
45
|
+
def: { en: 'Something decided or a constraint that must be respected.', zh: '已经决定的事,或必须遵守的约束。' },
|
|
46
|
+
also: { en: 'ADR (architecture decision record) · decision log · policy · constraint · non-goal · guardrail', zh: 'ADR(架构决策记录)· 决定记录(decision log)· 策略(policy)· 约束 · 非目标(non-goal)· 护栏(guardrail)' },
|
|
47
|
+
keeper: { en: 'Extracted from decision logs and from what the owner says in the Keeper chat. A decision the owner stated but no document records gets an Undocumented decision mark. A new product decision creates a change record and starts propagation to what it affects.', zh: '从决定记录和 owner 在 Keeper 对话里说的话提取。owner 说了但文档没记的,挂 Undocumented decision 标记。新的产品决定建立变化记录,并向受影响的条目传播。' } },
|
|
48
|
+
{ name: 'Plan',
|
|
49
|
+
def: { en: 'A plan or milestone: what comes in which order.', zh: '计划、里程碑:分几步、先后顺序。' },
|
|
50
|
+
also: { en: 'roadmap · milestone · release plan · sprint / iteration plan · project plan · phase', zh: '路线图(roadmap)· 里程碑 · 发布计划 · sprint / 迭代计划 · 项目计划 · 阶段(phase)' },
|
|
51
|
+
keeper: { en: 'Read in round 1 from the plan documents: each increment, milestone or phase is one Plan item under the Product (or the Goal it advances), with the progress the material reports. It sits in the Work & plan column with the work items and is drawn in the same shape. Items a plan moves out of scope become Deferred; items it strikes out become Replaced or Abandoned, each with a change record dated from the material.', zh: '第一轮从计划文档读:每个增量、里程碑或阶段是一项 Plan,挂在 Product(或它推进的 Goal)下,进度按材料所报。它和工作项同在「工作与计划」一列,图上形状相同。计划移出范围的条目变 Deferred,划掉的变 Replaced 或 Abandoned,各建一条按材料日期的变化记录。' } },
|
|
52
|
+
{ name: 'Work item',
|
|
53
|
+
def: { en: 'One piece of work: a task, a contract being executed, an issue, a TODO line.', zh: '一项工作:task、正在执行的 contract、Issue、TODO 条目。' },
|
|
54
|
+
also: { en: 'task · ticket · issue · story card · PR / MR · TODO · work package', zh: '任务(task)· 工单(ticket)· issue · 故事卡(story card)· PR / MR · TODO · 工作包(work package)' },
|
|
55
|
+
keeper: { en: 'Round 1 takes the work items from the project\'s own plan: one per task contract, task or issue the plan documents name, placed under the Area whose effect it delivers, with the plan\'s acceptance criteria as Done means. Later, fact records from sessions, commits and reports are attached to these work items (progress, what changed, results, what is unresolved). Work the plan does not name becomes a new work item; an execution batch, a session or a commit never does. Shown under the project\'s own id, drawn as an elongated diamond.', zh: '第一轮从项目自己的计划里取:计划文档里写到的每份任务合同、每个 task 或 issue 各一项,挂在它贡献效果的区域下,Done means 取计划里的验收条件。之后,会话、提交、报告整理出的事实底稿挂到这些工作项上(进度、改了什么、结果、未解决什么)。计划里没有的工作才新建工作项;一个执行批次、一段会话、一次提交都不单独成为工作项。显示项目自己的编号,图上画成拉长的菱形。' } },
|
|
56
|
+
{ name: 'Session',
|
|
57
|
+
def: { en: 'One recorded session of an agent or a person working in the project.', zh: '一段 agent 或人在项目里的工作会话。' },
|
|
58
|
+
also: { en: 'chat / agent session transcript · pairing session · meeting notes', zh: '对话 / agent 会话记录 · 结对会话 · 会议记录' },
|
|
59
|
+
keeper: { en: 'Read from agent session logs (Claude Code, Codex homes) as sources and cited as evidence: the owner\'s words become decisions and corrections, the agents\' work becomes progress on work items. Sessions are not drawn on the graph; Project scope lists them.', zh: '从 agent 会话记录(Claude Code、Codex 的 home)读成来源,作为证据被引用:owner 的原话成为决定和纠正,agent 的工作成为工作项的进展。会话不画在图上,Project scope 里能看到。' } },
|
|
60
|
+
{ name: 'Run',
|
|
61
|
+
def: { en: 'One execution: an execution prompt, a run, a result message.', zh: '一次执行:执行 prompt、运行、结果消息。' },
|
|
62
|
+
also: { en: 'job run · pipeline / CI run · batch · agent run · deployment', zh: 'job run · 流水线 / CI run · 批处理 · agent run · 部署' },
|
|
63
|
+
keeper: { en: 'Execution prompts, run directories and result messages are read as sources and cited as evidence (produced, verifies). They are not drawn on the graph.', zh: '执行 prompt、run 目录、结果消息读成来源,作为证据被引用(produced、verifies)。不画在图上。' } },
|
|
64
|
+
{ name: 'Review',
|
|
65
|
+
def: { en: 'A QC or review report.', zh: 'QC、审阅报告。' },
|
|
66
|
+
also: { en: 'code review · QC report · audit · design review · retrospective findings', zh: '代码审查(code review)· QC 报告 · 审计 · 设计评审 · 复盘发现' },
|
|
67
|
+
keeper: { en: 'Read as sources; cited by verifies relations, which carry the Keeper\'s assessment of whether the reviewed claim holds.', zh: '读成来源;由 verifies 关系引用,关系上带 Keeper 对「被验证的主张是否成立」的评估。' } },
|
|
68
|
+
{ name: 'Test',
|
|
69
|
+
def: { en: 'A test or other verification.', zh: '测试或其他验证。' },
|
|
70
|
+
also: { en: 'unit / integration / e2e test · test report · benchmark · acceptance test · manual verification', zh: '单元 / 集成 / 端到端测试 · 测试报告 · 基准测试 · 验收测试 · 人工验证' },
|
|
71
|
+
keeper: { en: 'Test files and reports read as sources; cited by verifies relations.', zh: '测试文件和报告读成来源;由 verifies 关系引用。' } },
|
|
72
|
+
{ name: 'Result',
|
|
73
|
+
def: { en: 'An actual outcome: a code area, a commit, a build, a run result, a status statement.', zh: '实际结果:代码区域、提交、构建产物、运行结果、状态陈述。' },
|
|
74
|
+
also: { en: 'artifact · deliverable · commit · build · release · report · status update', zh: '产物(artifact)· 交付物 · 提交(commit)· 构建 · 发布 · 报告 · 状态更新' },
|
|
75
|
+
keeper: { en: 'Read as sources (code, commits, status) and cited by implements or produced relations. Code existing does not make a work item Done: progress stays what the material reports; a contradiction gets a Suspected stale mark.', zh: '读成来源(代码、提交、状态),由 implements 或 produced 关系引用。代码存在不等于 Done:进度保持材料所报,矛盾时挂 Suspected stale。' } },
|
|
76
|
+
{ name: 'Change',
|
|
77
|
+
def: { en: 'One change record: when, from which material, what went from A to B, and what it affects.', zh: '一条变化记录:什么时候、由什么材料、把什么从 A 变成 B、影响到谁。' },
|
|
78
|
+
also: { en: 'changelog entry · decision history · version diff · amendment · deprecation notice', zh: '变更日志条目(changelog)· 决定历史 · 版本差异 · 修订 · 弃用通知' },
|
|
79
|
+
keeper: { en: 'Written by the Keeper only when product meaning or state changed: a decision made, replaced, deferred or abandoned; a proposal adopted; a work item completed or stopped; an owner correction. Not for every commit. Change records are not drawn as nodes: the objects they affect get a star (★) — new or changed since your last visit, or within the last one to three days — and Recent changes and the Changes view list them.', zh: 'Keeper 只在产品含义或状态变了时写:一个决定作出、被替代、延后或放弃;提议被采纳;工作项完成或停下;owner 纠正。不是每次提交都写。变化记录不画成节点:受它影响的对象打 ★(自你上次来之后新增或变动的,或最近一到三天内的),Recent changes 和 Changes 视图列出它们。' } },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const RELATIONS = [
|
|
83
|
+
{ type: 'serves', meaning: { en: 'Contributes to an effect or a higher object (a contribution claim).', zh: '为某个效果或上层对象作贡献(贡献声明)。' }, example: { en: 'a piece of work serves an area; a shared capability serves several areas', zh: '一项工作服务某个区域;一项共用能力同时服务多个区域' } },
|
|
84
|
+
{ type: 'refines', meaning: { en: 'Makes a higher object more specific.', zh: '细化上层内容。' }, example: { en: 'a requirement refines a Module; a design refines a requirement', zh: '需求细化 Module;设计细化需求' } },
|
|
85
|
+
{ type: 'implements', meaning: { en: 'Realises it.', zh: '实现。' }, example: { en: 'a code area implements a work item', zh: '代码区域实现某项工作' } },
|
|
86
|
+
{ type: 'verifies', meaning: { en: 'Checks it.', zh: '验证。' }, example: { en: 'a test or QC verifies a behaviour or result', zh: '测试或 QC 验证某个行为或结果' } },
|
|
87
|
+
{ type: 'depends on', meaning: { en: 'Needs the other\'s result first.', zh: '依赖。' }, example: { en: 'one work item depends on another\'s result', zh: '一项工作依赖另一项的结果' } },
|
|
88
|
+
{ type: 'produced', meaning: { en: 'Came out of a session or run.', zh: '由某次会话或执行产生。' }, example: { en: 'a session produced a decision or a result', zh: '一次会话产生了一个决定或结果' } },
|
|
89
|
+
{ type: 'replaces', meaning: { en: 'Takes the place of.', zh: '取代。' }, example: { en: 'a new plan replaces part of an old one', zh: '新计划取代旧计划的一部分' } },
|
|
90
|
+
{ type: 'contradicts', meaning: { en: 'Does not agree with.', zh: '相互矛盾。' }, example: { en: 'a status record and the code disagree', zh: '状态记录和代码对不上' } },
|
|
91
|
+
{ type: 'affects', meaning: { en: 'A change reaches it.', zh: '变化影响到。' }, example: { en: 'the items a change record affects', zh: '一条变化影响的条目' } },
|
|
92
|
+
{ type: 'carries out', meaning: { en: 'A piece of work carries out a decision that asks for something to be done.', zh: '一项工作落实一条要求做某件事的决定。' }, example: { en: 'the work that removes an old setting carries out the decision to drop it', zh: '删掉旧配置的那项工作落实了「旧配置不再保留」的决定' } },
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const STATUS = [
|
|
96
|
+
{ word: { en: 'Validity', zh: '有效性' }, values: 'Current / Proposed / Deferred / Replaced / Abandoned / Removed',
|
|
97
|
+
rule: { en: 'Current is what the material says is in force; Proposed is a draft, a discussion or an unadopted suggestion; Deferred was moved out of scope; Replaced points at what replaced it; Abandoned is a direction no longer taken; Removed is an object whose material was deleted from the project\'s current version, the deletion itself saying it is no longer needed. The first judgement is not a change. Only a replacement, deferral, abandonment, adoption, deletion or owner correction changes validity afterwards, each with a change record; age, completion or a move never do. A Removed object is not on the graph or in the List: it appears, struck through, only in Recent changes and the change details.', zh: 'Current 是材料写明现行的;Proposed 是草稿、讨论中或未被采纳的建议;Deferred 移出了范围;Replaced 指向替代它的内容;Abandoned 是不再走的方向;Removed 是材料已从项目当前版本中删除的对象,删除本身说明它不再需要。第一次判断不算变化;此后只有替代、延后、放弃、采纳、删除或 owner 纠正改变有效性,每次一条变化记录;变旧、完成、移动都不改。Removed 的对象不进图和 List,只在 Recent changes 和变化详情里以划线出现。' } },
|
|
98
|
+
{ word: { en: 'Progress', zh: '进度' }, values: 'Planned / In progress / Done / On hold',
|
|
99
|
+
rule: { en: 'Reported by the material. Code existing is not Done. When the Keeper sees contradicting evidence it keeps the reported progress, adds a Suspected stale mark and explains in a note; it does not re-judge on its own.', zh: '按材料所报。代码存在不等于 Done。Keeper 看到矛盾证据时保持材料的进度,挂 Suspected stale 并在 note 里说明,不自行改判。' } },
|
|
100
|
+
{ word: { en: 'Basis', zh: '依据' }, values: 'Explicit / Inferred',
|
|
101
|
+
rule: { en: 'Explicit: the project wrote it. Inferred: the Keeper concluded it from material; on the graph an inferred object carries the word Inferred and an inferred relation is a dotted line. A dashed frame is not Inferred: it is a work item\'s copy in another module it serves, its main one solid elsewhere.', zh: 'Explicit:项目写明。Inferred:Keeper 从材料归纳;图上推断的对象写着 Inferred,推断的关系画成点线。虚线框不是 Inferred:它是一项工作在它服务的另一个模块里的副本,主的那个实线框在别处。' } },
|
|
102
|
+
{ word: { en: 'Takeover depth', zh: '接手深度' }, values: 'Full / Focused / First picture only',
|
|
103
|
+
rule: { en: 'Chosen on the Takeover page of the Keeper view before you press Start. The first usable picture (product reference, work items, graph, project-level notes) comes first; then the Keeper goes on to the depth chosen without asking again. Full: every plan, stage and topic dug by question, every planned material accounted for. Focused: what the current objects, the open work and the breakpoint candidates involve. First picture only: no deepening; history is read on demand. From Start on the depths cannot be pressed; once done, a deeper one goes on from what is done, and the same or a shallower one needs Clear first. Before Start the project is Not organized yet.', zh: '在 Keeper 视图的接手页上、按 Start 之前选。先出第一份可用(产品参照、工作项、graph、项目级 note),然后 Keeper 不再问,接着做到选的那一档。Full:每个计划、阶段与主题按问题深挖,计划里每份材料都有交代。Focused:只挖现行对象、还开着的工作和断点候选牵涉的部分。First picture only:不加深,历史需要时再读。按下 Start 之后三档不能再点;做完之后更深的一档可以接着做,同一档或更浅的要先清空。按 Start 之前项目是 Not organized yet。' } },
|
|
104
|
+
{ word: { en: 'Organizing level', zh: '整理层次' }, values: 'Read in full / Conclusions only / Sampled / Indexed / Not organized / Skipped: too large',
|
|
105
|
+
rule: { en: 'Per material, computed from what the Keeper actually did. Read in full: a fact record exists. Conclusions only: only conclusions, known failures and fix reasons were kept. Sampled: belongs to a group whose samples were read; classified by the rule. Indexed: searchable and read on demand, not organized under the chosen depth. Not organized: pending or failed. Skipped: too large: a file over the size intake reads of one file; its text is not read, the ledger still records it, and it is neither pending nor a failure (listed in Project scope). A material an investigation reads later moves up to the level actually read.', zh:'按单份材料统计,来自 Keeper 实际做过的事。Read in full:已有事实底稿。Conclusions only:只留了结论、已知失败和修复理由。Sampled:属于某个规律,读了样本,其余按规律归类。Indexed:可以搜索、需要时读,所选深度下不整理。Not organized:待处理或失败。Skipped: too large:文件超过读取的大小上限,正文没有读入,账本照记;不算待处理,也不算失败(列在 Project scope)。后来被调查读到的材料,按实际读到的程度升级。' } },
|
|
106
|
+
{ word: { en: 'Assessment', zh: '评估' }, values: 'Holds / Questioned / Not assessed',
|
|
107
|
+
rule: { en: 'A relation\'s existence is a claim, not a fact. Only a product relook or an owner correction sets Holds or Questioned; everything else stays Not assessed.', zh: '关系存在只是声明,不代表成立。只有产品回看或 owner 纠正才给出 Holds 或 Questioned,其余都是 Not assessed。' } },
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const LANG_KEY = 'pk.vocabulary.lang';
|
|
111
|
+
function savedLang() { try { const v = localStorage.getItem(LANG_KEY); return v === 'zh' || v === 'en' ? v : 'en'; } catch { return 'en'; } }
|
|
112
|
+
function saveLang(lang) { try { localStorage.setItem(LANG_KEY, lang); } catch { /* per-viewer convenience only */ } }
|
|
113
|
+
|
|
114
|
+
function content(lang, counts) {
|
|
115
|
+
const t = T[lang];
|
|
116
|
+
const chip = (name) => h('span', { class: 'chip' }, h('i', { style: { background: CATEGORY_COLOR[name] || '#888' } }), name);
|
|
117
|
+
return [
|
|
118
|
+
h('p', { class: 'muted' }, t.intro),
|
|
119
|
+
h('h3', {}, t.categories),
|
|
120
|
+
h('table', { class: 'glossary' },
|
|
121
|
+
h('thead', {}, h('tr', {}, h('th', {}, t.category), h('th', {}, t.definition), h('th', {}, t.also), h('th', {}, t.keeper))),
|
|
122
|
+
h('tbody', {}, ...CATEGORIES.map((c) => h('tr', {},
|
|
123
|
+
h('td', {}, chip(c.name), h('div', { class: 'faint' }, t.column[CATEGORY_COLUMN[c.name]] || ''), counts[c.name] ? h('div', { class: 'faint' }, t.inProject(counts[c.name])) : null),
|
|
124
|
+
h('td', {}, c.def[lang]),
|
|
125
|
+
h('td', { class: 'also' }, c.also[lang]),
|
|
126
|
+
h('td', {}, c.keeper[lang]))))),
|
|
127
|
+
h('h3', {}, t.relations),
|
|
128
|
+
h('p', { class: 'muted' }, t.relIntro),
|
|
129
|
+
h('table', { class: 'glossary' },
|
|
130
|
+
h('thead', {}, h('tr', {}, h('th', {}, t.type), h('th', {}, t.meaning), h('th', {}, t.example))),
|
|
131
|
+
h('tbody', {}, ...RELATIONS.map((r) => h('tr', {}, h('td', {}, h('code', {}, r.type)), h('td', {}, r.meaning[lang]), h('td', { class: 'also' }, r.example[lang]))))),
|
|
132
|
+
h('h3', {}, t.status),
|
|
133
|
+
h('table', { class: 'glossary' },
|
|
134
|
+
h('thead', {}, h('tr', {}, h('th', {}, t.word), h('th', {}, t.values), h('th', {}, t.rule))),
|
|
135
|
+
h('tbody', {}, ...STATUS.map((s) => h('tr', {}, h('td', {}, h('strong', {}, s.word[lang])), h('td', { class: 'also' }, s.values), h('td', {}, s.rule[lang]))))),
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function openGlossary() {
|
|
140
|
+
let counts = {};
|
|
141
|
+
try {
|
|
142
|
+
if (state.projectId) { const g = await api(`/api/projects/${encodeURIComponent(state.projectId)}/graph`); for (const n of g.nodes) counts[n.category] = (counts[n.category] ?? 0) + 1; }
|
|
143
|
+
} catch { counts = {}; }
|
|
144
|
+
let lang = savedLang();
|
|
145
|
+
const body = h('div', { class: 'glossary-body' });
|
|
146
|
+
const buttons = {};
|
|
147
|
+
const render = () => {
|
|
148
|
+
body.replaceChildren(...content(lang, counts));
|
|
149
|
+
for (const [k, b] of Object.entries(buttons)) b.classList.toggle('active', k === lang);
|
|
150
|
+
};
|
|
151
|
+
const toggle = h('div', { class: 'segmented lang-toggle' },
|
|
152
|
+
...[['en', 'English'], ['zh', '中文']].map(([k, label]) => (buttons[k] = h('button', { onClick: () => { lang = k; saveLang(k); render(); } }, label))));
|
|
153
|
+
const dialog = openDialog('Vocabulary · 词表', [toggle, body], { onClose: () => dialog.classList.remove('wide') });
|
|
154
|
+
dialog.classList.add('wide');
|
|
155
|
+
render();
|
|
156
|
+
}
|
package/ui/graph-fit.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// How the project graph sits in its window (Spec §6.3 "布局"; CKC-09 AC-39, D68): on opening the whole picture is in the
|
|
2
|
+
// visible area; the graph never chooses a view so small that the words on the objects stop being readable; once that
|
|
3
|
+
// floor is reached the picture is panned instead. The floor is for the views the graph chooses. The owner may go below
|
|
4
|
+
// it — by wheel or pinch, down to a hard limit, or with `Show all anyway` — to see the shape of a large project at a
|
|
5
|
+
// glance (WorkflowKeeper does the same: a floor for opening, a lower limit for the owner's own zoom). The graph's
|
|
6
|
+
// viewport and its readability self-check both ask this module, so they cannot disagree about what "fits". No DOM here:
|
|
7
|
+
// it is tested with `node --test` (src/ui/graph-fit.test.ts).
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The smallest size body text has in the interface: `small` in styles.css, 11px — the size of every secondary line the
|
|
11
|
+
* owner is expected to read (sub-headings, hints, counts beside a name). The 10.5px of the legend and of the line under
|
|
12
|
+
* a strip item, and the 10px inside chips, label things rather than say them. A name on an object is never smaller than
|
|
13
|
+
* this on screen. A test reads styles.css, so the two cannot drift apart unnoticed.
|
|
14
|
+
*/
|
|
15
|
+
export const MIN_TEXT_PX = 11;
|
|
16
|
+
/** The smallest size any text has in the interface (the counts in chips). The small print on an object — its kind, its
|
|
17
|
+
* progress, its counts — is never smaller than this on screen. */
|
|
18
|
+
export const MIN_LABEL_PX = 10;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Sizes of the words drawn on objects, in px at 100%. They are large against the object they sit on, and the objects
|
|
22
|
+
* sit close together, so that a whole project fits a window while its words can still be read: the lever for fitting
|
|
23
|
+
* more is the ratio of these sizes to the sizes in graph.js, never a lower floor.
|
|
24
|
+
* `name`: every object's name. `product`: the product's name. `kind`: the line of kind, validity and progress, and the
|
|
25
|
+
* other small print (`No established link`, a folder's lines, a group's label). `count`: numbers in chips and corner
|
|
26
|
+
* marks. `total`: the number on an Observed reality folder.
|
|
27
|
+
*/
|
|
28
|
+
export const TEXT = Object.freeze({ name: 14, product: 15, kind: 13, count: 13, total: 20 });
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The zoom no view of the graph's own choosing goes below: opening, fitting again while the view is still the graph's
|
|
32
|
+
* (the window or the graph's container changed size, the scale changed), `Fit`, what a change touched brought into view.
|
|
33
|
+
* A name is then exactly MIN_TEXT_PX on screen (and the small print 13 × 11/14 = 10.2px, above MIN_LABEL_PX). It is not
|
|
34
|
+
* a limit on the owner: their own wheel and pinch go below it (hardMinZoom), and so does `Show all anyway` (wholeView).
|
|
35
|
+
*/
|
|
36
|
+
export const ZOOM_FLOOR = MIN_TEXT_PX / TEXT.name;
|
|
37
|
+
/** How far the owner's own wheel and pinch go when the picture needs no more than that (WorkflowKeeper's number for the
|
|
38
|
+
* same thing). Far below the floor: at 0.3 a name is 4px, the project is read as a shape, not as words. */
|
|
39
|
+
export const ZOOM_HARD_MIN = 0.3;
|
|
40
|
+
/** A fitted picture is not magnified past the size it was drawn for (WorkflowKeeper's rule): a project of five objects
|
|
41
|
+
* does not fill the window with 30px letters. The owner can still zoom in by hand. */
|
|
42
|
+
export const FIT_CEILING = 1;
|
|
43
|
+
/** Air around a fitted picture, px on screen. */
|
|
44
|
+
export const FIT_PAD = 10;
|
|
45
|
+
/** After a change of size, this much of the picture (px on screen, each way) stays in the window at least. */
|
|
46
|
+
export const KEEP_PX = 120;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The box around objects given by centre and size ({ x, y, w, h }), including what stands out of an object: `top` (its
|
|
50
|
+
* corner marks) and `right` (its link count), in the same units. Null when there is nothing.
|
|
51
|
+
*/
|
|
52
|
+
export function pictureBox(nodes) {
|
|
53
|
+
if (!nodes.length) return null;
|
|
54
|
+
let x1 = Infinity, y1 = Infinity, x2 = -Infinity, y2 = -Infinity;
|
|
55
|
+
for (const n of nodes) {
|
|
56
|
+
x1 = Math.min(x1, n.x - n.w / 2); x2 = Math.max(x2, n.x + n.w / 2 + (n.right ?? 0));
|
|
57
|
+
y1 = Math.min(y1, n.y - n.h / 2 - (n.top ?? 0)); y2 = Math.max(y2, n.y + n.h / 2);
|
|
58
|
+
}
|
|
59
|
+
return { x1, y1, x2, y2 };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The view that shows a picture in a window. `box`: { x1, y1, x2, y2 } in the picture's own units; `viewport`: { w, h }
|
|
64
|
+
* in px. Returns the zoom and the pan (px, of the picture's origin from the window's top left corner), and:
|
|
65
|
+
* `fits` — the whole picture is inside the window at this zoom; `fitZoom` — the zoom that would show all of it, however
|
|
66
|
+
* small; `across`, `down` — how many windows the picture takes at the returned zoom.
|
|
67
|
+
* The zoom is `fitZoom` held between the floor and the ceiling. An axis that fits is centred; one that does not starts at
|
|
68
|
+
* the picture's left or top edge, where the top levels and the first column are, and the rest is reached by panning.
|
|
69
|
+
* `padTop`: px kept clear at the top instead of `pad` — the graph's buttons float over its top right corner (CKC-09
|
|
70
|
+
* AC-37), and a view the graph chooses does not put objects under them. The picture is fitted to, and centred in, what
|
|
71
|
+
* is left below it. Everything that works out a view takes the same option (wholeView, hardMinZoom, openingView).
|
|
72
|
+
*/
|
|
73
|
+
export function fitView(box, viewport, { floor = ZOOM_FLOOR, ceiling = FIT_CEILING, pad = FIT_PAD, padTop = pad } = {}) {
|
|
74
|
+
const bw = Math.max(1, box.x2 - box.x1), bh = Math.max(1, box.y2 - box.y1);
|
|
75
|
+
const availW = Math.max(1, viewport.w - 2 * pad), availH = Math.max(1, viewport.h - pad - padTop);
|
|
76
|
+
const fitZoom = Math.min(availW / bw, availH / bh);
|
|
77
|
+
const zoom = Math.min(ceiling, Math.max(floor, fitZoom));
|
|
78
|
+
const slack = 0.5; // half a pixel of rounding is not "does not fit"
|
|
79
|
+
const fitsX = bw * zoom <= availW + slack, fitsY = bh * zoom <= availH + slack;
|
|
80
|
+
const pan = {
|
|
81
|
+
x: (fitsX ? (viewport.w - bw * zoom) / 2 : pad) - box.x1 * zoom,
|
|
82
|
+
y: (fitsY ? padTop + (availH - bh * zoom) / 2 : padTop) - box.y1 * zoom,
|
|
83
|
+
};
|
|
84
|
+
return { zoom, pan, fits: fitsX && fitsY, fitsX, fitsY, fitZoom, across: (bw * zoom) / availW, down: (bh * zoom) / availH };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The whole picture in the window, whatever that does to the size of its words: `Show all anyway`, the owner's explicit
|
|
89
|
+
* choice. fitView without the floor — still centred, still not magnified. It is never below hardMinZoom of the same
|
|
90
|
+
* picture and window, so the graph library, which refuses a view below its minimum zoom, accepts it.
|
|
91
|
+
*/
|
|
92
|
+
export function wholeView(box, viewport, opts = {}) {
|
|
93
|
+
return fitView(box, viewport, { ...opts, floor: 0 });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* How far the owner's own wheel and pinch go: ZOOM_HARD_MIN, or less when the whole picture needs less — never higher
|
|
98
|
+
* than the zoom that shows all of it, so the whole of a large project can always be reached by hand. The constant when
|
|
99
|
+
* there is no picture, or no window yet.
|
|
100
|
+
*/
|
|
101
|
+
export function hardMinZoom(box, viewport, { hardMin = ZOOM_HARD_MIN, pad = FIT_PAD, padTop = pad } = {}) {
|
|
102
|
+
if (!box || !(viewport.w >= 2) || !(viewport.h >= 2)) return hardMin;
|
|
103
|
+
const whole = wholeView(box, viewport, { pad, padTop }).fitZoom;
|
|
104
|
+
return Number.isFinite(whole) && whole > 0 ? Math.min(hardMin, whole) : hardMin;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The view a picture opens on: fitView of the box around `nodes`; null when there are none. One thing more when the
|
|
109
|
+
* picture is too big both ways. What is centred over the whole picture — the product, the owner's words — then stands
|
|
110
|
+
* far to the right of the columns the window starts on, and the window would open on empty rows above the first goals
|
|
111
|
+
* and areas. Those rows are skipped: the view starts at the topmost object that stands (by half its width or more) over
|
|
112
|
+
* the part of the picture the window shows across. Nothing is hidden; the rows above are one drag away.
|
|
113
|
+
*/
|
|
114
|
+
export function openingView(nodes, viewport, opts = {}) {
|
|
115
|
+
const box = pictureBox(nodes);
|
|
116
|
+
if (!box) return null;
|
|
117
|
+
const v = fitView(box, viewport, opts);
|
|
118
|
+
if (v.fitsX || v.fitsY) return v;
|
|
119
|
+
const pad = opts.pad ?? FIT_PAD;
|
|
120
|
+
const right = box.x1 + Math.max(1, viewport.w - 2 * pad) / v.zoom;
|
|
121
|
+
const tops = nodes.filter((n) => Math.min(n.x + n.w / 2, right) - Math.max(n.x - n.w / 2, box.x1) >= n.w / 2).map((n) => n.y - n.h / 2 - (n.top ?? 0));
|
|
122
|
+
if (tops.length) v.pan.y = (opts.padTop ?? pad) - Math.min(...tops) * v.zoom;
|
|
123
|
+
return v;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The owner's own view after the window changed size: their zoom and pan are kept. Only when that would leave less than
|
|
128
|
+
* `keep` px of the picture in the window is the pan moved, by just enough to bring that much back.
|
|
129
|
+
*/
|
|
130
|
+
export function keepInView(box, viewport, zoom, pan, keep = KEEP_PX) {
|
|
131
|
+
const axis = (lo, hi, size, p) => {
|
|
132
|
+
const a = lo * zoom + p, b = hi * zoom + p;
|
|
133
|
+
const need = Math.min(keep, b - a, size);
|
|
134
|
+
if (b < need) return p + (need - b);
|
|
135
|
+
if (a > size - need) return p - (a - (size - need));
|
|
136
|
+
return p;
|
|
137
|
+
};
|
|
138
|
+
const x = axis(box.x1, box.x2, viewport.w, pan.x), y = axis(box.y1, box.y2, viewport.h, pan.y);
|
|
139
|
+
return x === pan.x && y === pan.y ? pan : { x, y };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Whether a box of the picture is wholly inside the window under a view. */
|
|
143
|
+
export function inView(box, viewport, zoom, pan, margin = 0) {
|
|
144
|
+
return box.x1 * zoom + pan.x >= margin && box.y1 * zoom + pan.y >= margin && box.x2 * zoom + pan.x <= viewport.w - margin && box.y2 * zoom + pan.y <= viewport.h - margin;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The pan that brings a box into the window by the shortest move, `margin` px from the edge; a box larger than the
|
|
148
|
+
* window shows its top left. The zoom is not touched. */
|
|
149
|
+
export function panToShow(box, viewport, zoom, pan, margin = 24) {
|
|
150
|
+
const axis = (lo, hi, size, p) => {
|
|
151
|
+
const a = lo * zoom + p, b = hi * zoom + p;
|
|
152
|
+
if (a < margin || b - a > size - 2 * margin) return p + (margin - a);
|
|
153
|
+
if (b > size - margin) return p - (b - (size - margin));
|
|
154
|
+
return p;
|
|
155
|
+
};
|
|
156
|
+
return { x: axis(box.x1, box.x2, viewport.w, pan.x), y: axis(box.y1, box.y2, viewport.h, pan.y) };
|
|
157
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// The card's material, lent to the graph's objects by a theme (Spec §6.3, §6.16, after WorkflowKeeper's D87): what an
|
|
2
|
+
// SVG picture can do of a machined card — a hard lip and a soft shadow under the shape, the fill, a brushed hairline
|
|
3
|
+
// texture, a band in the object's own colour along the top edge, a chamfer highlight under it and a shade along the
|
|
4
|
+
// bottom. The shapes stay the graph's own (D53): the document, the long hexagon, the oval, the round card of a folded
|
|
5
|
+
// group, the folder. Pure strings in, strings out, so it is tested with `node --test` (src/ui/graph-palette.test.ts);
|
|
6
|
+
// graph.js draws these as a second picture under the words and marks, behind cytoscape's own border.
|
|
7
|
+
|
|
8
|
+
/** The shapes cytoscape draws, in its -1..1 polygon points (graph.js uses the same strings). */
|
|
9
|
+
export const DOC_POLYGON = [[-1, -1], [0.72, -1], [1, -0.62], [1, 1], [-1, 1]]; // a document with a folded corner
|
|
10
|
+
export const HEX_POLYGON = [[-0.8, -1], [0.8, -1], [1, 0], [0.8, 1], [-0.8, 1], [-1, 0]]; // a long hexagon
|
|
11
|
+
const CARD_R = 6;
|
|
12
|
+
const FOLDER_TAB = { w: 78, h: 8 };
|
|
13
|
+
|
|
14
|
+
const num = (v) => String(Math.round(v * 100) / 100);
|
|
15
|
+
|
|
16
|
+
/** The outline of a shape as an SVG path, sized to the object (its top left at 0,0). */
|
|
17
|
+
export function shapePath(kind, w, h) {
|
|
18
|
+
const poly = (pts) => `M${pts.map(([px, py]) => `${num((px + 1) / 2 * w)},${num((py + 1) / 2 * h)}`).join(' L')} Z`;
|
|
19
|
+
if (kind === 'hex') return poly(HEX_POLYGON);
|
|
20
|
+
if (kind === 'doc') return poly(DOC_POLYGON);
|
|
21
|
+
if (kind === 'ellipse') return `M0,${num(h / 2)} A${num(w / 2)},${num(h / 2)} 0 1,0 ${num(w)},${num(h / 2)} A${num(w / 2)},${num(h / 2)} 0 1,0 0,${num(h / 2)} Z`;
|
|
22
|
+
if (kind === 'folder') {
|
|
23
|
+
const { w: tw, h: th } = FOLDER_TAB;
|
|
24
|
+
return `M1,${th + 4} V4 Q1,1 4,1 H${tw - 10} L${tw},${th} H${num(w - 4)} Q${num(w - 1)},${th} ${num(w - 1)},${th + 4} V${num(h - 4)} Q${num(w - 1)},${num(h - 1)} ${num(w - 4)},${num(h - 1)} H4 Q1,${num(h - 1)} 1,${num(h - 4)} Z`;
|
|
25
|
+
}
|
|
26
|
+
const r = CARD_R;
|
|
27
|
+
return `M${r},0 H${num(w - r)} A${r},${r} 0 0 1 ${num(w)},${r} V${num(h - r)} A${r},${r} 0 0 1 ${num(w - r)},${num(h)} H${r} A${r},${r} 0 0 1 0,${num(h - r)} V${r} A${r},${r} 0 0 1 ${r},0 Z`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The material a palette lends, or null when it lends none (the factory): then nothing is drawn under the words. */
|
|
31
|
+
export function materialOf(p) {
|
|
32
|
+
if (!p.nodeChamfer && !p.nodeShade && !p.nodeBrush && !p.nodeLip && !p.nodeShadow) return null;
|
|
33
|
+
return { bg: p.nodeBg, lip: p.nodeLip, shadow: p.nodeShadow, chamfer: p.nodeChamfer, shade: p.nodeShade, brush: p.nodeBrush };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** How far the material reaches beyond the object: a lip and a shadow fall below it and a little to each side. */
|
|
37
|
+
export function materialRoom(m) {
|
|
38
|
+
return m && (m.lip || m.shadow) ? { left: 12, bottom: 24 } : { left: 0, bottom: 0 };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The material drawn for one shape at one size, in the object's own colour for the band. One line, so a picture can
|
|
43
|
+
* be searched; ids are fixed because each picture is an SVG document of its own.
|
|
44
|
+
*/
|
|
45
|
+
export function materialSvg(kind, w, h, m, colour) {
|
|
46
|
+
if (!m) return '';
|
|
47
|
+
const d = shapePath(kind, w, h);
|
|
48
|
+
const clip = 'clip-path="url(#ckm-clip)"';
|
|
49
|
+
const defs = [`<clipPath id="ckm-clip"><path d="${d}"/></clipPath>`];
|
|
50
|
+
if (m.brush) defs.push(`<pattern id="ckm-brush" width="3" height="1" patternUnits="userSpaceOnUse"><rect width="1" height="1" fill="${m.brush}"/></pattern>`);
|
|
51
|
+
if (m.shadow) defs.push(`<filter id="ckm-shadow" x="-20%" y="-20%" width="140%" height="180%"><feDropShadow dx="0" dy="6" stdDeviation="5" flood-color="${m.shadow}"/></filter>`);
|
|
52
|
+
const parts = [`<defs>${defs.join('')}</defs>`];
|
|
53
|
+
if (m.shadow) parts.push(`<path d="${d}" fill="${m.bg}" filter="url(#ckm-shadow)"/>`);
|
|
54
|
+
if (m.lip) parts.push(`<path d="${d}" fill="${m.lip}" transform="translate(0,3)"/>`);
|
|
55
|
+
parts.push(`<path d="${d}" fill="${m.bg}"/>`);
|
|
56
|
+
if (m.brush) parts.push(`<rect x="0" y="0" width="${num(w)}" height="${num(h)}" fill="url(#ckm-brush)" ${clip}/>`);
|
|
57
|
+
parts.push(`<rect x="0" y="0" width="${num(w)}" height="3" fill="${colour}" ${clip}/>`);
|
|
58
|
+
if (m.chamfer) parts.push(`<rect x="0" y="3" width="${num(w)}" height="2" fill="${m.chamfer}" ${clip}/>`);
|
|
59
|
+
if (m.shade) parts.push(`<rect x="0" y="${num(h - 3)}" width="${num(w)}" height="3" fill="${m.shade}" ${clip}/>`);
|
|
60
|
+
return parts.join('');
|
|
61
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// The graph's colours, read from the theme (Spec §6.3, §6.16; CKC-09 AC-41). Cytoscape draws on a canvas and the
|
|
2
|
+
// objects are SVG pictures, so CSS variables cannot reach them: this reads the `--pk-graph-*` tokens (with the general
|
|
3
|
+
// tokens standing in, and the factory values last) into one palette that graph.js and the legend draw from, and a
|
|
4
|
+
// theme change asks the graph to read it again and repaint (graph.js repaint). No DOM here — `getVar` is handed in —
|
|
5
|
+
// so it is tested with `node --test` (src/ui/graph-palette.test.ts). The token names are the contract in
|
|
6
|
+
// ui/themes/TOKENS.md.
|
|
7
|
+
|
|
8
|
+
/** The factory graph, as it was before any theme: D53's four colours, and no material on the objects. */
|
|
9
|
+
export const FACTORY_PALETTE = Object.freeze({
|
|
10
|
+
// The four kinds (Spec §6.3): checked with palette-check.py — Plan / Work item 30.4 normal, 25.2 and 21.1 simulated.
|
|
11
|
+
intent: '#9fbdd5', plan: '#c4b0dc', work: '#a3c5ac', reality: '#e0a98e',
|
|
12
|
+
// Words and surfaces on an object. `nodeKind` null: the kind is written in the object's own colour (the factory way);
|
|
13
|
+
// a theme may give it a text colour instead (i1 does, Spec §6.3 "节点上的字用正文字色").
|
|
14
|
+
nodeBg: '#232320', nodeText: '#e7e7df', nodeKind: null, nodeMuted: '#a5a59b', nodeFaint: '#85857b',
|
|
15
|
+
// The card's material, lent to the objects by a theme (Spec §6.16: nodes borrow the card's material, not its shape).
|
|
16
|
+
// All off in the factory, so the factory picture is drawn exactly as before.
|
|
17
|
+
nodeChamfer: null, nodeShade: null, nodeBrush: null, nodeLip: null, nodeShadow: null,
|
|
18
|
+
// Lines and what is written on them.
|
|
19
|
+
edge: '#6e6e66', edgeOther: '#8c8c84', edgeRelated: '#cfcfc6', edgeLit: '#e7e7df', edgeSelected: '#f0d7a3', questioned: '#e3b76f',
|
|
20
|
+
labelBg: '#10100f', chipBg: '#1c1c1a', chipBorder: '#4a4a43',
|
|
21
|
+
// Selection, focus and Compare: white and the star colour, never one of the kinds (owner 2026-09-17).
|
|
22
|
+
selection: '#ffffff', focus: '#ffd54a', diffAdded: '#7fd08a', diffChanged: '#e3b76f', diffRemoved: '#dca6a0', replaced: '#5a5a50', danger: '#dca6a0',
|
|
23
|
+
// A folded group: a small stack of cards.
|
|
24
|
+
groupBg: '#20201e', groupBorder: '#5a5a50', groupBack: '#1d1d1b', groupBackBorder: '#45453f', groupChip: '#2b2b26',
|
|
25
|
+
// The corner marks (D53).
|
|
26
|
+
markStar: '#f3ead2', markFlag: '#e3b76f', markNote: '#e7e7df', markAsk: '#e3b76f', markPending: '#9fbdd5',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Which CSS variables each entry reads, first found wins: the graph's own token, then the general token a theme is
|
|
31
|
+
* likelier to have set, so a theme that only filled the general palette gets a coherent graph and refines from there.
|
|
32
|
+
*/
|
|
33
|
+
export const PALETTE_VARS = Object.freeze({
|
|
34
|
+
intent: ['--pk-graph-intent', '--pk-blue'], plan: ['--pk-graph-plan', '--pk-purple'], work: ['--pk-graph-work', '--pk-green'], reality: ['--pk-graph-reality', '--pk-accent'],
|
|
35
|
+
nodeBg: ['--pk-graph-node-bg', '--pk-card-bg', '--pk-surface'], nodeText: ['--pk-graph-node-text', '--pk-text-strong', '--pk-text'], nodeKind: ['--pk-graph-node-kind'],
|
|
36
|
+
nodeMuted: ['--pk-graph-node-muted', '--pk-text-muted'], nodeFaint: ['--pk-graph-node-faint', '--pk-text-faint'],
|
|
37
|
+
nodeChamfer: ['--pk-graph-node-chamfer'], nodeShade: ['--pk-graph-node-shade'], nodeBrush: ['--pk-graph-node-brush'], nodeLip: ['--pk-graph-node-lip'], nodeShadow: ['--pk-graph-node-shadow'],
|
|
38
|
+
edge: ['--pk-graph-edge', '--pk-line-strong'], edgeOther: ['--pk-graph-edge-other', '--pk-text-faint'], edgeRelated: ['--pk-graph-edge-related', '--pk-text-muted'], edgeLit: ['--pk-graph-edge-lit', '--pk-text-strong', '--pk-text'],
|
|
39
|
+
edgeSelected: ['--pk-graph-edge-selected', '--pk-accent-hi', '--pk-accent'], questioned: ['--pk-graph-questioned', '--pk-accent'],
|
|
40
|
+
labelBg: ['--pk-graph-label-bg', '--pk-ground'], chipBg: ['--pk-graph-chip-bg', '--pk-surface'], chipBorder: ['--pk-graph-chip-border', '--pk-line-strong', '--pk-line'],
|
|
41
|
+
selection: ['--pk-graph-selection'], focus: ['--pk-graph-focus', '--pk-star'], diffAdded: ['--pk-graph-diff-added', '--pk-green'], diffChanged: ['--pk-graph-diff-changed', '--pk-accent'], diffRemoved: ['--pk-graph-diff-removed', '--pk-red'],
|
|
42
|
+
replaced: ['--pk-graph-replaced', '--pk-line-strong'], danger: ['--pk-graph-danger', '--pk-red'],
|
|
43
|
+
groupBg: ['--pk-graph-group-bg', '--pk-raised'], groupBorder: ['--pk-graph-group-border', '--pk-line-strong'], groupBack: ['--pk-graph-group-back', '--pk-surface'], groupBackBorder: ['--pk-graph-group-back-border', '--pk-line'], groupChip: ['--pk-graph-group-chip', '--pk-raised'],
|
|
44
|
+
markStar: ['--pk-graph-mark-star', '--pk-text-strong'], markFlag: ['--pk-graph-mark-flag', '--pk-accent'], markNote: ['--pk-graph-mark-note', '--pk-text'], markAsk: ['--pk-graph-mark-ask', '--pk-accent'], markPending: ['--pk-graph-mark-pending', '--pk-blue'],
|
|
45
|
+
});
|
|
46
|
+
/** Entries a theme may leave out (null): the material parts, and the kind's own text colour. */
|
|
47
|
+
const OPTIONAL = new Set(['nodeKind', 'nodeChamfer', 'nodeShade', 'nodeBrush', 'nodeLip', 'nodeShadow']);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A CSS colour as numbers: hex in 3, 4, 6 or 8 digits, rgb()/rgba() with commas or spaces. `none` and `transparent`
|
|
51
|
+
* are null (nothing to draw); anything else — a var() a browser left unresolved, a word — is undefined (not a colour).
|
|
52
|
+
*/
|
|
53
|
+
export function parseColor(text) {
|
|
54
|
+
const s = String(text ?? '').trim().toLowerCase();
|
|
55
|
+
if (!s) return undefined;
|
|
56
|
+
if (s === 'none' || s === 'transparent') return null;
|
|
57
|
+
let m = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.exec(s);
|
|
58
|
+
if (m) {
|
|
59
|
+
let hex = m[1];
|
|
60
|
+
if (hex.length <= 4) hex = [...hex].map((c) => c + c).join('');
|
|
61
|
+
const n = (i) => parseInt(hex.slice(i, i + 2), 16);
|
|
62
|
+
return { r: n(0), g: n(2), b: n(4), a: hex.length === 8 ? n(6) / 255 : 1 };
|
|
63
|
+
}
|
|
64
|
+
m = /^rgba?\(\s*([\d.]+)\s*[, ]\s*([\d.]+)\s*[, ]\s*([\d.]+)\s*(?:[,/]\s*([\d.]+)(%?)\s*)?\)$/.exec(s);
|
|
65
|
+
if (m) {
|
|
66
|
+
const a = m[4] === undefined ? 1 : m[5] ? Number(m[4]) / 100 : Number(m[4]);
|
|
67
|
+
return { r: Number(m[1]), g: Number(m[2]), b: Number(m[3]), a };
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
/** The colour written back for SVG and cytoscape: hex when opaque, rgba() when not. */
|
|
72
|
+
export function cssColor(c) {
|
|
73
|
+
const hex = (v) => Math.round(v).toString(16).padStart(2, '0');
|
|
74
|
+
return c.a >= 1 ? `#${hex(c.r)}${hex(c.g)}${hex(c.b)}` : `rgba(${Math.round(c.r)},${Math.round(c.g)},${Math.round(c.b)},${Math.round(c.a * 1000) / 1000})`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The palette in force. `getVar(name)` gives a CSS variable's computed value on the page (or '' when it is not set).
|
|
79
|
+
* Each entry takes the first variable of its chain that holds a colour; `none` turns an optional part off; a value
|
|
80
|
+
* that is not a colour is passed over, so a mistake in a theme leaves the factory value and never an unreadable graph.
|
|
81
|
+
*/
|
|
82
|
+
export function readPalette(getVar) {
|
|
83
|
+
const out = {};
|
|
84
|
+
for (const [key, factory] of Object.entries(FACTORY_PALETTE)) {
|
|
85
|
+
let value = factory;
|
|
86
|
+
for (const name of PALETTE_VARS[key]) {
|
|
87
|
+
const c = parseColor(getVar(name));
|
|
88
|
+
if (c === undefined) continue;
|
|
89
|
+
if (c === null) { if (OPTIONAL.has(key)) value = null; break; }
|
|
90
|
+
value = cssColor(c);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
out[key] = value;
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A colour moved part of the way (`amount`) from a base colour: the badges' fills, tinted from the node's surface. */
|
|
99
|
+
export function tint(hex, amount = 0.3, base = '#1c1c1a') {
|
|
100
|
+
const c = parseColor(hex), b = parseColor(base);
|
|
101
|
+
if (!c || !b) return hex;
|
|
102
|
+
const mix = (x, y) => Math.round(y + (x - y) * amount);
|
|
103
|
+
return cssColor({ r: mix(c.r, b.r), g: mix(c.g, b.g), b: mix(c.b, b.b), a: 1 });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The colour of each corner mark, from a palette: the marks in graph.js keep their identity and read through these. */
|
|
107
|
+
export const MARK_COLOURS = Object.freeze({
|
|
108
|
+
star: (p) => p.markStar, flag: (p) => p.markFlag, note: (p) => p.markNote, ask: (p) => p.markAsk, pending: (p) => p.markPending,
|
|
109
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// What the Project graph's control row says (Spec §6.1, §6.3 "控件放在哪" and "可读性自检 · 在哪里说"; CKC-09 AC-37): which
|
|
2
|
+
// filters are in force and what the `Filter` button therefore reads, and the number on the readability mark with the
|
|
3
|
+
// words of its hover. No DOM here: data in, words and numbers out, so the rules are tested with `node --test`
|
|
4
|
+
// (src/ui/graph-tools.test.ts).
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The filters, in the order the Filter panel lists them. A filter is a choice that can take objects off the picture.
|
|
8
|
+
* What only changes how the picture is drawn — `Show replaced & deferred` (it adds objects), the period the stars mark,
|
|
9
|
+
* how the other links are drawn (D54) — sits in the same panel or on the graph's corner but is not a filter, is not
|
|
10
|
+
* counted on the button and is not touched by `Clear filters`.
|
|
11
|
+
*/
|
|
12
|
+
export const FILTERS = Object.freeze([
|
|
13
|
+
{ key: 'category', label: 'Category' }, { key: 'validity', label: 'Validity' }, { key: 'progress', label: 'Progress' },
|
|
14
|
+
{ key: 'acceptance', label: 'Acceptance' }, { key: 'assessment', label: 'Assessment' },
|
|
15
|
+
{ key: 'withNotes', label: 'With notes', tick: true }, { key: 'recent', label: '★ only', tick: true },
|
|
16
|
+
// The objects an attention note hangs on, marked ❓ (Spec §6.2, D100): what the drawer's ❓ count turns on. The ids are
|
|
17
|
+
// the page's (`attentionIds`, from the overview), so this filter is a tick like the two before it.
|
|
18
|
+
{ key: 'attention', label: '❓ Waiting on you', tick: true },
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
/** The filters in force: `{ key, label, value }` each, a tick's value being `on`. */
|
|
22
|
+
export function activeFilters(filters) {
|
|
23
|
+
const f = filters ?? {};
|
|
24
|
+
return FILTERS.filter((x) => (x.tick ? f[x.key] === true : Boolean(f[x.key]))).map((x) => ({ key: x.key, label: x.label, value: x.tick ? 'on' : String(f[x.key]) }));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** What the button reads: `Filter`, or `Filter · 2` while two filters are in force. */
|
|
28
|
+
export const filterLabel = (n) => (n > 0 ? `Filter · ${n}` : 'Filter');
|
|
29
|
+
|
|
30
|
+
/** Put every filter out of force, in place (the graph keeps a reference to the same object); returns it. */
|
|
31
|
+
export function clearFilters(filters) {
|
|
32
|
+
for (const x of FILTERS) filters[x.key] = x.tick ? false : '';
|
|
33
|
+
return filters;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── The readability mark (D46) ───────────────────────────────────────────
|
|
37
|
+
export const READ_KIND = Object.freeze({ overlap: 'On top of each other', behind: 'Lines behind objects', size: 'Too big to read at once', bundle: 'Bunched lines' });
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* How many places one kind of problem makes hard to read. A place is something the owner can point at: a pair of
|
|
41
|
+
* objects on top of each other, a line that runs behind an object, an object on which twenty or more lines meet — and
|
|
42
|
+
* the picture itself, once, when it cannot be read at one size that fits the window.
|
|
43
|
+
*/
|
|
44
|
+
export function readabilityPlaces(issue) {
|
|
45
|
+
if (issue.kind === 'overlap') return issue.pairs?.length ?? issue.count ?? 0;
|
|
46
|
+
if (issue.kind === 'size') return 1;
|
|
47
|
+
return issue.count ?? 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The mark in the control row: `◐ n`, n being the places that are hard to read, added up over the kinds of problem.
|
|
52
|
+
* At 0 it stays, quietly. The hover names each kind found with its places; the panel behind the mark has the objects
|
|
53
|
+
* and the ways out. `report` is graph.js readabilityReport's, or null before the first check.
|
|
54
|
+
*/
|
|
55
|
+
export function readabilityMark(report) {
|
|
56
|
+
const kinds = (report?.issues ?? []).map((i) => ({ kind: i.kind, label: READ_KIND[i.kind] ?? i.kind, places: readabilityPlaces(i) })).filter((k) => k.places > 0);
|
|
57
|
+
const count = kinds.reduce((n, k) => n + k.places, 0);
|
|
58
|
+
const title = count === 0
|
|
59
|
+
? 'Readability check: nothing in this picture is hard to read'
|
|
60
|
+
: `Hard to read in ${count} place${count === 1 ? '' : 's'}: ${kinds.map((k) => (k.kind === 'size' ? k.label : `${k.label} ${k.places}`)).join(' · ')}. Nothing is hidden to tidy it; open for the objects and what you can do.`;
|
|
61
|
+
return { count, text: `◐ ${count}`, title, kinds };
|
|
62
|
+
}
|