dsh-hams 0.1.2 → 0.1.4

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 (49) hide show
  1. package/README.md +4 -0
  2. package/dist/index.js +11335 -0
  3. package/package.json +9 -5
  4. package/src/domain/check.ts +0 -193
  5. package/src/domain/errors.ts +0 -59
  6. package/src/domain/evidence.ts +0 -134
  7. package/src/domain/ids.ts +0 -31
  8. package/src/host/chat-runtime.ts +0 -489
  9. package/src/host/dsh-adapter.ts +0 -867
  10. package/src/host/session-adapter.ts +0 -276
  11. package/src/http/handler.ts +0 -1132
  12. package/src/index.js +0 -53
  13. package/src/service/audit.ts +0 -92
  14. package/src/service/chat.ts +0 -1100
  15. package/src/service/contracts.ts +0 -193
  16. package/src/service/defaults.ts +0 -182
  17. package/src/service/deliverables.ts +0 -240
  18. package/src/service/hams.ts +0 -3103
  19. package/src/service/knowledge.ts +0 -940
  20. package/src/service/member-config.ts +0 -39
  21. package/src/service/members.ts +0 -506
  22. package/src/service/memory.ts +0 -375
  23. package/src/service/permissions.ts +0 -118
  24. package/src/service/project-lifecycle.ts +0 -233
  25. package/src/service/projects.ts +0 -840
  26. package/src/service/receipts.ts +0 -63
  27. package/src/service/templates.ts +0 -140
  28. package/src/service/tick.ts +0 -751
  29. package/src/service/tools.ts +0 -711
  30. package/src/service/types.ts +0 -219
  31. package/src/service/work-item-create.ts +0 -159
  32. package/src/service/workspace-profile.ts +0 -443
  33. package/src/store/backup.ts +0 -239
  34. package/src/store/bootstrap.ts +0 -225
  35. package/src/store/db.ts +0 -179
  36. package/src/store/lock.ts +0 -194
  37. package/src/store/migrations.ts +0 -211
  38. package/src/store/schema/001_p1.sql +0 -846
  39. package/src/store/schema/001_p1.sql.ts +0 -865
  40. package/src/store/schema/002_knowledge.sql +0 -54
  41. package/src/store/schema/002_knowledge.sql.ts +0 -68
  42. package/src/store/schema/003_projects.sql.ts +0 -3
  43. package/src/store/schema/004_team.sql.ts +0 -50
  44. package/src/store/schema/005_target_check.sql.ts +0 -22
  45. package/src/store/schema/006_target_flow.sql.ts +0 -19
  46. package/src/store/schema/007_project_delete.sql.ts +0 -17
  47. package/src/store/schema/008_memory.sql.ts +0 -47
  48. package/src/store/schema/generate-projects.mjs +0 -16
  49. package/src/store/seed.ts +0 -114
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-hams",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "HAMS:DeepSeek Harness 内的人机工作台插件(项目 / 任务 / 人机成员 / 知识 / 记忆)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,12 +32,13 @@
32
32
  "schema:split": "node scripts/split-schema.mjs",
33
33
  "schema:check": "node scripts/split-schema.mjs --check",
34
34
  "store:init": "node scripts/init-store.mjs",
35
- "store:backup": "node scripts/backup-store.mjs"
35
+ "store:backup": "node scripts/backup-store.mjs",
36
+ "build": "esbuild src/index.js --bundle --platform=node --format=esm --target=node24 --outfile=dist/index.js --external:@deepseek-ai/*"
36
37
  },
