@arcfold/dsh 0.0.0-stage → 0.4.0

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 (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +8 -2
  3. package/THIRD_PARTY_NOTICES.md +11 -0
  4. package/cordis.patch.yml +138 -0
  5. package/package.json +70 -4
  6. package/patches/deepseek-0.2.2-rc.1-transport.json +65 -0
  7. package/patches/deepseek-0.2.2-rc.1-transport.patch +61 -0
  8. package/presets/arcfold.patch.yml +110 -0
  9. package/runtime/README.md +19 -0
  10. package/runtime/bootstrap.json +15 -0
  11. package/runtime/desktop-python.tar.gz +0 -0
  12. package/runtime/host/package.json +11 -0
  13. package/runtime/host/pnpm-workspace.yaml +9 -0
  14. package/runtime/initial-policy.json +94 -0
  15. package/runtime/profile/pnpm-workspace.yaml +7 -0
  16. package/scripts/exact-package-patch.mjs +106 -0
  17. package/scripts/patch-deepseek-transport.mjs +14 -0
  18. package/src/agent.mjs +56 -0
  19. package/src/browse-scope.mjs +14 -0
  20. package/src/budget.mjs +369 -0
  21. package/src/business.mjs +334 -0
  22. package/src/context-material.mjs +119 -0
  23. package/src/deepseek-budget-plugin.mjs +58 -0
  24. package/src/deepseek-provider.mjs +57 -0
  25. package/src/deepseek-receipt.mjs +97 -0
  26. package/src/deepseek-transport.mjs +97 -0
  27. package/src/desktop/adapter.mjs +16 -0
  28. package/src/desktop/bootstrap.mjs +84 -0
  29. package/src/desktop/configuration.mjs +117 -0
  30. package/src/desktop/provider.mjs +46 -0
  31. package/src/desktop/python-runtime.mjs +105 -0
  32. package/src/desktop/setup-service.mjs +81 -0
  33. package/src/desktop/vendor/LICENSE.deepseek +21 -0
  34. package/src/desktop/vendor/NOTICE.md +5 -0
  35. package/src/desktop/vendor/deepseek-0.2.2-rc.1.mjs +2919 -0
  36. package/src/evidence-ownership.mjs +51 -0
  37. package/src/evidence.mjs +35 -0
  38. package/src/goal-context.mjs +52 -0
  39. package/src/goal-execution-tool.mjs +74 -0
  40. package/src/goal-execution.mjs +333 -0
  41. package/src/goal-tool.mjs +56 -0
  42. package/src/goal-work-control.mjs +63 -0
  43. package/src/goal-work.mjs +257 -0
  44. package/src/goals.mjs +578 -0
  45. package/src/importer.mjs +298 -0
  46. package/src/index.mjs +53 -0
  47. package/src/learning-contracts.mjs +220 -0
  48. package/src/learning-defaults.mjs +98 -0
  49. package/src/learning-goals.mjs +165 -0
  50. package/src/learning-plugin.mjs +28 -0
  51. package/src/learning-runner.mjs +275 -0
  52. package/src/learning.mjs +534 -0
  53. package/src/legacy-export.mjs +332 -0
  54. package/src/legacy-import-source.mjs +356 -0
  55. package/src/metered-provider-host.mjs +101 -0
  56. package/src/metered-transport.mjs +435 -0
  57. package/src/method-context.mjs +57 -0
  58. package/src/method-lifecycle.mjs +34 -0
  59. package/src/method-tool.mjs +61 -0
  60. package/src/methods.mjs +425 -0
  61. package/src/model-selection.mjs +41 -0
  62. package/src/native-context-call.mjs +88 -0
  63. package/src/native-continuation.mjs +217 -0
  64. package/src/native-request-authority.mjs +90 -0
  65. package/src/openai-models.mjs +4 -0
  66. package/src/openai-receipt.mjs +98 -0
  67. package/src/openai-transport.mjs +167 -0
  68. package/src/openai-usage.mjs +194 -0
  69. package/src/opportunities.mjs +295 -0
  70. package/src/preparation-artifacts.mjs +213 -0
  71. package/src/preparation-plugin.mjs +290 -0
  72. package/src/runtime-command.mjs +264 -0
  73. package/src/runtime-config.mjs +417 -0
  74. package/src/runtime-guard.mjs +284 -0
  75. package/src/runtime-host.mjs +212 -0
  76. package/src/runtime-init.mjs +278 -0
  77. package/src/runtime-installation.mjs +518 -0
  78. package/src/runtime-lifecycle.mjs +227 -0
  79. package/src/runtime-model-settings.mjs +111 -0
  80. package/src/runtime-readiness.mjs +285 -0
  81. package/src/runtime-workspace.mjs +18 -0
  82. package/src/session-bridge.mjs +365 -0
  83. package/src/store.mjs +394 -0
  84. package/src/tree/LICENSE +21 -0
  85. package/src/tree/attribution.mjs +120 -0
  86. package/src/tree/clusters.mjs +61 -0
  87. package/src/tree/consolidation.mjs +103 -0
  88. package/src/tree/context.mjs +211 -0
  89. package/src/tree/contracts.mjs +142 -0
  90. package/src/tree/experience.mjs +140 -0
  91. package/src/tree/experiment-budget.mjs +84 -0
  92. package/src/tree/hooks.mjs +254 -0
  93. package/src/tree/host-context.mjs +107 -0
  94. package/src/tree/host-process.mjs +288 -0
  95. package/src/tree/host.mjs +37 -0
  96. package/src/tree/improvement.mjs +106 -0
  97. package/src/tree/items.mjs +294 -0
  98. package/src/tree/jobs.mjs +184 -0
  99. package/src/tree/learning.mjs +202 -0
  100. package/src/tree/metrics.mjs +203 -0
  101. package/src/tree/model-owner.mjs +254 -0
  102. package/src/tree/observations.mjs +73 -0
  103. package/src/tree/prompts.mjs +500 -0
  104. package/src/tree/reports.mjs +67 -0
  105. package/src/tree/repository.mjs +236 -0
  106. package/src/tree/runtime.mjs +745 -0
  107. package/src/tree/script-writer.mjs +149 -0
  108. package/src/tree/scripts.mjs +208 -0
  109. package/src/tree/signals.mjs +118 -0
  110. package/src/tree/snapshots.mjs +379 -0
  111. package/src/tree/token-counter.mjs +75 -0
  112. package/src/tree/token-counter.py +37 -0
  113. package/src/tree/tokenizer-resources/LICENSE +21 -0
  114. package/src/tree/tokenizer-resources/README.md +31 -0
  115. package/src/tree/tokenizer-resources/deepseek_recipe-0.2.0-cp310-abi3-macosx_11_0_arm64.whl +0 -0
  116. package/src/tree/tokenizer-resources/manifest.json +61 -0
  117. package/src/tree/tokenizer-resources/native.pyi +1042 -0
  118. package/src/tree/tokenizer-resources/tokenizer-NOTICE.md +26 -0
  119. package/src/tree/tokenizer-resources/tokenizer-v4-NOTICE.md +27 -0
  120. package/src/tree/tokenizer-resources/tokenizer-v4.json +267359 -0
  121. package/src/tree/tokenizer-resources/tokenizer.json +267359 -0
  122. package/src/tree/tools.mjs +99 -0
  123. package/src/tree/tree.mjs +202 -0
  124. package/src/tree/ui.mjs +285 -0
  125. package/src/tree/verification.mjs +143 -0
  126. package/src/understandings.mjs +309 -0
  127. package/src/web-budget-recovery.mjs +181 -0
@@ -0,0 +1,298 @@
1
+ import { DomainError, canonicalJson, identity } from './store.mjs';
2
+ import { IMPORT_LIMITS, readLegacyImport, importDigest, textDigest, importMethodField } from './legacy-import-source.mjs';
3
+ import { invalidateDependents } from './learning-goals.mjs';
4
+ import { projectUnderstanding } from './learning-contracts.mjs';
5
+ import { usageForVersion } from './methods.mjs';
6
+ import { browseScope } from './browse-scope.mjs';
7
+
8
+ const fail = (code,message) => { throw new DomainError(code,message); };
9
+ const nonnegative = value => Number.isSafeInteger(value) && value >= 0;
10
+ const positive = value => Number.isSafeInteger(value) && value > 0;
11
+ const usable = row => ['enabled','trial','paused'].includes(row.eligibility);
12
+ const safeErrors = new Set(['import_token_limit','import_depth_limit','import_key_limit','import_duplicate_key','import_number_limit',
13
+ 'invalid_import_file','import_chunk_limit','import_byte_limit','import_digest_mismatch','import_incomplete','invalid_import_json',
14
+ 'invalid_import_format','import_projection_limit','import_string_limit','import_record_limit','invalid_import_identity',
15
+ 'import_scope_limit','empty_import','invalid_import_export','invalid_import_source','import_scope_unknown']);
16
+ function page(rows,offset,limit) { return {items:rows.slice(offset,offset+limit),total:rows.length,nextOffset:offset+limit<rows.length?offset+limit:null}; }
17
+ function pagination(offset,limit,max) { if(!nonnegative(offset)||!positive(limit)||limit>max) fail('invalid_import_page','分页范围无效。'); }
18
+ function mapping(sessionId,scopeMap=[]) {
19
+ if(!Array.isArray(scopeMap)||scopeMap.length>1) fail('invalid_import_scope','仅可选择一个旧交流。');
20
+ return scopeMap.map(row=>{
21
+ if(!row||Object.keys(row).sort().join(',')!=='sourceConversationId,targetSessionId'||row.targetSessionId!==sessionId)
22
+ fail('invalid_import_scope','旧交流只能映射到当前主交流。');
23
+ identity(row.sourceConversationId,'Old conversation');return {...row};
24
+ });
25
+ }
26
+ const commandId = (op,sessionId,requestId) => 'import-command:'+importDigest([op,sessionId,requestId]);
27
+ const batchId = (sessionId,requestId) => 'import:'+importDigest([sessionId,requestId]);
28
+ const publicBatch = row => Object.fromEntries(['id','revision','sessionId','status','file','format','previewHash','scopeMap','sourceScopes','counts','createdAt','updatedAt','error'].map(key=>[key,row[key]]));
29
+ const emptyCounts = () => ({sources:0,records:0,enabled:0,trial:0,paused:0,history:0,needsReview:0});
30
+ const methodBody=row=>({title:row.title??'',guidance:row.guidance??'',appliesTo:row.appliesTo??'',doesNotApplyTo:row.doesNotApplyTo??''});
31
+ const targetScope=row=>({...(row.scope.topics?.length?{topics:row.scope.topics}:{}),...(row.scope.kind==='mapped'?{conversationId:row.scope.targetSessionId}:{})});
32
+ function normalizedExistingScope(row){
33
+ const scope=row.scope??{};
34
+ if(!scope||typeof scope!=='object'||Array.isArray(scope)||Object.keys(scope).some(key=>!['conversationId','topics'].includes(key))||
35
+ scope.topics!==undefined&&!Array.isArray(scope.topics))return null;
36
+ return {...(scope.topics?.length?{topics:scope.topics}:{}),...(scope.conversationId?{conversationId:scope.conversationId}:{})};
37
+ }
38
+
39
+ /** Business import only: the caller proves owner/live-main/native receipt. */
40
+ export class ImportService {
41
+ constructor({business}) {
42
+ if(!business?.storage) fail('invalid_import_config','需要业务存储。');
43
+ this.store=business.storage;this.inflight=new Map();this.closed=false;this.abort=new AbortController();
44
+ this.store.transaction(()=>{
45
+ for(const row of this.store.listAll('import_batches',{where:{status:'preparing'}}))
46
+ this.store.save('import_batches',{...row,status:'failed',error:{code:'import_interrupted',message:'上次准备未完成;没有自动重试或确认。'}},{expectedRevision:row.revision});
47
+ });
48
+ }
49
+ async close() { this.closed=true;this.abort.abort();await Promise.allSettled([...this.inflight.values()]); }
50
+ compareMethod(row,existing){
51
+ const bodyHash=importDigest(methodBody(row.original));
52
+ const sameIdentity=existing.filter(old=>old.importProvenance?.originalId===row.originalId&&old.importProvenance.originalVersion===row.version&&
53
+ (!old.importProvenance.originalFormat||old.importProvenance.originalFormat===row.originalFormat));
54
+ const matches=sameIdentity.length?sameIdentity:existing.filter(old=>importDigest(methodBody(old))===bodyHash);
55
+ const evidence=matches.map(old=>{let usage;try{usage=usageForVersion(this.store,old);}catch{usage=null;}
56
+ return {id:old.id,revision:old.revision,recordHash:importDigest(old),usage};});
57
+ if(!matches.length)return {match:'new',evidence};
58
+ const same=matches.length===1&&importDigest(methodBody(matches[0]))===bodyHash&&normalizedExistingScope(matches[0])!==null&&
59
+ canonicalJson(normalizedExistingScope(matches[0]))===canonicalJson(targetScope(row))&&evidence[0].usage!==null;
60
+ return {match:same?'duplicate':'conflict',existingRecordId:matches[0].id,evidence};
61
+ }
62
+ preparationRequest({sessionId,receiptId,requestId,scopeMap=[]}) {
63
+ identity(sessionId,'Session');identity(receiptId,'Receipt');identity(requestId,'Request');
64
+ return {sessionId,receiptId,requestId,scopeMap:mapping(sessionId,scopeMap)};
65
+ }
66
+ replayPreparation(input) {
67
+ const request=this.preparationRequest(input),old=this.store.get('import_batches',batchId(request.sessionId,request.requestId));
68
+ if(!old)return null;
69
+ if(old.requestHash!==importDigest(request))fail('import_identity_conflict','这个提交编号已绑定另一份内容。');
70
+ return this.inflight.get(old.id) || publicBatch(old);
71
+ }
72
+ async prepare(input) {
73
+ const request=this.preparationRequest(input),prior=this.replayPreparation(request);
74
+ if(prior)return await prior;
75
+ if(this.closed)fail('import_unavailable','导入服务已停止。');
76
+ const {file,read,signal,userIdentity}=input;
77
+ if(!file||typeof file.name!=='string'||file.name.length>512||!nonnegative(file.bytes)||file.bytes>IMPORT_LIMITS.maxFileBytes||
78
+ !/^[a-f0-9]{64}$/.test(file.sha256)||typeof read!=='function')fail('invalid_import_file','原生附件元数据无效。');
79
+ identity(userIdentity,'Owner');
80
+ const row=this.store.save('import_batches',{id:batchId(request.sessionId,request.requestId),...request,
81
+ requestHash:importDigest(request),status:'preparing',file:{name:file.name,bytes:file.bytes,sha256:file.sha256},
82
+ format:null,previewHash:null,scopeMap:request.scopeMap,sourceScopes:[],counts:emptyCounts(),error:null,owner:userIdentity});
83
+ const pending=Promise.resolve().then(async()=>{
84
+ try {
85
+ const abort=signal?AbortSignal.any([signal,this.abort.signal]):this.abort.signal;
86
+ abort.throwIfAborted();
87
+ const parsed=await readLegacyImport({stream:read(abort),expectedBytes:file.bytes,expectedSha256:file.sha256,signal:abort});
88
+ abort.throwIfAborted();
89
+ if(request.scopeMap.some(m=>!parsed.sourceScopes.some(s=>s.sourceConversationId===m.sourceConversationId)))
90
+ fail('import_scope_unknown','映射的旧交流不在这份完整导出中。');
91
+ const mapped=new Map(request.scopeMap.map(m=>[m.sourceConversationId,m.targetSessionId]));
92
+ const records=parsed.records.map(item=>{
93
+ const target=item.scope.global?null:mapped.get(item.scope.sourceConversationId)||null;
94
+ const scope={kind:item.scope.global?'global':target?'mapped':'historical',
95
+ sourceConversationId:item.scope.sourceConversationId,targetSessionId:target,...(item.scope.topics?{topics:item.scope.topics}:{})};
96
+ return {...item,originalFormat:parsed.format,scope,...(!item.scope.global&&!target&&usable(item)?{eligibility:'history',reasonCodes:[...item.reasonCodes,'scope_not_mapped']}:{})};
97
+ });
98
+ for(const item of records)if(item.kind==='understandings'){
99
+ const direct=parsed.sources.map(source=>({id:source.id,role:source.role,content:source.content,revision:source.originalVersion,withdrawn:!source.available}));
100
+ const dependencies=(item.original.dependencies||[]).map(ref=>{
101
+ const kind=ref.kind||['events','understandings','methods'].find(kind=>kind==='events'?parsed.sources.some(source=>source.originalId===ref.id):records.some(record=>record.kind===kind&&record.originalId===ref.id));
102
+ return {...ref,kind:kind??'unknown',id:kind+':'+ref.id};
103
+ });
104
+ item.claimType=parsed.format==='arcfold2-database-snapshot'?'inference':projectUnderstanding({content:item.content,
105
+ claimType:item.original.claimType||'inference',proposedClaimType:item.original.proposedClaimType??item.original.claimType??'inference',
106
+ sourceIds:item.sourceIds,dependencies},direct).claimType;
107
+ }
108
+ const methodGroups=new Map();
109
+ for(const item of records)if(item.kind==='methods'&&usable(item)){
110
+ const key=importDigest(methodBody(item.original));
111
+ methodGroups.set(key,[...(methodGroups.get(key)||[]),item]);
112
+ }
113
+ for(const group of methodGroups.values())if(group.length>1)for(const item of group){
114
+ item.eligibility='needs_review';item.reasonCodes.push('method_batch_conflict');
115
+ }
116
+ const existingMethods=this.store.listAll('methods');
117
+ for(const item of records){
118
+ if(item.kind!=='methods'||!usable(item))continue;
119
+ item.methodCheck=this.compareMethod(item,existingMethods);
120
+ if(item.methodCheck.match!=='new'){
121
+ item.existingRecordId=item.methodCheck.existingRecordId;
122
+ item.eligibility=item.methodCheck.match==='duplicate'?'history':'needs_review';
123
+ item.reasonCodes.push(item.methodCheck.match==='duplicate'?'method_already_present':'method_existing_conflict');
124
+ }
125
+ }
126
+ // A qualified dependency cannot be silently widened through a global parent.
127
+ const byId=new Map(records.map(item=>[item.id,item]));let changed;
128
+ do {changed=false;for(const item of records){
129
+ if(!usable(item))continue;
130
+ for(const ref of item.original.dependencies || []){
131
+ const kind=ref.kind||['events','understandings','methods'].find(k=>k==='events'?parsed.sources.some(s=>s.originalId===ref.id):byId.has(k+':'+ref.id));
132
+ if(kind==='events')continue;
133
+ const dep=byId.get(kind+':'+ref.id);
134
+ if(!dep||!usable(dep)||dep.eligibility==='paused'){
135
+ item.eligibility='needs_review';item.reasonCodes.push('dependency_not_eligible');changed=true;break;
136
+ }
137
+ if(dep.scope.kind!=='global'&&(item.scope.kind!=='mapped'||dep.scope.kind!=='mapped'||item.scope.targetSessionId!==dep.scope.targetSessionId)){
138
+ item.eligibility='needs_review';item.reasonCodes.push('dependency_scope_mismatch');changed=true;break;
139
+ }
140
+ }
141
+ }}while(changed);
142
+ const counts=emptyCounts();counts.sources=parsed.sources.length;counts.records=records.length;
143
+ for(const item of records)counts[item.eligibility==='needs_review'?'needsReview':item.eligibility]++;
144
+ const previewHash=importDigest({file:row.file,format:parsed.format,scopeMap:request.scopeMap,
145
+ provenance:parsed.provenance,sources:parsed.sources,records,history:parsed.history});
146
+ return this.store.transaction(()=>{
147
+ abort.throwIfAborted();
148
+ for(const source of parsed.sources)this.store.save('import_sources',{...source,id:row.id+':source:'+importDigest(source.id),
149
+ batchId:row.id,sourceKey:source.id,textKey:'source:'+importDigest(source.id),sha256:textDigest(source.content)});
150
+ for(const record of records)this.store.save('import_records',{...record,id:row.id+':record:'+importDigest(record.id),
151
+ batchId:row.id,recordKey:record.id,textKey:'record:'+importDigest(record.id),originalTextKey:'original:'+importDigest(record.id)});
152
+ for(const [index,entry] of parsed.history.entries())this.store.save('import_history',{id:row.id+':history:'+index,batchId:row.id,...entry});
153
+ const projectionHash=importDigest(['import_sources','import_records','import_history'].map(kind=>this.store.listAll(kind,{where:{batchId:row.id}})));
154
+ const saved=this.store.save('import_batches',{...row,status:'ready',format:parsed.format,previewHash,projectionHash,
155
+ sourceScopes:parsed.sourceScopes,counts,provenance:parsed.provenance},{expectedRevision:row.revision});
156
+ return publicBatch(saved);
157
+ });
158
+ }catch(error){
159
+ const current=this.store.get('import_batches',row.id);
160
+ const code=safeErrors.has(error?.code)?error.code:'import_prepare_failed';
161
+ return publicBatch(this.store.save('import_batches',{...current,status:'failed',previewHash:null,
162
+ error:{code,message:safeErrors.has(code)?error.message.slice(0,240):'附件准备未完成;原文未启用,没有自动重试。'}},{expectedRevision:current.revision}));
163
+ }
164
+ });
165
+ this.inflight.set(row.id,pending);
166
+ try{return await pending;}finally{this.inflight.delete(row.id);}
167
+ }
168
+ list({sessionId,viewScope,offset=0,limit=20}) {
169
+ identity(sessionId,'Session');pagination(offset,limit,100);
170
+ if(browseScope(viewScope,'all')==='current') {
171
+ const rows=this.store.listAll('import_batches',{where:{sessionId}}).reverse();
172
+ return page(rows.map(publicBatch),offset,limit);
173
+ }
174
+ const total=this.store.db.prepare("SELECT COUNT(*) AS n FROM records WHERE kind='import_batches'").get().n;
175
+ const items=this.store.list('import_batches',{offset,limit,order:'desc'}).map(publicBatch);
176
+ return {items,total,nextOffset:offset+limit<total?offset+limit:null};
177
+ }
178
+ batch(id) {identity(id,'Batch');const row=this.store.get('import_batches',id);if(!row)fail('import_not_found','导入批次不存在。');return row;}
179
+ details({sessionId,batchId:id,view='records',offset=0,limit,textKey}) {
180
+ identity(sessionId,'Session');const batch=this.batch(id),header=publicBatch(batch);
181
+ if(!['records','sources','text'].includes(view))fail('invalid_import_page','预览类型无效。');
182
+ limit??=view==='text'?16000:20;pagination(offset,limit,view==='text'?32000:50);
183
+ if(view==='text'){
184
+ identity(textKey,'Text');
185
+ const ordinary=['import_sources','import_records'].flatMap(kind=>this.store.list(kind,{where:{batchId:id,textKey},limit:1}))[0];
186
+ const original=this.store.list('import_records',{where:{batchId:id,originalTextKey:textKey},limit:1})[0];
187
+ const row=ordinary||original;
188
+ if(!row)fail('import_text_denied','该原文不属于此批预览。');
189
+ const text=original?canonicalJson(row.original):row.content;
190
+ return {batch:header,view,textKey,text:text.slice(offset,offset+limit),offset,total:text.length,nextOffset:offset+limit<text.length?offset+limit:null};
191
+ }
192
+ if(textKey!==undefined)fail('invalid_import_page','记录列表不接受原文编号。');
193
+ const rows=this.store.listAll(view==='records'?'import_records':'import_sources',{where:{batchId:id}});
194
+ const projected=rows.map(row=>view==='sources'?{id:row.sourceKey,originalId:row.originalId,role:row.role,
195
+ sourceConversationId:row.sourceConversationId,occurredAt:row.occurredAt,textPreview:row.content.slice(0,240),textKey:row.textKey,
196
+ sha256:row.sha256,available:row.available}:{id:row.recordKey,kind:row.kind,originalId:row.originalId,
197
+ title:row.kind==='methods'?importMethodField(row.original,'title'):String(row.original.title || '').slice(0,120),
198
+ textPreview:row.content.slice(0,240),textKey:row.textKey,...(row.originalTextKey?{originalTextKey:row.originalTextKey}:{}),
199
+ eligibility:row.eligibility,reasonCodes:row.reasonCodes,sourceIds:row.sourceIds,scope:row.scope,
200
+ ...(row.kind==='methods'?{appliesTo:importMethodField(row.original,'appliesTo'),doesNotApplyTo:importMethodField(row.original,'doesNotApplyTo')}:{}),
201
+ ...(row.kind==='understandings'?{claimType:row.claimType}:{}),
202
+ ...(row.methodUsage?{methodUsage:row.methodUsage}:{}),...(row.existingRecordId?{existingRecordId:row.existingRecordId}:{})});
203
+ return {batch:header,view,...page(projected,offset,limit)};
204
+ }
205
+ mutation(op,input,apply) {
206
+ const {sessionId,batchId:id,expectedRevision,previewHash,requestId,userIdentity}=input;
207
+ for(const value of [sessionId,id,requestId,userIdentity])identity(value,'Request identity');
208
+ if(!positive(expectedRevision)||!/^[a-f0-9]{64}$/.test(previewHash))fail('invalid_import_request','确认必须绑定完整预览和版本。');
209
+ const request={sessionId,batchId:id,expectedRevision,previewHash,requestId},key=commandId(op,sessionId,requestId),requestHash=importDigest(request);
210
+ return this.store.transaction(()=>{
211
+ const prior=this.store.get('import_commands',key);
212
+ if(prior){if(prior.requestHash!==requestHash)fail('import_identity_conflict','这个提交编号已绑定另一项决定。');return prior.result;}
213
+ const batch=this.batch(id);
214
+ if(batch.revision!==expectedRevision||batch.previewHash!==previewHash)fail('revision_conflict','预览版本已变化,请重新查看。');
215
+ const result=publicBatch(apply(batch));
216
+ this.store.save('import_commands',{id:key,operation:op,request,requestHash,actor:'user',userIdentity,result,
217
+ authority:'listed-knowledge-guidance-only; no-tools-execution-spending-or-goal-control'});
218
+ return result;
219
+ });
220
+ }
221
+ confirm(input) {return this.mutation('confirm',input,batch=>{
222
+ if(batch.sessionId!==input.sessionId||batch.status!=='ready')fail('import_not_confirmable','仅能在准备预览的当前主交流中确认完整预览。');
223
+ const fingerprint=importDigest([batch.file.sha256,batch.scopeMap]);
224
+ const prior=this.store.get('import_applications',fingerprint);
225
+ if(prior){const existing=this.batch(prior.batchId);if(existing.status==='withdrawn')fail('import_already_withdrawn','相同来源批次已撤销,不会借重复准备重新启用。');return existing;}
226
+ const sources=this.store.listAll('import_sources',{where:{batchId:batch.id}}),records=this.store.listAll('import_records',{where:{batchId:batch.id}});
227
+ if(importDigest([sources,records,this.store.listAll('import_history',{where:{batchId:batch.id}})])!==batch.projectionHash)
228
+ fail('import_dependency_changed','保存的预览正文已变化,未导入。');
229
+ const existingMethods=this.store.listAll('methods');
230
+ for(const row of records)if(row.methodCheck&&canonicalJson(row.methodCheck)!==canonicalJson(this.compareMethod(row,existingMethods)))
231
+ fail('import_dependency_changed','已有方法或试用记录已变化,请重新准备预览。');
232
+ const saved=new Map(),byKey=new Map(records.map(row=>[row.recordKey,row]));
233
+ const targetId=key=>'legacy:'+importDigest([fingerprint,key]);
234
+ const mapped=new Map(batch.scopeMap.map(row=>[row.sourceConversationId,row.targetSessionId]));
235
+ for(const source of sources){
236
+ const conversationId=mapped.get(source.sourceConversationId)||'legacy-conversation:'+importDigest([fingerprint,source.sourceConversationId]);
237
+ const event=this.store.save('events',{id:targetId(source.sourceKey),conversationId,sessionId:conversationId,role:source.role,author:source.role,
238
+ content:source.content,occurredAt:source.occurredAt,importBatchId:batch.id,
239
+ provenance:{...source.provenance,importBatchId:batch.id,originalId:source.originalId,originalRevision:source.originalVersion,
240
+ originalConversationId:source.sourceConversationId,fileSha256:batch.file.sha256,bodySha256:source.sha256}});
241
+ saved.set(source.sourceKey,event);
242
+ if(!source.available)this.store.save('source_controls',{id:event.id,withdrawn:true,useProhibited:true,reason:'legacy_source_unavailable',importBatchId:batch.id});
243
+ }
244
+ function currentDependency(ref){
245
+ const kind=ref.kind||['events','understandings','methods'].find(kind=>saved.has(kind+':'+ref.id)||byKey.has(kind+':'+ref.id));
246
+ const key=kind+':'+ref.id;
247
+ if(!saved.has(key))write(byKey.get(key));
248
+ const row=saved.get(key);if(!row)fail('import_dependency_changed','预览的依赖不能完整写入。');
249
+ return {kind,id:row.id,revision:row.revision};
250
+ }
251
+ const store=this.store;
252
+ function write(row){
253
+ if(!row||!usable(row)||saved.has(row.recordKey))return;
254
+ const original=row.original,scope=targetScope(row);
255
+ const sourceIds=row.sourceIds.map(key=>saved.get(key)?.id);
256
+ if(sourceIds.some(id=>!id))fail('import_dependency_changed','预览的原话缺失。');
257
+ const dependencies=(original.dependencies||[]).map(currentDependency);
258
+ for(const key of row.sourceIds){const source=saved.get(key);if(!dependencies.some(ref=>ref.id===source.id))dependencies.push({kind:'events',id:source.id,revision:source.revision});}
259
+ const provenance={importBatchId:batch.id,originalFormat:row.originalFormat,originalId:row.originalId,originalRevision:original.revision ?? null,
260
+ originalVersion:row.version,fileSha256:batch.file.sha256};
261
+ const common={id:targetId(row.recordKey),conversationId:row.scope.targetSessionId||'legacy-conversation:'+importDigest([fingerprint,row.scope.sourceConversationId]),
262
+ scope,sourceIds,dependencies,version:row.version,importBatchId:batch.id,importProvenance:provenance};
263
+ let value;
264
+ if(row.kind==='understandings'){
265
+ const old2=row.originalFormat==='arcfold2-database-snapshot';
266
+ value=projectUnderstanding({...common,content:row.content,status:'active',claimType:old2?'inference':original.claimType||'inference',
267
+ proposedClaimType:old2?'inference':original.proposedClaimType??original.claimType??'inference'},row.sourceIds.map(key=>saved.get(key)));
268
+ if(value.claimType!==row.claimType)fail('import_dependency_changed','理解性质与已确认预览不符,未导入。');
269
+ }
270
+ else if(row.kind==='methods')value={...common,title:original.title,guidance:original.guidance,appliesTo:original.appliesTo,
271
+ doesNotApplyTo:original.doesNotApplyTo,status:original.status,trialUses:row.methodUsage.trialUses,maxTrialUses:row.methodUsage.maxTrialUses,
272
+ validated:false,origin:'imported_guidance',signature:importDigest({title:original.title,guidance:original.guidance,appliesTo:original.appliesTo,doesNotApplyTo:original.doesNotApplyTo}),
273
+ pauseReason:original.pauseReason ?? null,authority:'work-guidance-only; no-new-tool-or-permission',
274
+ activationSourceId:original.activationSourceId?saved.get('events:'+original.activationSourceId)?.id ?? null:null,
275
+ importedTrialBasis:{trialUses:row.methodUsage.trialUses,version:row.version,source:'complete-legacy-export'}};
276
+ else return;
277
+ saved.set(row.recordKey,store.save(row.kind,value));
278
+ }
279
+ for(const row of records)write(row);
280
+ const appliedIds=[...saved.values()].map(row=>row.id);
281
+ this.store.save('import_applications',{id:fingerprint,batchId:batch.id,fileSha256:batch.file.sha256,scopeMap:batch.scopeMap,appliedIds});
282
+ return this.store.save('import_batches',{...batch,status:'confirmed',fingerprint,appliedIds},{expectedRevision:batch.revision});
283
+ });}
284
+ withdraw(input) {return this.mutation('withdraw',input,batch=>{
285
+ if(batch.status!=='confirmed')fail('import_not_withdrawable','只有已确认批次可以撤销。');
286
+ const ids=[];
287
+ for(const source of this.store.listAll('events',{where:{importBatchId:batch.id}})){
288
+ const old=this.store.get('source_controls',source.id);
289
+ if(!old?.withdrawn)this.store.save('source_controls',{...old,id:source.id,withdrawn:true,useProhibited:true,
290
+ reason:'import_batch_withdrawn',importBatchId:batch.id},{expectedRevision:old?.revision ?? null});
291
+ ids.push(source.id);
292
+ }
293
+ const changes=invalidateDependents(this.store,{ids,pendingId:batch.id,reason:'import_batch_withdrawn'});
294
+ this.store.save('audit',{id:'import-withdraw:'+batch.id,actor:'user',action:'import_batch_withdrawn',batchId:batch.id,
295
+ sourceIds:ids,changes,meaning:'future knowledge eligibility only; original sources, independent later experience and goal control retained'});
296
+ return this.store.save('import_batches',{...batch,status:'withdrawn'},{expectedRevision:batch.revision});
297
+ });}
298
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,53 @@
1
+ import { OpportunityService } from './opportunities.mjs';
2
+ import { ImportService } from './importer.mjs';
3
+ import { BusinessStore } from './business.mjs';
4
+ import { SessionBridge } from './session-bridge.mjs';
5
+ import { MethodService } from './methods.mjs';
6
+ import { GoalService } from './goals.mjs';
7
+ import { UnderstandingService } from './understandings.mjs';
8
+ import { GoalExecution } from './goal-execution.mjs';
9
+
10
+ export const name = 'arcfold-business';
11
+ export const inject = ['sessions', 'sessionPersistence', 'llm'];
12
+ export async function apply(ctx, config = {}) {
13
+ if (config.goalTreeV3?.enabled !== undefined && typeof config.goalTreeV3.enabled !== 'boolean') throw new Error('goalTreeV3.enabled must be an explicit boolean');
14
+ ctx.provide('arcfoldTreeMode', Object.freeze({ enabled: config.goalTreeV3?.enabled === true }));
15
+ if (config.goalTreeV3?.enabled === true) {
16
+ const { installTreeRuntime } = await import('./tree/runtime.mjs');
17
+ await installTreeRuntime(ctx, config.goalTreeV3, { legacyFilename: config.filename });
18
+ return;
19
+ }
20
+ const desktopState = ctx.get('arcfoldDesktopSetup')?.status().state;
21
+ if (!config.filename && ['needs_setup', 'configured', 'activation_failed', 'interrupted', 'unavailable'].includes(desktopState)) {
22
+ ctx.provide('arcfoldLegacyMode', Object.freeze({ enabled: true }));
23
+ return;
24
+ }
25
+ // The native Loader adds this dependency to the original execution entries.
26
+ // Dependency activation is reactive; disabled expressions are not.
27
+ ctx.provide('arcfoldLegacyMode', Object.freeze({ enabled: true }));
28
+ if (config.goalTreeV3?.enabled !== undefined && config.goalTreeV3.enabled !== false) throw new Error('goalTreeV3.enabled must be an explicit boolean');
29
+ if (typeof config.filename !== 'string' || !config.filename) throw new Error('An explicit Arcfold business database path is required');
30
+ const business = new BusinessStore({ filename: config.filename });
31
+ const opportunities = new OpportunityService({ business });
32
+ const imports = new ImportService({ business });
33
+ const methods = new MethodService({ business });
34
+ const goals = new GoalService({ business });
35
+ const understandings = new UnderstandingService({ business, goals });
36
+ const bridge = new SessionBridge({ business, methods, goals, understandings, sessions: ctx.sessions, persistence: ctx.sessionPersistence,
37
+ logger: ctx.logger, maxContextCharacters: config.maxContextCharacters ?? 24000 });
38
+ const work = new GoalExecution({ business });
39
+ bridge.work = work;
40
+ goals.execution = work;
41
+ ctx.effect(() => async () => { await imports.close(); await bridge.close(); }, 'close Arcfold business storage after reconciliation');
42
+ ctx.on('session/event', (session, event) => bridge.observe(session, event));
43
+ ctx.on('llm/stream', (options, next) => bridge.guardStream(options, next));
44
+ await bridge.recover();
45
+ ctx.provide('arcfoldBusiness', business);
46
+ ctx.provide('arcfoldOpportunities', opportunities);
47
+ ctx.provide('arcfoldImports', imports);
48
+ ctx.provide('arcfoldBridge', bridge);
49
+ ctx.provide('arcfoldMethods', methods);
50
+ ctx.provide('arcfoldGoals', goals);
51
+ ctx.provide('arcfoldWork', work);
52
+ ctx.provide('arcfoldUnderstandings', understandings);
53
+ }
@@ -0,0 +1,220 @@
1
+ // Extracted from Arcfold 3.0 src/contracts.mjs (four Learn schemas and parsers),
2
+ // src/evidence.mjs (literal-user projection), src/prompts.mjs (learning policy).
3
+ // No Service, Work, model client or execution state is imported.
4
+ import { createHash } from 'node:crypto';
5
+ import { canonicalJson } from './store.mjs';
6
+ export class ContractError extends Error {
7
+ constructor(code, message, details = null) { super(message); this.name = 'ContractError'; this.code = code; this.details = details; }
8
+ }
9
+ export const digest = value => createHash('sha256').update(canonicalJson(value)).digest('hex');
10
+ export const clone = value => structuredClone(value);
11
+ const str = (maxLength = 4000, minLength = 0) => ({ type: 'string', minLength, maxLength });
12
+ const list = (items, maxItems = 12) => ({ type: 'array', items, maxItems });
13
+ const obj = properties => ({ type: 'object', properties, required: Object.keys(properties), additionalProperties: false });
14
+ const enumeration = values => ({ type: 'string', enum: values });
15
+ const ids = max => list(str(200, 1), max);
16
+
17
+ // One finite action definition supplies both model instructions and executable
18
+ // schema branches. Membership/current-version checks still belong to commit.
19
+ const newTarget = { schema: { ...str(200), enum: [''] }, example: '',
20
+ instruction: '必须严格为 "";新记录编号由程序生成,不能填空白、拟造编号或临时别名' };
21
+ const existingTarget = collection => ({ schema: { ...str(200, 1), pattern: '\\S' },
22
+ example: `${collection}-visible-id`,
23
+ instruction: `必须逐字选用 ${collection} 中显示的现有编号;不能为空或自行编造,提交时仍核当前记录与版本` });
24
+ const goalAlias = { schema: { ...str(200), pattern: '^(?:|new-goal:[\\s\\S]*)(?![\\s\\S])' }, example: 'new-goal:example',
25
+ instruction: '可为 "",或以 new-goal: 开头的本区新建目标临时别名;非空别名须在本区唯一,只用于关系引用,不是持久编号' };
26
+ const noAlias = { schema: { ...str(200), enum: [''] }, example: '', instruction: '必须严格为 "";更新已有目标不能声明新建别名' };
27
+ const actionRules = {
28
+ understandings: { create: { targetId: newTarget }, supersede: { targetId: existingTarget('existingUnderstandings') }, withdraw: { targetId: existingTarget('existingUnderstandings') } },
29
+ methods: { create: { targetId: newTarget }, revise: { targetId: existingTarget('existingMethods') }, pause: { targetId: existingTarget('existingMethods') } },
30
+ opportunities: { create: { targetId: newTarget }, reuse: { targetId: existingTarget('existingOpportunities') } },
31
+ goals: { create: { targetId: newTarget, clientRef: goalAlias }, update: { targetId: existingTarget('existingGoals'), clientRef: noAlias } },
32
+ };
33
+ const operations = section => enumeration(Object.keys(actionRules[section]));
34
+ function actionObject(section, properties) {
35
+ // Keep shared properties/limits available to the legacy import projection.
36
+ // The outer object rejects unknown fields; branches constrain identifiers only.
37
+ return { ...obj(properties), oneOf: Object.entries(actionRules[section]).map(([operation, fields]) => ({
38
+ type: 'object', properties: { operation: enumeration([operation]),
39
+ ...Object.fromEntries(Object.entries(fields).map(([key, rule]) => [key, rule.schema])) },
40
+ required: ['operation', ...Object.keys(fields)], additionalProperties: true,
41
+ })) };
42
+ }
43
+ export const LEARN_ACTION_INSTRUCTIONS = Object.entries(actionRules).flatMap(([section, actions]) =>
44
+ Object.entries(actions).map(([operation, fields]) => `${section}.${operation}:${Object.entries(fields)
45
+ .map(([key, rule]) => `${key} ${rule.instruction}`).join(';')}。`)).join('\n');
46
+ export const LEARN_ACTION_EXAMPLES = Object.freeze(Object.fromEntries(Object.entries(actionRules).map(([section, actions]) =>
47
+ [section, Object.entries(actions).map(([operation, fields]) => ({ operation,
48
+ ...Object.fromEntries(Object.entries(fields).map(([key, rule]) => [key, rule.example])) }))])));
49
+ export const UNDERSTANDING_SCHEMA = actionObject('understandings', {
50
+ operation: operations('understandings'), targetId: str(200), content: str(4000),
51
+ claimType: enumeration(['user_statement', 'inference', 'open_question']),
52
+ scope: enumeration(['conversation', 'global']), topics: list(str(100, 1), 8), sourceIds: ids(12),
53
+ });
54
+ export const METHOD_SCHEMA = actionObject('methods', {
55
+ operation: operations('methods'), targetId: str(200), title: str(200), guidance: str(4000),
56
+ appliesTo: str(2000), doesNotApplyTo: str(2000), origin: enumeration(['explicit', 'inferred']), sourceIds: ids(12), understandingIds: ids(8),
57
+ });
58
+ export const OPPORTUNITY_SCHEMA = actionObject('opportunities', {
59
+ operation: operations('opportunities'), targetId: str(200),
60
+ description: str(2000), rationale: str(2000), sourceIds: ids(12), suggestedPreparation: str(3000),
61
+ });
62
+ export const GOAL_SCHEMA = actionObject('goals', {
63
+ operation: operations('goals'), targetId: str(200), clientRef: str(200),
64
+ title: str(200, 1), outcome: str(3000, 1), scope: enumeration(['conversation', 'global']),
65
+ sourceIds: ids(12), understandingIds: ids(8),
66
+ progress: obj({ content: str(3000), sourceIds: ids(12) }),
67
+ relations: list(obj({ type: enumeration(['part_of', 'supports', 'derived_from']), targetId: str(200, 1), reason: str(1500, 1), sourceIds: ids(12) }), 8),
68
+ controlProposal: enumeration(['none', 'pause', 'resume', 'close']),
69
+ });
70
+ // Optional for already frozen/legacy four-section outputs; new models receive the explicit field.
71
+ GOAL_SCHEMA.properties.workLinks = list(obj({ workId: str(200, 1), reason: str(1500, 1), sourceIds: ids(12) }), 4);
72
+ export const LEARN_SECTIONS = Object.freeze(['understandings', 'methods', 'opportunities', 'goals']);
73
+ export const LEARN_SCHEMA = obj({
74
+ understandings: list(UNDERSTANDING_SCHEMA, 8), methods: list(METHOD_SCHEMA, 4), opportunities: list(OPPORTUNITY_SCHEMA, 3), goals: list(GOAL_SCHEMA, 4),
75
+ });
76
+ export const LEARN_EXAMPLE = Object.freeze({ understandings: [], methods: [], opportunities: [], goals: [] });
77
+ export const LEARN_SOURCE_POLICY = Object.freeze({
78
+ understandings: '概括、解释和判断统一是 inference(AI整理/判断),即使忠实概括也不能自称逐字用户原话。user_statement 仅用于 content 与引用的一条完整 role=user 原话严格相同的情况;程序核字面身份和版本,不核其中各命题是否被用户认可,也不因此新增授权。通常保留简洁的 inference 和来源即可,不为取得原话标签复制长对话。未决问题是 open_question。',
79
+ methods: '模型生成的方法统一是 inferred 建议并只可有限试用;来源包含用户明确要求、或模型将 origin 声称为 explicit,都不等于整条方法已获长期授权。程序保留 proposedOrigin 供审查,只有真实用户对具体方法的确认才能继续采用;validated 仍为 false。understandingIds 只能放仍有效的已显示理解编号,不需要依赖时填 [],不能放空字符串。',
80
+ opportunities: 'create 的每条机会须至少引用一个此次可见、直接表达相关目标或需要的 role=user 原话来源;纯评价不建立新目标。助手答复或工具材料只能作为补充,不能单独证明用户目标。找不到这样的用户原话就不新建。reuse 仅引用 existingOpportunities 中真实 targetId,其他文字填空、sourceIds填[],不提出新内容或恢复执行。',
81
+ goals: '目标、进展、关系及控制提议都是 inference,不表示用户决定。定义必须有可见用户原话来源,进展与关系另引真实来源。controlProposal 是对本轮真实owner原话的可纠正追踪解释:pause/close停止把该目标作为背景追求,resume恢复背景追踪;none保持原状态。程序核原话与版本,保留旧人工决定,只有可证原始接收晚于人工控制的新原话才能覆盖其追踪状态。它不暂停、创建或恢复原生执行。workLinks只在确有依据时把本次goalWorks中的一项真实工作联系到这一目标,填写workId、reason及显示的sourceIds;没有联系填[]。同一工作在本区只联系一个目标,不把每个短期执行另建成长远目标。现有联系待复核时结合原文重新判断,不猜最近目标或标题,也不把工作完成当长期目标达成。',
82
+ });
83
+ export const requiresUserSource = (section, row) => section === 'opportunities' && row.operation !== 'reuse' || section === 'understandings' && row.claimType === 'user_statement' || section === 'methods' && row.origin === 'explicit';
84
+
85
+ export function schemaIssues(schema, value, path = '$') {
86
+ const issues = [];
87
+ const error = message => issues.push({ path, message });
88
+ if (schema.type === 'object') {
89
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return [{ path, message: '必须是对象' }];
90
+ for (const key of schema.required || []) if (!Object.hasOwn(value, key)) issues.push({ path: `${path}.${key}`, message: '缺少必填字段' });
91
+ for (const key of Object.keys(value)) {
92
+ if (Object.hasOwn(schema.properties, key)) issues.push(...schemaIssues(schema.properties[key], value[key], `${path}.${key}`));
93
+ else if (schema.additionalProperties === false) issues.push({ path: `${path}.${key}`, message: '未知字段' });
94
+ }
95
+ } else if (schema.type === 'array') {
96
+ if (!Array.isArray(value)) return [{ path, message: '必须是数组' }];
97
+ if (value.length > schema.maxItems) error(`最多 ${schema.maxItems} 项`);
98
+ value.forEach((item, index) => issues.push(...schemaIssues(schema.items, item, `${path}[${index}]`)));
99
+ } else if (schema.type === 'string') {
100
+ if (typeof value !== 'string') return [{ path, message: '必须是字符串' }];
101
+ if (schema.enum && !schema.enum.includes(value)) error(`必须选择 ${schema.enum.map(item => JSON.stringify(item)).join(' / ')}`);
102
+ if (schema.minLength && value.trim().length < schema.minLength) error('文字不可为空');
103
+ if (schema.maxLength && value.length > schema.maxLength) error(`文字超过 ${schema.maxLength} 字符`);
104
+ if (schema.pattern && !new RegExp(schema.pattern).test(value)) error('文字不符合编号格式');
105
+ }
106
+ if (schema.oneOf) {
107
+ const branches = schema.oneOf.map(branch => ({ branch, issues: schemaIssues(branch, value, path) }));
108
+ const matches = branches.filter(result => result.issues.length === 0);
109
+ if (matches.length !== 1) {
110
+ // These authored branches discriminate on operation. Report the selected
111
+ // action's exact field failures, never silently accept another branch.
112
+ const selected = branches.filter(({ branch }) => branch.properties.operation.enum.includes(value?.operation));
113
+ if (matches.length === 0 && selected.length === 1) issues.push(...selected[0].issues);
114
+ else error('必须且只能符合一个动作分支');
115
+ }
116
+ }
117
+ return issues.filter((issue, index) => issues.findIndex(other => other.path === issue.path && other.message === issue.message) === index);
118
+ }
119
+ export function assertSchema(schema, value) {
120
+ const issues = schemaIssues(schema, value);
121
+ if (issues.length) throw new ContractError('invalid_output', '模型输出未符合本次合同', issues);
122
+ return value;
123
+ }
124
+ export function parseJson(text) {
125
+ if (typeof text !== 'string' || !text.trim()) throw new ContractError('invalid_output', '模型未返回 JSON 内容');
126
+ try { return JSON.parse(text); }
127
+ catch { throw new ContractError('invalid_output', '模型输出不是一份完整 JSON 对象;不提取或修补局部文字'); }
128
+ }
129
+ // All originals relied on by the actual selections share display and revalidation.
130
+ export function parseLearn(text) {
131
+ const value = parseJson(text);
132
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length !== LEARN_SECTIONS.length || !LEARN_SECTIONS.every(k => Object.hasOwn(value, k))) throw new ContractError('invalid_output', '学习输出须且仅须包含 understandings / methods / opportunities / goals');
133
+ const issues = Object.fromEntries(Object.entries(LEARN_SCHEMA.properties).map(([key, schema]) => [key, schemaIssues(schema, value[key], key)]));
134
+ return { value, issues };
135
+ }
136
+ export function assertVisibleSources(sourceIds, shownSources, { requireUser = false } = {}) {
137
+ if (!Array.isArray(sourceIds) || !sourceIds.length || new Set(sourceIds).size !== sourceIds.length) throw new ContractError('invalid_source', '每项理解、方法或机会须引用非空且不重复的已显示来源');
138
+ const map = new Map(shownSources.map(source => [source.id, source]));
139
+ const sources = sourceIds.map(id => map.get(id));
140
+ if (sources.some(source => !source)) throw new ContractError('invalid_source', '引用了本次未显示的来源');
141
+ if (requireUser && !sources.some(source => source.role === 'user')) throw new ContractError('invalid_source', '用户主张、明确方法要求及每条机会须至少引用一个可见的 role=user 原话来源,不能仅引用助手或工具');
142
+ return sources.map(source => ({ kind: 'events', id: source.id, revision: source.revision }));
143
+ }
144
+ export const UNDERSTANDING_EVIDENCE_VERSION = 'literal-user-source-v1';
145
+
146
+ /**
147
+ * Classify text provenance, not truth, endorsement, scope or permission.
148
+ * Callers supply current direct event records, after their normal visibility
149
+ * and access checks. Missing originals or dependency versions fail closed.
150
+ * This is also the read-only projection for records saved under older rules.
151
+ */
152
+ export function projectUnderstanding(record, directSources = []) {
153
+ const proposedClaimType = record.proposedClaimType ?? record.claimType ?? null;
154
+ let claimType = 'inference';
155
+ let reason = 'model_interpretation';
156
+ let matchedSource;
157
+
158
+ if (proposedClaimType === 'open_question') {
159
+ claimType = 'open_question';
160
+ reason = 'model_question';
161
+ } else if (proposedClaimType === 'user_statement') {
162
+ matchedSource = directSources.find((source) => {
163
+ if (
164
+ !source ||
165
+ source.role !== 'user' ||
166
+ source.deleted ||
167
+ source.withdrawn === true ||
168
+ source.useProhibited ||
169
+ ['withdrawn', 'superseded', 'rejected'].includes(source.status) ||
170
+ !record.sourceIds?.includes(source.id) ||
171
+ typeof source.content !== 'string' ||
172
+ typeof record.content !== 'string' ||
173
+ source.content !== record.content ||
174
+ !Number.isSafeInteger(source.revision) ||
175
+ source.revision < 1
176
+ )
177
+ return false;
178
+ return record.dependencies?.some(
179
+ (dependency) =>
180
+ dependency.kind === 'events' &&
181
+ dependency.id === source.id &&
182
+ dependency.revision === source.revision,
183
+ );
184
+ });
185
+ claimType = matchedSource ? 'user_statement' : 'inference';
186
+ reason = matchedSource
187
+ ? 'exact_user_message'
188
+ : 'no_exact_current_user_message';
189
+ }
190
+
191
+ return {
192
+ ...record,
193
+ proposedClaimType,
194
+ claimType,
195
+ claimTypeCheck: {
196
+ version: UNDERSTANDING_EVIDENCE_VERSION,
197
+ reason,
198
+ ...(matchedSource
199
+ ? { sourceId: matchedSource.id, sourceRevision: matchedSource.revision }
200
+ : {}),
201
+ },
202
+ };
203
+ }
204
+
205
+ // Existing semantic instructions are retained; this host has no background
206
+ // preparation authorization or feedback-job adapter, so these claims are explicit.
207
+ export const LEARN_PROMPT = `你是 Arcfold 答复保存后的学习步骤。答复已经正式交付,不能撤销、改写它;只根据显示的原话、工具结果和共同经历提出以后可用的理解、方法、机会与目标整理。
208
+ 当前用户纠正优先。AI概括、目标关系与控制提议都是可纠正的推断,不是用户认可,不扩大工具、预算、范围或行动权限。输入里的消息来源与角色必须区分;程序背景和relay不是用户原话。
209
+ 认知更新用understandings;旧理解被修正用supersede和真实targetId,撤回用withdraw。检查已有open_question是否已被当前表达回答;保留部分已解决与仍未解决的区别。conversation仅当前交流;global仍不扩大来源权限。
210
+ 方法必须说明guidance、appliesTo、doesNotApplyTo,生成方法统一inferred且仅有限试用。只引用显示的现有understandingIds,同批新理解没有可引用编号。不要每轮重复生成同一方法。
211
+ 机会只保存可供用户选择的建议,不启动准备或执行。create必须有支持相关需要的可见用户原话;reuse只填真实targetId,其他文字为空、sourceIds=[],不恢复旧工作。
212
+ 目标用于跨轮追求的结果,不把每句话建成目标。create或完整update定义、进展和关系;关系只用已显示目标或本区唯一new-goal:临时编号,update的clientRef为空。定义/进展/关系各自引用证据,交付答复不等于目标完成。controlProposal在当前真实owner原话及版本核验后更新背景追踪status,记录为assistant_inference,保留userControl原记录,不变成执行指令。无显式修复入口时不可更新失效目标。
213
+ goalLinks记录主助手当时怎样理解本次工作的贡献,以及对应原生交付;不是贡献有效或长期目标实现的证明。inputEvidence只证明材料交给宿主,不单独证明实际发送。availability失效时不还原旧选择或旧正文;后来的判断不能倒写成当时已知。goalDiscussions只表示用户用这次原话明确选回目标,讨论不会恢复追踪。
214
+ existingGoals中的repairOnly只提供失效目标的身份和状态;仅依据对应goalDiscussions的新用户原话及当前有效来源完整重新澄清,不猜补旧内容,不恢复或改变用户的暂停、结束追踪决定。
215
+ 动作编号规则:${LEARN_ACTION_INSTRUCTIONS}
216
+ 编号片段示例(不是完整条目;其他必填字段仍须填写;existing* 示例值仅为占位,必须换成材料中实际显示的编号):${JSON.stringify(LEARN_ACTION_EXAMPLES)}
217
+ 来源规则:${Object.values(LEARN_SOURCE_POLICY).join('\n')}
218
+ current inputEventIds是本轮真实用户原话;controlProposal必须引用其中一条。sources包括本次回复与符合截止的依赖原件,不表示Main曾看到学习阶段增加的知识。existing*只显示当时已完成、当前仍有效的版本;省略不表示不存在。
219
+ 每区独立提交;没有内容填[]。仅返回一份完整JSON,不要Markdown、修复片段或额外字段。合同:${JSON.stringify(LEARN_SCHEMA)}
220
+ 空形状:${JSON.stringify(LEARN_EXAMPLE)}`;