Merge remote-tracking branch 'origin/master' into worktree/windows-acl-hardening-followup

# Conflicts:
#	docs/config-catalog.i18n.yaml
This commit is contained in:
Tianyi Cui
2026-08-10 16:50:19 +08:00
48 changed files with 482 additions and 92 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/feedback/command-feedback/README.md
README.md: d7849e25fc62897e4ac6793f40bdc139adf9ba3d
README.zh.md: c3b7b59d90d924de6042aeac1e7eec39457c6c83
README.md: 52b8fb6a423fca69f76397deec36ecd22a6a6023
README.zh.md: ca74d53f2531a46c2c16aa1423cee52e89c8256f

View File

@@ -8,7 +8,7 @@ Trigger-independent session feedback plus human-facing `/feedback` capture. The
| Input | Result |
|---|---|
| `/feedback <text>` | Append `feedback/record` and acknowledge with `Feedback recorded.` |
| `/feedback <text>` | Append `feedback/record` and acknowledge with `Feedback recorded for session {sessionId}` followed by `User: {userId}`. |
| `/feedback` | Return a direct usage error. Whitespace-only input is treated as empty. |
Surrounding whitespace is discarded, but feedback is otherwise unparsed: no truncation, case folding, or control words. Text that looks like another command, such as `/feedback /plan felt slow`, is feedback content. Repeated commands each produce their own event; nothing is replaced or merged.
@@ -17,7 +17,7 @@ Surrounding whitespace is discarded, but feedback is otherwise unparsed: no trun
`recordFeedback(session, text)` is the command-independent write path. It rejects empty normalized text and appends `feedback/record { text }`; a different UI, hook, or host integration can call it without constructing a slash command. The `/feedback` handler uses that producer and starts no model work. The optional [`dsh-session-telemetry-otel`](../../session/session-telemetry-otel) consumer observes the event without changing its capture contract.
The feedback text appears in exactly one durable payload: `feedback/record`. [`dsh-commands`](../../interaction/commands/README.md) still appends its generic `command/run` / `command/done` pairing, but this definition sets `recordInput: false`, so `command/run` omits `args`; the paired `command/done` carries only the outcome. All three events are log-only and absent from the ordered surface, `deriveMessages()`, and model requests. These appends start persistence's ordinary eager drain, but neither producer forces `session/flush`, so acknowledgement means the feedback is in the log, not that it has reached disk. Rejected empty input leaves only the command pairing settled as `kind: 'error'`, with no `feedback/record`.
The feedback text appears in exactly one durable payload: `feedback/record`. [`dsh-commands`](../../interaction/commands/README.md) still appends its generic `command/run` / `command/done` pairing, but this definition sets `recordInput: false`, so `command/run` omits `args`; the paired `command/done` carries only the outcome. All three events are log-only and absent from the ordered surface, `deriveMessages()`, and model requests. These appends start persistence's ordinary eager drain, but neither producer forces `session/flush`, so acknowledgement means the feedback is in the log, not that it has reached disk. The acknowledgement identifies both the receiving session and the [shared anonymous user](../../session/user-id/); the first accepted feedback for a harness home can create `$DSH_HOME/.userid`. Rejected empty input leaves only the command pairing settled as `kind: 'error'`, with no `feedback/record` and no user-id lookup.
The event is authoritative rather than the command record because feedback may arrive through a trigger other than `/feedback`. Keeping the payload out of `command/run` avoids two records carrying the same text.

View File

