@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,332 @@
1
+ // Legacy3 writers: store.mjs; service.mjs; importer.mjs; context.mjs;
2
+ // execution.mjs; learning.mjs; tools.mjs; read-coverage.mjs (2026-11-05 source).
3
+ // This is a finite projection, never a general database dump or old Store boot.
4
+ import assert from 'node:assert/strict';
5
+ import { createHash } from 'node:crypto';
6
+ import { constants, closeSync, createReadStream, fstatSync, fsyncSync, linkSync, lstatSync,
7
+ mkdtempSync, openSync, readFileSync, realpathSync, writeFileSync, writeSync } from 'node:fs';
8
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { DatabaseSync } from 'node:sqlite';
11
+ import { canonicalJson } from './store.mjs';
12
+ import { IMPORT_LIMITS, importDigest, readLegacyImport } from './legacy-import-source.mjs';
13
+
14
+ export const LEGACY_EXPORT_VERSION = 'legacy3-finite-projection-v1';
15
+ export const LEGACY_COLLECTIONS = Object.freeze('events conversations understandings methods goals opportunities adoptions audit feedback deliveries imports source_controls'.split(' '));
16
+ const scalar = 'scalar', array = item => ({ item }), object = (fields, strict = false) => ({ fields, strict });
17
+ const fields = names => Object.fromEntries(names.split(' ').filter(Boolean).map(name => [name, scalar]));
18
+ const ids = array(scalar), refs = array(object(fields('kind id revision'), true));
19
+ const common = fields('id revision createdAt updatedAt createdSequence updatedSequence sequenceEstablishedBy status deleted withdrawn useProhibited withdrawalReason');
20
+ const scope = object({ ...fields('conversationId'), topics: ids }, true);
21
+ const sourcePackage = object(fields('id name product system version format path sha256 originalUntouched exportedAt'), true);
22
+ const original = object({ ...fields('sourceRunId kind role status answerValid originalTextSha256 credentialRedacted'),
23
+ error: { scalarOrObject: object(fields('code message'), true) } }, true);
24
+ const provenance = object({ ...fields('kind type idempotencyKey deliveryId assessment interaction methodId methodRevision methodVersion appliesTo doesNotApplyTo goalId definitionVersion action importId sourceRunId originalOrdinal originalTimeKnown requestId name callId toolRecordId path fileSha256 documentSha256 offset totalChars url query contentAuthor userEndorsement artifactSourceId sha256 instructionAuthority'),
25
+ importedOriginalSource: original, sourcePackage }, true);
26
+ const artifact = object({ ...fields('path sha256 bytes toolRecordId'), sourceIds: ids }, true);
27
+ const evidence = array(object({ ...fields('kind runId sourceId previousStatus previousRevision'), sourceIds: ids,
28
+ scope: object(fields('appliesTo doesNotApplyTo'), true) }, true));
29
+ const method = object({ ...common, ...fields('title guidance appliesTo doesNotApplyTo origin proposedOrigin trialUses maxTrialUses version previousId learnedFromRunId signature validated authority pauseReason activationSourceId activationReason activatedAt changedByRunId feedbackId'),
30
+ scope, sourceIds: ids, dependencies: refs, evidence, changeSourceIds: ids });
31
+ const understanding = object({ ...common, ...fields('content claimType proposedClaimType version previousId learnedFromRunId changedByRunId authority'),
32
+ scope, sourceIds: ids, dependencies: refs, changeSourceIds: ids,
33
+ claimTypeCheck: object(fields('version reason sourceId sourceRevision'), true) });
34
+ const goal = object({ ...common, ...fields('conversationId title outcome definitionVersion claimType contentAuthor userEndorsement learnedFromRunId updatedFromRunId'), scope,
35
+ understandingIds: ids, sourceIds: ids, dependencies: refs,
36
+ sourceInvalidation: object(fields('kind reason'), true),
37
+ progress: object({ ...fields('content runId'), sourceIds: ids, dependencies: refs }, true),
38
+ relations: array(object({ ...fields('type targetId targetDefinitionVersion reason'), sourceIds: ids, dependencies: refs }, true)),
39
+ controlProposal: object({ ...fields('action definitionVersion claimType runId'), sourceIds: ids, dependencies: refs }, true),
40
+ userControl: object(fields('action eventId definitionVersion at'), true) });
41
+ const coverage = object({ ...fields('version basis unit retrievalResults documentCount observationCount omittedDocuments omittedObservations'),
42
+ documents: array(object({ ...fields('kind identity sha256 totalChars inconsistentMetadata displayedChars complete multipleVersions'), ranges: array(array(scalar)) }, true)),
43
+ observations: array(object({ ...fields('kind tool callId code path returnedEntries scanCompleteWithinToolScope moreEntries returnedMatches morePossible reason'),
44
+ skipped: object(fields('sensitive dependencies symlinks unavailable otherTypes'), true) }, true)) }, true);
45
+ const TABLES = {
46
+ events: object({ ...common, ...fields('role content conversationId runId sourceRunId importId occurredAt'), provenance }),
47
+ conversations: object({ ...common, ...fields('title importId') }), understandings: understanding, methods: method, goals: goal,
48
+ opportunities: object({ ...common, ...fields('conversationId description rationale suggestedPreparation preparationStatus cancellationReason preparationRunId learnedFromRunId signature authority jobId preparedContent preparedAt'), sourceIds: ids, dependencies: refs, artifactIds: ids, preparationArtifact: artifact,
49
+ preparationError: { scalarOrObject: object(fields('code message'), true) } }),
50
+ adoptions: object({ ...common, ...fields('runId conversationId proof sourceDigest'), methodIds: ids, selectedMethodIds: ids,
51
+ included: object({ understandingIds: ids, methodIds: ids, goalIds: ids }, true),
52
+ declared: object({ understandingIds: ids, methodIds: ids,
53
+ methodAssessments: array(object({ ...fields('methodId applicability reason'), sourceIds: ids }, true)),
54
+ goalSelection: object({ ...fields('status goalId'), sourceIds: ids }, true) }, true),
55
+ selectedVersions: array(object(fields('id semanticVersion version revision trialUse'), true)),
56
+ goalRef: object({ ...fields('id revision definitionVersion'), sourceIds: ids }, true),
57
+ behaviorEvidence: object({ ...fields('deliveryId opportunityId'), artifactIds: ids, toolRecordIds: ids, modelRequestIds: ids, preparationArtifact: artifact }, true),
58
+ // The old writer creates [] and has no writer for outcome claims.
59
+ outcomeEvidence: { emptyArray: true } }),
60
+ audit: object({ ...common, ...fields('action kind recordId goalId commandId actor reason runId sourceId'), sourceIds: ids }),
61
+ feedback: object({ ...common, ...fields('runId deliveryId sourceId content assessment') }),
62
+ deliveries: object({ ...common, ...fields('runId conversationId inputEventId eventId content sourceDigest learningStatus readCoverageRequestId'), readCoverage: coverage }),
63
+ imports: object({ ...common, ...fields('fingerprint conversationId messageCount oldUnderstandingCount note'), eventIds: ids, sourcePackage,
64
+ sourceStatusCounts: object(fields('unknown queued pending running completed succeeded failed cancelled canceled aborted delivered ready invalid'), false),
65
+ answerValidationCounts: object(fields('valid invalid unknown'), true) }),
66
+ };
67
+ const sha = value => createHash('sha256').update(value).digest('hex');
68
+ const positive = value => Number.isSafeInteger(value) && value > 0;
69
+ // SQL bounds every decoded text before it crosses the native/JS boundary.
70
+ // UTF-8 needs at most three bytes per valid UTF-16 code unit. JS retains the
71
+ // consumer's exact code-unit limit. The byte bound also covers text after NUL,
72
+ // where SQLite length(TEXT) stops; supplementary code points are checked in JS.
73
+ const boundedText = (value, limit) => `(typeof(${value})='text' AND length(CAST(${value} AS BLOB))<=${limit * 3} AND length(${value})<=${limit})`;
74
+ const decodeText = value => new TextDecoder('utf-8',{fatal:true,ignoreBOM:true}).decode(value);
75
+
76
+ const assertExport = (condition, code) => { if (!condition) throw Object.assign(new Error(code), { code }); };
77
+ const forbidden = /^(config|settings|credentials?|secrets?|password|api[_-]?key|env|environment|transport|authorization|budget(?:_settings|_requests)?|jobs|leases|sourceSnapshot)$/i;
78
+ const authorityKey = /(?:authority|authorization|permission|endorsement|control|provenance)/i;
79
+ const jsonPath = (path, key) => typeof key === 'number' ? `${path}[${key}]` : `${path}.${JSON.stringify(key)}`;
80
+ const fsyncDirectory = path => { const fd = openSync(path, 'r'); try { fsyncSync(fd); } finally { closeSync(fd); } };
81
+ const jsonFile = (path, value) => { const fd = openSync(path, 'wx', 0o600); try { writeFileSync(fd, canonicalJson(value) + '\n'); fsyncSync(fd); } finally { closeSync(fd); } };
82
+ function identity(path) { const s = lstatSync(path); assertExport(s.isFile() && !s.isSymbolicLink(), 'export_source_not_regular'); return { dev: s.dev, ino: s.ino }; }
83
+ function sameSource(path, expected) { assert.deepEqual(identity(path), expected, 'export_source_replaced'); assert.equal(realpathSync(path), path, 'export_source_alias'); }
84
+ function canonical(path, existing) {
85
+ assertExport(typeof path === 'string' && isAbsolute(path) && resolve(path) === path, 'export_path_not_absolute');
86
+ assertExport(realpathSync(existing ? path : dirname(path)) === (existing ? path : dirname(path)), 'export_path_alias');
87
+ return path;
88
+ }
89
+ function schema(db) {
90
+ for (const [name, expected] of Object.entries({
91
+ records: [['kind','TEXT',1,1],['id','TEXT',1,2],['revision','INTEGER',1,0],['created_at','TEXT',1,0],['updated_at','TEXT',1,0],['body','TEXT',1,0]],
92
+ store_meta: [['key','TEXT',0,1],['value','TEXT',1,0]],
93
+ })) {
94
+ const table = db.prepare('SELECT type FROM sqlite_schema WHERE name=?').get(name);
95
+ assertExport(table?.type === 'table', 'export_schema_shape');
96
+ assert.deepEqual(db.prepare(`PRAGMA table_info(${name})`).all().map(r => [r.name,r.type,r.notnull,r.pk]), expected, 'export_schema_shape');
97
+ }
98
+ const valid = boundedText('value',32);
99
+ const meta = db.prepare(`SELECT key,${valid} AS valid,CASE WHEN ${valid} THEN CAST(value AS BLOB) END AS value FROM store_meta WHERE key IN ('schema_version','mutation_sequence')`).all();
100
+ assertExport(meta.every(row=>row.valid===1),'export_schema_version');
101
+ const values = Object.fromEntries(meta.map(r => [r.key,decodeText(r.value)]));
102
+ assertExport(values.schema_version === '2' && /^(0|[1-9]\d*)$/.test(values.mutation_sequence ?? '') && Number.isSafeInteger(Number(values.mutation_sequence)), 'export_schema_version');
103
+ return Number(values.mutation_sequence);
104
+ }
105
+
106
+ /** SQL only returns allowed scalar values, key/type metadata and finite array
107
+ * indexes. It never returns body, a container value or an excluded atom to JS.
108
+ * SQLite's own JSON scan is not claimed to have constant memory. */
109
+ function projector(db, excluded) {
110
+ const queries = new Map(); let characters = 0, nodes = 0;
111
+ function note(kind, path) {
112
+ const key = canonicalJson([kind,path]);
113
+ if(!excluded.has(key)) characters += key.length;
114
+ assertExport(++nodes <= IMPORT_LIMITS.maxTokens && characters <= IMPORT_LIMITS.maxProjectionCharacters, 'export_projection_limit');
115
+ excluded.set(key, (excluded.get(key) ?? 0) + 1);
116
+ }
117
+ function children(kind, id, path, shape) {
118
+ const isArray = !!(shape.item || shape.emptyArray);
119
+ const permitsScalar = item => item === scalar || !!item?.scalarOrObject;
120
+ const scalarKeys = isArray ? [] : Object.keys(shape.fields).filter(key=>permitsScalar(shape.fields[key]));
121
+ const key = canonicalJson(shape);
122
+ let query = queries.get(key);
123
+ if (!query) {
124
+ const keyValid = isArray ? `(typeof(key)='integer' AND key>=0 AND key<${IMPORT_LIMITS.maxRows})` : boundedText('key',IMPORT_LIMITS.maxKeyCharacters);
125
+ const textValid = `(type!='text' OR ${boundedText('atom',IMPORT_LIMITS.maxStringCharacters)})`;
126
+ const selected = isArray ? (permitsScalar(shape.item)?'1':'0') : `key IN (${scalarKeys.map(()=>'?').join(',') || 'NULL'})`;
127
+ query = db.prepare(`SELECT ${keyValid} AS keyValid,
128
+ CASE WHEN ${keyValid} THEN ${isArray?'key':'CAST(key AS BLOB)'} END AS key,type,${textValid} AS textValid,
129
+ CASE WHEN ${keyValid} AND ${selected} AND ${textValid} AND type NOT IN ('object','array')
130
+ THEN CASE WHEN type='text' THEN CAST(atom AS BLOB) ELSE atom END END AS value
131
+ FROM records,json_each(records.body,?) WHERE records.kind=? AND records.id=?`);
132
+ queries.set(key,query);
133
+ }
134
+ return query.iterate(...scalarKeys,path,kind,id);
135
+ }
136
+ function project(kind, id, path, shape, type = 'object', atom, textValid = 1) {
137
+ assertExport(++nodes <= IMPORT_LIMITS.maxTokens, 'export_structure_limit');
138
+ if (type === 'null') return null;
139
+ if (shape.scalarOrObject && !['object','array'].includes(type)) return project(kind,id,path,scalar,type,atom,textValid);
140
+ if (shape.scalarOrObject) shape = shape.scalarOrObject;
141
+ if (shape === scalar) {
142
+ assertExport(!['object','array'].includes(type), 'export_unsupported_shape');
143
+ if (type === 'text') { assertExport(textValid === 1, 'export_string_limit'); atom=decodeText(atom); assertExport(atom.length <= IMPORT_LIMITS.maxStringCharacters, 'export_string_limit'); characters += atom.length; }
144
+ else assertExport(['integer','real','true','false'].includes(type) && (typeof atom !== 'number' || Number.isFinite(atom)), 'export_scalar_shape');
145
+ assertExport(characters <= IMPORT_LIMITS.maxProjectionCharacters, 'export_projection_limit');
146
+ return type === 'true' ? true : type === 'false' ? false : atom;
147
+ }
148
+ if (shape.item || shape.emptyArray) {
149
+ assertExport(type === 'array', 'export_unsupported_shape');
150
+ const result = [], seen = new Set();
151
+ for (const entry of children(kind,id,path,shape)) {
152
+ assertExport(entry.keyValid === 1 && !shape.emptyArray && entry.key === result.length && !seen.has(entry.key) && result.length < IMPORT_LIMITS.maxRows, 'export_array_shape');
153
+ seen.add(entry.key); result.push(project(kind,id,jsonPath(path,entry.key),shape.item,entry.type,entry.value,entry.textValid));
154
+ }
155
+ return result;
156
+ }
157
+ assertExport(type === 'object' && shape.fields, 'export_unsupported_shape');
158
+ const result = {}, seen = new Set();
159
+ for (const entry of children(kind,id,path,shape)) {
160
+ assertExport(entry.keyValid === 1,'export_object_shape');
161
+ entry.key=decodeText(entry.key);
162
+ assertExport(typeof entry.key === 'string' && entry.key.length <= IMPORT_LIMITS.maxKeyCharacters && !seen.has(entry.key) && seen.size < IMPORT_LIMITS.maxRows, 'export_object_shape');
163
+ seen.add(entry.key);
164
+ assertExport(++nodes <= IMPORT_LIMITS.maxTokens,'export_structure_limit');
165
+ const next = jsonPath(path,entry.key);
166
+ if (!Object.hasOwn(shape.fields,entry.key)) {
167
+ assertExport(!shape.strict && !authorityKey.test(entry.key), 'export_unknown_authority_shape');
168
+ note(kind,next); continue;
169
+ }
170
+ assertExport(!forbidden.test(entry.key), 'export_excluded_field');
171
+ Object.defineProperty(result,entry.key,{ value: project(kind,id,next,shape.fields[entry.key],entry.type,entry.value,entry.textValid), enumerable:true, writable:true, configurable:true });
172
+ }
173
+ if(shape === provenance) {
174
+ const knownKinds=['direct_user','explicit_feedback','explicit_method_authorization','direct_goal_control','imported_original'];
175
+ const knownTypes=['delivered_answer','tool_receipt','file','file_search','url','artifact'];
176
+ assertExport((knownKinds.includes(result.kind) && result.type === undefined) ||
177
+ (knownTypes.includes(result.type) && result.kind === undefined), 'export_unknown_provenance');
178
+ }
179
+ return result;
180
+ }
181
+ return { project };
182
+ }
183
+ function checkRow(row, columns, cutoff) {
184
+ for (const [body,column] of [['id','id'],['revision','revision'],['createdAt','created_at'],['updatedAt','updated_at']])
185
+ assertExport(row[body] === columns[column], 'export_column_body_mismatch');
186
+ assertExport(typeof row.id === 'string' && row.id.length > 0 && row.id.length <= 512 && positive(row.revision) &&
187
+ positive(row.createdSequence) && positive(row.updatedSequence) && row.createdSequence <= row.updatedSequence && row.updatedSequence <= cutoff,
188
+ 'export_record_identity');
189
+ }
190
+ function referenceGaps(rows) {
191
+ const maps = new Map(LEGACY_COLLECTIONS.map(kind => [kind,new Map()]));
192
+ for (const {kind,record} of rows) maps.get(kind).set(record.id,record);
193
+ const gaps = [], seen = new Set();
194
+ function ref(owner,path,kind,id,revision) {
195
+ if (typeof id !== 'string' || !id) return;
196
+ const target = maps.get(kind)?.get(id), reason = !maps.has(kind) ? 'intentionally_excluded' : !target ? 'missing' : revision !== undefined && target.revision !== revision ? 'revision_mismatch' : null;
197
+ if (reason) { const gap={kind:owner.kind,id:owner.record.id,path,targetKind:kind,targetId:id,reason,...(revision===undefined?{}:{expectedRevision:revision}),...(target?{actualRevision:target.revision}:{})};
198
+ const key=canonicalJson(gap); if(!seen.has(key)){seen.add(key);gaps.push(gap);} }
199
+ }
200
+ function scan(owner,value,path='$') {
201
+ if (!value || typeof value !== 'object') return;
202
+ if (Array.isArray(value)) { value.forEach((item,i)=>scan(owner,item,`${path}[${i}]`)); return; }
203
+ for (const [key,item] of Object.entries(value)) {
204
+ const at=jsonPath(path,key);
205
+ if (key==='dependencies' && Array.isArray(item)) for (const d of item) ref(owner,at,d.kind || 'events',d.id,d.revision);
206
+ else if (['sourceIds','changeSourceIds','eventIds','artifactIds'].includes(key) && Array.isArray(item)) for(const id of item) ref(owner,at,'events',id);
207
+ else if (['understandingIds','methodIds','selectedMethodIds','goalIds'].includes(key) && Array.isArray(item)) for(const id of item) ref(owner,at,key==='understandingIds'?'understandings':key==='goalIds'?'goals':'methods',id);
208
+ else if (key==='selectedVersions' && Array.isArray(item)) for(const v of item) ref(owner,at,'methods',v.id); // historical revision is not the current version
209
+ else if (['sourceId','artifactSourceId','activationSourceId','inputEventId','eventId','feedbackId'].includes(key)) ref(owner,at,'events',item);
210
+ else if (key==='previousId') ref(owner,at,owner.kind==='audit'?(owner.record.kind || (owner.record.goalId?'goals':'audit')):owner.kind,item);
211
+ else if (key==='recordId' && owner.kind==='audit' && owner.record.kind) ref(owner,at,owner.record.kind,item);
212
+ else if (key==='goalId') ref(owner,at,'goals',item);
213
+ else if (key==='methodId') ref(owner,at,'methods',item);
214
+ else if (key==='importId') ref(owner,at,'imports',item);
215
+ else if (key==='goalRef' && item) ref(owner,at,'goals',item.id);
216
+ else if (key==='conversationId') ref(owner,at,'conversations',item);
217
+ else if (key==='targetId' && path.includes('relations')) ref(owner,at,'goals',item);
218
+ else if (key==='deliveryId') ref(owner,at,'deliveries',item);
219
+ else if (key==='opportunityId') ref(owner,at,'opportunities',item);
220
+ else if (['runId','sourceRunId','learnedFromRunId','updatedFromRunId','changedByRunId','preparationRunId','jobId'].includes(key)) ref(owner,at,key==='sourceRunId'&&owner.record.provenance?.kind==='imported_original'?'legacy_runs':'jobs',item);
221
+ else if (key==='toolRecordId') ref(owner,at,'tool_calls',item);
222
+ else if (['toolRecordIds','modelRequestIds'].includes(key) && Array.isArray(item)) for(const id of item) ref(owner,at,key==='toolRecordIds'?'tool_calls':'model_calls',id);
223
+ scan(owner,item,at);
224
+ }
225
+ }
226
+ for(const row of rows) scan(row,row.record);
227
+ return gaps;
228
+ }
229
+
230
+ /** onProgress is an optional local observation hook, never a CLI/config flag;
231
+ * it receives no connection or source data and cannot change the projection.
232
+ * It permits real process-interruption/concurrent-writer boundary tests. */
233
+ export async function exportLegacy3({ sourceDb, output, signal, onProgress } = {}) {
234
+ canonical(sourceDb,true); canonical(output,false);
235
+ const expected = identity(sourceDb);
236
+ try { lstatSync(output); throw Object.assign(new Error('export_output_exists'),{code:'export_output_exists'}); }
237
+ catch(error) { if(error.code!=='ENOENT') throw error; }
238
+ assertExport(!['','-wal','-shm','-journal'].some(suffix=>output===sourceDb+suffix), 'export_output_alias');
239
+ const attempt = mkdtempSync(join(dirname(output),'.arcfold-export-'));
240
+ const producer = { version:LEGACY_EXPORT_VERSION,sha256:sha(readFileSync(fileURLToPath(import.meta.url))),
241
+ consumerSha256:sha(readFileSync(new URL('./legacy-import-source.mjs',import.meta.url))),
242
+ contractsSha256:sha(readFileSync(new URL('./learning-contracts.mjs',import.meta.url))),
243
+ canonicalSourceSha256:sha(readFileSync(new URL('./store.mjs',import.meta.url))) };
244
+ let db, sourceFd, spoolFd, completeFd, published=false, receipt;
245
+ const progress = async phase => { signal?.throwIfAborted(); jsonFile(join(attempt,phase+'.json'),{phase}); await onProgress?.(Object.freeze({phase,attempt})); signal?.throwIfAborted(); };
246
+ try {
247
+ sourceFd=openSync(sourceDb,constants.O_RDONLY | constants.O_NOFOLLOW);
248
+ assert.deepEqual({dev:fstatSync(sourceFd).dev,ino:fstatSync(sourceFd).ino},expected);
249
+ jsonFile(join(attempt,'started.json'),{source:{path:sourceDb,...expected},output,producer});
250
+ sameSource(sourceDb,expected);
251
+ db = new DatabaseSync(sourceDb,{readOnly:true});
252
+ db.exec('PRAGMA query_only=ON; BEGIN');
253
+ const cutoff=schema(db),sqliteVersion=db.prepare('SELECT sqlite_version() AS version').get().version;
254
+ assertExport(!db.prepare(`SELECT 1 FROM records WHERE NOT (${boundedText('kind',IMPORT_LIMITS.maxKeyCharacters)} AND ${boundedText('id',512)}) LIMIT 1`).get(),'export_record_identity');
255
+ assertExport(db.prepare('SELECT COUNT(DISTINCT kind) AS count FROM records').get().count <= IMPORT_LIMITS.maxRows,'export_structure_limit');
256
+ const kindCounts=db.prepare('SELECT CAST(kind AS BLOB) AS kind,COUNT(*) AS count FROM records GROUP BY kind ORDER BY kind').all().map(row=>({...row,kind:decodeText(row.kind)}));
257
+ const counts=Object.fromEntries(LEGACY_COLLECTIONS.map(kind=>[kind,kindCounts.find(r=>r.kind===kind)?.count ?? 0]));
258
+ assertExport(counts.source_controls===0,'export_unsupported_source_controls');
259
+ assertExport(Object.values(counts).reduce((a,b)=>a+b,0)<=IMPORT_LIMITS.maxRows,'export_record_limit');
260
+ const excludedKinds=kindCounts.filter(row=>!LEGACY_COLLECTIONS.includes(row.kind)).map(row=>({kind:row.kind,count:row.count}));
261
+ await progress('snapshot');
262
+ const excludedPaths=new Map(), {project}=projector(db,excludedPaths), rows=[];
263
+ spoolFd=openSync(join(attempt,'rows.jsonl'),'wx',0o600);
264
+ const rowHash=createHash('sha256').update('['); let first=true;
265
+ for(const kind of LEGACY_COLLECTIONS) {
266
+ const createdValid=boundedText('created_at',IMPORT_LIMITS.maxStringCharacters),updatedValid=boundedText('updated_at',IMPORT_LIMITS.maxStringCharacters);
267
+ const query=db.prepare(`SELECT CAST(id AS BLOB) AS id,CASE WHEN typeof(revision)='integer' AND revision>0 AND revision<=9007199254740991 THEN revision END AS revision,
268
+ ${createdValid} AND ${updatedValid} AND typeof(revision)='integer' AND revision>0 AND revision<=9007199254740991 AS metadataValid,
269
+ CASE WHEN ${createdValid} THEN CAST(created_at AS BLOB) END AS created_at,
270
+ CASE WHEN ${updatedValid} THEN CAST(updated_at AS BLOB) END AS updated_at,
271
+ json_valid(body) AS valid,json_type(body) AS type FROM records WHERE kind=? ORDER BY id COLLATE BINARY`);
272
+ for(const columns of query.iterate(kind)) {
273
+ signal?.throwIfAborted(); assertExport(columns.valid===1&&columns.type==='object','export_record_shape');
274
+ assertExport(columns.metadataValid===1,'export_record_identity');
275
+ for(const key of ['id','created_at','updated_at']) columns[key]=decodeText(columns[key]);
276
+ let shape=TABLES[kind];
277
+ if(kind==='audit') {
278
+ const tag = name => {
279
+ const path=`'$.${name}'`,type=`json_type(body,${path})`,value=`json_extract(body,${path})`;
280
+ const valid=`(${type} IS NULL OR ${type}='null' OR (${type}='text' AND ${boundedText(value,IMPORT_LIMITS.maxStringCharacters)}))`;
281
+ return `${valid} AS ${name}Valid,CASE WHEN ${valid} AND ${type}='text' THEN CAST(${value} AS BLOB) END AS ${name}`;
282
+ };
283
+ const tags=db.prepare(`SELECT ${tag('kind')},${tag('goalId')} FROM records WHERE kind='audit' AND id=?`).get(columns.id);
284
+ assertExport(tags.kindValid===1&&tags.goalIdValid===1,'export_audit_target_shape');
285
+ const target=tags.kind===null ? tags.goalId!==null && decodeText(tags.goalId) ? 'goals' : null : decodeText(tags.kind);
286
+ // Only known business snapshots; jobs/config audit bodies stay inside SQLite.
287
+ shape=object({...TABLES.audit.fields,...(TABLES[target] && !['audit','imports'].includes(target)?{before:TABLES[target],after:TABLES[target]}:{})});
288
+ }
289
+ const row=project(kind,columns.id,'$',shape); checkRow(row,columns,cutoff);
290
+ const envelope={type:'record',kind,record:row}, text=canonicalJson(envelope);
291
+ rowHash.update((first?'':',')+text);first=false;writeSync(spoolFd,text+'\n');rows.push(envelope);
292
+ }
293
+ assertExport(rows.filter(row=>row.kind===kind).length===counts[kind],'export_count_changed');
294
+ }
295
+ fsyncSync(spoolFd); closeSync(spoolFd);spoolFd=undefined;
296
+ const digest=rowHash.update(']').digest('hex');
297
+ const excluded={kinds:excludedKinds,paths:[...excludedPaths].map(([key,count])=>{const[kind,path]=JSON.parse(key);return{kind,path,count};}).sort((a,b)=>canonicalJson(a).localeCompare(canonicalJson(b)))};
298
+ const gaps=referenceGaps(rows);
299
+ const snapshotDigest=importDigest({projection:LEGACY_EXPORT_VERSION,schemaVersion:2,mutationSequence:cutoff,digest});
300
+ const header={type:'header',format:'arcfold-legacy3-business-export-v1',exportId:'legacy3-'+snapshotDigest,
301
+ exporter:producer,source:{snapshotDigest,identity:'business_snapshot_not_persistent_lineage_or_authorship'},
302
+ schemaVersion:2,mutationSequence:cutoff,collections:LEGACY_COLLECTIONS,excluded};
303
+ const footer={type:'footer',complete:true,counts,digest};
304
+ sameSource(sourceDb,expected); db.exec('COMMIT');db.close();db=undefined;
305
+ await progress('projected');
306
+ completeFd=openSync(join(attempt,'complete.jsonl'),'wx',0o600);
307
+ writeSync(completeFd,canonicalJson(header)+'\n');
308
+ for await(const chunk of createReadStream(join(attempt,'rows.jsonl'),{highWaterMark:65536})) { signal?.throwIfAborted();writeSync(completeFd,chunk); }
309
+ await progress('before-footer');
310
+ writeSync(completeFd,canonicalJson(footer)+'\n');fsyncSync(completeFd);closeSync(completeFd);completeFd=undefined;
311
+ const fileHash=createHash('sha256');let bytes=0;
312
+ for await(const chunk of createReadStream(join(attempt,'complete.jsonl'),{highWaterMark:65536})) { bytes+=chunk.length;fileHash.update(chunk);assertExport(bytes<=IMPORT_LIMITS.maxFileBytes,'export_file_limit'); }
313
+ const fileSha256=fileHash.digest('hex');
314
+ // The existing consumer remains the single final capacity/format authority.
315
+ await readLegacyImport({stream:createReadStream(join(attempt,'complete.jsonl'),{highWaterMark:IMPORT_LIMITS.maxChunkBytes}),expectedBytes:bytes,expectedSha256:fileSha256,signal});
316
+ receipt={schema:'arcfold-legacy3-export-receipt-v1',source:{path:sourceDb,...expected,sqliteVersion},producer,
317
+ schemaVersion:2,mutationSequence:cutoff,snapshotDigest,counts,excluded,referenceGaps:gaps,output:{path:output,bytes,sha256:fileSha256},attempt};
318
+ jsonFile(join(attempt,'validated.json'),receipt);
319
+ await progress('before-publication');sameSource(sourceDb,expected);canonical(output,false);
320
+ linkSync(join(attempt,'complete.jsonl'),output);published=true;fsyncDirectory(dirname(output));
321
+ await progress('published');
322
+ jsonFile(join(attempt,'receipt.json'),{...receipt,complete:true});fsyncDirectory(attempt);
323
+ return {...receipt,complete:true};
324
+ } catch(error) {
325
+ // Never serialize arbitrary SQLite/source text or observer errors.
326
+ try {jsonFile(join(attempt,'failure.json'),{complete:false,published,code:typeof error.code==='string'?error.code:'export_failed'});} catch { /* original failure remains */ }
327
+ throw Object.assign(new Error('Legacy export failed; private attempt retained at ' + attempt,{cause:error}),{code:typeof error.code==='string'?error.code:'export_failed',attempt,published});
328
+ } finally {
329
+ if(spoolFd!==undefined)closeSync(spoolFd);if(completeFd!==undefined)closeSync(completeFd);
330
+ try{db?.close();}finally{if(sourceFd!==undefined)closeSync(sourceFd);}
331
+ }
332
+ }