dsh-hams 0.1.3 → 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 (50) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/README.md +4 -0
  3. package/dist/index.js +11335 -0
  4. package/package.json +9 -5
  5. package/src/domain/check.ts +0 -193
  6. package/src/domain/errors.ts +0 -59
  7. package/src/domain/evidence.ts +0 -134
  8. package/src/domain/ids.ts +0 -31
  9. package/src/host/chat-runtime.ts +0 -489
  10. package/src/host/dsh-adapter.ts +0 -867
  11. package/src/host/session-adapter.ts +0 -276
  12. package/src/http/handler.ts +0 -1132
  13. package/src/index.js +0 -53
  14. package/src/service/audit.ts +0 -92
  15. package/src/service/chat.ts +0 -1100
  16. package/src/service/contracts.ts +0 -193
  17. package/src/service/defaults.ts +0 -182
  18. package/src/service/deliverables.ts +0 -240
  19. package/src/service/hams.ts +0 -3103
  20. package/src/service/knowledge.ts +0 -940
  21. package/src/service/member-config.ts +0 -39
  22. package/src/service/members.ts +0 -506
  23. package/src/service/memory.ts +0 -375
  24. package/src/service/permissions.ts +0 -118
  25. package/src/service/project-lifecycle.ts +0 -233
  26. package/src/service/projects.ts +0 -840
  27. package/src/service/receipts.ts +0 -63
  28. package/src/service/templates.ts +0 -140
  29. package/src/service/tick.ts +0 -751
  30. package/src/service/tools.ts +0 -711
  31. package/src/service/types.ts +0 -219
  32. package/src/service/work-item-create.ts +0 -159
  33. package/src/service/workspace-profile.ts +0 -443
  34. package/src/store/backup.ts +0 -239
  35. package/src/store/bootstrap.ts +0 -225
  36. package/src/store/db.ts +0 -179
  37. package/src/store/lock.ts +0 -194
  38. package/src/store/migrations.ts +0 -211
  39. package/src/store/schema/001_p1.sql +0 -846
  40. package/src/store/schema/001_p1.sql.ts +0 -865
  41. package/src/store/schema/002_knowledge.sql +0 -54
  42. package/src/store/schema/002_knowledge.sql.ts +0 -68
  43. package/src/store/schema/003_projects.sql.ts +0 -3
  44. package/src/store/schema/004_team.sql.ts +0 -50
  45. package/src/store/schema/005_target_check.sql.ts +0 -22
  46. package/src/store/schema/006_target_flow.sql.ts +0 -19
  47. package/src/store/schema/007_project_delete.sql.ts +0 -17
  48. package/src/store/schema/008_memory.sql.ts +0 -47
  49. package/src/store/schema/generate-projects.mjs +0 -16
  50. package/src/store/seed.ts +0 -114