@@ -8,7 +8,7 @@
| 输入 | 结果 |
|---|---|
| `/feedback <text>` | 追加 `feedback/record`,并以 `Feedback recorded.` 确认。 |
| `/feedback <text>` | 追加 `feedback/record`,并以 `Feedback recorded for session {sessionId}` 确认,随后显示 `User: {userId}`。 |
| `/feedback` | 返回一个直接用法错误。仅含空白的输入视为空输入。 |
前后空白会被丢弃,但除此之外,反馈内容不会被解析:没有截断、大小写折叠或控制词。看起来像另一个命令的文本(例如 `/feedback /plan felt slow`)就是反馈内容。重复执行命令时,每次都会产生一个事件;不会发生替换或合并。
@@ -17,7 +17,7 @@
`recordFeedback(session, text)` 是不依赖命令的写入路径。它拒绝规范化后为空的文本,并追加 `feedback/record { text }`;其他 UI、钩子或 host 集成无需构造斜杠命令即可调用它。`/feedback` 处理器通过该生产方写入,且不启动任何模型工作。可选的 [`dsh-session-telemetry-otel`](../../session/session-telemetry-otel) 消费方会观察该事件,但不改变它的采集约定。
反馈文本只出现在一个持久载荷中:`feedback/record`。[`dsh-commands`](../../interaction/commands/README.md) 仍会追加通用的 `command/run` / `command/done` 配对,但此定义设置了 `recordInput: false`,因此 `command/run` 会省略 `args`;配对的 `command/done` 只携带结果。三个事件都仅写入日志,不出现在有序 surface、`deriveMessages()` 以及模型请求中。这些追加会启动持久化的常规即时排空,但两个生产方都不会强制 `session/flush`,因此确认文本表示反馈已进入日志,而不表示它已经落盘。被拒绝的空输入只会留下以 `kind: 'error'` 结算的命令配对,不会产生 `feedback/record`
反馈文本只出现在一个持久载荷中:`feedback/record`。[`dsh-commands`](../../interaction/commands/README.md) 仍会追加通用的 `command/run` / `command/done` 配对,但此定义设置了 `recordInput: false`,因此 `command/run` 会省略 `args`;配对的 `command/done` 只携带结果。三个事件都仅写入日志,不出现在有序 surface、`deriveMessages()` 以及模型请求中。这些追加会启动持久化的常规即时排空,但两个生产方都不会强制 `session/flush`,因此确认文本表示反馈已进入日志,而不表示它已经落盘。确认文本同时标明接收反馈的会话和[共享匿名用户](../../session/user-id/);对于某个 harness home首次接受反馈时可能创建 `$DSH_HOME/.userid`被拒绝的空输入只会留下以 `kind: 'error'` 结算的命令配对,不会产生 `feedback/record`,也不会查找用户 id
权威记录是该事件,而不是命令记录,因为反馈可能来自 `/feedback` 之外的触发方式。让载荷不进入 `command/run`,可避免两条记录携带相同文本。

View File

@@ -28,6 +28,7 @@
"@deepseek-ai/dsh-commands": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-user-id": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
"devDependencies": {
@@ -38,6 +39,7 @@
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-user-id": "workspace:^",
"cordis": "^4.0.0-rc.7"
}
}

View File