37
- "main": "./src/index.js",
38
+ "main": "./dist/index.js",
38
39
  "exports": {
39
40
  ".": {
40
- "default": "./src/index.js"
41
+ "default": "./dist/index.js"
41
42
  },
42
43
  "./client": {
43
44
  "default": "./client/index.js"
@@ -46,7 +47,7 @@
46
47
  "./package.json": "./package.json"
47
48
  },
48
49
  "files": [
49
- "src",
50
+ "dist",
50
51
  "client/index.js",
51
52
  "cordis.patch.yml",
52
53
  "README.md",
@@ -72,5 +73,8 @@
72
73
  "@deepseek-ai/cordis": "~4.1.0",
73
74
  "@deepseek-ai/dsh-tools": "*",
74
75
  "@deepseek-ai/schemastery": "*"
76
+ },
77
+ "devDependencies": {
78
+ "esbuild": "^0.28.2"
75
79
  }
76
80
  }
@@ -1,193 +0,0 @@
1
- /**
2
- * 复查时点与模板策略解析。
3
- *
4
- * 三种复查节奏(backend-design §4.3 / 画板 ③):
5
- * - `next_day_at`:次日当地 `HH:mm`(默认 09:00);
6
- * - `after_hours`:N 小时后;
7
- * - `next_working_day`:下一工作日当地 `HH:mm`。
8
- *
9
- * 时区换算不引依赖:用 `Intl.DateTimeFormat` 取某时区的挂钟分量,再用
10
- * 「把挂钟当 UTC 求偏移、迭代两次」的方式把挂钟时间转回 UTC 瞬时。跨 DST 的
11
- * 边界会落在 ±1 小时以内,对「次日 09:00 复查」这种粒度足够;真需要分钟级
12
- * 精确的场合应改用带时区库,这一点写在这里而不是偷偷假设。
13
- */
14
- import { stateInvalid } from './errors.ts'
15
-
16
- export type CheckMode = 'next_day_at' | 'after_hours' | 'next_working_day'
17
-
18
- export interface CheckPolicy {
19
- mode: CheckMode
20
- time: string
21
- tz: string
22
- hours: number
23
- maxRechecks: number | null
24
- }
25
-
26
- export interface ReviewPolicy {
27
- /** `owner` 或 `position:<id>`。 */
28
- approver: string
29
- expiresHours: number
30
- autoClose: boolean
31
- }
32
-
33
- const DEFAULT_TZ = 'Asia/Shanghai'
34
-
35
- function two(n: number): string {
36
- return String(n).padStart(2, '0')
37
- }
38
-
39
- function parseClock(time: string): { hour: number; minute: number } {
40
- const match = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(time.trim())
41
- if (!match) throw stateInvalid(`时间格式必须是 HH:mm,收到 ${time}`)
42
- return { hour: Number(match[1]), minute: Number(match[2]) }
43
- }
44
-
45
- function zoneParts(ms: number, timeZone: string): {
46
- year: number
47
- month: number
48
- day: number
49
- hour: number
50
- minute: number
51
- second: number
52
- } {
53
- const formatter = new Intl.DateTimeFormat('en-US', {
54
- timeZone,
55
- year: 'numeric',
56
- month: '2-digit',
57
- day: '2-digit',
58
- hour: '2-digit',
59
- minute: '2-digit',
60
- second: '2-digit',
61
- hour12: false,
62
- })
63
- const parts = formatter.formatToParts(new Date(ms))
64
- const pick = (type: string): number => Number(parts.find((part) => part.type === type)?.value ?? '0')
65
- return {
66
- year: pick('year'),
67
- month: pick('month'),
68
- day: pick('day'),
69
- hour: pick('hour') % 24,
70
- minute: pick('minute'),
71
- second: pick('second'),
72
- }
73
- }
74
-
75
- function offsetAt(ms: number, timeZone: string): number {
76
- const parts = zoneParts(ms, timeZone)
77
- const wallAsUtc = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second)
78
- return wallAsUtc - Math.floor(ms / 1000) * 1000
79
- }
80
-
81
- /** 把某时区的挂钟时间转成 UTC 毫秒。 */
82
- export function wallToUtc(
83
- timeZone: string,
84
- year: number,
85
- month: number,
86
- day: number,
87
- hour: number,
88
- minute: number,
89
- ): number {
90
- const guess = Date.UTC(year, month - 1, day, hour, minute)
91
- let candidate = guess - offsetAt(guess, timeZone)
92
- // DST 切换附近,候选时刻的偏移可能和 guess 处不同;只有挂钟分量对不上时才再校准一次。
93
- const local = zoneParts(candidate, timeZone)
94
- if (local.hour !== hour || local.minute !== minute) {
95
- candidate = guess - offsetAt(candidate, timeZone)
96
- }
97
- return candidate
98
- }
99
-
100
- function normalizeDate(year: number, month: number, day: number): { year: number; month: number; day: number } {
101
- const date = new Date(Date.UTC(year, month - 1, day))
102
- return { year: date.getUTCFullYear(), month: date.getUTCMonth() + 1, day: date.getUTCDate() }
103
- }
104
-
105
- function weekdayOf(year: number, month: number, day: number): number {
106
- return new Date(Date.UTC(year, month - 1, day)).getUTCDay()
107
- }
108
-
109
- export function parseCheckPolicy(json: string | null | undefined): CheckPolicy {
110
- const fallback: CheckPolicy = {
111
- mode: 'after_hours',
112
- time: '09:00',
113
- tz: DEFAULT_TZ,
114
- hours: 24,
115
- maxRechecks: null,
116
- }
117
- if (!json || !json.trim()) return fallback
118
- let raw: unknown
119
- try {
120
- raw = JSON.parse(json)
121
- } catch {
122
- throw stateInvalid(`check_policy_json 不是合法 JSON:${json}`)
123
- }
124
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return fallback
125
- const item = raw as Record<string, unknown>
126
- const mode = typeof item.mode === 'string' ? (item.mode as CheckMode) : fallback.mode
127
- if (!['next_day_at', 'after_hours', 'next_working_day'].includes(mode)) {
128
- throw stateInvalid(`未知的复查节奏:${String(item.mode)}`)
129
- }
130
- const tz = typeof item.tz === 'string' && item.tz.trim() ? item.tz.trim() : DEFAULT_TZ
131
- const time = typeof item.time === 'string' && item.time.trim() ? item.time.trim() : fallback.time
132
- parseClock(time)
133
- try {
134
- new Intl.DateTimeFormat('en-US', { timeZone: tz })
135
- } catch {
136
- throw stateInvalid(`未知时区:${tz}`)
137
- }
138
- const hours = item.hours === undefined ? fallback.hours : Number(item.hours)
139
- if (!Number.isFinite(hours) || hours <= 0) throw stateInvalid(`复查间隔小时数必须是正数:${String(item.hours)}`)
140
- const maxRaw = item.max_rechecks ?? item.maxRechecks
141
- const maxRechecks =
142
- maxRaw === undefined || maxRaw === null ? null : Number.isInteger(Number(maxRaw)) && Number(maxRaw) >= 0 ? Number(maxRaw) : null
143
- return { mode, time, tz, hours, maxRechecks }
144
- }
145
-
146
- export function nextCheckAt(policy: CheckPolicy, fromMs: number): number {
147
- if (policy.mode === 'after_hours') return fromMs + policy.hours * 3_600_000
148
-
149
- const { hour, minute } = parseClock(policy.time)
150
- const local = zoneParts(fromMs, policy.tz)
151
-
152
- if (policy.mode === 'next_day_at') {
153
- const nextDay = normalizeDate(local.year, local.month, local.day + 1)
154
- return wallToUtc(policy.tz, nextDay.year, nextDay.month, nextDay.day, hour, minute)
155
- }
156
-
157
- for (let offset = 1; offset <= 8; offset += 1) {
158
- const candidate = normalizeDate(local.year, local.month, local.day + offset)
159
- const weekday = weekdayOf(candidate.year, candidate.month, candidate.day)
160
- if (weekday === 0 || weekday === 6) continue
161
- return wallToUtc(policy.tz, candidate.year, candidate.month, candidate.day, hour, minute)
162
- }
163
- throw stateInvalid('无法计算下一工作日:日期滚动超过 8 天')
164
- }
165
-
166
- export function parseReviewPolicy(json: string | null | undefined): ReviewPolicy {
167
- const fallback: ReviewPolicy = { approver: 'owner', expiresHours: 24, autoClose: false }
168
- if (!json || !json.trim()) return fallback
169
- let raw: unknown
170
- try {
171
- raw = JSON.parse(json)
172
- } catch {
173
- throw stateInvalid(`review_policy_json 不是合法 JSON:${json}`)
174
- }
175
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return fallback
176
- const item = raw as Record<string, unknown>
177
- const approver = typeof item.approver === 'string' && item.approver.trim() ? item.approver.trim() : fallback.approver
178
- if (approver !== 'owner' && !/^position:.+$/.test(approver)) {
179
- throw stateInvalid(`review_policy.approver 只支持 owner 或 position:<id>,收到 ${approver}`)
180
- }
181
- const expiresRaw = item.expires_hours ?? item.expiresHours ?? fallback.expiresHours
182
- const expiresHours = Number(expiresRaw)
183
- if (!Number.isFinite(expiresHours) || expiresHours <= 0) {
184
- throw stateInvalid(`review_policy.expires_hours 必须是正数:${String(expiresRaw)}`)
185
- }
186
- const autoClose = item.auto_close === true || item.autoClose === true
187
- return { approver, expiresHours, autoClose }
188
- }
189
-
190
- export function formatClock(time: string): string {
191
- const { hour, minute } = parseClock(time)
192
- return `${two(hour)}:${two(minute)}`
193
- }
@@ -1,59 +0,0 @@
1
- /**
2
- * 错误码:与 backend-design §9 的 15 类错误一一对应。
3
- *
4
- * 这 15 个码是界面文案的分支依据,**不要随手新增**——新增一个码就意味着界面要
5
- * 多一个分支说明。内部配置/实现错误尽量复用到语义最近的码,并在 message 里写清,
6
- * 例如「模板的复查策略无法解析」用 STATE_INVALID 而不是造一个 CONFIG_INVALID。
7
- */
8
- export const HAMS_ERROR_CODES = [
9
- 'VERSION_CONFLICT',
10
- 'EVIDENCE_CHANGED',
11
- 'APPROVAL_EXPIRED',
12
- 'NOT_APPROVER',
13
- 'POLICY_DENIED',
14
- 'STATE_INVALID',
15
- 'EPOCH_STALE',
16
- 'APPROVAL_REQUIRED',
17
- 'EVIDENCE_INCOMPLETE',
18
- 'LEASE_LOST',
19
- 'IDEMPOTENCY_MISMATCH',
20
- 'CURSOR_EXPIRED',
21
- 'DATA_STALE',
22
- 'EXTERNAL_RESULT_UNKNOWN',
23
- 'HOST_CAPABILITY_MISSING',
24
- ] as const
25
-
26
- export type HamsErrorCode = (typeof HAMS_ERROR_CODES)[number]
27
-
28
- export class HamsError extends Error {
29
- code: HamsErrorCode
30
- nextStep: string | undefined
31
- detail: unknown
32
-
33
- constructor(code: HamsErrorCode, message: string, options: { nextStep?: string; detail?: unknown } = {}) {
34
- super(message)
35
- this.name = 'HamsError'
36
- this.code = code
37
- this.nextStep = options.nextStep
38
- this.detail = options.detail
39
- }
40
- }
41
-
42
- export function isHamsError(error: unknown): error is HamsError {
43
- return error instanceof HamsError
44
- }
45
-
46
- export function denied(message: string, nextStep?: string): HamsError {
47
- return new HamsError('POLICY_DENIED', message, { nextStep })
48
- }
49
-
50
- export function stateInvalid(message: string, nextStep?: string): HamsError {
51
- return new HamsError('STATE_INVALID', message, { nextStep })
52
- }
53
-
54
- export function evidenceIncomplete(message: string, detail?: unknown): HamsError {
55
- return new HamsError('EVIDENCE_INCOMPLETE', message, {
56
- detail,
57
- nextStep: '补齐缺的证据后重新申请',
58
- })
59
- }
@@ -1,134 +0,0 @@
1
- /**
2
- * 证据集与关单判据。
3
- *
4
- * 两条规则来自 backend-design §4.6 / §6:
5
- * 1. `evidence_set_hash` 只覆盖 `status='active'` 的证据,按 id 固定排序拼接后取 sha256;
6
- * id 是 ULID,所以「固定顺序」= 追加顺序,任何人重算都能得到同一个值。
7
- * 2. 关单需要的证据种类与最低条数写在 `work_template_version.required_evidence_json`,
8
- * 形如 `[{kind:'reading',min_count:2,max_age_hours:24}]`。缺一项就是 EVIDENCE_INCOMPLETE,
9
- * 不靠人肉记忆。
10
- *
11
- * 已知缺口(记在 progress §9):证据行没有数值字段,所以「连续两次读数 ≥ 80%」这类
12
- * 数值判据目前只能靠具名验证器从 `ref` 指向的外部内容里读,或改用 metric_observation。
13
- * 本文件只做「种类 / 条数 / 时效」这层可判定的部分,并在 requestClosure 里对未注册的
14
- * 验证器**失败关闭**(HOST_CAPABILITY_MISSING),不假装已经校验过。
15
- */
16
- import { createHash } from 'node:crypto'
17
-
18
- export interface EvidenceSnapshotRow {
19
- id: string
20
- kind: string
21
- contentHash: string
22
- observedAt: number
23
- status: string
24
- }
25
-
26
- export interface EvidenceRequirement {
27
- kind: string
28
- minCount: number
29
- maxAgeHours: number | null
30
- }
31
-
32
- export interface RequirementMissing {
33
- kind: string
34
- need: number
35
- have: number
36
- reason: 'count' | 'stale'
37
- }
38
-
39
- export interface RequirementCheck {
40
- satisfied: boolean
41
- missing: RequirementMissing[]
42
- used: number
43
- totalActive: number
44
- }
45
-
46
- export function evidenceSetHash(rows: EvidenceSnapshotRow[]): string {
47
- const active = rows
48
- .filter((row) => row.status === 'active')
49
- .map((row) => `${row.id}:${row.kind}:${row.contentHash}:${row.observedAt}`)
50
- .sort()
51
- return createHash('sha256').update(active.join('\n')).digest('hex')
52
- }
53
-
54
- function toRequirement(raw: unknown): EvidenceRequirement | null {
55
- if (!raw || typeof raw !== 'object') return null
56
- const item = raw as Record<string, unknown>
57
- const kind = typeof item.kind === 'string' ? item.kind.trim() : ''
58
- if (!kind) return null
59
- const minCountRaw = item.min_count ?? item.minCount ?? 1
60
- const maxAgeRaw = item.max_age_hours ?? item.maxAgeHours ?? null
61
- const minCount = Number(minCountRaw)
62
- const maxAgeHours = maxAgeRaw === null || maxAgeRaw === undefined ? null : Number(maxAgeRaw)
63
- if (!Number.isInteger(minCount) || minCount < 1) {
64
- throw new Error(`required_evidence 的 min_count 必须是 ≥1 的整数:${JSON.stringify(raw)}`)
65
- }
66
- if (maxAgeHours !== null && (!Number.isFinite(maxAgeHours) || maxAgeHours <= 0)) {
67
- throw new Error(`required_evidence 的 max_age_hours 必须是正数:${JSON.stringify(raw)}`)
68
- }
69
- return { kind, minCount, maxAgeHours }
70
- }
71
-
72
- export function parseEvidenceRequirements(json: string | null | undefined): EvidenceRequirement[] {
73
- if (!json || !json.trim()) return []
74
- let parsed: unknown
75
- try {
76
- parsed = JSON.parse(json)
77
- } catch {
78
- throw new Error(`required_evidence_json 不是合法 JSON:${json}`)
79
- }
80
- if (!Array.isArray(parsed)) throw new Error('required_evidence_json 必须是数组')
81
- return parsed.map(toRequirement).filter((item): item is EvidenceRequirement => item !== null)
82
- }
83
-
84
- export function evaluateRequirements(
85
- rows: EvidenceSnapshotRow[],
86
- requirements: EvidenceRequirement[],
87
- nowMs: number,
88
- ): RequirementCheck {
89
- const active = rows.filter((row) => row.status === 'active')
90
- const missing: RequirementMissing[] = []
91
- let used = 0
92
-
93
- for (const requirement of requirements) {
94
- const ofKind = active.filter((row) => row.kind === requirement.kind)
95
- const fresh =
96
- requirement.maxAgeHours === null
97
- ? ofKind
98
- : ofKind.filter((row) => nowMs - row.observedAt <= requirement.maxAgeHours! * 3_600_000)
99
- if (fresh.length >= requirement.minCount) {
100
- used += requirement.minCount
101
- continue
102
- }
103
- missing.push({
104
- kind: requirement.kind,
105
- need: requirement.minCount,
106
- have: fresh.length,
107
- reason: fresh.length < ofKind.length ? 'stale' : 'count',
108
- })
109
- }
110
-
111
- return { satisfied: missing.length === 0, missing, used, totalActive: active.length }
112
- }
113
-
114
- /** 判据快照:写进 approval.criteria_json,让「批的是哪一版规则」可回看。 */
115
- export interface CriteriaSnapshot {
116
- templateVersionId: string | null
117
- requirements: EvidenceRequirement[]
118
- result: RequirementCheck
119
- evidenceSetHash: string
120
- }
121
-
122
- export function buildCriteriaSnapshot(
123
- templateVersionId: string | null,
124
- requirements: EvidenceRequirement[],
125
- rows: EvidenceSnapshotRow[],
126
- nowMs: number,
127
- ): CriteriaSnapshot {
128
- return {
129
- templateVersionId,
130
- requirements,
131
- result: evaluateRequirements(rows, requirements, nowMs),
132
- evidenceSetHash: evidenceSetHash(rows),
133
- }
134
- }
package/src/domain/ids.ts DELETED
@@ -1,31 +0,0 @@
1
- /**
2
- * 主键:ULID 文本(26 位 Crockford Base32,前 10 位毫秒时间戳)。
3
- *
4
- * 没有引入依赖:ULID 的规范只有两段——时间前缀 + 80 位随机数。这里不做同毫秒单调
5
- * 递增的严格实现(同一毫秒内靠随机位去重),因为所有插入都发生在单写者事务里,
6
- * 冲突由主键约束兜底。
7
- */
8
- const ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
9
-
10
- export type IdFactory = () => string
11
-
12
- export function ulidAt(nowMs: number, random: () => number = Math.random): string {
13
- if (!Number.isFinite(nowMs) || nowMs < 0) throw new Error(`非法时间戳:${nowMs}`)
14
- let timePart = ''
15
- let rest = Math.floor(nowMs)
16
- for (let i = 0; i < 10; i += 1) {
17
- timePart = ENCODING[rest % 32] + timePart
18
- rest = Math.floor(rest / 32)
19
- }
20
- let randomPart = ''
21
- for (let i = 0; i < 16; i += 1) {
22
- randomPart += ENCODING[Math.floor(random() * 32)]
23
- }
24
- return timePart + randomPart
25
- }
26
-
27
- export function createIdFactory(now: () => number, random: () => number = Math.random): IdFactory {
28
- return () => ulidAt(now(), random)
29
- }
30
-
31
- export const ULID_PATTERN = /^[0-9A-HJKMNP-TV-Z]{26}$/