dsh-plugin-guide 0.1.2 → 0.3.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.
Files changed (51) hide show
  1. package/README.es.md +89 -20
  2. package/README.hi.md +89 -20
  3. package/README.md +92 -21
  4. package/README.pt.md +89 -20
  5. package/README.zh.md +92 -21
  6. package/SKILL.md +11 -1
  7. package/bin/dsh-plugin-dev.js +6 -0
  8. package/cordis.patch.yml +5 -0
  9. package/dist/dsh-plugin-dev.js +1623 -0
  10. package/guide/plugin-dev-guide.md +1 -1
  11. package/guide/quick-reference.es.md +1 -1
  12. package/guide/quick-reference.hi.md +1 -1
  13. package/guide/quick-reference.md +1 -1
  14. package/guide/quick-reference.pt.md +1 -1
  15. package/guide/quick-reference.zh-CN.md +1 -1
  16. package/package.json +50 -5
  17. package/references/harness-repo.md +1 -1
  18. package/references/official-docs/AGENTS.md +1 -1
  19. package/references/official-docs/docs/persistence-catalog.md +1 -12
  20. package/references/official-docs/docs/persistence-catalog.zh.md +1 -12
  21. package/references/official-docs/docs/subsystems/persistence.md +1 -1
  22. package/references/official-docs/docs/subsystems/persistence.zh.md +1 -1
  23. package/references/official-docs/docs/subsystems/session.md +0 -11
  24. package/references/official-docs/docs/subsystems/session.zh.md +0 -11
  25. package/scripts/check-docs-drift.ps1 +55 -0
  26. package/scripts/check-readme-sync.mjs +91 -0
  27. package/scripts/verify-artifacts.mjs +70 -0
  28. package/templates/js/LICENSE +15 -0
  29. package/templates/js/README.es.md +42 -0
  30. package/templates/js/README.hi.md +42 -0
  31. package/templates/js/README.md +42 -0
  32. package/templates/js/README.pt.md +42 -0
  33. package/templates/js/README.zh.md +42 -0
  34. package/templates/js/cordis.patch.yml +10 -0
  35. package/templates/js/index.js +38 -0
  36. package/templates/js/package.json +35 -0
  37. package/templates/js/tests/index.test.js +12 -0
  38. package/templates/ts/LICENSE +15 -0
  39. package/templates/ts/README.es.md +44 -0
  40. package/templates/ts/README.hi.md +44 -0
  41. package/templates/ts/README.md +44 -0
  42. package/templates/ts/README.pt.md +44 -0
  43. package/templates/ts/README.zh.md +44 -0
  44. package/templates/ts/cordis.patch.yml +10 -0
  45. package/templates/ts/package.json +44 -0
  46. package/templates/ts/src/config.ts +12 -0
  47. package/templates/ts/src/index.ts +36 -0
  48. package/templates/ts/tests/index.test.ts +12 -0
  49. package/templates/ts/tsconfig.json +17 -0
  50. package/templates/ts/tsdown.config.mjs +15 -0
  51. package/templates/ts/vitest.config.ts +8 -0