@@ -9,6 +9,7 @@
import type { Context } from 'cordis'
import type { CommandInvocation, CommandResult } from '@deepseek-ai/dsh-commands'
import type { Session } from '@deepseek-ai/dsh-session'
import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-user-id'
export const name = 'command-feedback'
export const inject = ['commands']
@@ -41,14 +42,18 @@ export function recordFeedback(session: Session, text: string): void {
* Validate, record, and acknowledge one feedback entry. Returning an error
* leaves no `feedback/record` event.
* @param invocation - receiving agent, raw command input, and UI cancellation.
* @returns an acknowledgement, or a usage error when no feedback text was supplied.
* @returns an acknowledgement containing the receiving session and anonymous
* user ids, or a usage error when no feedback text was supplied.
*/
function executeFeedbackCommand(invocation: CommandInvocation): CommandResult {
if (invocation.rawInput.trim().length === 0) {
return { kind: 'error', text: `Feedback text is required. ${USAGE}` }
}
recordFeedback(invocation.agent.session, invocation.rawInput)
return { kind: 'success', text: 'Feedback recorded.' }
return {
kind: 'success',
text: `Feedback recorded for session ${invocation.agent.session.id}\nUser: ${getOrCreateAnonymousUserId()}`,
}
}
/** Register the global `/feedback` command for every composed command adapter. */

View File

@@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
@@ -7,6 +7,17 @@ import CommandService from '@deepseek-ai/dsh-commands'
import SessionStore, { foldSurface, Session, SessionId } from '@deepseek-ai/dsh-session'
import * as commandFeedback from '@deepseek-ai/dsh-command-feedback'
const { USER_ID, getOrCreateAnonymousUserId } = vi.hoisted(() => {
const USER_ID = '01234567-89ab-4cde-8f01-23456789abcd'
return { USER_ID, getOrCreateAnonymousUserId: vi.fn(() => USER_ID) }
})
vi.mock('@deepseek-ai/dsh-user-id', () => ({
getOrCreateAnonymousUserId,
}))
beforeEach(() => getOrCreateAnonymousUserId.mockClear())
interface Harness {
readonly ctx: Context
readonly agent: Agent
@@ -93,7 +104,7 @@ describe('/feedback human command', () => {
const test = await harness()
await expect(run(test, ' the diff view is unreadable')).resolves.toEqual({
kind: 'success',
text: 'Feedback recorded.',
text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}`,
})
expect(feedbackTexts(test.session)).toEqual(['the diff view is unreadable'])
const commandRun = test.session.events.find(event => event.type === 'command/run')
@@ -141,8 +152,8 @@ describe('/feedback human command', () => {
test.ctx.commands.execute(test.agent, '/feedback second', signal),
])
expect(settled.map(item => item?.result)).toEqual([
{ kind: 'success', text: 'Feedback recorded.' },
{ kind: 'success', text: 'Feedback recorded.' },
{ kind: 'success', text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}` },
{ kind: 'success', text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}` },
])
expect(feedbackTexts(test.session)).toEqual(['first', 'second'])
})
@@ -167,6 +178,7 @@ describe('/feedback human command', () => {
}
await expect(run(test)).resolves.toEqual(expected)
await expect(run(test, ' \n\t ')).resolves.toEqual(expected)
expect(getOrCreateAnonymousUserId).not.toHaveBeenCalled()
expect(feedbackTexts(test.session)).toEqual([])
const done = test.session.events.filter(event => event.type === 'command/done')
expect(done.map(event => event.data.kind)).toEqual(['error', 'error'])

View File

@@ -2,7 +2,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { afterEach, describe, expect, it } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import Include from '@cordisjs/plugin-include'
@@ -11,6 +11,7 @@ import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent'
import CommandService from '@deepseek-ai/dsh-commands'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import * as CommandFeedback from '@deepseek-ai/dsh-command-feedback'
import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-user-id'
let root: string | undefined
let context: Context | undefined
@@ -20,6 +21,7 @@ afterEach(async () => {
context = undefined
if (root !== undefined) await rm(root, { recursive: true, force: true })
root = undefined
vi.unstubAllEnvs()
})
/** Register one idle agent over a store-owned session, as an app's spine does. */
@@ -51,6 +53,7 @@ function agent(ctx: Context): Agent {
describe('/feedback real Loader composition through cordis.yml', () => {
it('boots cordis.yml and records feedback without model-visible output', async () => {
root = await mkdtemp(join(tmpdir(), 'dsh-command-feedback-loader-'))
vi.stubEnv('DSH_HOME', root)
const configPath = join(root, 'cordis.yml')
await writeFile(configPath, [
"- name: '@deepseek-ai/dsh-agent'",
@@ -87,7 +90,11 @@ describe('/feedback real Loader composition through cordis.yml', () => {
expect(context.commands.list(owner).map(command => command.name)).toContain('feedback')
const accepted = await context.commands.execute(owner, '/feedback the diff view is unreadable', signal)
expect(accepted?.result).toEqual({ kind: 'success', text: 'Feedback recorded.' })
const userId = getOrCreateAnonymousUserId({ env: { DSH_HOME: root } })
expect(accepted?.result).toEqual({
kind: 'success',
text: `Feedback recorded for session feedback-loader-agent\nUser: ${userId}`,
})
const rejected = await context.commands.execute(owner, '/feedback', signal)
expect(rejected?.result).toEqual({
kind: 'error',

View File

@@ -20,6 +20,9 @@
{
"path": "../../core/session"
},
{
"path": "../../session/user-id"
},
{
"path": "../../support/invariants"
}

View File

@@ -35,23 +35,21 @@
},
"peerDependencies": {
"@deepseek-ai/dsh-command-feedback": "^0.0.1",
"@deepseek-ai/dsh-brand": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-paths": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-session-telemetry": "^0.0.1",
"@deepseek-ai/dsh-user-id": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
"devDependencies": {
"@cordisjs/plugin-loader": "workspace:^",
"@deepseek-ai/dsh-command-feedback": "workspace:^",
"@deepseek-ai/dsh-brand": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-telemetry": "workspace:^",
"@deepseek-ai/dsh-user-id": "workspace:^",
"cordis": "^4.0.0-rc.7"
}
}

View File

@@ -25,7 +25,7 @@ import {
type TelemetrySeverity,
} from '@deepseek-ai/dsh-session-telemetry'
import { APP_IDENTITY } from '@deepseek-ai/dsh-llm'
import { getOrCreateAnonymousUserId } from './user-id.ts'
import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-user-id'
import {
BatchLogRecordProcessor,
LoggerProvider,

View File

@@ -13,7 +13,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { gunzipSync } from 'node:zlib'
import { Context } from 'cordis'
import { getOrCreateAnonymousUserId } from '../src/user-id.ts'
import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-user-id'
import Loader from '@cordisjs/plugin-loader'
import { recordFeedback } from '@deepseek-ai/dsh-command-feedback'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'

View File

@@ -30,10 +30,7 @@
"path": "../session-telemetry"
},
{
"path": "../../util/brand"
},
{
"path": "../../util/paths"
"path": "../user-id"
},
{
"path": "../../support/invariants"

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/session/user-id/README.md
README.md: 31a72f5e7b58b90b165b16374c2301389cbe2ca0
README.zh.md: 013097b3038c43ff740660ef9159ca2b13f7b743

View File

@@ -0,0 +1,29 @@
# @deepseek-ai/dsh-user-id
English | [中文](README.zh.md)
Shared anonymous identity for session telemetry and direct feedback acknowledgement. `getOrCreateAnonymousUserId()` returns a random UUID v4 scoped to one harness home, persisted as the bare line `$DSH_HOME/.userid` (`~/.dsh/.userid` when `DSH_HOME` is unset). The OpenTelemetry backend reports it as Resource `user.id`; `/feedback` includes the same value in its acknowledgement so an operator can correlate a submitted session and user with exported telemetry.
The identity is never derived from the hostname, network address, git remote, or another identifying source. Deleting `.userid` resets the identity on the next process launch. Separate harness homes have separate identities, and the dsh-sdk launcher telemetry intentionally keeps its own unrelated store.
## Storage contract
Reads and writes are synchronous because both boot-time telemetry construction and direct command execution need one API. The result is memoized per resolved file path for the process lifetime. A first writer uses exclusive creation and a concurrent loser adopts the persisted winner; a corrupt file is replaced. Persistence is best-effort, so an unwritable home still receives a process-local UUID rather than blocking telemetry or feedback.
## Composition
This package is a shared library, not a Cordis plugin. Consumers import `getOrCreateAnonymousUserId()` directly. Its invariant companion is intentionally empty because the package owns no event stream or public mutable relation that can be checked without creating the identity as a side effect.
## Model Experience
None, as the identifier is used only in telemetry metadata and a direct human command response; it never enters a model request.
#### KV Cache effect
None; this package never contributes to a model request.
## Known Limitations and Deferred Work
- **No recovery after deletion** — loss mints a new anonymous identity by design; recovery would require stable derivation material that weakens anonymity.
- **Best-effort concurrency** — a reader landing in the narrow interval between a concurrent process's exclusive create and completed write can use a different in-memory UUID for that run; later launches converge on the persisted value.
- **No cross-home identity** — different `$DSH_HOME` values cannot be correlated, and this package does not unify the separate dsh-sdk launcher telemetry identity.

View File

@@ -0,0 +1,29 @@
# @deepseek-ai/dsh-user-id
[English](README.md) | 中文
会话遥测与直接反馈确认共用的匿名身份。`getOrCreateAnonymousUserId()` 返回一个限定于单个 harness home 的随机 UUID v4并以裸行形式持久化到 `$DSH_HOME/.userid`(未设置 `DSH_HOME` 时为 `~/.dsh/.userid`。OpenTelemetry 后端将其作为 Resource 的 `user.id` 上报;`/feedback` 在确认文本中包含同一个值,以便运维人员将所报告的会话和用户与导出的遥测相关联。
该身份绝不从 hostname、网络地址、git remote 或其他可用于识别身份的来源派生。删除 `.userid` 后,下次启动进程时会重置身份。不同 harness home 拥有不同身份dsh-sdk launcher telemetry 则刻意使用与此无关的独立存储。
## 存储契约
读写采用同步方式,因为启动时构造遥测和直接执行命令都需要使用同一个 API。结果在进程生命周期内按解析后的文件路径缓存。首个写入方采用独占创建并发竞争中失败的一方会采用已持久化的胜出值。损坏的文件会被替换。持久化采用 best-effort因此即使 home 不可写,系统仍会返回进程本地 UUID而不会阻塞遥测或反馈。
## 组合
本包是共享库,并非 Cordis 插件。消费方直接导入 `getOrCreateAnonymousUserId()`。其不变式伴生插件刻意留空,因为本包既不拥有事件流,也不拥有任何可以在不触发创建身份这一副作用的情况下检查的公开可变关系。
## 模型体验
无,因为该标识符只用于遥测元数据和面向用户的直接命令响应;它绝不会进入模型请求。
#### KV Cache 影响
无;本包绝不会向模型请求贡献任何内容。
## 已知限制与暂缓工作
- **删除后无法恢复**:身份丢失后会按设计生成新的匿名身份;若要恢复身份,就需要稳定的派生材料,这会削弱匿名性。
- **Best-effort 并发**:如果读取方恰好落在并发进程完成独占创建但尚未写完的狭窄时间窗内,本次运行可能使用不同的内存 UUID后续启动会收敛到已持久化的值。
- **没有跨 home 身份**:不同 `$DSH_HOME` 值之间无法关联,本包也不会统一 dsh-sdk launcher telemetry 的独立身份。

View File

@@ -0,0 +1,39 @@
{
"name": "@deepseek-ai/dsh-user-id",
"description": "Shared anonymous user identity for DeepSeek Harness telemetry and feedback correlation",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-brand": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-paths": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
"devDependencies": {
"@deepseek-ai/dsh-brand": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",
"cordis": "^4.0.0-rc.7"
}
}

View File

@@ -1,22 +1,20 @@
/**
* Per-harness-home anonymous user id for the OTel Resource.
* Per-harness-home anonymous user id shared by telemetry and feedback.
*
* The id is a random UUID persisted as a bare line in `.userid` inside the
* harness home resolved by {@link resolveDshHome} (`$DSH_HOME` > `~/.dsh`),
* and never derived from the hostname, network address, git remote, or any
* other identifying source a derived id would make "anonymous" a fiction.
* The id is scoped to the harness home, not the machine: every process
* sharing one `$DSH_HOME` reports the same id, and deleting the file simply
* mints a fresh identity on the next launch (loss is accepted by design).
* This identity belongs to the OTel feed alone; the dsh-sdk launcher
* telemetry keeps its own separate store.
* other identifying source. It is scoped to the harness home, not the
* machine: every process sharing one `$DSH_HOME` reports the same id, and
* deleting the file mints a fresh identity on the next launch. The dsh-sdk
* launcher telemetry keeps its own separate store.
*
* Reads and writes are synchronous so the backend constructor can call this
* on its boot path, and the result is memoized per resolved file path: one
* process touches the disk once, and a file deleted mid-run keeps the
* process's id until the next launch.
* Reads and writes are synchronous so boot-time and command consumers can
* use one API. The result is memoized per resolved file path: one process
* touches the disk once, and a file deleted mid-run keeps the process's id
* until the next launch.
*
* @module @deepseek-ai/dsh-session-telemetry-otel/user-id
* @module @deepseek-ai/dsh-user-id
*/
import { randomUUID } from 'node:crypto'
@@ -64,8 +62,8 @@ function readPersistedId(file: string): AnonymousUserId | undefined {
* narrow create-to-write window can still yield two per-process ids for that
* run; the next launch converges on the persisted one.) Persistence is
* best-effort a write failure (read-only home) still returns a usable id
* for the current run so telemetry is never blocked.
* @param options - Home-location and UUID-generation hooks.
* for the current run so feedback and telemetry are never blocked.
* @param options - home-location and UUID-generation seams.
* @returns the stable per-harness-home anonymous user id.
*/
export function getOrCreateAnonymousUserId(options: AnonymousUserIdOptions = {}): AnonymousUserId {

View File

@@ -0,0 +1,31 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-user-id`.
* @module @deepseek-ai/dsh-user-id/invariant
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-user-id'
/** Cordis companion plugin name. */
export const name = 'user-id-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: the API owns one private memo and one best-effort
* file, with no independent event stream or public mutable relation for a
* companion to compare without creating the identity as a side effect.
*/
const install: InvariantInstaller = () => {}
/**
* Register this package's invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */

View File

@@ -0,0 +1,12 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import InvariantService from '@deepseek-ai/dsh-invariants'
import * as UserIdInvariant from '@deepseek-ai/dsh-user-id/invariant'
describe('invariant companion', () => {
it('registers the package ownership with an empty installer', async () => {
const ctx = new Context()
await ctx.plugin(InvariantService, { enabled: true })
await expect(ctx.plugin(UserIdInvariant).await()).resolves.toBeDefined()
})
})

View File

@@ -5,7 +5,7 @@ import { afterEach, describe, expect, it } from 'vitest'
import {
USER_ID_FILE_NAME,
getOrCreateAnonymousUserId,
} from '../src/user-id.ts'
} from '../src/index.ts'
const dirs: string[] = []

View File

@@ -0,0 +1,21 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../util/brand"
},
{
"path": "../../util/paths"
},
{
"path": "../../support/invariants"
}
]
}