dsh-rewind-plugin 0.15.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/format.md DELETED
@@ -1,271 +0,0 @@
1
- # Durable format
2
-
3
- The on-disk format of the dsh-rewind checkpoint store, pinned as a spec. The
4
- implementation is `src/snapshot.ts`; this document is the reference for what
5
- readers may rely on and what a future incompatible change must do. If the code
6
- and this spec disagree, the code wins and this spec is a bug.
7
-
8
- ## State root
9
-
10
- The store root defaults to `<dsh home>/rewind-snapshots/` — the dsh data
11
- directory (`~/.dsh/rewind-snapshots/` when `DSH_HOME` is unset) — overridable in
12
- order by the `snapshotDir` plugin config, then the `DSH_REWIND_SNAPSHOT_DIR`
13
- environment variable. It is a sibling of the workspace, never a subtree of it.
14
- Deleting the root only removes file backups; the store rebuilds from scratch.
15
-
16
- ```
17
- <root>/
18
- └── <sessionId>/ # safeSessionId(sessionId)
19
- ├── store # store-format marker ("2")
20
- ├── format # DSH session-format marker (session.header.version)
21
- ├── <anchorSeq>/ # decimal seq of the anchoring user/message
22
- │ ├── <base>.json # one committed before-backup (metadata)
23
- │ └── <base>.before # its raw byte sidecar (the before content)
24
- ├── .pending/ # captures staged but not yet committed
25
- │ └── <base>.before
26
- ├── rescue/<opId>/<n>.before # one pre-restore ("rescue") copy per op
27
- └── journal-<opId>.json # one restore-op journal
28
- ```
29
-
30
- - `sessionId` is sanitized to `[a-zA-Z0-9._-]`; the bare values `.` and `..`
31
- are replaced (`safeSessionId`) so a hostile id cannot traverse out of the
32
- root.
33
- - `callId` is sanitized to `[a-zA-Z0-9._-]` (`safeFileId`) and extended with an
34
- 8-hex digest of the unsanitized id — `<base>` is
35
- `<safeFileId(callId)>-<sha256(callId)[0..8]>`, so two call ids that sanitize
36
- to the same name (e.g. `a:b` / `a_b`) cannot collide. Readers never infer a
37
- name: every reference names its file, so pre-digest (released v1) names keep
38
- resolving.
39
- - `<anchorSeq>` is a decimal integer; directories with non-integer names are
40
- ignored by readers.
41
- - Checkpoint entries are read from those numeric directories only
42
- (`<anchorSeq>/<base>.json`). Neither marker is `.json`, and a stray `.json` in
43
- the session root is ignored.
44
- - Journal files are recognized by the `journal-` prefix (current format) or the
45
- released `restore-journal-` prefix (read-only compatibility).
46
- - `store` is the session's store-format marker (a decimal version, written
47
- atomically); a missing marker means the released v1 string format. `format` is
48
- the DSH **session**-format marker the snapshots were anchored under (see
49
- `SECURITY.md`) — the two are independent.
50
-
51
- ## Checkpoint entry
52
-
53
- One JSON file per before-backup, named `<base>.json` next to its `<base>.before`
54
- sidecar:
55
-
56
- ```ts
57
- interface CheckpointEntryJson { // one `<base>.json`
58
- store: 2 // on-disk store format (absent/1 = the released v1 string format)
59
- callId: string // the tool call that mutated the file
60
- file: string // resolved display path (absolute)
61
- parent?: string // realpath of the file's directory at commit time (the location pin)
62
- blob: string | null // sibling sidecar file name; null = the file was created
63
- size: number // byte length of the sidecar (0 when `blob` is null)
64
- mode?: number // permission bits, applied when content is restored
65
- lossy?: true // recorded from v1 content that had already lost bytes
66
- time: number // epoch ms, strictly increasing within a store instance
67
- }
68
- ```
69
-
70
- A dedup link (see below) carries `callId`, `file`, `ref`, `time` and an
71
- optional `parent` instead of `blob` and `size`.
72
-
73
- Semantics:
74
-
75
- - **`blob` is the pre-edit state**: a string names the sidecar holding the exact
76
- before bytes (copied raw, never through a JS string, so binary and non-UTF-8
77
- files round-trip byte-exactly); `null` means the call created the file — valid
78
- only with `size: 0` and never with `lossy`. The name is an invariant: it must
79
- equal the entry's own file name minus `.json` plus `.before`.
80
- - **`anchorSeq` is the parent directory**, deliberately not a field of the
81
- entry: rewinding to message N applies every entry anchored at or after N (the
82
- boundary is inclusive).
83
- - **`parent` is the location pin**: the `realpath` of `dirname(file)` at commit
84
- time. A restore refuses the path when its directory no longer resolves there.
85
- Only the path's FINAL component is checked for links (`lstat().nlink > 1` or a
86
- symlink; see `SECURITY.md`), so a repointed ancestor directory would otherwise
87
- redirect the write — or the unlink of a recorded creation — outside the
88
- recorded location. Absent means "no pin" and falls back to the final-component
89
- check alone.
90
- - **`time` is the ordering key within an anchor group**: it is monotonic per
91
- store instance (bumped past the previous commit), so same-millisecond
92
- commits stay capture-ordered and a re-read always picks the same "earliest"
93
- entry per path.
94
- - **`mode` never decides an action** (a mode-only difference is a no-op); it is
95
- applied, best-effort, only when content is written back.
96
- - **`lossy` marks a record that had already lost bytes** (v1 content that was
97
- decoded lossily, or a materialized link to one): it is comparable but never
98
- written back, so a lossy record can never overwrite a live file.
99
- - Synthetic re-check entries (external edits/deletions seen at a user-message
100
- boundary) use `callId = recheck-<anchorSeq>-<sha256(path) first 8 hex>`.
101
-
102
- ### Dedup link entry
103
-
104
- A tracked file that records the same `before` content as its immediately-prior
105
- entry for that path is stored as a **link** instead of a second byte copy: the
106
- entry carries a `ref` (the `<anchorSeq>/<base>.json` of that prior entry) and
107
- omits `blob`/`size`, so identical content is never duplicated across entries. A
108
- reader resolves the `ref` back to the terminal real snapshot; `blob: null` (i.e.
109
- a recorded creation) still means "the file was created". A `ref` is validated as
110
- a single-level, `<digits>/<file>.json` relative reference (no traversal) so a
111
- corrupt or hostile ref cannot escape the store root when followed; because refs
112
- name the actual file, a link may point at a released-v1 entry. Because links
113
- reference prior entries, `prune` materializes a surviving link whose `ref` lands
114
- on a group it is about to drop before deleting that group, so no kept link is
115
- left dangling; the materialized entry keeps the bytes and the link's own
116
- location pin, but not the referent's `mode` (a link records no permissions of
117
- its own).
118
-
119
- ## Restore journal
120
-
121
- One JSON file per restore operation, written **before any mutation** and
122
- updated as the pass applies:
123
-
124
- ```ts
125
- type ByteRef = { blob: string } | { text: string } // session-relative byte file, or inline text
126
-
127
- interface RestoreJournal {
128
- version: 2
129
- id: string // `op-<base36 ms>-<random>`; file name suffix
130
- sessionId: string
131
- targetSeq: number // rewind target the restore belongs to
132
- startedAt: number // epoch ms
133
- finishedAt?: number // set on a terminal state
134
- state: 'running' | 'rollback-running' | 'completed' | 'rolled-back' | 'recovery-required'
135
- actions: RestoreJournalAction[]
136
- rollbackError?: string // set when a rollback pass failed partway
137
- }
138
-
139
- interface RestoreJournalAction {
140
- path: string
141
- action: 'restore' | 'delete' // restore = write the content back; delete = unlink
142
- before: ByteRef | null // target content for restore; null for delete
143
- rescue: ByteRef | null // pre-restore disk state; null = file was absent
144
- mode?: number // recorded target permissions
145
- rescueMode?: number // recorded pre-restore permissions
146
- parent?: string // checkpoint-time location pin (see the entry)
147
- rescueError?: string // set when the rescue capture failed (rollback skips it)
148
- done: boolean // true once the action's fs op completed and was marked
149
- failed?: string // per-action failure message (the pass never aborts)
150
- }
151
- ```
152
-
153
- A `{blob}` reference is safe and session-relative: `<anchorSeq>/<base>.before`
154
- (an entry sidecar) or `rescue/<opId>/<n>.before` (a rescue copy), validated on
155
- both write and read so a corrupt or hostile journal can never point a restore or
156
- a rollback outside the store. `{text}` refs carry content that has no sidecar:
157
- released-v1 entry content, or a legacy journal's inline strings.
158
-
159
- States: `running` and `rollback-running` are non-terminal; `completed` /
160
- `rolled-back` are terminal. Reconciliation *reports* a still non-terminal op as
161
- `interrupted` (or `recovery-required` when the journal is corrupt or a rollback
162
- could not complete), while the journal itself stays `running` /
163
- `rollback-running`. A journal read back from a legacy file (`restore-journal-`
164
- prefix, `version: 1`, inline strings) is rewritten IN PLACE as `version: 2` once
165
- a redo or rollback pass completes, so the same op never leaves two divergent
166
- versions on disk.
167
-
168
- ## Write guarantees
169
-
170
- - **Atomicity**: every JSON write serializes to a sibling `<target>.tmp` and
171
- renames over the target. A crash between the two steps leaves only the temp
172
- file — never a readable half-written target — and readers ignore temp files
173
- (they do not end in `.json`). The next write of the same target overwrites
174
- a leftover temp.
175
- - **Bytes before metadata**: a checkpoint commit places the sidecar first
176
- (a staged capture is `rename`d out of `.pending/`; other sources are written
177
- temp-then-rename) and only then publishes the entry JSON, so a crash can
178
- leave an unreferenced sidecar but never an entry whose bytes are missing.
179
- A sidecar that is missing or shorter than `size` is a per-file failure, never
180
- a silent "the file was created".
181
- - **Pinned location**: every record this build writes — entry, link and journal
182
- action — carries where the tracked file's directory resolved at commit time
183
- (the `parent` pin, best-effort), and a restore re-checks it before touching the
184
- path — the initial pass, a post-restart `continueRestore` and a
185
- `rollbackRestore` alike (the journal carries the pin). A directory that
186
- resolves elsewhere is refused and reported, so a restore can never write or
187
- unlink outside the recorded location; a record with no pin (released-v1 data,
188
- or a commit whose parent could not be resolved) falls back to the
189
- final-component link check alone. A parent chain that is GONE is still
190
- recreated — the plugin restores files whose directory was deleted — but only
191
- while its nearest surviving ancestor resolves inside the pin. A stable
192
- symlinked ancestor is never refused: both sides of the comparison are
193
- `realpath`s.
194
- - **Journal before mutation**: the rescue state of every planned path is
195
- captured as a raw byte copy and the intent journal — references only —
196
- persisted atomically BEFORE the first fs mutation; each action is marked
197
- `done` as it is applied.
198
- - **Disk is truth**: after a restart, reconciliation compares the real disk
199
- against each action's goal (the restore target for `running` journals, the
200
- rescue state for `rollback-running` / `recovery-required` ones). A path
201
- whose disk already matches is marked done without being touched.
202
- - **Bounded storage**: `prune` keeps the newest 100 anchor groups per session
203
- (`MAX_ANCHOR_GROUPS`), materializing any surviving dedup link that references
204
- a group being dropped before deleting whole anchor directories; it also
205
- recycles terminal journals (`completed` / `rolled-back`) together with their
206
- `rescue/<opId>/` bytes, and collects `.pending/` captures older than 24 h that
207
- no commit consumed. A group a non-terminal journal still references is pinned
208
- — evicting it would make "continue finishes the interrupted op" impossible —
209
- so the effective window may exceed `keep` until that op is resolved.
210
- Non-terminal and corrupt journals are always kept. Across sessions,
211
- `pruneStale` removes whole long-inactive session directories whose newest
212
- member stamp is older than a configurable idle cutoff (default off), so the
213
- store root does not grow without bound either.
214
-
215
- ## Validation and failure policy
216
-
217
- - **Entries**: a missing, malformed, or self-contradictory entry is read as
218
- `undefined` (silently skipped) — losing one backup, never the recovery path.
219
- Contradictions are never guessed at: a `blob: null` with `size !== 0` or with
220
- `lossy`, a `blob` that is not this entry's sidecar name, and a non-string
221
- `before` in a v1 record are all corruption, and guessing an entry's kind is
222
- how a restore turns into a delete. An absent or malformed `parent` is not
223
- corruption: it means "no pin", so the entry falls back to the released
224
- final-component rule. Two cases are *not* silent skips: an entry
225
- whose `store` is newer than this build fails the whole operation closed (see
226
- Versioning), and a record whose sidecar is missing or too short is a per-file
227
- failure that the restore reports — never a delete of the live file.
228
- - **Journals**: a corrupt or schema-invalid journal **fails loud** —
229
- `reconcileRestores` reports it as `recovery-required` and never drops it,
230
- because dropping it would silently erase the interrupted restore's recovery
231
- record. A `ref` with a traversal, absolute, or unknown-root segment counts as
232
- corrupt.
233
- - **Journal IO**: best-effort by design — if a journal cannot be written, the
234
- restore proceeds with pre-journal semantics (crash safety degrades,
235
- behavior does not).
236
-
237
- ## Versioning policy
238
-
239
- The format is versioned twice: a session-level `store` marker (the value is the
240
- current `CURRENT_STORE_VERSION`, 2) and a self-describing field on every record
241
- (`store` on entries, `version` on journals). A missing `store` marker (or `1`)
242
- means the released v1 string format, which is still read but never migrated. A
243
- `store` **above** the current version fails the whole operation closed — no file
244
- restore and no new entry written into a store a newer build owns (an explicit
245
- clear or the age-based sweep is not version-gated) — while the conversation
246
- rewind itself keeps working (it does not depend on snapshots). A journal whose
247
- `version` is present but neither 1 nor 2 is corrupt and is reported
248
- `recovery-required` (see Validation).
249
-
250
- Because compatibility is not safe in both directions, the byte format reuses
251
- none of v1's path/state keys (`path`, `anchorSeq`, `before`) and does not carry
252
- `anchorSeq` (it equals the parent directory). A released v1 build therefore
253
- rejects each such entry instead of reading it as "the file was created", so a
254
- downgrade cannot delete workspace files; that field-name contract is pinned by
255
- `tests/downgrade-safety.test.ts`. Compatibility means reading old data, not
256
- repairing it: bytes the v1 build had already lost cannot be recovered.
257
-
258
- A future incompatible format should raise the marker/version (readers already
259
- fail closed above their own version) or move the state root and ship an explicit
260
- migration tool. Old-format data is never silently re-interpreted.
261
-
262
- ## Cleanup policy persistence
263
-
264
- The snapshot auto-cleanup policy (the `autoCleanupEnabled` switch and the
265
- `autoCleanupMaxAgeDays` idle cutoff) is no longer a file: it is this plugin
266
- entry's own live configuration (both fields declared `.volatile()`, validated by
267
- the entry's schemastery `Config` schema, defaults on the schema). The policy was
268
- previously persisted in a legacy `<dsh home>/snapshot-cleanup.json` file; that
269
- file store and its one-time startup migration have been removed. The last-sweep
270
- clock stays in its own `<dsh home>/snapshot-cleanup-last-sweep.json` state file,
271
- which is unchanged.
@@ -1,75 +0,0 @@
1
- # DeepSeek Harness interface reference
2
-
3
- > Maintainer doc: the harness subsystems this plugin depends on, and the key
4
- > source files behind each interface. Official repo:
5
- > [github.com/deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness)
6
-
7
- ## Subsystem docs (`docs/subsystems/`)
8
-
9
- - [session.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/subsystems/session.md) — `Session` / `SessionStore` / event model (`Session.append`, `surfaceOp`, `sourceEventSeqs`)
10
- - [core.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/subsystems/core.md) — `Agent` (`status`, `session`) and other core types
11
- - [commands.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/subsystems/commands.md) — command registration (`ctx.commands.register`, `CommandInvocation`, `CommandResult`)
12
- - [tools.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/subsystems/tools.md) — tool execution seam (`tools/pre-execute` / `tools/post-execute`, `ToolExecution`)
13
- - [session-query.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/subsystems/session-query.md) — session query / `foldSurface` read-only interfaces
14
- - [token-meter.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/subsystems/token-meter.md) — replay-aware token meter (`TokenMeter.measure`, surface pricing)
15
- - [compaction.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/subsystems/compaction.md) — compaction seam / `compaction/*` events / `BasicCompactionEngine` (`summarize()` hook)
16
- - [session-projection.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/subsystems/session-projection.md) — projection registry (`SessionProjectionRegistry.snapshot/checkpoint`)
17
- - [session-stats](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/session/session-stats) — whole-log stats projection (folds ALL events; rewind does not roll back counts)
18
- - [session-title.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/subsystems/session-title.md) — durable title state (`foldSessionTitle`)
19
- - [goal.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/subsystems/goal.md) — goal fold (`foldGoal`) and round admission
20
-
21
- Compatibility probes against these subsystems (test-driven investigation, the
22
- `compat-invariants` / `compat-interop` vitest suites + the `verify-host` real
23
- `/compact` chain): [compat/audit.md](compat/audit.md).
24
-
25
- Also in the harness repo's `docs/`: `persistence-catalog.md` (full
26
- `SessionEventMap`), `tool-catalog.md` (tool inventory), `config-catalog.md`
27
- (configuration inventory).
28
-
29
- ## Key source (`packages/`)
30
-
31
- | Interface | File |
32
- |---|---|
33
- | `Session.append`, surface validation | [packages/core/session/src/index.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/core/session/src/index.ts) |
34
- | `foldSurface`, replacement rules | [packages/core/session/src/surface.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/core/session/src/surface.ts) |
35
- | `SessionEventMap`, `SurfaceOp` | [packages/core/session/src/types.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/core/session/src/types.ts) |
36
- | `createUserMessage`, `MessageSource` | [packages/llm/llm/src/message.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/llm/llm/src/message.ts) |
37
- | `CommandDefinition`, `CommandInvocation` | [packages/interaction/commands/src/index.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/interaction/commands/src/index.ts) |
38
- | `Agent` (`status` / `session`) | [packages/core/agent/src/runtime-types.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/core/agent/src/runtime-types.ts) |
39
- | `tools/pre-execute` / `execute` / `post-execute` | [packages/core/tools/src/index.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/core/tools/src/index.ts) |
40
- | Client DOM anchors (`data-chat-flow-kind` / `data-chat-anchor-key`) | [packages/client/ui-chat/src/client/chat/ChatNodeSeat.tsx](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/client/ui-chat/src/client/chat/ChatNodeSeat.tsx) |
41
- | User bubble rendering | [packages/client/ui-chat/src/client/chat/MessageItem.tsx](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/client/ui-chat/src/client/chat/MessageItem.tsx) |
42
- | Client `SessionFace` (`command` / `cancel`) | [packages/api/session-controller/src/client/contract/session.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/api/session-controller/src/client/contract/session.ts) |
43
- | Client pending interaction (SessionPendingInteraction) | [packages/client/ui-session/src/client/index.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/client/ui-session/src/client/index.ts) |
44
-
45
- ## Plugin source layout
46
-
47
- The module map below duplicates `AGENTS.md` (`Layout at a glance`), which is
48
- the canonical source; this block only adds the finer-grained client-side files
49
- and packaging entries.
50
-
51
- ```
52
- src/client/index.ts client plugin: /rewind command decoration + per-message ↶ button portals;
53
- re-exports the client contract (hiddenSeqsOf / targetSeqOfArgs / HiddenChat)
54
- src/client/popover.ts mode-selection popover (both-mode impact confirm)
55
- src/client/hidden.ts withdrawn-span computation (hiddenSeqsOf), pure
56
- src/client/candidates.ts rewind candidate listing (rewindCandidatesOf), pure
57
- src/client/pending.ts pending-steering bubble ↔ inbox next-step (user-sourced) matching, pure
58
- src/client/portals.tsx per-user-message ↶ button portals (React bridge into the actions row)
59
- src/client/settings-card.tsx snapshot-cleanup form on the bundle page (plugins.bundle.config)
60
- src/client/locales.ts zh / en copy (LocaleNamespaceMap)
61
- src/client/styles.ts injected styles (dsh design tokens)
62
- src/client/build-info.ts client build identity (__DSH_REWIND_VERSION__ / __DSH_REWIND_BUILD__)
63
- src/client/log.ts client logger (namespaced, dsh-rewind.debug-gated)
64
- scripts/build.mjs esbuild: lib/index.js (host ESM) + lib/client.js (loader closure) + .d.ts
65
- scripts/check-dsh-version.mjs DSH peer-tuple check (latest dist-tag vs peers)
66
- scripts/update-badge.mjs regenerate the tests badge (CI only)
67
- scripts/verify-host.mjs end-to-end host verification (full check suite)
68
- scripts/session-decode.mjs / session-encode.mjs `.jsonl.zstd` codec (test fixtures)
69
- tests/ vitest suites (rewind / snapshot / hidden / session-cwd / integration)
70
- docs/ maintainer docs: contract/, compat/, release/ subdirectories
71
- assets/screenshots/ UI screenshots
72
- cordis.patch.yml bundle patch (mounts the dual-face plugin row)
73
- package.json dsh.bundle + dsh.client manifests, optional peerDependencies
74
- ```
75
-
@@ -1,136 +0,0 @@
1
- # Release
2
-
3
- [简体中文](release.zh.md)
4
-
5
- ## First release (manual, one-time)
6
-
7
- Trusted Publisher can only be configured once the package exists, so the first
8
- version is published locally:
9
-
10
- ```sh
11
- npm login
12
- npm publish --access public
13
- ```
14
-
15
- - If prompted for `EOTP`: complete the browser auth link the CLI prints, or
16
- retry with a 6-digit code — `npm publish --otp=<code>`.
17
- - The first version carries no provenance (local path) — acceptable; every CI
18
- release after that publishes with Sigstore/SLSA provenance automatically.
19
-
20
- ## Configure Trusted Publisher (npmjs.com, one-time)
21
-
22
- Open `https://www.npmjs.com/package/dsh-rewind-plugin` → package **settings** →
23
- **Trusted Publisher**:
24
-
25
- | Field | Value |
26
- | --- | --- |
27
- | Provider | GitHub Actions |
28
- | Organization or user | `SiriLee` |
29
- | Repository | `dsh-rewind` (the GitHub repo, not the npm name) |
30
- | Workflow filename | `publish.yml` |
31
- | Environment | empty |
32
- | Allowed actions | `npm publish` |
33
-
34
- ## Subsequent releases (CI, automatic)
35
-
36
- The release workflow is **version-driven**: it derives the npm dist-tag from the
37
- version in `package.json`, independent of branch. A stable version publishes to
38
- `latest`; a pre-release publishes to the dist-tag named by its pre-release
39
- identifier (e.g. `0.9.0-alpha.1` → `alpha`, `0.9.0-rc.1` → `rc`). The dist-tag is
40
- never passed by hand.
41
-
42
- The line being released determines the branch and the bump:
43
-
44
- | Release | Branch | Bump | dist-tag |
45
- | --- | --- | --- | --- |
46
- | Stable patch (current line) | `release/<line>.x` | `npm version patch` | `latest` |
47
- | Pre-release (next line) | `main` | `npm version prerelease --preid=alpha` | `alpha` |
48
- | Stable (next line) | `main` | `npm version <next>` | `latest` |
49
-
50
- Each release is `git push <branch>` followed by `git push <branch> --tags`.
51
-
52
- - The release commit is **`chore: release vX.Y.Z`**; the **lightweight `vX.Y.Z`
53
- tag** sits on it. Do **not** create tag/release first with `gh release create
54
- <tag>` (it tags the remote `main` head and breaks the tag/version match).
55
- - **Before bumping, manually confirm there is no newer DSH version the plugin
56
- has not been verified against** (a pre-release can ship in DSH Desktop
57
- without being on npm; see docs/compat/audit.md).
58
- - The workflow verifies the tag matches `package.json`, runs typecheck + tests +
59
- a full build + artifact verification, publishes with `--provenance` (Sigstore)
60
- to the version-derived dist-tag, and creates a GitHub Release (a pre-release
61
- is created as a GitHub pre-release, not `latest`). It is **idempotent** — an
62
- already published version is skipped.
63
- - CI (`.github/workflows/ci.yml`) runs `npm run check` — typecheck + tests +
64
- build + artifact verification + a `npm pack --dry-run` — on every push / PR
65
- across both Node engines boundary versions; the tarball layout is guarded by
66
- `tests/package-layout.test.ts`.
67
- - The GitHub Release body is auto-created with `--generate-notes` as a
68
- **placeholder** (`--latest` / `--prerelease` per version). After the publish
69
- run succeeds, overwrite the body by hand in the repo's bilingual style
70
- (Chinese first, then English) — never keep the auto text as the final note.
71
-
72
- ## DSH version alignment (single peer tuple)
73
-
74
- DSH is still in rc; npm's prerelease matching rules require a peer range to
75
- share the host version's `[major, minor, patch]` tuple. So `peerDependencies`
76
- uses one peer tuple per DSH line (e.g. `^0.2.0-rc.1`). The range is
77
- conservative: it declares only what was verified, and guarantees nothing
78
- outside it.
79
-
80
- - **When to update**: when the verified range changes — a new DSH tuple, or a
81
- deliberate narrowing (e.g. dropping the internal `alpha` series); a release
82
- already inside the range changes nothing **unless it changes interfaces inside
83
- the tuple** (`0.1.6-alpha.2` removed the client session-ownership and
84
- queue-mirror APIs, so the floor moved with it). All `@deepseek-ai/*` packages
85
- release together; `npm view @deepseek-ai/dsh dist-tags` is the signal.
86
- - **Published-tuple check (optional)**: `node scripts/check-dsh-version.mjs`
87
- compares the `latest` dist-tag version against the tuple the peers cover
88
- (exit 0 = nothing to do, exit 1 = update). It reads the `latest` tag only; a
89
- pre-release published under another tag or bundled without
90
- going to npm is a manual pre-release check — see the "Before bumping" step above.
91
- - **Update steps**: point every `@deepseek-ai/dsh-*` peer and devDependency at
92
- the verified range → `npm install` → `npm run check` → release.
93
- - **After DSH goes final**: final releases are not bound by the prerelease
94
- tuple rule, so the peers can converge to a single stable range (e.g.
95
- `^0.1.x`); this section can then be deleted.
96
- - **Declared minimum (`engines.dsh`)**: alongside the peer tuple, each
97
- release declares the DSH runtime floor under `engines.dsh` (e.g.
98
- `>=0.2.0-rc.1`). No runtime consumer reads it; its form and its alignment with
99
- the peer tuple are pinned by `tests/package-layout.test.ts`. Bump it in
100
- the same release that changes the peer range; never leave code raised while
101
- the declared floor stays behind. Only the `>=X.Y.Z[-pre]` form is used.
102
- - **The peers are enforced at startup (`0.1.7-rc.1`)**: DSH checks a profile
103
- bundle's own `@deepseek-ai/dsh-*` peers against the running version and skips
104
- the whole bundle when one does not satisfy it, so a peer range that is merely
105
- stale now costs the plugin its load instead of a warning. See
106
- `docs/compat/audit.md`.
107
-
108
- ## Versioned-line release model
109
-
110
- **One release targets one DSH version line.** The plugin's own version is
111
- independent of the host; a release declares its DSH line through the peer
112
- constraint (a single companion tuple), never through the plugin version.
113
-
114
- The DSH line a release targets is the peer constraint, and its npm dist-tag is
115
- derived from the version (see above), so this model does not track the plugin's
116
- own version number.
117
-
118
- **Versioning.** A DSH version-line break is a MAJOR bump (incompatible with the
119
- prior DSH line). Within a line, MINOR/PATCH remain compatible.
120
-
121
- **Branching (trunk-based).** `main` is the single integration and release line
122
- and is always releasable. The currently-shipped stable is cut into a short-lived
123
- `release/<version>.x` maintenance branch from its release commit; that branch
124
- receives backported fixes while `main` advances to the next line. The prior
125
- (broad-compat) line is frozen as a tag, with no branch.
126
-
127
- **Support window / EOL.** A DSH line is supported within a declared window. By
128
- default the window runs until the next DSH line ships as `latest`; after that
129
- the line is EOL, frozen, and receives no further patches.
130
-
131
- **Bug-fix flow (forward-fix then backport).** A fix affecting multiple supported
132
- lines is applied on `main` first, then backported to each still-supported
133
- release branch. A fix specific to one line is applied only on that line.
134
-
135
- The single-line model uses one peer tuple per release (see DSH version
136
- alignment above); the OR-union multi-line practice it replaced is not used.
@@ -1,124 +0,0 @@
1
- # 发布流程
2
-
3
- [English](release.md)
4
-
5
- ## 首次发布(手动,一次性)
6
-
7
- Trusted Publisher 要求**包已存在**才能配置,因此首个版本走本地发布:
8
-
9
- ```sh
10
- npm login
11
- npm publish --access public
12
- ```
13
-
14
- - 若提示 `EOTP`(一次性密码):按 CLI 输出的浏览器认证链接完成认证,或用
15
- authenticator 的 6 位码 `npm publish --otp=<code>` 重试。
16
- - 首个版本无 provenance(本地路径),合规;之后每次 CI 发布自动带
17
- Sigstore/SLSA provenance。
18
-
19
- ## 配置 Trusted Publisher(npmjs.com,一次性)
20
-
21
- 打开 `https://www.npmjs.com/package/dsh-rewind-plugin` → 包右上角 **settings**
22
- → **Trusted Publisher**:
23
-
24
- | 字段 | 值 |
25
- | --- | --- |
26
- | Provider | GitHub Actions |
27
- | Organization or user | `SiriLee` |
28
- | Repository | `dsh-rewind`(GitHub 仓库名,与 npm 包名可不同) |
29
- | Workflow filename | `publish.yml` |
30
- | Environment | 留空 |
31
- | Allowed actions | `npm publish` |
32
-
33
- ## 后续发布(CI 自动)
34
-
35
- 发布 workflow 为**版本驱动**:npm dist-tag 由 `package.json` 中的版本号决定、与分支无关。
36
- 稳定版发 `latest`;pre-release 发到与其 pre-release 标识符同名的 dist-tag
37
- (如 `0.9.0-alpha.1` → `alpha`、`0.9.0-rc.1` → `rc`)。dist-tag 无需手传。
38
-
39
- 按要发布的版本线选择分支与版本步进:
40
-
41
- | 发布类型 | 分支 | 版本步进 | dist-tag |
42
- | --- | --- | --- | --- |
43
- | 当前线稳定补丁 | `release/<line>.x` | `npm version patch` | `latest` |
44
- | 下一线 pre-release | `main` | `npm version prerelease --preid=alpha` | `alpha` |
45
- | 下一线正式版 | `main` | `npm version <next>` | `latest` |
46
-
47
- 每次发布为 `git push <分支>`,再 `git push <分支> --tags`。
48
-
49
- - 发布提交为 **`chore: release vX.Y.Z`**,**轻量 `vX.Y.Z` tag** 落在该提交上。
50
- **不要**用 `gh release create <tag>` 事先建 tag/release(会打到远端 `main`
51
- HEAD,导致 tag/版本校验失败)。
52
- - **升版前手动确认**:确认无插件未针对其验证过的更新 DSH 版本(pre-release
53
- 可能只随 Desktop 捆绑、而不发到 npm;见 docs/compat/audit.md)。
54
- - workflow 校验 tag 与 `package.json` 版本一致,跑 typecheck + 测试 + 完整
55
- 构建 + 产物验证,以 `--provenance`(Sigstore)发布到版本推导出的 dist-tag,
56
- 并创建 GitHub Release(pre-release 会建成 GitHub pre-release,而非 `latest`)。
57
- **幂等**——已发布的版本会跳过。
58
- - CI(`.github/workflows/ci.yml`)在每次 push / PR 跑 `npm run check`——
59
- typecheck + 测试 + 构建 + 产物验证 + `npm pack --dry-run`,且覆盖
60
- engines 两个边界版本;tarball 布局由 `tests/package-layout.test.ts` 守护。
61
- - GitHub Release 正文由 workflow 以 `--generate-notes` 自动生成,仅是**占位**
62
- (按版本取 `--latest` / `--prerelease`)。发布运行成功后,请用**手写双语**
63
- 正文(中文在前、英文在后)覆盖——切勿把自动文本当作最终发布说明。
64
-
65
- ## DSH 版本适配(单一 peer 元组)
66
-
67
- DSH 仍在 rc 阶段,npm 的 prerelease 匹配规则要求 peer 范围与宿主版本
68
- **同 `[major, minor, patch]` 元组**才能匹配。因此 peerDependencies 采用
69
- **每一条 DSH 线一个 peer 元组**(如 `^0.1.2-rc.1`);范围是保守的,只声明实际验证过的内容。
70
-
71
- - **何时需要更新**:**已验证范围**变化时——DSH 发新元组,或主动收窄(例如不再
72
- 声明内测的 DSH `alpha` 系列);落在已声明范围内的发布不改变任何东西,**除非它在
73
- 同一元组内改变了接口**(`0.1.6-alpha.2` 删除了客户端会话所有权与 queue 镜像
74
- API,下限随之移动)。DSH 所有包同版本发布;信号是
75
- `npm view @deepseek-ai/dsh dist-tags`。
76
- - **已发布元组检查(可选)**:`node scripts/check-dsh-version.mjs` 用 npm `latest`
77
- dist-tag 版本对比 peer 覆盖的元组(exit 0 无需动作,exit 1 需要)。它**只读
78
- `latest` tag**;发布在其它 tag 的 pre-release 走**手动发布前检查**
79
- ——见上文"升版前手动确认"。
80
- - **更新步骤**:把每个 `@deepseek-ai/dsh-*` peer 与 devDependency 指向已验证
81
- 范围 → `npm install` → `npm run check` → 发版。
82
- - **正式版后收敛**:DSH 发布 final 版本后,正式版不受 prerelease 元组规则
83
- 限制,peer 可收敛为稳定的 `^0.1.x` 单范围,此节即可删除。
84
- - **声明的最低运行时(`engines.dsh`)**:与 peer 元组一起,每个发布在
85
- `engines.dsh` 声明 DSH 运行时下限(如 `>=0.1.2-rc.1`)。当前线上还没有
86
- 任何读取方,因此它只是前置声明、尚未被强制执行。**与 peer 范围持平的同一发布里
87
- 一并 bump**;不可只升代码、声明下限停留在旧值。仅使用 `>=X.Y.Z[-pre]` 形式。
88
- - **peer 在启动时会被强制执行(`0.1.7-rc.1`)**:DSH 会拿 bundle 自身的
89
- `@deepseek-ai/dsh-*` peer 与运行版本比对,只要有一个不满足就**整包跳过**,
90
- 因此过期的 peer 范围不再是"仅告警",而是直接导致插件不加载。见
91
- `docs/compat/audit.md`。
92
-
93
- ## 发布版本线模型
94
-
95
- **一个发布对准一条 DSH 版本线。** 插件自身版本号与宿主解耦;某发布所对准的
96
- DSH 线由 peer 约束(单一 companion 元组)声明,而非插件版本号。
97
-
98
- | 插件版本 | DSH 线 | 角色(示例) |
99
- | --- | --- | --- |
100
- | `0.7.x` | `0.1.1` + `0.1.2`(广兼容) | 冻结 / EOL |
101
- | `0.8.x` | `0.1.2-rc.1`(单线) | 当前线 |
102
- | `0.9.x` | `0.1.3`(单线) | 后续线 |
103
-
104
- 各行仅为示例——某个发布对准的 DSH 线由 peer 约束声明,其 npm dist-tag 按版本号
105
- 派生(见上文),因此本模型不跟随插件自身版本号变化。
106
-
107
- **版本号。** DSH 版本线破坏性变更 = MAJOR 升版(与更早 DSH 线向后不兼容)。
108
- 同一线内 MINOR/PATCH 保持向后兼容。
109
-
110
- **分支(主干开发)。** `main` 是唯一集成与发布线,始终可发版。当前已发布
111
- 稳定版从发布提交点切出短命 `release/<版本>.x` 维护分支;该分支承接
112
- backport 修复,同时 `main` 前进到下一线。更早的(广兼容)线冻结成 tag,无分支。
113
-
114
- **dist-tag 路由。** 发布 workflow 按版本号取 npm dist-tag:稳定版发 `latest`;
115
- pre-release 发到与其 pre-release 标识符同名的 dist-tag(`0.9.0-alpha.1` →
116
- `alpha`、`0.9.0-rc.1` → `rc`)。pre-release 不占据 `latest`。
117
-
118
- **支持窗口 / EOL。** 一条 DSH 线仅在声明的窗口内受支持。默认窗口到下一
119
- DSH 线作为 `latest` 发布为止;此后该线 EOL、冻结、不再发补丁。
120
-
121
- **Bug 修复流程(先向前修,再回迁)。** 跨多条支持线的修复,先在 `main` 上
122
- 修复,再回迁到各仍受支持的 release 分支。仅特定线的修复,只在对应线修复。
123
-
124
- 单线模型每个发布使用单一 peer 元组(见上文「DSH 版本适配」);其取代的 OR 并集多线做法已不使用。