@@ -185,7 +185,7 @@ export function apply(ctx: Context, config: Config) { /* config 已校验+补默
185
185
  2. **waterfall 监听器必须调 `next()`**。
186
186
  3. **模型可见 ⟺ 已记录**(Model-visible ⟺ logged):任何进入模型请求的内容必须能从会话日志重建;新增模型可见输入必须新增会话事件。运行时不变式会断言这一点。
187
187
  4. **跨边界 opaque id 用 branded**:`Branded<B>`(`dsh-brand`,纯类型、零运行时依赖),从不裸 `string`;构造走 per-type factory(`SessionId` / `CallId` / `JobId` / `GoalId` 等),防止不同 id 在类型层互换。
188
- 5. **会话事件版本规则**:`SessionEventMap` 成员默认 required-on-read——不认识该事件类型的 build 拒绝日志,除非事件信封带 `ignorable: true`;只有结构格式变更才 bump `SESSION_FORMAT_VERSION`。插件新增会话事件时按此契约设计(新增模型可见输入见红线 3)。
188
+ 5. **会话事件版本规则**:`SessionEventMap` 成员默认 required-on-read,无信封豁免——`0.1.2-alpha.1` 已移除 `ignorable` 信封、读路径 fail-closed,不认识该事件类型的 build 一律拒绝日志;只有结构格式变更才 bump `SESSION_FORMAT_VERSION`。插件新增会话事件时按此契约设计:下游插件事件目前无注册面,写自定义事件的插件用自适应门在无信封宿主上停写(新增模型可见输入见红线 3)。
189
189
 
190
190
  ---
191
191
 
@@ -138,7 +138,7 @@ Protocolo de chunks: `block-start` → `text-delta*` → `block-end` (bloque com
138
138
  5. Paquetes de plugin independientes: cordis es peerDependency con la misma identidad del host (mezclar scoped `@deepseek-ai/cordis` y unscoped divide identidades); ESM; manifiesto `dsh.bundle`; instalaciones git necesitan `prepare` + `allowBuilds`; publica `lib/` o un tarball.
139
139
  6. Documentación bilingüe en pares; descripciones/prompts son comportamiento; cambios no triviales llevan Agent Note; ejecuta el conjunto mínimo de checks antes de empujar (dsh-pre-push-checks).
140
140
  7. Los ids opacos entre límites son branded (`Branded<B>` de `dsh-brand`), nunca `string` pelado.
141
- 8. Los miembros de `SessionEventMap` son required-on-read: los eventos de tipo desconocido deben llevar `ignorable: true` (o el log se rechaza); solo los cambios de formato estructural bumpan `SESSION_FORMAT_VERSION`. El switch sobre `SessionEvent` cae por un `default` documentado — sin `assertNever` (unión merge-extensible).
141
+ 8. Los miembros de `SessionEventMap` son required-on-read: el sobre `ignorable` desaparece en 0.1.2-alpha.1 (la lectura falla cerrada — un build que no conoce un tipo de evento rechaza el log), y los appends de eventos propios de plugins cruzan una puerta adaptativa que deja de escribir en hosts sin sobre; solo los cambios de formato estructural bumpan `SESSION_FORMAT_VERSION`. El switch sobre `SessionEvent` cae por un `default` documentado — sin `assertNever` (unión merge-extensible).
142
142
 
143
143
  ## Lista rápida de trampas comunitarias (detalles: guía §7.3 / community-repo-deep-dive.md)
144
144
 
@@ -138,7 +138,7 @@ Consumer (`dsh-tool-my-cap`): `inject = ['tools','myCap']`, `ctx.tools.register(
138
138
  5. स्वतंत्र प्लगइन पैकेज: cordis peerDependency है और होस्ट पहचान से मेल खाना चाहिए (scoped `@deepseek-ai/cordis` और unscoped मिलाना पहचान बाँट देता है); ESM; `dsh.bundle` मैनिफ़ेस्ट; git इंस्टॉल को `prepare` + `allowBuilds` चाहिए; `lib/` या tarball प्रकाशित करें।
139
139
  6. दस्तावेज़ द्विभाषी जोड़ों में; टूल विवरण/प्रॉम्प्ट ही व्यवहार हैं; गैर-तुच्छ बदलाव में Agent Note चाहिए; पुश से पहले न्यूनतम जाँच सेट चलाएँ (dsh-pre-push-checks)।
140
140
  7. सीमाओं के पार अपारदर्शी ids branded होते हैं (`Branded<B>` from `dsh-brand`), कभी भी नंगे `string` नहीं।
141
- 8. `SessionEventMap` सदस्य required-on-read हैं: अज्ञात प्रकार के इवेंट पर `ignorable: true` होना चाहिए (वरना log अस्वीकृत); केवल संरचनात्मक प्रारूप बदलाव ही `SESSION_FORMAT_VERSION` bump करते हैं। `SessionEvent` पर switch दस्तावेज़ित `default` में गिरता है — `assertNever` नहीं (merge-extensible union)।
141
+ 8. `SessionEventMap` सदस्य required-on-read हैं: 0.1.2-alpha.1 में `ignorable` लिफ़ाफ़ा हट गया है (पठन विफल-बंद है — जो build किसी इवेंट प्रकार को नहीं जानता वह log अस्वीकार करता है), और plugins के अपने इवेंट के append एक अनुकूली द्वार से चलते हैं जो बिना-लिफ़ाफ़े वाले hosts पर लिखना रोक देता है; केवल संरचनात्मक प्रारूप बदलाव ही `SESSION_FORMAT_VERSION` bump करते हैं। `SessionEvent` पर switch दस्तावेज़ित `default` में गिरता है — `assertNever` नहीं (merge-extensible union)।
142
142
 
143
143
  ## समुदाय की त्वरित समस्या-सूची (विवरण: गाइड §7.3 / community-repo-deep-dive.md)
144
144
 
@@ -138,7 +138,7 @@ Chunk protocol: `block-start` → `text-delta*` → `block-end` (complete block)
138
138
  5. Standalone plugin packages: cordis is a peerDependency matching the host identity (mixing scoped `@deepseek-ai/cordis` and unscoped splits identities); ESM; `dsh.bundle` manifest; git installs need `prepare` + `allowBuilds`; ship `lib/` or a tarball.
139
139
  6. Bilingual docs in pairs; tool descriptions/prompts are behavior; non-trivial changes need an Agent Note; run the minimal check set before pushing (dsh-pre-push-checks).
140
140
  7. Opaque cross-boundary ids are branded (`Branded<B>` from `dsh-brand`), never bare `string`.
141
- 8. `SessionEventMap` members are required-on-read: unknown event types must carry `ignorable: true` (or the log is refused); only structural format changes bump `SESSION_FORMAT_VERSION`. Switch over `SessionEvent` falls through a documented `default` — no `assertNever` (merge-extensible union).
141
+ 8. `SessionEventMap` members are required-on-read: the `ignorable` envelope is gone on 0.1.2-alpha.1 (reads fail closed — a build that does not know an event type refuses the log), and plugin appends of custom events ride an adaptive gate that stops writing on envelope-less hosts; only structural format changes bump `SESSION_FORMAT_VERSION`. Switch over `SessionEvent` falls through a documented `default` — no `assertNever` (merge-extensible union).
142
142
 
143
143
  ## Community pitfalls quick list (details: guide §7.3 / community-repo-deep-dive.md)
144
144
 
@@ -138,7 +138,7 @@ Protocolo de chunks: `block-start` → `text-delta*` → `block-end` (bloco comp
138
138
  5. Pacotes de plugin independentes: cordis é peerDependency com a mesma identidade do host (misturar scoped `@deepseek-ai/cordis` e unscoped divide identidades); ESM; manifesto `dsh.bundle`; instalações git precisam de `prepare` + `allowBuilds`; publique `lib/` ou um tarball.
139
139
  6. Documentação bilíngue em pares; descrições/prompts são comportamento; mudanças não triviais levam Agent Note; rode o conjunto mínimo de verificações antes de empurrar (dsh-pre-push-checks).
140
140
  7. IDs opacos entre limites são branded (`Branded<B>` de `dsh-brand`), nunca `string` crua.
141
- 8. Membros de `SessionEventMap` são required-on-read: eventos de tipo desconhecido devem levar `ignorable: true` (ou o log é recusado); apenas mudanças de formato estrutural bumpam `SESSION_FORMAT_VERSION`. O switch sobre `SessionEvent` cai num `default` documentado — sem `assertNever` (união merge-extensible).
141
+ 8. Membros de `SessionEventMap` são required-on-read: o envelope `ignorable` desaparece no 0.1.2-alpha.1 (a leitura falha fechada — um build que não conhece um tipo de evento recusa o log), e appends de eventos próprios de plugins cruzam uma porta adaptativa que para de gravar em hosts sem envelope; apenas mudanças de formato estrutural bumpam `SESSION_FORMAT_VERSION`. O switch sobre `SessionEvent` cai num `default` documentado — sem `assertNever` (união merge-extensible).
142
142
 
143
143
  ## Lista rápida de armadilhas da comunidade (detalhes: guia §7.3 / community-repo-deep-dive.md)
144
144
 
@@ -137,7 +137,7 @@ Chunk 协议:`block-start` → `text-delta*` → `block-end`(完整块)→
137
137
  5. 独立插件包:cordis 是 peerDependency(与宿主同身份:scoped `@deepseek-ai/cordis` 与 unscoped 混用会"双 Cordis 分裂");ESM;`dsh.bundle` 清单;git 安装配 `prepare` + `allowBuilds`;发布带 `lib/` 或 tarball。
138
138
  6. 文档双语成对;工具描述/提示词即行为;非平凡变更加 Agent Note;提交前跑最小检查集(dsh-pre-push-checks)。
139
139
  7. 跨边界 opaque id 用 branded(`Branded<B>` from `dsh-brand`),从不裸 `string`。
140
- 8. `SessionEventMap` 成员默认 required-on-read:不认识类型的会话事件必须带 `ignorable: true`(否则日志被拒读);只有结构格式变更才 bump `SESSION_FORMAT_VERSION`。对 `SessionEvent` 的 switch 落入文档化 `default`——**禁用 `assertNever`**(merge-extensible union)。
140
+ 8. `SessionEventMap` 成员默认 required-on-read:`ignorable` 信封在 0.1.2-alpha.1 已移除(读路径 fail-closed——不认识该事件类型的 build 一律拒绝日志),插件自定义事件的追加走自适应门、在无信封宿主上停写;只有结构格式变更才 bump `SESSION_FORMAT_VERSION`。对 `SessionEvent` 的 switch 落入文档化 `default`——**禁用 `assertNever`**(merge-extensible union)。
141
141
 
142
142
  ## 社区实测坑速查(详见 guide §7.3 / community-repo-deep-dive.md)
143
143
 
package/package.json CHANGED
@@ -1,11 +1,21 @@
1
1
  {
2
2
  "name": "dsh-plugin-guide",
3
- "version": "0.1.2",
4
- "description": "The dsh-plugin-guide knowledge base as an installable DeepSeek Harness plugin: official docs, Cordis primer, community deep-dives, and battle-tested pitfalls registered as an on-demand agent skill.",
3
+ "version": "0.3.1",
4
+ "description": "The dsh-plugin-guide knowledge base plus the dsh-plugin-dev CLI toolchain: official docs, Cordis primer, community deep-dives, and battle-tested pitfalls registered as an on-demand agent skill, with a scaffolder, static checker, and pack verifier for building DSH plugins.",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
+ "bin": {
8
+ "dsh-plugin-dev": "./bin/dsh-plugin-dev.js"
9
+ },
10
+ "exports": {
11
+ ".": "./index.js",
12
+ "./package.json": "./package.json"
13
+ },
7
14
  "files": [
8
15
  "index.js",
16
+ "bin",
17
+ "dist",
18
+ "templates",
9
19
  "cordis.patch.yml",
10
20
  "SKILL.md",
11
21
  "guide",
@@ -26,13 +36,36 @@
26
36
  "cordis",
27
37
  "agent-skill",
28
38
  "plugin-development",
29
- "knowledge-base"
39
+ "knowledge-base",
40
+ "cli",
41
+ "scaffold",
42
+ "checker"
30
43
  ],
31
44
  "license": "Apache-2.0",
32
45
  "repository": {
33
46
  "type": "git",
34
47
  "url": "git+https://github.com/PerryLink/dsh-plugin-guide.git"
35
48
  },
49
+ "scripts": {
50
+ "build": "tsdown",
51
+ "typecheck": "tsc --noEmit",
52
+ "typecheck:ci": "tsc --noEmit --pretty false",
53
+ "test": "vitest run",
54
+ "check:readmes": "node scripts/check-readme-sync.mjs",
55
+ "check:kit": "pwsh -File scripts/verify-kit.ps1",
56
+ "verify:artifacts": "node scripts/verify-artifacts.mjs",
57
+ "verify:self-contained": "pnpm run build && node dist/dsh-plugin-dev.js verify",
58
+ "prepare": "tsdown",
59
+ "prepack": "tsdown"
60
+ },
61
+ "dependencies": {
62
+ "tsdown": "^0.22.14",
63
+ "typescript": "^5.9.3"
64
+ },
65
+ "devDependencies": {
66
+ "@types/node": "^22.20.1",
67
+ "vitest": "^3.2.7"
68
+ },
36
69
  "peerDependencies": {
37
70
  "@deepseek-ai/dsh": ">=0.1.0-rc.8 <0.2.0"
38
71
  },
@@ -46,6 +79,10 @@
46
79
  "patch": "./cordis.patch.yml"
47
80
  }
48
81
  },
82
+ "engines": {
83
+ "node": "^22.19.0 || >=24.0.0"
84
+ },
85
+ "packageManager": "pnpm@11.7.0",
49
86
  "dshWorkshop": {
50
87
  "schema": "omdsh-workshop-package/v1",
51
88
  "type": "plugin",
@@ -63,9 +100,13 @@
63
100
  "activation": "restart-profile",
64
101
  "dispose": "supported"
65
102
  },
66
- "permissions": ["filesystem:read"],
103
+ "permissions": [
104
+ "filesystem:read"
105
+ ],
67
106
  "compatibility": {
68
- "dshVersions": ["0.1.1-rc.2"]
107
+ "dshVersions": [
108
+ "0.1.1-rc.2"
109
+ ]
69
110
  },
70
111
  "capability": {
71
112
  "id": "dsh-plugin-guide",
@@ -79,5 +120,9 @@
79
120
  "hotReload": null,
80
121
  "remove": null
81
122
  }
123
+ },
124
+ "funding": {
125
+ "type": "individual",
126
+ "url": "https://github.com/sponsors/PerryLink"
82
127
  }
83
128
  }
@@ -489,7 +489,7 @@ skill 目录结构:`packages/skill/` 下有 `skill`(registry/Service Definit
489
489
  - `strict: true` + `noImplicitAny`;每个残留 `any` 解释为何无法收窄。
490
490
  - 每个 module/export 有简洁 JSDoc(`@param`/`@returns`),由 `verify-export-jsdoc` 强制。
491
491
  - typed events 用 declaration merging + merge-extensible maps;event JSDoc 需 `@mode` 和 payload `@param`;scoped keys 缺 payload 需 `@dshScopeScan unsupported`。
492
- - `SessionEventMap` 成员默认 required-on-read;不认其类型的 build 拒绝该 log,除非 event 带 envelope 的 `ignorable: true`;只有结构格式变更才 bump `SESSION_FORMAT_VERSION`。
492
+ - `SessionEventMap` 成员默认 required-on-read,无信封豁免:不认其类型的 build 一律拒绝该 log;只有结构格式变更才 bump `SESSION_FORMAT_VERSION`。
493
493
  - switch 用 discriminant tags:closed union 结尾 `assertNever`;merge-extensible union 走文档化 default。
494
494
  - opaque 跨边界 id 用 branded(`Branded<B>` from `dsh-brand`),从不裸 `string`。
495
495
  - 每个包拥有 `./invariant`(注册 manifest 名;检查 event/data 关系或给空 installer 一个包特定 `No runtime invariant:` 理由),由 `verify-package-invariants` 强制(`packages/AGENTS.md` L18)。
@@ -101,7 +101,7 @@ Real-API tests and demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`,
101
101
  - ESM everywhere (`"type": "module"`). Use package names across packages and `.ts` in local relative imports. Config subprocesses run built `lib/` under plain Node; source regressions use their declared launcher ([testing policy](docs/testing.md#test-subprocess-launch-modes)). The `dsh` CLI source launch runs through tsx's ESM-only hook (`node --import tsx/esm`); modules it reaches must stay ESM (no CJS-only exports) — Node's native TypeScript modes are unavailable across the engines range ([source-launch contract](.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md)). Raw/Web `cordis.yml` bare plugins must appear in their resolver manifest's `dependencies`; `verify-cordis-config` enforces it.
102
102
  - **Registrations are effects**: every contribution goes through `ctx.effect()` / `ctx.on()`; a registry's `register()` returns the disposer.
103
103
  - **Runtime invariants assert owned relationships.** Check authoritative event streams or mutable data, not service or method presence, plugin metadata or effects, or fixed pure examples. Without a plausible relationship, an explained empty companion is correct ([package invariant rules](packages/AGENTS.md)).
104
- - **Typed events use declaration merging** and merge-extensible maps. Event JSDoc needs `@mode` and payload `@param`; scoped keys absent from payloads need `@dshScopeScan unsupported`. Public service methods document parameters and non-void returns. A `SessionEventMap` member is required-on-read by default — builds that do not know its type refuse the log unless the event carries the envelope's `ignorable: true`; only structural format changes bump `SESSION_FORMAT_VERSION` ([mechanism](.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md)).
104
+ - **Typed events use declaration merging** and merge-extensible maps. Event JSDoc needs `@mode` and payload `@param`; scoped keys absent from payloads need `@dshScopeScan unsupported`. Public service methods document parameters and non-void returns. Every `SessionEventMap` member is required-on-read: builds that do not know its type refuse the log; only structural format changes bump `SESSION_FORMAT_VERSION` ([mechanism](.agents/notes/implemented/simplification/2026-08-25-fail-closed-session-event-vocabulary.md)).
105
105
  - **Switch on discriminant tags.** Closed unions end in `assertNever`; merge-extensible unions fall through a documented default.
106
106
  - **Waterfall listeners MUST call `next()`** to delegate; returning without it short-circuits the chain ([semantics](docs/cordis-primer.md#cordis-waterfall-semantics)).
107
107
  - **Model-visible ⟺ logged**: anything that reaches a model request must be reconstructable from the session log; a new model-visible input requires a session event.
@@ -7,7 +7,7 @@ Every event type that can appear in a session's durable event log: the complete
7
7
 
8
8
  This file is GENERATED from source (`scripts/gen-persistence-catalog.ts`) and verified fresh by `pnpm run verify-persistence-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks retain the source declaration and nested property JSDoc, removing only the indentation imposed by a containing interface/module, and use a `ts persistence-catalog` fence (skipped by doc-typecheck because declarations reference types from their owning modules). Type names in a payload link to the page that documents them. See [the persistence-log-catalog Agent Note](../.agents/notes/archived/process/2026-07-04-persistence-log-catalog.md).
9
9
 
10
- The envelope declarations below compose each event's `type`, monotonic `seq`, epoch-ms `time`, `data`, the optional `ignorable` unknown-type skip marker, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](subsystems/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.
10
+ The envelope declarations below compose each event's `type`, monotonic `seq`, epoch-ms `time`, `data`, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](subsystems/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.
11
11
 
12
12
  ## Event envelope
13
13
 
@@ -63,17 +63,6 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
63
63
  /** Unix epoch milliseconds. */
64
64
  time: number
65
65
  data: SessionEventMap[K]
66
- /**
67
- * Marks an event a reader may safely skip when it does not recognize
68
- * `type`. Absent means required: a reader meeting an unrecognized type
69
- * without this marker MUST refuse to reconstruct the session instead of
70
- * silently dropping the event, because an unrecognized required event may
71
- * change how the rest of the log is interpreted. A writer sets `true` only
72
- * on purely informational records whose loss cannot affect reconstruction;
73
- * defaulting to required means a forgotten marker over-refuses (an
74
- * inconvenience) rather than silently resuming a gutted session.
75
- */
76
- ignorable?: true
77
66
  } & (K extends SurfaceEventType ? {
78
67
  /**
79
68
  * Seq numbers of earlier events that this event cites as sources
@@ -9,7 +9,7 @@
9
9
 
10
10
  英文源文件根据源码生成(`scripts/gen-persistence-catalog.ts`),并由 `pnpm run verify-persistence-catalog`(`doc-sync`(文档同步门禁)的一部分)验证新鲜度;本中文文件作为经评审对侧通过双语配对维护。声明块保留源码声明和嵌套属性的 JSDoc,只移除其所在接口/模块带来的缩进,并使用 `ts persistence-catalog` 围栏(doc-typecheck 会跳过这些围栏,因为声明引用了其所属模块中的类型)。payload 中的类型名称会链接到记录该类型的页面。参见 [persistence-log-catalog Agent Note](../.agents/notes/archived/process/2026-07-04-persistence-log-catalog.md)。
11
11
 
12
- 以下信封声明组合了每个事件的 `type`、单调递增的 `seq`、以 epoch 毫秒表示的 `time`、`data`、可选的未知类型跳过标记 `ignorable`,以及条件字段 `surfaceOp`/`sourceEventSeqs`。**surface** 表示 `SurfaceEventType` 成员:它会生成一条 LLM(大语言模型)消息,并声明该事件如何加入 surface 列表。**log-only** 表示其他所有事件:这类记录可持久化、可回放,但不参与派生历史。每个 payload 均可进行 JSON 序列化(在 `Session.append` 处强制执行),整个格式固定为 `SESSION_FORMAT_VERSION = 0`:这是预发布格式,不暗示任何兼容性(参见[版本立场](subsystems/persistence.md))。范围仅限本仓库中的包;下游插件可以继续合并其他事件类型,而这些类型按设计不属于本目录。
12
+ 以下信封声明组合了每个事件的 `type`、单调递增的 `seq`、以 epoch 毫秒表示的 `time`、`data`,以及条件字段 `surfaceOp`/`sourceEventSeqs`。**surface** 表示 `SurfaceEventType` 成员:它会生成一条 LLM(大语言模型)消息,并声明该事件如何加入 surface 列表。**log-only** 表示其他所有事件:这类记录可持久化、可回放,但不参与派生历史。每个 payload 均可进行 JSON 序列化(在 `Session.append` 处强制执行),整个格式固定为 `SESSION_FORMAT_VERSION = 0`:这是预发布格式,不暗示任何兼容性(参见[版本立场](subsystems/persistence.md))。范围仅限本仓库中的包;下游插件可以继续合并其他事件类型,而这些类型按设计不属于本目录。
13
13
 
14
14
  ## 事件信封
15
15
 
@@ -65,17 +65,6 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
65
65
  /** Unix epoch milliseconds. */
66
66
  time: number
67
67
  data: SessionEventMap[K]
68
- /**
69
- * Marks an event a reader may safely skip when it does not recognize
70
- * `type`. Absent means required: a reader meeting an unrecognized type
71
- * without this marker MUST refuse to reconstruct the session instead of
72
- * silently dropping the event, because an unrecognized required event may
73
- * change how the rest of the log is interpreted. A writer sets `true` only
74
- * on purely informational records whose loss cannot affect reconstruction;
75
- * defaulting to required means a forgotten marker over-refuses (an
76
- * inconvenience) rather than silently resuming a gutted session.
77
- */
78
- ignorable?: true
79
68
  } & (K extends SurfaceEventType ? {
80
69
  /**
81
70
  * Seq numbers of earlier events that this event cites as sources
@@ -91,7 +91,7 @@ interface SessionHeader {
91
91
 
92
92
  ## Format refusal — logs a build cannot faithfully read
93
93
 
94
- A backend refuses a log it cannot faithfully interpret with `SessionFormatUnsupportedError`, distinct from `SessionPersistenceCorruptionError` because nothing is damaged. A header `version` ahead of `SESSION_FORMAT_VERSION` names the direction ("written by a newer harness — upgrade the harness to open it"); one behind it states that this build ships no upgrade path. After legacy-shape normalization, an event type outside this build's generated vocabulary (`KNOWN_SESSION_EVENT_TYPES`, emitted by `gen-persistence-catalog`) refuses the same way unless the event's envelope carries `ignorable: true` — silently skipping an unrecognized required event could change how the rest of the log must be read. The message appends the raw log path when the backend keeps one artifact per session, so the refused text stays reachable. The JSONL backend refuses a foreign version straight from the raw header line, before validating today's header shape or decoding any event row — a structurally different future format still reports the upgrade direction, never "corrupt"; SQLite gates whole-file structure through its own `SCHEMA_VERSION` pragma first. Design rationale and the deferred upgrader chain live in the [session-log-version-mechanism note](../../.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md).
94
+ A backend refuses a log it cannot faithfully interpret with `SessionFormatUnsupportedError`, distinct from `SessionPersistenceCorruptionError` because nothing is damaged. A header `version` ahead of `SESSION_FORMAT_VERSION` names the direction ("written by a newer harness — upgrade the harness to open it"); one behind it states that this build ships no upgrade path. After legacy-shape normalization, an event type outside this build's generated set (`KNOWN_SESSION_EVENT_TYPES`, emitted by `gen-persistence-catalog`) also refuses reconstruction because silently skipping it could change how the rest of the log must be read. The message appends the raw log path when the backend keeps one artifact per session, so the refused text stays reachable. The JSONL backend refuses a foreign version straight from the raw header line, before validating this format version's header fields or decoding any event row — a structurally different future format still reports the upgrade direction, never "corrupt"; SQLite gates whole-file structure through its own `SCHEMA_VERSION` pragma first. Design rationale and the deferred upgrader chain live in the [fail-closed event-vocabulary note](../../.agents/notes/implemented/simplification/2026-08-25-fail-closed-session-event-vocabulary.md).
95
95
 
96
96
  ## `CreateSessionOptions` — seeding and metadata
97
97
 
@@ -91,7 +91,7 @@ interface SessionHeader {
91
91
 
92
92
  ## 格式拒绝:本构建无法可靠读取的日志
93
93
 
94
- 后端用 `SessionFormatUnsupportedError` 拒绝无法可靠解读的日志,它与 `SessionPersistenceCorruptionError` 区分,因为数据没有损坏。header 的 `version` 比 `SESSION_FORMAT_VERSION` 新时,消息说明方向("由更新的 harness 写入,请升级 harness 后打开");比它旧时说明本构建没有升级路径。经过 legacy 形状归一化后,本构建生成词汇表(`KNOWN_SESSION_EVENT_TYPES`,由 `gen-persistence-catalog` 生成)之外的事件类型同样被拒绝,除非该事件的信封带 `ignorable: true`:静默跳过一个不认识的必需事件可能改变日志其余部分的解读方式。后端为每个会话保留独立文件时,消息附上原始日志路径,被拒绝的文本仍然可读。JSONL 后端直接从原始 header 行拒绝外来版本,先于当前 header 形状校验和任何事件行解码,因此结构完全不同的未来格式仍会报告升级方向,绝不会报"损坏";SQLite 则先由自己的 `SCHEMA_VERSION` pragma 把关整个文件的结构。设计理由与推迟建设的升级器链见 [session-log 版本机制 Agent Note](../../.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md)。
94
+ 后端用 `SessionFormatUnsupportedError` 拒绝无法可靠解读的日志,它与 `SessionPersistenceCorruptionError` 区分,因为数据没有损坏。header 的 `version` 比 `SESSION_FORMAT_VERSION` 新时,消息说明方向("由更新的 harness 写入,请升级 harness 后打开");比它旧时说明本构建没有升级路径。经过 legacy 形状归一化后,本构建生成集合(`KNOWN_SESSION_EVENT_TYPES`,由 `gen-persistence-catalog` 生成)之外的事件类型也会拒绝重建,因为静默跳过该事件可能改变日志其余部分的解读方式。后端为每个会话保留独立文件时,消息附上原始日志路径,被拒绝的文本仍然可读。JSONL 后端直接从原始 header 行拒绝外来版本,先于校验本格式版本的 header 字段和解码任何事件行,因此结构完全不同的未来格式仍会报告升级方向,绝不会报"损坏";SQLite 则先由自己的 `SCHEMA_VERSION` pragma 把关整个文件的结构。设计理由与推迟建设的升级器链见[事件词汇表显式拒绝 Agent Note](../../.agents/notes/implemented/simplification/2026-08-25-fail-closed-session-event-vocabulary.zh.md)。
95
95
 
96
96
  ## `CreateSessionOptions`:seed 与元数据
97
97
 
@@ -217,17 +217,6 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
217
217
  /** Unix epoch milliseconds. */
218
218
  time: number
219
219
  data: SessionEventMap[K]
220
- /**
221
- * Marks an event a reader may safely skip when it does not recognize
222
- * `type`. Absent means required: a reader meeting an unrecognized type
223
- * without this marker MUST refuse to reconstruct the session instead of
224
- * silently dropping the event, because an unrecognized required event may
225
- * change how the rest of the log is interpreted. A writer sets `true` only
226
- * on purely informational records whose loss cannot affect reconstruction;
227
- * defaulting to required means a forgotten marker over-refuses (an
228
- * inconvenience) rather than silently resuming a gutted session.
229
- */
230
- ignorable?: true
231
220
  } & (K extends SurfaceEventType ? {
232
221
  /**
233
222
  * Seq numbers of earlier events that this event cites as sources
@@ -217,17 +217,6 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
217
217
  /** Unix epoch milliseconds. */
218
218
  time: number
219
219
  data: SessionEventMap[K]
220
- /**
221
- * Marks an event a reader may safely skip when it does not recognize
222
- * `type`. Absent means required: a reader meeting an unrecognized type
223
- * without this marker MUST refuse to reconstruct the session instead of
224
- * silently dropping the event, because an unrecognized required event may
225
- * change how the rest of the log is interpreted. A writer sets `true` only
226
- * on purely informational records whose loss cannot affect reconstruction;
227
- * defaulting to required means a forgotten marker over-refuses (an
228
- * inconvenience) rather than silently resuming a gutted session.
229
- */
230
- ignorable?: true
231
220
  } & (K extends SurfaceEventType ? {
232
221
  /**
233
222
  * Seq numbers of earlier events that this event cites as sources
@@ -0,0 +1,55 @@
1
+ # dsh-plugin-guide official-docs freshness probe.
2
+ # Purpose: detect drift between the KB mirror (references/official-docs) and the
3
+ # upstream deepseek-harness repository WITHOUT a local checkout. It compares the
4
+ # upstream branch tip SHA (git ls-remote) against the pinned "Source commit" SHA
5
+ # recorded in references/official-docs/SNAPSHOT.md — the SHA-locked authority the
6
+ # mirror was synced from. It never edits the mirror; drift is only reported.
7
+ # Usage: pwsh -File scripts/check-docs-drift.ps1 [-Repo <url>] [-Branch <name>] [-Snapshot <path>] [-FailOnDrift]
8
+ # Exit codes: 0 = FRESH, 1 = DRIFT (with -FailOnDrift), 2 = probe could not run (snapshot/parse/fetch failure).
9
+ # In CI it also writes `drift=yes|no|error` to $env:GITHUB_OUTPUT when that variable is set.
10
+ param(
11
+ [string]$Repo = 'https://github.com/deepseek-ai/deepseek-harness.git',
12
+ [string]$Branch = 'master',
13
+ [string]$Snapshot = 'references/official-docs/SNAPSHOT.md',
14
+ [switch]$FailOnDrift
15
+ )
16
+ $ErrorActionPreference = 'Stop'
17
+ $kit = Split-Path $PSScriptRoot -Parent
18
+ $snapshotPath = Join-Path $kit $Snapshot
19
+
20
+ # ---- 1) Read the pinned source commit SHA from SNAPSHOT.md ----
21
+ $pinned = $null
22
+ if (-not (Test-Path $snapshotPath)) { Write-Error "snapshot not found: $snapshotPath"; exit 2 }
23
+ foreach ($line in Get-Content $snapshotPath -Encoding UTF8) {
24
+ if ($line -match '^\|\s*Source commit\s*\|\s*`([0-9a-f]{40})`\s*\|') {
25
+ $pinned = $Matches[1]
26
+ break
27
+ }
28
+ }
29
+ if (-not $pinned) { Write-Error "cannot parse the pinned 'Source commit' SHA from $Snapshot"; exit 2 }
30
+
31
+ # ---- 2) Fetch the upstream branch tip SHA (no clone required) ----
32
+ $remoteLine = (& git ls-remote $Repo "refs/heads/$Branch" 2>$null | Select-Object -First 1)
33
+ if (-not $remoteLine -or $remoteLine -notmatch '^([0-9a-f]{40})\s') {
34
+ $message = "cannot reach upstream $Repo (branch $Branch)"
35
+ Write-Output "ERROR: $message"
36
+ if ($env:GITHUB_OUTPUT) { Add-Content -Path $env:GITHUB_OUTPUT -Value 'drift=error' }
37
+ exit 2
38
+ }
39
+ $upstream = $Matches[1]
40
+
41
+ # ---- 3) Compare and report ----
42
+ $fresh = ($upstream -eq $pinned)
43
+ Write-Output "pinned (SNAPSHOT.md): $pinned"
44
+ Write-Output "upstream ($Branch): $upstream"
45
+ if ($fresh) {
46
+ Write-Output 'FRESH: references/official-docs matches the upstream branch tip'
47
+ } else {
48
+ Write-Output "DRIFT: references/official-docs is pinned to $pinned but upstream is at $upstream"
49
+ Write-Output 'Action: run pwsh -File scripts/sync-official-docs.ps1 to refresh the mirror (do not hand-edit the mirror).'
50
+ }
51
+ if ($env:GITHUB_OUTPUT) {
52
+ Add-Content -Path $env:GITHUB_OUTPUT -Value ($(if ($fresh) { 'drift=no' } else { 'drift=yes' }))
53
+ }
54
+ if (-not $fresh -and $FailOnDrift) { exit 1 }
55
+ exit 0
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ // Five-language README sync gate: the English file is the source of truth.
3
+ // Every translation must carry the same `## ` section headings in the same
4
+ // order and the same configuration-table keys, so a config change cannot
5
+ // ship without its translations.
6
+ import { readFileSync, existsSync } from 'node:fs'
7
+ import path from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+
10
+ const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)))
11
+ const LANGUAGES = [
12
+ ['README.md', 'en'],
13
+ ['README.zh.md', 'zh'],
14
+ ['README.es.md', 'es'],
15
+ ['README.pt.md', 'pt'],
16
+ ['README.hi.md', 'hi'],
17
+ ]
18
+
19
+ const headingsOf = (text) => text.split(/\r?\n/u)
20
+ .map(line => /^##\s+(.+)$/u.exec(line)?.[1]?.trim())
21
+ .filter((heading) => heading !== undefined)
22
+
23
+ // Configuration-table keys: the first cell of every row under the
24
+ // "Configuration" section (until the next `## ` heading).
25
+ const configKeysOf = (text) => {
26
+ const lines = text.split(/\r?\n/u)
27
+ const start = lines.findIndex(line => /^##\s+Configuration\s*$/u.test(line))
28
+ if (start === -1) return []
29
+ const end = lines.findIndex((line, index) => index > start && line.startsWith('## '))
30
+ const section = lines.slice(start, end === -1 ? undefined : end)
31
+ const keys = []
32
+ let headerSkipped = false
33
+ for (const line of section) {
34
+ if (!line.startsWith('|')) continue
35
+ if (!headerSkipped) { headerSkipped = true; continue }
36
+ const cells = line.split('|').map(cell => cell.trim())
37
+ if (cells.length < 2) continue
38
+ const key = cells[1]
39
+ if (key === '' || key === 'Key') continue
40
+ if (/^-+$/u.test(key)) continue
41
+ keys.push(key)
42
+ }
43
+ return keys
44
+ }
45
+
46
+ const failures = []
47
+ const reference = LANGUAGES[0]
48
+ if (reference === undefined) process.exit(1)
49
+ const refPath = path.join(root, reference[0])
50
+ if (!existsSync(refPath)) {
51
+ console.error(`${reference[0]} is missing`)
52
+ process.exit(1)
53
+ }
54
+ const refText = readFileSync(refPath, 'utf8')
55
+ const refHeadings = headingsOf(refText)
56
+ const refKeys = configKeysOf(refText)
57
+
58
+ for (const [file, label] of LANGUAGES.slice(1)) {
59
+ const filePath = path.join(root, file)
60
+ if (!existsSync(filePath)) {
61
+ failures.push(`${file}: missing (${label})`)
62
+ continue
63
+ }
64
+ const text = readFileSync(filePath, 'utf8')
65
+ const headings = headingsOf(text)
66
+ for (let index = 0; index < refHeadings.length; index++) {
67
+ if (headings[index] !== refHeadings[index]) {
68
+ failures.push(`${file}: heading ${index + 1} is ${JSON.stringify(headings[index])}, expected ${JSON.stringify(refHeadings[index])}`)
69
+ }
70
+ }
71
+ if (headings.length !== refHeadings.length) {
72
+ failures.push(`${file}: ${headings.length} headings, expected ${refHeadings.length}`)
73
+ }
74
+ const keys = configKeysOf(text)
75
+ if (keys.length !== refKeys.length) {
76
+ failures.push(`${file}: ${keys.length} config keys, expected ${refKeys.length}`)
77
+ continue
78
+ }
79
+ for (let index = 0; index < refKeys.length; index++) {
80
+ if (keys[index] !== refKeys[index]) {
81
+ failures.push(`${file}: config key ${index + 1} is ${JSON.stringify(keys[index])}, expected ${JSON.stringify(refKeys[index])}`)
82
+ }
83
+ }
84
+ }
85
+
86
+ if (failures.length > 0) {
87
+ console.error('readme-sync failed:')
88
+ for (const failure of failures) console.error(` - ${failure}`)
89
+ process.exit(1)
90
+ }
91
+ console.log(`readme-sync: all ${LANGUAGES.length} READMEs share section structure and config keys`)
@@ -0,0 +1,70 @@
1
+ // Artifact gate for the dsh-plugin-guide CLI. Runs after `pnpm run build` and
2
+ // verifies the shipped surfaces without any network access:
3
+ // 1. the built CLI, bin shim, and template trees exist;
4
+ // 2. the CLI self-checks this repo (dogfood) and reports `ok: true`;
5
+ // 3. the scaffolder produces TS and JS skeletons inside a mkdtemp sandbox
6
+ // (never touching a real home directory), which are then removed.
7
+ import { spawnSync } from 'node:child_process'
8
+ import { existsSync, mkdtempSync, rmSync, readdirSync } from 'node:fs'
9
+ import { tmpdir } from 'node:os'
10
+ import { dirname, join, resolve } from 'node:path'
11
+ import { fileURLToPath } from 'node:url'
12
+
13
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
14
+ const failures = []
15
+
16
+ function check(condition, message) {
17
+ if (condition) {
18
+ console.log(`ok - ${message}`)
19
+ } else {
20
+ failures.push(message)
21
+ console.error(`FAIL - ${message}`)
22
+ }
23
+ }
24
+
25
+ function runNode(args, options = {}) {
26
+ return spawnSync(process.execPath, args, { cwd: root, encoding: 'utf8', ...options })
27
+ }
28
+
29
+ // 1. Shipped surfaces.
30
+ check(existsSync(join(root, 'dist', 'dsh-plugin-dev.js')), 'dist/dsh-plugin-dev.js exists (run pnpm run build first)')
31
+ check(existsSync(join(root, 'bin', 'dsh-plugin-dev.js')), 'bin/dsh-plugin-dev.js exists')
32
+ check(existsSync(join(root, 'templates', 'ts')), 'templates/ts exists')
33
+ check(existsSync(join(root, 'templates', 'js')), 'templates/js exists')
34
+
35
+ // 2. Version + dogfood self-check.
36
+ const version = runNode(['dist/dsh-plugin-dev.js', '--version'])
37
+ check(version.status === 0 && /\d+\.\d+\.\d+/.test(version.stdout.trim()), `--version prints a semver (${version.stdout.trim()})`)
38
+
39
+ const selfCheck = runNode(['dist/dsh-plugin-dev.js', 'check', '--json'], { stdio: ['ignore', 'pipe', 'pipe'] })
40
+ let ok = false
41
+ if (selfCheck.status === 0) {
42
+ try {
43
+ ok = JSON.parse(selfCheck.stdout).ok === true
44
+ } catch {
45
+ ok = false
46
+ }
47
+ }
48
+ check(selfCheck.status === 0 && ok, `self-check (dogfood) reports ok (exit ${selfCheck.status})`)
49
+
50
+ // 3. Scaffold smoke in a mkdtemp sandbox.
51
+ const sandbox = mkdtempSync(join(tmpdir(), 'dsh-pd-artifacts-'))
52
+ try {
53
+ for (const lang of ['ts', 'js']) {
54
+ const target = join(sandbox, lang, 'demo-plugin')
55
+ const result = runNode(['dist/dsh-plugin-dev.js', 'new', 'demo-plugin', '--lang', lang, '--dir', target])
56
+ const hasEntry = lang === 'ts' ? existsSync(join(target, 'src', 'index.ts')) : existsSync(join(target, 'index.js'))
57
+ const hasPatch = existsSync(join(target, 'cordis.patch.yml'))
58
+ const readmeCount = readdirSync(target).filter((f) => /^README(\.\w{2})?\.md$/.test(f)).length
59
+ check(result.status === 0 && hasEntry && hasPatch, `${lang} scaffold produces entry + cordis.patch.yml (exit ${result.status})`)
60
+ check(readmeCount === 5, `${lang} scaffold produces 5 README languages (${readmeCount})`)
61
+ }
62
+ } finally {
63
+ rmSync(sandbox, { recursive: true, force: true })
64
+ }
65
+
66
+ if (failures.length > 0) {
67
+ console.error(`\nverify:artifacts FAILED (${failures.length})`)
68
+ process.exit(1)
69
+ }
70
+ console.log('\nverify:artifacts OK')
@@ -0,0 +1,15 @@
1
+ Apache License 2.0
2
+
3
+ Copyright (c) {{year}} {{pkgName}} contributors
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
@@ -0,0 +1,42 @@
1
+ # {{pkgName}}
2
+
3
+ Un plugin de DeepSeek Harness (DSH) generado con [`dsh-plugin-dev new`](https://github.com/PerryLink/dsh-plugin-guide).
4
+
5
+ ## Compatibility
6
+
7
+ | Superficie | Estado |
8
+ |---|---|
9
+ | Harness | DeepSeek Harness `0.1.1-rc.2` |
10
+ | Node | `^22.19.0 || >=24.0.0` |
11
+ | Plataformas | Todas (ESM puro; sin código nativo, sin red) |
12
+
13
+ ## What it does
14
+
15
+ Registra la herramienta `{{name}}_echo`. Al llamarla responde `<greeting>: <text>`, donde `<greeting>` proviene de la configuración del plugin y `<text>` es el argumento.
16
+
17
+ ## Install
18
+
19
+ ```sh
20
+ pnpm pack
21
+ dsh plugin --profile <name> add ./{{pkgName}}-{{version}}.tgz
22
+ dsh --profile <name> --dump-config | grep '{{pkgName}}'
23
+ ```
24
+
25
+ ## Configuration
26
+
27
+ | Clave | Tipo | Valor por defecto | Descripción |
28
+ |---|---|---|---|
29
+ | `greeting` | string | `Hello` | Prefijo de la respuesta echo |
30
+
31
+ La configuración se valida con el esquema Schemastery `Config` de `index.js`; ningún ajuste está codificado de forma fija.
32
+
33
+ ## Development
34
+
35
+ ```sh
36
+ pnpm install
37
+ pnpm test
38
+ ```
39
+
40
+ ## License
41
+
42
+ [Apache License 2.0](LICENSE) © {{year}} {{pkgName}} contributors.