@@ -1,233 +0,0 @@
1
- import type { DatabaseSync } from 'node:sqlite'
2
- import type { IdFactory } from '../domain/ids.ts'
3
- import { stateInvalid, denied } from '../domain/errors.ts'
4
- import { insertMetric, type MetricInput, type ProjectRow } from './projects.ts'
5
-
6
- type Row = Record<string, any>
7
- export function projectRow(db: DatabaseSync, id: string): ProjectRow {
8
- const row = db.prepare('SELECT * FROM project WHERE id = ?').get(id) as ProjectRow | undefined
9
- if (!row || row.deleted_at) throw stateInvalid('项目不存在')
10
- return row
11
- }
12
- function human(db: DatabaseSync, id: string): string {
13
- const row = db.prepare('SELECT kind, status FROM member WHERE id = ?').get(id) as Row | undefined
14
- if (!row || row.kind !== 'human' || row.status !== 'active') throw stateInvalid('确认人与验收人必须是在用的人类成员')
15
- return id
16
- }
17
- function text(value: unknown, label: string, max = 4000): string {
18
- if (typeof value !== 'string' || !value.trim() || value.trim().length > max) throw stateInvalid(`${label}不能为空且最多${max}字`)
19
- return value.trim()
20
- }
21
- function time(value: unknown): number {
22
- if (typeof value !== 'number' || !Number.isSafeInteger(value)) throw stateInvalid('时间必须是整数毫秒')
23
- return value
24
- }
25
- export interface PublishTargetInput { projectId: string; goalText: string; metrics?: MetricInput[] }
26
- export function publishTarget(db: DatabaseSync, ids: IdFactory, now: number, actor: string, input: PublishTargetInput) {
27
- const p = projectRow(db, input.projectId)
28
- if (p.status === 'archived') throw stateInvalid('归档项目不能发布目标')
29
- const goal = text(input.goalText, '目标')
30
- if (!Array.isArray(input.metrics ?? []) || (input.metrics?.length ?? 0) > 12) throw stateInvalid('指标必须是最多12项的数组')
31
- const id = ids(), version = p.current_target_version + 1
32
- db.prepare('INSERT INTO project_target_version (id,project_id,version,goal_text,published_by,published_at) VALUES (?,?,?,?,?,?)').run(id,p.id,version,goal,actor,now)
33
- // Omitting metrics copies definitions into NEW metric IDs; historical observations stay bound to their version.
34
- const metrics = input.metrics ?? (db.prepare('SELECT * FROM project_metric WHERE project_id=? AND version_id=(SELECT id FROM project_target_version WHERE project_id=? AND version=?)').all(p.id,p.id,p.current_target_version) as Row[]).map(m => ({ name:m.name,direction:m.direction,targetValue:m.target_value,targetMax:m.target_max,unit:m.unit,windowKind:m.window_kind,definitionText:m.definition_text,sourceKind:m.source_kind,sourceRef:m.source_ref,calculatorKey:m.calculator_key,calculatorParams:JSON.parse(m.calculator_params_json) }))
35
- for (const metric of metrics) insertMetric(db,ids,p.id,id,metric)
36
- db.prepare("UPDATE project SET goal_text=?,current_target_version=?,status=CASE WHEN status='draft' THEN 'active' ELSE status END WHERE id=?").run(goal,version,p.id)
37
- return { targetVersionId:id, version }
38
- }
39
- /** 项目至少有一个阶段与一个目标:新建项目/启动时补齐「默认阶段」。 */
40
- export function ensureDefaultPlan(db:DatabaseSync, ids:IdFactory, now:number, projectId:string) {
41
- const project=projectRow(db,projectId)
42
- const phases=db.prepare('SELECT id FROM project_phase WHERE project_id=? ORDER BY seq').all(projectId) as Row[]
43
- if(phases.length) return { phaseId:String(phases[0].id), created:false }
44
- const phaseId=ids()
45
- db.prepare("INSERT INTO project_phase (id,project_id,seq,name,starts_at,ends_at,status) VALUES (?,?,1,?,?,?, 'active')").run(phaseId,projectId,'默认阶段',now,253402300799000)
46
- db.prepare('INSERT INTO project_phase_target (id,phase_id,metric_id,description,target_value,verifier_human_id,detail) VALUES (?,?,NULL,?,NULL,?,?)').run(ids(),phaseId,'首个目标',project.owner_human_id,'写清这个目标怎么算、怎么验收;之后可以改成真正的阶段目标。')
47
- return { phaseId, created:true }
48
- }
49
-
50
- export function ensureAllDefaultPlans(db:DatabaseSync, ids:IdFactory, now:number) {
51
- const projects=db.prepare("SELECT id FROM project WHERE status IN ('active','paused')").all() as Row[]
52
- let created=0
53
- for(const project of projects) if(ensureDefaultPlan(db,ids,now,String(project.id)).created) created+=1
54
- return { created }
55
- }
56
-
57
- /** 给阶段加一个目标:描述(简略说明)、详情(计算方式与验收要求)、目标值、验收人。 */
58
- export function addPhaseTarget(db:DatabaseSync, ids:IdFactory, input:{projectId:string;phaseId:string;description:string;detail?:string;targetValue?:number|null;metricId?:string;verifierHumanId?:string}) {
59
- const project=projectRow(db,input.projectId)
60
- const phase=db.prepare('SELECT * FROM project_phase WHERE id=? AND project_id=?').get(input.phaseId,input.projectId) as Row|undefined
61
- if(!phase) throw stateInvalid('阶段不属于该项目')
62
- if(['confirmed','done'].includes(String(phase.status))) throw stateInvalid('已确认的阶段不能再加目标')
63
- const count=Number((db.prepare('SELECT COUNT(*) AS n FROM project_phase_target WHERE phase_id=?').get(input.phaseId) as Row).n)
64
- if(count>=30) throw stateInvalid('一个阶段最多 30 个目标')
65
- if(input.metricId){ if(!db.prepare('SELECT id FROM project_metric WHERE id=? AND project_id=?').get(input.metricId,input.projectId)) throw stateInvalid('目标指标必须属于本项目') }
66
- const targetValue=input.targetValue ?? null
67
- if(targetValue!==null && (typeof targetValue!=='number'||!Number.isFinite(targetValue))) throw stateInvalid('目标值必须为有限数字')
68
- const verifier=human(db,input.verifierHumanId??project.owner_human_id)
69
- const id=ids()
70
- db.prepare('INSERT INTO project_phase_target (id,phase_id,metric_id,description,target_value,verifier_human_id,detail) VALUES (?,?,?,?,?,?,?)').run(id,input.phaseId,input.metricId??null,text(input.description,'目标描述'),targetValue,verifier,(input.detail??'').slice(0,2000))
71
- return { targetId:id }
72
- }
73
-
74
- export interface CreatePhaseInput {
75
- projectId:string; name:string; startsAt:number; endsAt:number; confirmerId?:string
76
- targets: {description:string; metricId?:string; targetValue?:number; verifierHumanId:string}[]
77
- checks?:string[]
78
- }
79
- export function createPhase(db:DatabaseSync, ids:IdFactory, actor:string, input:CreatePhaseInput) {
80
- const p=projectRow(db,input.projectId)
81
- const start=time(input.startsAt), end=time(input.endsAt)
82
- if (end < start) throw stateInvalid('阶段结束不能早于开始')
83
- if (!Array.isArray(input.targets) || !input.targets.length || input.targets.length>30) throw stateInvalid('阶段需要1至30项验收目标')
84
- if (input.checks !== undefined && (!Array.isArray(input.checks) || input.checks.length>50)) throw stateInvalid('检查项最多50项')
85
- const id=ids(), seq=Number((db.prepare('SELECT COALESCE(MAX(seq),0)+1 AS n FROM project_phase WHERE project_id=?').get(p.id) as Row).n)
86
- db.prepare('INSERT INTO project_phase (id,project_id,seq,name,starts_at,ends_at,confirmer_id) VALUES (?,?,?,?,?,?,?)').run(id,p.id,seq,text(input.name,'阶段名称',100),start,end,human(db,input.confirmerId??p.owner_human_id))
87
- for(const t of input.targets) {
88
- if(t.metricId && !db.prepare('SELECT id FROM project_metric WHERE id=? AND project_id=?').get(t.metricId,p.id)) throw stateInvalid('阶段指标必须属于本项目')
89
- if(t.targetValue !== undefined && (typeof t.targetValue !== 'number'||!Number.isFinite(t.targetValue))) throw stateInvalid('目标值必须为有限数字')
90
- db.prepare('INSERT INTO project_phase_target (id,phase_id,metric_id,description,target_value,verifier_human_id) VALUES (?,?,?,?,?,?)').run(ids(),id,t.metricId??null,text(t.description,'目标描述'),t.targetValue??null,human(db,t.verifierHumanId))
91
- }
92
- for(const label of input.checks??[]) db.prepare('INSERT INTO project_phase_check (id,phase_id,label) VALUES (?,?,?)').run(ids(),id,text(label,'检查项',200))
93
- return { phaseId:id }
94
- }
95
- export interface UpdatePhaseInput {projectId:string;phaseId:string;status?:string;checkId?:string;checked?:boolean}
96
- function phase(db:DatabaseSync,input:{projectId:string;phaseId:string}):Row {
97
- const row=db.prepare('SELECT * FROM project_phase WHERE id=? AND project_id=?').get(input.phaseId,input.projectId) as Row|undefined
98
- if(!row) throw stateInvalid('阶段不属于该项目')
99
- if(row.status==='done') throw stateInvalid('已确认阶段不可修改')
100
- return row
101
- }
102
- export function updatePhase(db:DatabaseSync,now:number,actor:string,input:UpdatePhaseInput) {
103
- const row=phase(db,input)
104
- if(input.status!==undefined) {
105
- const allowed:Record<string,string[]>={planned:['active'],active:['awaiting_confirm'],awaiting_confirm:['active']}
106
- if(!allowed[row.status]?.includes(input.status)) throw stateInvalid('非法阶段状态转换;日期不自动完成阶段')
107
- db.prepare('UPDATE project_phase SET status=? WHERE id=?').run(input.status,row.id)
108
- }
109
- if(input.checkId!==undefined) {
110
- if(typeof input.checked!=='boolean') throw stateInvalid('勾选状态必须为布尔值')
111
- const result=db.prepare('UPDATE project_phase_check SET checked=?,checked_by=?,checked_at=? WHERE id=? AND phase_id=?').run(input.checked?1:0,actor,now,input.checkId,row.id)
112
- if(!result.changes) throw stateInvalid('检查项不属于本阶段')
113
- }
114
- if(input.status===undefined && input.checkId===undefined) throw stateInvalid('没有阶段修改内容')
115
- return {phaseId:row.id}
116
- }
117
- export function confirmPhase(db:DatabaseSync,now:number,actor:string,input:{projectId:string;phaseId:string}) {
118
- const row=phase(db,input)
119
- if(row.status!=='awaiting_confirm') throw stateInvalid('阶段需先提交人工确认')
120
- if(row.confirmer_id!==actor) throw denied('只有指定确认人能确认阶段')
121
- db.prepare("UPDATE project_phase SET status='done',confirmed_by=?,confirmed_at=? WHERE id=?").run(actor,now,row.id)
122
- return {phaseId:row.id}
123
- }
124
- export interface TrialResult { value:number|null; verdict:'met'|'missed'|'unknown'; complete:boolean; reason?:string; windowStart:number;windowEnd:number;evidenceRef:string|null }
125
- export function tryMetric(db:DatabaseSync,now:number,projectId:string|undefined,metric:MetricInput,period?:{start:number;end:number},metricId?:string):TrialResult {
126
- const end=period?.end??now, start=period?.start??(end-({day:1,week:7,month:30,snapshot:0}[metric.windowKind]??0)*86400000)
127
- let value:number|null=null,evidenceRef:string|null=null,reason:string|undefined
128
- if(!projectId) reason='先创建项目后试算'
129
- else if(!metric.calculatorKey || !metric.sourceKind) reason='缺少计算口径或数据来源'
130
- else if(metric.calculatorKey==='manual_latest' && metric.sourceKind==='manual') {
131
- if(!metric.sourceRef) reason='人工读数需要明确数据来源引用'
132
- else if(metricId) {
133
- const row=db.prepare('SELECT value,source_ref FROM metric_observation WHERE metric_id=? AND observed_at<=? AND window_end<=? AND window_end>=? AND source_ref=? ORDER BY window_end DESC,observed_at DESC,rowid DESC LIMIT 1').get(metricId,end,end,start,metric.sourceRef) as Row|undefined
134
- value=row?.value??null; evidenceRef=row?.source_ref??null
135
- } else reason='尚无已发布指标读数'
136
- } else if(['work_item_count','work_item_completion_rate'].includes(metric.calculatorKey) && metric.sourceKind==='work_item') {
137
- const params=metric.calculatorParams??{}, clauses=['project_id=?','created_at<=?'],args:any[]=[projectId,end]
138
- if(metric.windowKind!=='snapshot') {clauses.push('created_at>=?');args.push(start)}
139
- for(const key of ['domain','status']) if(params[key]) {clauses.push(`${key}=?`);args.push(params[key])}
140
- // Historical snapshots use completion time, not today's terminal status.
141
- const rows=db.prepare(`SELECT completed_at FROM work_item WHERE ${clauses.join(' AND ')}`).all(...args) as Row[]
142
- value=metric.calculatorKey==='work_item_count'?rows.length:rows.length?100*rows.filter(r=>r.completed_at!==null&&r.completed_at<=end).length/rows.length:null
143
- evidenceRef=`hams:project:${projectId}:work_items:${start}:${end}`
144
- } else reason='未知或不匹配的计算方式与数据来源'
145
- if(value===null) reason??='当期缺少可用数据'
146
- let verdict:TrialResult['verdict']='unknown'
147
- if(value!==null && metric.targetValue!==null && metric.targetValue!==undefined) {
148
- const met=metric.direction==='higher'?value>=metric.targetValue:metric.direction==='lower'?value<=metric.targetValue:metric.targetMax!==null&&metric.targetMax!==undefined&&value>=metric.targetValue&&value<=metric.targetMax
149
- verdict=met?'met':'missed'
150
- }
151
- return {value,verdict,complete:value!==null&&!reason,reason,windowStart:start,windowEnd:end,evidenceRef}
152
- }
153
- export interface CreateReviewInput {projectId:string;kind:string;periodStart:number;periodEnd:number;conclusionText?:string;confirmerId?:string}
154
- export function createReview(db:DatabaseSync,ids:IdFactory,now:number,actor:string,input:CreateReviewInput) {
155
- const p=projectRow(db,input.projectId),start=time(input.periodStart),end=time(input.periodEnd)
156
- if(!['weekly','monthly','quarterly','phase'].includes(input.kind)||start>end||end>now) throw stateInvalid('复盘类型或周期不合法(不能冻结未来)')
157
- const target=db.prepare('SELECT id FROM project_target_version WHERE project_id=? AND published_at<=? ORDER BY version DESC LIMIT 1').get(p.id,end) as Row|undefined
158
- if(!target) throw stateInvalid('需要先发布项目目标')
159
- const id=ids()
160
- db.prepare("INSERT INTO project_review (id,project_id,kind,period_start,period_end,target_version_id,status,conclusion_text,confirmer_id) VALUES (?,?,?,?,?,?,'awaiting_confirm',?,?)").run(id,p.id,input.kind,start,end,target.id,input.conclusionText?text(input.conclusionText,'结论'):null,human(db,input.confirmerId??p.owner_human_id))
161
- const metrics=db.prepare('SELECT * FROM project_metric WHERE version_id=?').all(target.id) as Row[]
162
- for(const m of metrics) {
163
- const trial=tryMetric(db,now,p.id,{name:m.name,direction:m.direction,targetValue:m.target_value,targetMax:m.target_max,windowKind:m.window_kind,sourceKind:m.source_kind,sourceRef:m.source_ref,calculatorKey:m.calculator_key,calculatorParams:JSON.parse(m.calculator_params_json)},{start,end},m.id)
164
- db.prepare('INSERT INTO project_review_metric (review_id,metric_id,target_value,actual_value,verdict,evidence_ref) VALUES (?,?,?,?,?,?)').run(id,m.id,m.target_value,trial.value,trial.verdict,trial.evidenceRef)
165
- }
166
- return {reviewId:id,targetVersionId:target.id}
167
- }
168
- export function confirmReview(db:DatabaseSync,now:number,actor:string,input:{projectId:string;reviewId:string}) {
169
- const row=db.prepare('SELECT * FROM project_review WHERE id=? AND project_id=?').get(input.reviewId,input.projectId) as Row|undefined
170
- if(!row||row.status!=='awaiting_confirm') throw stateInvalid('复盘不存在或已确认')
171
- if(row.confirmer_id!==actor) throw denied('只有指定确认人能确认复盘')
172
- db.prepare("UPDATE project_review SET status='confirmed',confirmed_by=?,confirmed_at=? WHERE id=?").run(actor,now,row.id)
173
- return {reviewId:row.id}
174
- }
175
- export interface UpdateTargetInput { projectId:string; targetId:string; description?:string; detail?:string; targetValue?:number|null; verifierHumanId?:string }
176
- /** 编辑阶段目标:描述 / 目标值 / 验收人;已确认的阶段不允许再改。 */
177
- export function updatePhaseTarget(db:DatabaseSync, input:UpdateTargetInput) {
178
- const target=db.prepare('SELECT t.*, p.status AS phase_status FROM project_phase_target t JOIN project_phase p ON p.id=t.phase_id WHERE t.id=? AND p.project_id=?').get(input.targetId,input.projectId) as Row|undefined
179
- if(!target) throw stateInvalid('目标不属于该项目')
180
- if(['confirmed','done'].includes(String(target.phase_status))) throw stateInvalid('已确认的阶段不能再改目标')
181
- const description=input.description===undefined?target.description:text(input.description,'目标描述')
182
- let targetValue=target.target_value
183
- if(input.targetValue!==undefined) {
184
- if(input.targetValue===null) targetValue=null
185
- else if(typeof input.targetValue!=='number'||!Number.isFinite(input.targetValue)) throw stateInvalid('目标值必须为有限数字')
186
- else targetValue=input.targetValue
187
- }
188
- const verifier=input.verifierHumanId===undefined?target.verifier_human_id:human(db,input.verifierHumanId)
189
- const detail=input.detail===undefined?target.detail:(input.detail||'').slice(0,2000)
190
- db.prepare('UPDATE project_phase_target SET description=?,detail=?,target_value=?,verifier_human_id=? WHERE id=?').run(description,detail,targetValue,verifier,target.id)
191
- return { targetId:target.id }
192
- }
193
-
194
- export interface SubmitTargetCheckInput { projectId:string; targetId:string; approverId:string; summary:string; reportUrl?:string; note?:string }
195
- /**
196
- * 项目经理提交一次目标检验 = 申请一次审批:写检验记录(pending)+ 指定审批人。
197
- * 审批通过后目标才算达成;驳回会留下原因。可反复提交,历史都保留。
198
- */
199
- export function submitTargetCheck(db:DatabaseSync, ids:IdFactory, now:number, actor:string, input:SubmitTargetCheckInput) {
200
- const target=db.prepare('SELECT t.id,p.status AS phase_status FROM project_phase_target t JOIN project_phase p ON p.id=t.phase_id WHERE t.id=? AND p.project_id=?').get(input.targetId,input.projectId) as Row|undefined
201
- if(!target) throw stateInvalid('目标不属于该项目')
202
- const url=(input.reportUrl??'').trim()
203
- if(url && !/^https?:\/\//i.test(url)) throw stateInvalid('报告链接必须是 http(s) 地址')
204
- if(url.length>500) throw stateInvalid('报告链接最多 500 字')
205
- const note=(input.note??'').trim()
206
- if(note.length>1000) throw stateInvalid('说明最多 1000 字')
207
- const summary=text(input.summary,'验证摘要',1000)
208
- const approver=human(db,input.approverId)
209
- if(approver===actor) throw stateInvalid('提交人不能自己审批,请指定另一位审批人')
210
- const checkId=ids()
211
- db.prepare("INSERT INTO project_phase_target_check (id,project_id,target_id,submitted_by,submitted_at,result,report_url,note,approver_id,summary,status) VALUES (?,?,?,?,?, 'unknown',?,?,?,?, 'pending')").run(checkId,input.projectId,input.targetId,actor,now,url||null,note,approver,summary)
212
- const approvalId=ids()
213
- db.prepare(`INSERT INTO approval (id, subject_kind, subject_id, subject_revision, execution_epoch, evidence_set_hash, criteria_ref, criteria_json, reason_text, requested_by, requested_channel, approver_id, status, decision_note, expires_at, created_at)
214
- VALUES (?, 'target_change', ?, 0, 0, '', '', '{}', ?, ?, 'ui', ?, 'pending', '', ?, ?)`)
215
- .run(approvalId, checkId, summary, actor, approver, now+72*3_600_000, now)
216
- return { checkId, approvalId }
217
- }
218
-
219
- export function projectLifecycleDetail(db:DatabaseSync,projectId:string) {
220
- const phases=(db.prepare('SELECT * FROM project_phase WHERE project_id=? ORDER BY seq').all(projectId) as Row[]).map(p=>({...p,
221
- targets:(db.prepare('SELECT * FROM project_phase_target WHERE phase_id=? ORDER BY rowid').all(p.id) as Row[]).map(t=>{
222
- const checks=db.prepare("SELECT c.*,m.name AS submitted_by_name,a.name AS approver_name,d.name AS decided_by_name,ap.id AS approval_id,ap.expires_at AS approval_expires_at FROM project_phase_target_check c LEFT JOIN member m ON m.id=c.submitted_by LEFT JOIN member a ON a.id=c.approver_id LEFT JOIN member d ON d.id=c.decided_by LEFT JOIN approval ap ON ap.subject_kind='target_change' AND ap.subject_id=c.id WHERE c.target_id=? ORDER BY c.submitted_at DESC, c.rowid DESC LIMIT 20").all(t.id)
223
- return {...t,checks,achieved:checks.some(c=>c.status==='approved'),pending:checks.some(c=>c.status==='pending'),rejected:checks.length>0&&!checks.some(c=>c.status==='approved')&&!checks.some(c=>c.status==='pending')}
224
- }),
225
- checks:db.prepare('SELECT * FROM project_phase_check WHERE phase_id=? ORDER BY rowid').all(p.id)}))
226
- const reviews=(db.prepare('SELECT * FROM project_review WHERE project_id=? ORDER BY period_end DESC').all(projectId) as Row[]).map(r=>({...r,metrics:db.prepare('SELECT rm.*,m.name,m.direction,m.target_max,m.unit,m.window_kind,m.calculator_key,m.calculator_params_json,m.definition_text FROM project_review_metric rm JOIN project_metric m ON m.id=rm.metric_id WHERE review_id=?').all(r.id)}))
227
- const proposals=db.prepare('SELECT * FROM proposal WHERE project_id=? ORDER BY created_at DESC').all(projectId)
228
- const decisions=db.prepare('SELECT d.* FROM decision d JOIN proposal p ON p.id=d.proposal_id WHERE p.project_id=? ORDER BY d.created_at DESC').all(projectId)
229
- const projectEvents=db.prepare("SELECT * FROM audit_log WHERE subject_kind='project' AND subject_id=? ORDER BY created_at DESC,rowid DESC LIMIT 200").all(projectId) as Row[]
230
- const workEvents=db.prepare('SELECT e.*,w.title FROM work_item_event e JOIN work_item w ON w.id=e.work_item_id WHERE w.project_id=? ORDER BY e.created_at DESC,e.rowid DESC LIMIT 200').all(projectId) as Row[]
231
- const events=[...projectEvents.map(e=>({...e,source_kind:'audit_log'})),...workEvents.map(e=>({...e,source_kind:'work_item_event',action:e.kind,subject_kind:'work_item',subject_id:e.work_item_id,after_json:e.payload_json}))].sort((a,b)=>b.created_at-a.created_at).slice(0,200)
232
- return {phases,reviews,progress_records:{proposals,decisions,events}}
233
- }