dsh-sa-workbench 0.1.30 → 0.1.32

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.
@@ -1,3 +1,4 @@
1
+ import {knowledgeCatalog} from './knowledge-model.mjs'
1
2
  /**
2
3
  * dsh-pm-ui — host half.
3
4
  *
@@ -110,6 +111,7 @@ function statusFor(code) {
110
111
  return 400
111
112
  case 'stage_incomplete':
112
113
  case 'stale_artifact':
114
+ case 'conflict':
113
115
  case 'stale_requirement':
114
116
  case 'archived':
115
117
  case 'corrupt_state':
@@ -211,6 +213,21 @@ export function apply(ctx) {
211
213
 
212
214
  const setup = pm.getSettings()
213
215
  if ((setup.setupRequired || setup.restartRequired) && req.method !== 'GET' && route !== '/settings') return fail(res,409,'请先保存目录配置并完整重启 DSH。','setup_required')
216
+ if (route === '/knowledge-search' && req.method === 'GET') return json(res,200,{ok:true,...await pm.knowledgeWorkflow.search(url.searchParams.get('query') || '')})
217
+ if (route === '/knowledge-candidates' && req.method === 'GET') return json(res,200,{ok:true,...await pm.knowledgeWorkflow.load(url.searchParams.get('id') || '')})
218
+ if (route === '/knowledge-candidate-preview' && req.method === 'POST') return json(res,200,{ok:true,...await pm.knowledgeWorkflow.preview(await readBody(req))})
219
+ if (route === '/knowledge-candidate-approve' && req.method === 'POST') return json(res,200,{ok:true,receipt:await pm.knowledgeWorkflow.approve(await readBody(req))})
220
+ if (route === '/deliveries' && req.method === 'GET') return json(res,200,{ok:true,...await pm.deliveryLibrary.list(url.searchParams.get('query') || '')})
221
+ if (route === '/delivery' && req.method === 'GET') return json(res,200,{ok:true,...await pm.deliveryLibrary.detail(url.searchParams.get('id') || '')})
222
+ if (route === '/delivery-file' && req.method === 'GET') return json(res,200,{ok:true,...await pm.deliveryLibrary.read(url.searchParams.get('id') || '',url.searchParams.get('path') || '')})
223
+ if (route === '/delivery-capture' && req.method === 'POST') return json(res,200,{ok:true,entry:await pm.deliveryLibrary.capture(await readBody(req))})
224
+ if (route === '/delivery-assistant' && req.method === 'POST') return json(res,200,{ok:true,...await pm.deliveryLibrary.assistant((await readBody(req)).id)})
225
+ if (route === '/delivery-knowledge-candidates' && req.method === 'GET') return json(res,200,{ok:true,...await pm.deliveryLibrary.workflow.load(url.searchParams.get('id') || '')})
226
+ if (route === '/delivery-knowledge-candidate-preview' && req.method === 'POST') return json(res,200,{ok:true,...await pm.deliveryLibrary.workflow.preview(await readBody(req))})
227
+ if (route === '/delivery-knowledge-candidate-approve' && req.method === 'POST') return json(res,200,{ok:true,receipt:await pm.deliveryLibrary.workflow.approve(await readBody(req))})
228
+ if (route === '/product-context' && req.method === 'GET') return json(res,200,{ok:true,...await pm.productContext.read()})
229
+ if (route === '/product-context' && req.method === 'POST') return json(res,200,{ok:true,...await pm.productContext.save(await readBody(req))})
230
+ if (route === '/product-document' && req.method === 'GET') return json(res,200,{ok:true,...await pm.productContext.document(url.searchParams.get('path') || '')})
214
231
  if (route === '/directories' && req.method === 'GET') {
215
232
  return json(res, 200, { ok: true, ...await listDirectories(url.searchParams.get('path') ?? '') })
216
233
  }
@@ -254,7 +271,8 @@ export function apply(ctx) {
254
271
  }
255
272
 
256
273
  if (route === '/state' && req.method === 'GET') {
257
- const { requirements } = await pm.viewAll()
274
+ const view = await pm.viewAll()
275
+ const requirements = await pm.deliveryLibrary.withDeliveryFacts(view.requirements)
258
276
  return json(res, 200, {
259
277
  ok: true,
260
278
  workspace: {
@@ -367,9 +385,11 @@ export function apply(ctx) {
367
385
  return json(res, 200, { ok: true, id: record.id, title: record.title })
368
386
  }
369
387
 
388
+ if (route === '/requirement-delete-preview' && req.method === 'GET') return json(res,200,{ok:true,...await pm.requirementDeletion.preview(String(url.searchParams.get('id') || ''))})
389
+
370
390
  if (route === '/requirement' && req.method === 'DELETE') {
371
391
  const body = await readBody(req)
372
- const result = await pm.deleteRequirement(String(body.id ?? ''))
392
+ const result = await pm.deleteRequirement(String(body.id ?? ''), body)
373
393
  return json(res, 200, { ok: true, ...result })
374
394
  }
375
395
 
@@ -479,7 +499,7 @@ export function apply(ctx) {
479
499
 
480
500
  // 全局知识库:列表 + 单文件读取(只读;修改走会话或直接编辑文件)
481
501
  if (route === '/knowledge' && req.method === 'GET') {
482
- const files = await pm.listKnowledge()
502
+ const files = await knowledgeCatalog(pm)
483
503
  return json(res, 200, { ok: true, root: pm.knowledgeDir, files })
484
504
  }
485
505
 
@@ -0,0 +1,36 @@
1
+ // Deliberately restricted front matter: plain scalar fields and string lists only.
2
+ // Never infer business confirmation or domains from prose or filenames.
3
+ export function knowledgeRecord(file, text='') {
4
+ const fields={}, match=String(text).match(/^\uFEFF?---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)
5
+ const scalar=s=>{
6
+ const value=String(s).trim()
7
+ if(value.startsWith('"')){try{return String(JSON.parse(value))}catch{}}
8
+ return value.replace(/^(['"])(.*)\1$/,'$2')
9
+ }
10
+ if(match){let key='';for(const line of match[1].split(/\r?\n/)){
11
+ const pair=line.match(/^([a-z_]+):\s*(.*?)\s*$/)
12
+ if(pair){key=pair[1];fields[key]=pair[2] ? scalar(pair[2]) : []}
13
+ else {const item=line.match(/^\s+-\s+(.+)$/);if(item && Array.isArray(fields[key]))fields[key].push(scalar(item[1]))}
14
+ }}
15
+ const list=v=>Array.isArray(v)?v.map(scalar):typeof v==='string'?v.replace(/^\[|\]$/g,'').split(/[,,]/).map(scalar).filter(Boolean):[]
16
+ const body=match?text.slice(match[0].length):String(text)
17
+ return {...file,title:scalar(fields.title || body.match(/^#\s+(.+)$/m)?.[1] || file.path.replace(/.*\//,'').replace(/\.[^.]+$/,'')),
18
+ kind:scalar(fields.type || '未分类'),domains:list(fields.domains),status:scalar(fields.status || '未标注'),
19
+ summary:scalar(fields.summary || body.replace(/^#+\s+.*$/gm,'').replace(/[`*_>#\[\]]/g,'').trim().slice(0,160)),
20
+ source:scalar(fields.source || ''),owner:scalar(fields.owner || ''),version:scalar(fields.version || ''),
21
+ scope:scalar(fields.scope || ''),reviewedBy:scalar(fields.reviewed_by || ''),
22
+ sourceDeliveryId:scalar(fields.source_delivery_id || ''),sourceRequirementId:scalar(fields.source_requirement_id || ''),
23
+ related:list(fields.related),searchText:body.slice(0,262144),metadata:!!match}
24
+ }
25
+
26
+ export async function knowledgeCatalog(pm){
27
+ const files=await pm.listKnowledge(), records=[]
28
+ let budget=4*1024*1024
29
+ // Sequential, bounded reads avoid unbounded I/O on a large knowledge directory.
30
+ for(const file of files){
31
+ if(file.bytes>262144 || file.bytes>budget){records.push({...knowledgeRecord(file),previewLimited:true});continue}
32
+ try{const content=await pm.readKnowledge(file.path) || '';budget-=Math.max(file.bytes,content.length);records.push(knowledgeRecord(file,content))}
33
+ catch{records.push({...knowledgeRecord(file),metadataError:true})}
34
+ }
35
+ return records
36
+ }
@@ -1 +1 @@
1
- {"type":"module","version":"0.1.30"}
1
+ {"type":"module","version":"0.1.32"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-sa-workbench",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "description": "SA 工作台:需求整理、方案、PRD、原型和交付归档,一个插件统一安装与更新。",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -71,4 +71,4 @@
71
71
  "registry": "https://registry.npmjs.org/",
72
72
  "tag": "latest"
73
73
  }
74
- }
74
+ }