@awebai/oats 0.29.4 → 0.30.1

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 (263) hide show
  1. package/README.md +12 -6
  2. package/bin/oats.mjs +194 -50
  3. package/docs/capabilities.md +160 -171
  4. package/docs/capability-manifest.schema.json +6 -11
  5. package/docs/configuration.md +213 -64
  6. package/docs/design/2026-09-16-knowledge-capability-contract.md +36 -50
  7. package/docs/design/2026-09-23-workspace-module-contracts.md +377 -544
  8. package/docs/design/2026-09-26-okf-knowledge-operations.md +132 -357
  9. package/docs/design/2026-09-27-team-model-v2.md +97 -117
  10. package/docs/design/2026-09-28-automations-trust.md +38 -0
  11. package/docs/design/2026-09-28-soul-launch-preference.md +63 -0
  12. package/docs/design/HISTORY.md +65 -0
  13. package/docs/design/README.md +23 -54
  14. package/docs/desktop-cli-api.md +1787 -1777
  15. package/docs/desktop.md +30 -91
  16. package/docs/execution-targets.md +146 -292
  17. package/docs/first-team.md +31 -17
  18. package/docs/implementation.md +77 -288
  19. package/docs/integrations.md +118 -320
  20. package/docs/knowledge-capability-authoring.md +25 -52
  21. package/docs/knowledge-reference/acceptance.md +3 -3
  22. package/docs/knowledge-reference/adoption.md +1 -1
  23. package/docs/knowledge-reference/harvester.md +2 -2
  24. package/docs/knowledge-reference/package-craft.md +3 -3
  25. package/docs/knowledge-reference/provider-mapping.md +3 -6
  26. package/docs/knowledge-reference/reader-capture.md +3 -3
  27. package/docs/knowledge-theory.md +62 -166
  28. package/docs/knowledge.md +225 -404
  29. package/docs/layers.md +42 -97
  30. package/docs/oats-local.schema.json +58 -5
  31. package/docs/oats-membership.schema.json +1 -8
  32. package/docs/oats-package.schema.json +5 -5
  33. package/docs/oats-workspace.schema.json +8 -22
  34. package/docs/official-catalog.md +25 -28
  35. package/docs/packages.md +45 -63
  36. package/docs/plans/0.30-close-out.md +83 -0
  37. package/docs/release-lane.md +82 -0
  38. package/docs/release-notes/oats-framework-v1.1.3.md +10 -8
  39. package/docs/release-notes/v0.19.0.md +48 -147
  40. package/docs/release-notes/v0.19.1.md +2 -3
  41. package/docs/release-notes/v0.19.3.md +2 -15
  42. package/docs/release-notes/v0.20.0.md +0 -15
  43. package/docs/release-notes/v0.22.0.md +71 -138
  44. package/docs/release-notes/v0.22.1.md +42 -90
  45. package/docs/release-notes/v0.22.10.md +1 -1
  46. package/docs/release-notes/v0.22.11.md +1 -47
  47. package/docs/release-notes/v0.22.12.md +4 -13
  48. package/docs/release-notes/v0.22.13.md +1 -42
  49. package/docs/release-notes/v0.22.14.md +3 -11
  50. package/docs/release-notes/v0.22.15.md +1 -46
  51. package/docs/release-notes/v0.22.16.md +6 -8
  52. package/docs/release-notes/v0.22.18.md +1 -99
  53. package/docs/release-notes/v0.22.19.md +3 -14
  54. package/docs/release-notes/v0.22.2.md +6 -15
  55. package/docs/release-notes/v0.22.3.md +0 -1
  56. package/docs/release-notes/v0.22.4.md +1 -14
  57. package/docs/release-notes/v0.22.5.md +2 -12
  58. package/docs/release-notes/v0.22.6.md +0 -3
  59. package/docs/release-notes/v0.23.0.md +9 -25
  60. package/docs/release-notes/v0.23.1.md +9 -25
  61. package/docs/release-notes/v0.23.2.md +2 -4
  62. package/docs/release-notes/v0.24.0.md +56 -97
  63. package/docs/release-notes/v0.24.1.md +7 -11
  64. package/docs/release-notes/v0.24.10.md +34 -45
  65. package/docs/release-notes/v0.24.11.md +12 -20
  66. package/docs/release-notes/v0.24.12.md +35 -48
  67. package/docs/release-notes/v0.24.13.md +34 -41
  68. package/docs/release-notes/v0.24.2.md +9 -13
  69. package/docs/release-notes/v0.24.3.md +7 -11
  70. package/docs/release-notes/v0.24.4.md +6 -6
  71. package/docs/release-notes/v0.24.5.md +6 -10
  72. package/docs/release-notes/v0.24.6.md +2 -5
  73. package/docs/release-notes/v0.24.7.md +46 -75
  74. package/docs/release-notes/v0.24.8.md +58 -96
  75. package/docs/release-notes/v0.24.9.md +38 -54
  76. package/docs/release-notes/v0.25.0.md +59 -76
  77. package/docs/release-notes/v0.25.1.md +57 -81
  78. package/docs/release-notes/v0.25.2.md +51 -70
  79. package/docs/release-notes/v0.25.3.md +11 -13
  80. package/docs/release-notes/v0.25.4.md +9 -13
  81. package/docs/release-notes/v0.25.5.md +3 -5
  82. package/docs/release-notes/v0.25.6.md +20 -29
  83. package/docs/release-notes/v0.25.7.md +5 -7
  84. package/docs/release-notes/v0.25.8.md +26 -39
  85. package/docs/release-notes/v0.26.0.md +175 -646
  86. package/docs/release-notes/v0.27.0.md +4 -5
  87. package/docs/release-notes/v0.27.1.md +4 -6
  88. package/docs/release-notes/v0.27.2.md +1 -1
  89. package/docs/release-notes/v0.28.0.md +57 -124
  90. package/docs/release-notes/v0.29.0.md +89 -208
  91. package/docs/release-notes/v0.29.1.md +1 -1
  92. package/docs/release-notes/v0.29.2.md +3 -4
  93. package/docs/release-notes/v0.30.0.md +205 -0
  94. package/docs/release-notes/v0.30.1.md +123 -0
  95. package/docs/schedules.md +280 -363
  96. package/docs/servers.md +99 -117
  97. package/docs/soul.schema.json +2 -9
  98. package/docs/souls-and-instances.md +145 -158
  99. package/docs/workspaces.md +137 -215
  100. package/lib/automations.mjs +21 -6
  101. package/lib/core.mjs +226 -74
  102. package/lib/instance-events.mjs +1 -1
  103. package/lib/instance-inspect.mjs +109 -34
  104. package/lib/instance-lifecycle.mjs +14 -1
  105. package/lib/instance-resolution.mjs +26 -27
  106. package/lib/launch-preference.mjs +87 -0
  107. package/lib/materialize.mjs +3 -3
  108. package/lib/packages.mjs +1 -1
  109. package/lib/resolve.mjs +30 -88
  110. package/lib/schedule.mjs +1 -1
  111. package/lib/teams-verbs.mjs +195 -0
  112. package/lib/teams.mjs +190 -0
  113. package/lib/triggers.mjs +2 -2
  114. package/lib/workspace.mjs +54 -147
  115. package/package-catalog.json +10 -16
  116. package/package.json +1 -3
  117. package/skills/oats-getting-started/SKILL.md +25 -13
  118. package/capabilities/oats-authoring/LICENSE +0 -21
  119. package/capabilities/oats-authoring/oats-package.json +0 -11
  120. package/capabilities/oats-authoring/oats.json +0 -12
  121. package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +0 -84
  122. package/capabilities/oats-authoring/skills/skill-craft/SKILL.md +0 -109
  123. package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +0 -116
  124. package/capabilities/oats-aweb/bin/oats-aweb-binding.mjs +0 -11
  125. package/capabilities/oats-aweb/bin/oats-aweb.mjs +0 -1338
  126. package/capabilities/oats-aweb/injects/aweb.md +0 -47
  127. package/capabilities/oats-aweb/lib/binding-wire.mjs +0 -356
  128. package/capabilities/oats-aweb/lib/captured-execution.mjs +0 -91
  129. package/capabilities/oats-aweb/lib/captured-native.mjs +0 -91
  130. package/capabilities/oats-aweb/lib/grant-custody.mjs +0 -38
  131. package/capabilities/oats-aweb/lib/invocation-shape.mjs +0 -135
  132. package/capabilities/oats-aweb/lib/portable-binding.mjs +0 -146
  133. package/capabilities/oats-aweb/lib/session-readiness.mjs +0 -56
  134. package/capabilities/oats-aweb/lib/wake-receive.mjs +0 -56
  135. package/capabilities/oats-aweb/oats.json +0 -208
  136. package/capabilities/oats-aweb/skills/LICENSE +0 -21
  137. package/capabilities/oats-aweb/skills/VENDORED.md +0 -31
  138. package/capabilities/oats-aweb/skills/aweb-identity/SKILL.md +0 -201
  139. package/capabilities/oats-aweb/skills/aweb-messaging/SKILL.md +0 -161
  140. package/capabilities/oats-aweb/skills/aweb-messaging/references/messaging-scenarios.md +0 -61
  141. package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +0 -116
  142. package/capabilities/oats-aweb/skills/aweb-team-membership/references/team-membership-reference.md +0 -74
  143. package/capabilities/oats-aweb/skills/oats-aweb/SKILL.md +0 -216
  144. package/capabilities/oats-jira/bin/oats-jira.mjs +0 -40
  145. package/capabilities/oats-jira/injects/jira.md +0 -10
  146. package/capabilities/oats-jira/oats.json +0 -22
  147. package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +0 -179
  148. package/capabilities/oats-linear/bin/oats-linear-hook.mjs +0 -34
  149. package/capabilities/oats-linear/bin/oats-linear.mjs +0 -344
  150. package/capabilities/oats-linear/injects/linear.md +0 -8
  151. package/capabilities/oats-linear/oats.json +0 -24
  152. package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +0 -223
  153. package/capabilities/oats-okf/bin/oats-okf-binding.mjs +0 -14
  154. package/capabilities/oats-okf/bin/oats-okf.mjs +0 -209
  155. package/capabilities/oats-okf/injects/okf.md +0 -42
  156. package/capabilities/oats-okf/lib/binding-wire.mjs +0 -348
  157. package/capabilities/oats-okf/lib/captured-worker.mjs +0 -109
  158. package/capabilities/oats-okf/lib/config.mjs +0 -124
  159. package/capabilities/oats-okf/lib/consult.mjs +0 -518
  160. package/capabilities/oats-okf/lib/harvest-status.mjs +0 -88
  161. package/capabilities/oats-okf/lib/harvest-switch.mjs +0 -94
  162. package/capabilities/oats-okf/lib/inspection.mjs +0 -119
  163. package/capabilities/oats-okf/lib/invocation-context.mjs +0 -111
  164. package/capabilities/oats-okf/lib/invocation-shape.mjs +0 -135
  165. package/capabilities/oats-okf/lib/io.mjs +0 -118
  166. package/capabilities/oats-okf/lib/migration.mjs +0 -137
  167. package/capabilities/oats-okf/lib/okf-validate.mjs +0 -123
  168. package/capabilities/oats-okf/lib/portable-binding.mjs +0 -199
  169. package/capabilities/oats-okf/lib/source-contract.mjs +0 -46
  170. package/capabilities/oats-okf/lib/sources.mjs +0 -424
  171. package/capabilities/oats-okf/lib/stores.mjs +0 -473
  172. package/capabilities/oats-okf/lib/worker.mjs +0 -497
  173. package/capabilities/oats-okf/oats.json +0 -148
  174. package/capabilities/oats-okf/schemas/okf-base.schema.json +0 -46
  175. package/capabilities/oats-okf/schemas/okf-bindings.schema.json +0 -112
  176. package/capabilities/oats-okf/schemas/okf-portable-declaration.schema.json +0 -87
  177. package/capabilities/oats-okf/schemas/okf-portable-payload.schema.json +0 -113
  178. package/capabilities/oats-okf/schemas/okf-soul.schema.json +0 -37
  179. package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +0 -144
  180. package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +0 -86
  181. package/capabilities/oats-okf/skills/okf-instance-knowledge/SKILL.md +0 -104
  182. package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +0 -140
  183. package/capabilities/oats-okf-harvest/injects/harvester.md +0 -12
  184. package/capabilities/oats-okf-harvest/oats.json +0 -26
  185. package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +0 -168
  186. package/capabilities/oats-okf-harvest/skills/knowledge-theory/SKILL.md +0 -192
  187. package/capabilities/oats-okf-harvest/skills/okf-authoring/SKILL.md +0 -151
  188. package/capabilities/oats-okf-harvest/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
  189. package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +0 -170
  190. package/capabilities/oats-okf-maintenance/injects/maintainer.md +0 -12
  191. package/capabilities/oats-okf-maintenance/lib/provenance.mjs +0 -50
  192. package/capabilities/oats-okf-maintenance/oats.json +0 -21
  193. package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +0 -159
  194. package/capabilities/oats-okf-maintenance/skills/knowledge-theory/SKILL.md +0 -192
  195. package/capabilities/oats-okf-maintenance/skills/okf-authoring/SKILL.md +0 -151
  196. package/capabilities/oats-okf-maintenance/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
  197. package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +0 -146
  198. package/capabilities/oats-review/injects/review.md +0 -69
  199. package/capabilities/oats-review/oats.json +0 -10
  200. package/capabilities/oats-review/skills/code-review/SKILL.md +0 -44
  201. package/capabilities/oats-review/skills/security-review/SKILL.md +0 -59
  202. package/docs/conventions.md +0 -90
  203. package/docs/design/2026-09-07-architecture-reassessment.md +0 -131
  204. package/docs/design/2026-09-07-desktop-souls-capabilities.md +0 -50
  205. package/docs/design/2026-09-07-mobile-agent-management-proposal.md +0 -228
  206. package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +0 -558
  207. package/docs/design/2026-09-13-knowledge-and-memory-direction.md +0 -744
  208. package/docs/design/2026-09-13-knowledge-implementation.md +0 -127
  209. package/docs/design/2026-09-13-knowledge-location-contract.md +0 -340
  210. package/docs/design/2026-09-14-artifact-retention-contract.md +0 -190
  211. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +0 -708
  212. package/docs/design/2026-09-14-portable-souls-contract-amendments.md +0 -85
  213. package/docs/design/2026-09-14-portable-souls-explainer.md +0 -750
  214. package/docs/design/2026-09-15-captured-dispatch.md +0 -127
  215. package/docs/design/2026-09-15-captured-resolution-records.md +0 -143
  216. package/docs/design/2026-09-15-package-preparation.md +0 -100
  217. package/docs/design/2026-09-15-portable-data-contract.md +0 -121
  218. package/docs/design/2026-09-15-portable-declarations.md +0 -189
  219. package/docs/design/2026-09-15-portable-souls-handoff.md +0 -150
  220. package/docs/design/2026-09-15-portable-souls-implementation.md +0 -417
  221. package/docs/design/2026-09-15-selection-lock-and-approval.md +0 -122
  222. package/docs/design/2026-09-15-source-observation.md +0 -119
  223. package/docs/design/2026-09-16-captured-admission.md +0 -77
  224. package/docs/design/2026-09-16-captured-helper-dispatch.md +0 -105
  225. package/docs/design/2026-09-16-captured-launch-inputs.md +0 -42
  226. package/docs/design/2026-09-16-command-profile-preparation.md +0 -86
  227. package/docs/design/2026-09-16-fresh-install-first-rollout.md +0 -47
  228. package/docs/design/2026-09-16-fresh-operator-walkthrough.md +0 -282
  229. package/docs/design/2026-09-16-messaging-capability-contract.md +0 -59
  230. package/docs/design/2026-09-16-portable-migration-evidence.md +0 -158
  231. package/docs/design/2026-09-16-portable-onboarding.md +0 -179
  232. package/docs/design/2026-09-16-prepare-request-transport.md +0 -26
  233. package/docs/design/2026-09-16-provider-binding-codecs.md +0 -98
  234. package/docs/design/2026-09-16-provider-binding-wire.md +0 -274
  235. package/docs/design/2026-09-17-capability-helper-input-contract.md +0 -95
  236. package/docs/design/2026-09-17-captured-backend-parity.md +0 -53
  237. package/docs/design/2026-09-17-captured-native-start.md +0 -58
  238. package/docs/design/2026-09-17-portable-boundary-hookup.md +0 -19
  239. package/docs/design/2026-09-17-portable-boundary-resources.md +0 -52
  240. package/docs/design/2026-09-17-public-captured-start.md +0 -108
  241. package/docs/design/2026-09-17-public-prepare-request.md +0 -90
  242. package/docs/design/2026-09-18-captured-pi-host.md +0 -205
  243. package/docs/design/2026-09-18-first-cut-release-checklist.md +0 -131
  244. package/docs/design/2026-09-18-herdr-protocol-compatibility.md +0 -60
  245. package/docs/design/2026-09-20-redesign-program-board.md +0 -142
  246. package/docs/design/2026-09-20-workspace-and-portable-adoption-plan.md +0 -289
  247. package/docs/design/2026-09-20-workspace-onboarding-public.md +0 -207
  248. package/docs/design/2026-09-22-desktop-parity-seams.md +0 -58
  249. package/docs/design/2026-09-23-simplified-workspace-model.md +0 -711
  250. package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +0 -65
  251. package/docs/design/2026-09-24-desktop-phase-f-boundary.md +0 -242
  252. package/docs/design/2026-09-24-phase-d-plan.md +0 -305
  253. package/docs/design/2026-09-25-teams-contract.md +0 -258
  254. package/docs/design/2026-09-26-desktop-design-brief-architecture.md +0 -241
  255. package/docs/design/desktop-ux-plan.md +0 -362
  256. package/docs/design/launch-configurations.md +0 -168
  257. package/docs/design/okf-mirror-provenance.md +0 -105
  258. package/docs/design/operations-contract.md +0 -141
  259. package/docs/oats-member.schema.json +0 -38
  260. package/skills/integration-authoring/SKILL.md +0 -84
  261. package/skills/oats-support/SKILL.md +0 -79
  262. package/skills/skill-craft/SKILL.md +0 -109
  263. package/skills/soul-craft/SKILL.md +0 -116
@@ -1,137 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { fs, join, dirname, resolve, safePath, readJSON, save, atomic, tree, materialize, digest, hash, overlaps, withLock, fail, identifier } from './io.mjs';
3
- import { metadata, noGit, validateDeclaration, loadBindings, resolveNodes, splitRef } from './config.mjs';
4
- import { stageBase, validateBase, baseLock, journalPath, gitPublish, directoryPublish } from './stores.mjs';
5
- const b64=s=>Buffer.from(s).toString('base64');
6
- export function initBase(bindings,alias,nodesFile,output,{confirm=false}={}) {
7
- const base=Object.hasOwn(bindings.bases,alias)?bindings.bases[alias]:null;if(!base) fail('E_CONFIG','unknown base');
8
- const nodes=readJSON(nodesFile);const files={'okf-base.json':b64(JSON.stringify({version:1,id:base.id,nodes},null,2)+'\n'),'index.md':b64('---\nokf_version: "0.1"\n---\n\n# Knowledge base\n\n'+Object.entries(nodes).map(([n,s])=>`* [${n}](${s.path}/index.md) - owned by ${s.owner}.`).join('\n')+'\n'),'log.md':b64('# Knowledge log\n')};
9
- for(const [n,s] of Object.entries(nodes)) {identifier(n);files[`${s.path}/index.md`]=b64(`# ${n}\n`);files[`${s.path}/log.md`]=b64(`# ${n} log\n`);}
10
- metadata(files,base);
11
- const dest=safePath(output || (base.kind==='directory' && confirm ? base.path : fail('E_USAGE','init requires --output for a staged Git/operator proposal, or --confirm for a new directory base')));
12
- if(fs.existsSync(dest)) fail('E_BASE','init destination exists; never overwrite knowledge');
13
- for(const b of Object.values(bindings.bases)) {
14
- const path=b.kind==='directory'?b.path:(b.repository.startsWith('/')?resolve(b.repository,b.root):null);
15
- if(path && overlaps(path,dest) && !(b===base && b.kind==='directory' && dest===path && confirm)) fail('E_PATH','init stage overlaps accepted base');
16
- }
17
- if(overlaps(dest,bindings.stateDir)) fail('E_PATH','init cannot overlap durable state');
18
- const write=()=>{materialize(dest,files);validateBase(dest,base);};
19
- if(base.kind==='directory' && dest===base.path) {if(!confirm) fail('E_USAGE','directory provisioning needs --confirm');noGit(dest);withLock(baseLock(base),write);}
20
- else write();
21
- return {status:base.kind==='directory' && dest===base.path?'accepted':'staged',path:dest,next:base.kind==='git'?'Commit this initialized bundle in an operator-owned checkout and deliver through a reviewed PR before activating sources.':null};
22
- }
23
- export function migrate(bindings,{legacy,alias,node,output}) {
24
- const base=Object.hasOwn(bindings.bases,alias)?bindings.bases[alias]:null;if(!base) fail('E_CONFIG','unknown base');identifier(node);legacy=safePath(legacy);output=safePath(output);
25
- if(overlaps(legacy,output) || overlaps(legacy,bindings.stateDir) || overlaps(output,bindings.stateDir)) fail('E_PATH','migration source, stage and state must be disjoint');
26
- // Check ALL configured custody, before even creating preservation/staging
27
- // state. A destination alias does not grant writes to another accepted base.
28
- for(const b of Object.values(bindings.bases)) {
29
- const paths=b.kind==='directory'?[b.path,baseLock(b),journalPath(b)]:(b.repository.startsWith('/')?[resolve(b.repository,b.root)]:[]);
30
- if(paths.some(path=>overlaps(path,output))) fail('E_PATH','migration stage overlaps configured accepted base or coordination artifacts');
31
- }
32
- const original=tree(legacy);
33
- // Stage the accepted base before any record exists: a base that cannot be
34
- // read leaves nothing behind. From the record on, a failure is recorded in
35
- // it rather than left as an unexplained directory.
36
- const stage=stageBase(base,output,{alias});
37
- fs.mkdirSync(bindings.stateDir,{recursive:true,mode:0o700});
38
- const id=randomUUID();const dir=join(bindings.stateDir,'migrations',id);fs.mkdirSync(dir,{recursive:true,mode:0o700});
39
- save(join(dir,'legacy.json'),original); // byte-preserving backup BEFORE any delivery
40
- const recordFile=join(dir,'migration.json');
41
- try {
42
- const spec=stage.meta.nodes[node];if(!spec) fail('E_OWNER','migration node must be explicitly provisioned first');
43
- const prefix=spec.path+'/';
44
- if(Object.keys(stage.files).some(p=>p.startsWith(prefix) && ![prefix+'index.md',prefix+'log.md'].includes(p))) fail('E_MIGRATION','target node is not empty; merge migration requires human judgment');
45
- const after={...stage.files};for(const p of Object.keys(after)) if(p.startsWith(prefix)) delete after[p];
46
- for(const [p,content] of Object.entries(original)) {
47
- if(!p.endsWith('.md')) fail('E_MIGRATION',`legacy non-Markdown artifact requires manual migration: ${p}`);
48
- let text=Buffer.from(content,'base64').toString('utf8');
49
- text=text.replace(/\]\(\/(?!\/)([^)]+)\)/g,`](/${spec.path}/$1)`);
50
- if(p==='index.md') text=text.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/,'');
51
- after[prefix+p]=b64(text);
52
- }
53
- after[prefix+'log.md'] ||= b64(`# ${node} log\n`);
54
- // Replace only the empty pre-provisioned node, not unrelated accepted bytes.
55
- fs.rmSync(join(stage.root,spec.path),{recursive:true});materialize(stage.root,after);validateBase(stage.root,base);
56
- const proposalFile=join(dir,'proposal.json');const proposal={version:1,run:id,created:new Date().toISOString(),file:proposalFile,before:stage.files,after};save(proposalFile,proposal);
57
- const record={version:1,id,alias,node,base,owner:spec.owner,legacy,originalDigest:digest(original),stage:{root:stage.root,checkout:stage.checkout,head:stage.head},proposal:proposalFile,proposalHash:hash(proposal),receipt:{status:'staged'}};save(recordFile,record);
58
- return {status:'staged',migration:recordFile,originalPreserved:true,stage:stage.root};
59
- } catch(e) {
60
- save(recordFile,{version:1,id,alias,node,base,legacy,originalDigest:digest(original),receipt:{status:'failed',error:e.message,code:e.code||'E_MIGRATION',at:new Date().toISOString()}});
61
- throw e;
62
- }
63
- }
64
- /** Remove a migration record that never delivered anything (staged or failed).
65
- * The backup it holds is the legacy bundle, which still exists at its source;
66
- * a delivered record is custody and stays. */
67
- export function forgetMigration(bindings,id) {
68
- identifier(id);const dir=join(bindings.stateDir,'migrations',id);const file=join(dir,'migration.json');
69
- if(!fs.existsSync(dir)) fail('E_MIGRATION',`unknown migration ${id}`);
70
- const status=fs.existsSync(file)?readJSON(file).receipt?.status:'incomplete';
71
- if(!['failed','staged','incomplete'].includes(status)) fail('E_MIGRATION',`migration ${id} is ${status}: a delivered migration is custody and cannot be forgotten`);
72
- fs.rmSync(dir,{recursive:true,force:true});
73
- return {status:'forgotten',id,was:status};
74
- }
75
- export function deliverMigration(file) {
76
- safePath(file);const m=readJSON(file);const proposal=readJSON(m.proposal);if(hash(proposal)!==m.proposalHash) fail('E_MIGRATION','migration proposal changed');
77
- const persist=()=>save(file,m);
78
- if(m.receipt.status==='accepted' && m.base.kind==='directory' && !fs.existsSync(`${m.base.path}.okf-publication.json`)) return m.receipt;
79
- if(m.base.kind==='git') gitPublish(m.base,m.stage,proposal,m.receipt,persist);
80
- else directoryPublish(m.base,proposal,m.receipt,persist);
81
- return m.receipt;
82
- }
83
- export function cutoverMigration(file,soul) {
84
- safePath(file);soul=safePath(soul);const m=readJSON(file);
85
- if(m.receipt.status!=='accepted') fail('E_MIGRATION','cutover requires provider-confirmed acceptance; deliver/inspect merged PR first');
86
- if(resolve(m.legacy)!==join(soul,'knowledge')) fail('E_MIGRATION','legacy path must be this soul/knowledge');
87
- if(m.cutover?.status==='complete') return {status:'complete',backup:join(dirname(file),'original')};
88
- const savedOriginal=join(dirname(file),'original');
89
- const live=fs.existsSync(m.legacy)?m.legacy:(m.cutover?.status==='intent' && fs.existsSync(savedOriginal)?savedOriginal:m.legacy);
90
- if(digest(tree(live))!==m.originalDigest) fail('E_MIGRATION','legacy bundle changed since staging; preserve and migrate the new version');
91
- const original=readJSON(join(dirname(file),'legacy.json'));if(digest(original)!==m.originalDigest) fail('E_MIGRATION','backup differs');
92
- const declarationFile=join(soul,'okf.json');const ref=`${m.alias}/${m.node}`;
93
- let decl=fs.existsSync(declarationFile)?validateDeclaration(readJSON(declarationFile)):{version:1,owner:m.owner,owns:[],reads:[]};
94
- if(decl.owner!==m.owner) fail('E_OWNER','existing soul declaration owner differs');
95
- decl={...decl,owns:[...new Set([...decl.owns,ref])],reads:[...new Set([...decl.reads,ref])]};
96
- // The declaration resolves through TODAY's bindings, not the frozen delivery
97
- // locator. Validate it before any cutover intent, archive or soul edit.
98
- const bindings=loadBindings();
99
- if(!Object.hasOwn(bindings.bases,m.alias) || hash(bindings.bases[m.alias])!==hash(m.base)) fail('E_MIGRATION','current migration alias differs from frozen delivered base');
100
- const proposal=readJSON(m.proposal);
101
- if(hash(proposal)!==m.proposalHash) fail('E_MIGRATION','migration proposal changed');
102
- const frozen=metadata(proposal.after,m.base).nodes[m.node];
103
- if(!frozen || frozen.owner!==m.owner) fail('E_OWNER','migration proposal owner differs');
104
- const accepted={};
105
- const scratch=fs.mkdtempSync(join(bindings.stateDir,'cutover-check-'));
106
- try {
107
- for(const alias of new Set([...decl.owns,...decl.reads].map(r=>splitRef(r)[0]))) {
108
- if(!Object.hasOwn(bindings.bases,alias)) fail('E_CONFIG',`unresolved base: ${alias}`);
109
- const staged=stageBase(bindings.bases[alias],join(scratch,alias),{alias});
110
- accepted[alias]=staged.meta;
111
- if(alias===m.alias) {
112
- if(hash(staged.meta.nodes[m.node] || null)!==hash(frozen)) fail('E_OWNER','accepted migration ownership/path differs from delivered node');
113
- const nodeFiles=files=>Object.fromEntries(Object.entries(files).filter(([p])=>p.startsWith(frozen.path+'/')));
114
- if(digest(nodeFiles(staged.files))!==digest(nodeFiles(proposal.after))) fail('E_MIGRATION','accepted migration node no longer matches delivered content; inspect and reconcile before cutover');
115
- }
116
- }
117
- resolveNodes(decl,bindings,accepted);
118
- } finally {fs.rmSync(scratch,{recursive:true,force:true});}
119
- m.cutover={status:'intent',soul};save(file,m);save(join(soul,'.okf-cutover.json'),{migration:file});
120
- // Atomic rename, not deletion. Cross-device moves deliberately fail closed;
121
- // the operator can keep the preserved bundle and arrange explicit cutover.
122
- if(live===m.legacy) fs.renameSync(m.legacy,savedOriginal);
123
- save(declarationFile,decl);fs.rmSync(join(soul,'.okf-cutover.json'));m.cutover.status='complete';save(file,m);
124
- return {status:'complete',backup:join(dirname(file),'original'),next:'Update old soul/knowledge references in soul instructions to knowledge/bases/<alias>/<node>. Skills are unchanged.'};
125
- }
126
-
127
- export function migrateSource(bindings, home) {
128
- home=safePath(home);
129
- if(overlaps(home,bindings.stateDir)) fail('E_PATH','legacy source overlaps durable state');
130
- const files={};
131
- for(const name of ['STATE.md','log.md','.okf-harvest-record.json','.okf-harvest-record.next.json']) if(fs.existsSync(join(home,name))) {safePath(join(home,name));files[name]=fs.readFileSync(join(home,name)).toString('base64');}
132
- if(fs.existsSync(join(home,'notes'))) for(const [p,b] of Object.entries(tree(join(home,'notes')))) files['notes/'+p]=b;
133
- const id=randomUUID(),file=join(bindings.stateDir,'migrations',id,'legacy-source.json');
134
- save(file,{version:1,home,files,digest:digest(files)});
135
- save(join(home,'.okf-v1-migration.json'),{version:1,backup:file,digest:digest(files)});
136
- return {status:'preserved',backup:file,next:'Migrate any soul/knowledge bundle first; harvest re-registers this source and captures all visible evidence. Old v1 cursors are preserved, NOT accepted as v2 processing proof.'};
137
- }
@@ -1,123 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * okf-validate.mjs — OKF v0.1 bundle validator (no dependencies).
4
- *
5
- * Usage: node okf-validate.mjs <bundle-dir> [--strict] [--json]
6
- *
7
- * Conformance (errors): frontmatter parses; non-empty `type` on concepts;
8
- * reserved files (index.md, log.md) carry no `type`.
9
- * Producer lints (--strict, warnings): broken intra-bundle links (log.md exempt),
10
- * links missing .md, concepts unreachable from any index.md, missing title/description.
11
- * Exit: 0 conformant, 1 errors (or warnings with --strict), 2 usage.
12
- */
13
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
14
- import { join, relative, resolve, dirname, posix } from "node:path";
15
-
16
- const args = process.argv.slice(2);
17
- const strict = args.includes("--strict");
18
- const asJson = args.includes("--json");
19
- const dir = args.find((a) => !a.startsWith("--"));
20
- if (!dir || !existsSync(dir)) { console.error("usage: okf-validate.mjs <bundle-dir> [--strict] [--json]"); process.exit(2); }
21
- const root = resolve(dir);
22
-
23
- const files = [];
24
- (function walk(d) {
25
- for (const e of readdirSync(d, { withFileTypes: true })) {
26
- if (e.name.startsWith(".")) continue;
27
- const p = join(d, e.name);
28
- if (e.isDirectory()) walk(p);
29
- else if (e.name.endsWith(".md")) files.push(p);
30
- }
31
- })(root);
32
-
33
- const errors = [], warnings = [];
34
- const rel = (p) => relative(root, p).split("\\").join("/");
35
- const isReserved = (p) => ["index.md", "log.md"].includes(posix.basename(rel(p)));
36
-
37
- function parseFrontmatter(text) {
38
- if (!text.startsWith("---")) return { present: false };
39
- const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/);
40
- if (!m) return { present: true, parsed: false };
41
- const meta = {};
42
- for (const line of m[1].split("\n")) {
43
- if (/^\s*#/.test(line) || !line.trim()) continue;
44
- const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
45
- if (kv) meta[kv[1]] = kv[2].replace(/\s+#.*$/, "").replace(/^["']|["']$/g, "").trim();
46
- else if (!/^\s+/.test(line)) return { present: true, parsed: false };
47
- }
48
- return { present: true, parsed: true, meta, body: text.slice(m[0].length) };
49
- }
50
-
51
- const concepts = new Map(); // rel path -> { meta, body }
52
- for (const f of files) {
53
- const r = rel(f);
54
- const text = readFileSync(f, "utf8");
55
- const fm = parseFrontmatter(text);
56
- if (isReserved(f)) {
57
- if (fm.present && fm.parsed && fm.meta.type) errors.push(`${r}: reserved file must not carry a 'type'`);
58
- if (fm.present && fm.parsed && posix.basename(r) === "index.md" && r !== "index.md") {
59
- const keys = Object.keys(fm.meta);
60
- if (keys.some((k) => k !== "okf_version")) warnings.push(`${r}: only the bundle-root index.md may carry frontmatter`);
61
- }
62
- concepts.set(r, { reserved: true, body: fm.parsed ? fm.body : text });
63
- continue;
64
- }
65
- if (!fm.present) { errors.push(`${r}: missing YAML frontmatter`); continue; }
66
- if (!fm.parsed) { errors.push(`${r}: unparseable YAML frontmatter`); continue; }
67
- if (!fm.meta.type) errors.push(`${r}: missing or empty required field 'type'`);
68
- if (strict) {
69
- if (!fm.meta.title) warnings.push(`${r}: missing recommended field 'title'`);
70
- if (!fm.meta.description) warnings.push(`${r}: missing recommended field 'description'`);
71
- }
72
- concepts.set(r, { meta: fm.meta, body: fm.body });
73
- }
74
-
75
- if (strict) {
76
- // Link checks (log.md bodies exempt) + reachability from index files.
77
- const reachable = new Set();
78
- const linkRe = /\[[^\]]*\]\(([^)\s]+)\)/g;
79
- const resolveLink = (fromRel, target) => {
80
- if (/^[a-z]+:\/\//i.test(target) || target.startsWith("mailto:")) return null; // external
81
- const clean = target.split("#")[0];
82
- if (!clean) return null;
83
- const abs = clean.startsWith("/")
84
- ? posix.normalize(clean.slice(1))
85
- : posix.normalize(posix.join(posix.dirname(fromRel), clean));
86
- return abs;
87
- };
88
- for (const [r, c] of concepts) {
89
- const body = c.body ?? "";
90
- const fromLog = posix.basename(r) === "log.md";
91
- for (const m of body.matchAll(linkRe)) {
92
- const t = resolveLink(r, m[1]);
93
- if (t === null) continue;
94
- const isDir = t.endsWith("/") || concepts.has(posix.join(t, "index.md")) || existsSync(join(root, t)) && statSync(join(root, t)).isDirectory?.();
95
- if (posix.basename(r) === "index.md" || !fromLog) {
96
- if (!t.endsWith(".md") && !isDir) { if (!fromLog) warnings.push(`${r}: link missing .md extension: ${m[1]}`); continue; }
97
- }
98
- if (fromLog) continue; // history exempt from broken-link lint
99
- if (t.endsWith(".md") && !concepts.has(t)) warnings.push(`${r}: broken link: ${m[1]}`);
100
- if (posix.basename(r) === "index.md" && t.endsWith(".md") && concepts.has(t)) reachable.add(t);
101
- if (posix.basename(r) === "index.md" && isDir) reachable.add(posix.join(t.replace(/\/$/, ""), "index.md"));
102
- }
103
- }
104
- // Reachability: walk index closure (an index that lists a subdir makes that subdir's index reachable).
105
- for (const [r, c] of concepts) {
106
- if (c.reserved || reachable.has(r)) continue;
107
- // root-level concepts listed in root index handled above; report the rest
108
- const anyIndex = [...concepts.keys()].some((k) => posix.basename(k) === "index.md");
109
- if (anyIndex) warnings.push(`${r}: unreachable from any index.md`);
110
- }
111
- }
112
-
113
- const conceptCount = [...concepts.values()].filter((c) => !c.reserved).length;
114
- if (asJson) {
115
- console.log(JSON.stringify({ bundle: root, concepts: conceptCount, errors, warnings, conformant: errors.length === 0 }, null, 2));
116
- } else {
117
- console.log(`OKF validate — ${root}`);
118
- console.log(` ${conceptCount} concept(s), ${errors.length} error(s), ${warnings.length} warning(s)`);
119
- for (const e of errors) console.log(` ERROR ${e}`);
120
- for (const w of warnings) console.log(` warn ${w}`);
121
- console.log(errors.length === 0 ? (strict && warnings.length ? "PASS (with lints)" : "PASS — conformant") : "FAIL — nonconformant");
122
- }
123
- process.exit(errors.length > 0 ? 1 : strict && warnings.length > 0 && process.env.OKF_STRICT_EXIT ? 1 : 0);
@@ -1,199 +0,0 @@
1
- import { isAbsolute, resolve } from 'node:path';
2
- import { identifier, relPath, fail } from './io.mjs';
3
- import { resolveNodes, validateBindings, validateDeclaration } from './config.mjs';
4
- import { validateRepositoryLocator } from './source-contract.mjs';
5
-
6
- export const KNOWLEDGE_CONTRACT='oats.okf.locations';
7
- export const KNOWLEDGE_CONTRACT_VERSION=1;
8
- const candidateKinds=new Set(['workspace-default','import-adoption','operator']);
9
- const obj=value=>value && typeof value==='object' && !Array.isArray(value);
10
- const pointerKey=value=>String(value).replace(/~/g,'~0').replace(/\//g,'~1');
11
- const clone=value=>JSON.parse(JSON.stringify(value));
12
- const canonicalJson=value=>value===null || typeof value!=='object'?JSON.stringify(value):Array.isArray(value)?`[${value.map(canonicalJson).join(',')}]`:`{${Object.keys(value).sort().map(key=>`${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
13
- export const sameJson=(a,b)=>canonicalJson(a)===canonicalJson(b);
14
- function keys(value,allowed,required,label) {
15
- if(!obj(value)) fail('E_CONFIG',`${label} must be an object`);
16
- for(const key of Object.keys(value)) if(!allowed.includes(key)) fail('E_CONFIG',`unknown ${label} property: ${key}`);
17
- for(const key of required) if(!Object.hasOwn(value,key)) fail('E_CONFIG',`${label} requires ${key}`);
18
- return value;
19
- }
20
- function bindingKey(value) {
21
- if(typeof value!=='string') fail('E_CONFIG','invalid knowledge binding key');
22
- // Reserved stores.<alias> addresses ONE legal alias; dots inside that suffix
23
- // are literal identity characters, never another hierarchy or fuzzy match.
24
- const parts=value.startsWith('stores.')?[value.slice(7)]:value.split('.');
25
- if(parts.some(part=>{try{identifier(part);return false;}catch{return true;}})) fail('E_CONFIG','invalid knowledge binding key');
26
- return value;
27
- }
28
- export const bindingChoiceKey=value=>{
29
- bindingKey(value);
30
- return value.startsWith('stores.')?storeChoiceKey(value.slice(7)):`/bindings/knowledge/${value.split('.').map(pointerKey).join('/')}`;
31
- };
32
- export const storeChoiceKey=alias=>`/bindings/knowledge/stores/${pointerKey(identifier(alias))}`;
33
- function originAt({origins={},origin=null,pointer},suffix,kind) {
34
- const at=`${pointer}${suffix}`,specific=origins[at],found=specific ?? origin;
35
- if(!obj(found)) fail('E_CONFIG',`missing origin for ${at}`);
36
- return clone({...found,kind});
37
- }
38
-
39
- /** Portable, non-secret store locator. Filesystem existence/custody and remote
40
- * identity are checked later by bind/check; this function performs no I/O. */
41
- export function validateStoreLocator(value) {
42
- if(!obj(value)) fail('E_CONFIG','knowledge store locator must be an object');
43
- identifier(value.id);
44
- if(value.kind==='directory') {
45
- keys(value,['id','kind','path'],['id','kind','path'],'directory store locator');
46
- if(typeof value.path!=='string' || !value.path.startsWith('path:') || !isAbsolute(value.path.slice(5)) || resolve(value.path.slice(5))!==value.path.slice(5)) fail('E_CONFIG','portable directory store requires a normalized absolute path: locator');
47
- return {id:value.id,kind:value.kind,path:value.path};
48
- } else if(value.kind==='git') {
49
- keys(value,['id','kind','repository','root','acceptedBranch','pr'],['id','kind','repository','root','acceptedBranch','pr'],'git store locator');
50
- validateRepositoryLocator(value.repository);
51
- relPath(value.root,true);
52
- if(typeof value.acceptedBranch!=='string' || !/^[a-zA-Z0-9][a-zA-Z0-9._/-]*$/.test(value.acceptedBranch) || value.acceptedBranch.includes('..') || value.acceptedBranch.endsWith('/') || value.acceptedBranch.endsWith('.lock')) fail('E_CONFIG','invalid acceptedBranch');
53
- keys(value.pr,['repository'],['repository'],'git PR binding');if(typeof value.pr.repository!=='string' || !/^[\w.-]+\/[\w.-]+$/.test(value.pr.repository)) fail('E_CONFIG','git PR binding requires owner/repo');
54
- return {id:value.id,kind:value.kind,repository:value.repository,root:value.root,acceptedBranch:value.acceptedBranch,pr:{repository:value.pr.repository}};
55
- }
56
- fail('E_CONFIG',`unsupported knowledge store kind: ${value.kind}`);
57
- }
58
- function validateEnvelope(value) {
59
- keys(value,['contract','version','payload'],['contract','version','payload'],'knowledge declaration');
60
- if(value.contract!==KNOWLEDGE_CONTRACT || value.version!==KNOWLEDGE_CONTRACT_VERSION) fail('E_CONFIG',`unsupported knowledge contract: ${value.contract}@${value.version}`);
61
- return value.payload;
62
- }
63
-
64
- /** Validate the source-owned OKF payload and emit only field-level inputs for
65
- * the kernel's one resolver. No winner is selected here. */
66
- export function normalizeKnowledgeDeclaration(envelope,{origins={},origin=null,pointer='/knowledge/payload'}={}) {
67
- const payload=validateEnvelope(envelope);
68
- keys(payload,['owner','stores','reads','owns'],['owner','stores','reads','owns'],'OKF knowledge payload');
69
- const owner=identifier(payload.owner),stores={},requirements=[],candidates=[];
70
- if(!obj(payload.stores)) fail('E_CONFIG','knowledge stores must be an object');
71
- for(const [alias,spec] of Object.entries(payload.stores)) {
72
- identifier(alias);keys(spec,['fixed','default','inherit'],[],`knowledge store ${alias}`);
73
- const modes=['fixed','default','inherit'].filter(key=>Object.hasOwn(spec,key));
74
- if(modes.length!==1) fail('E_CONFIG',`knowledge store ${alias} needs exactly one of fixed, default or inherit`);
75
- const mode=modes[0],at=`/stores/${pointerKey(alias)}`,choiceKey=mode==='inherit'?bindingChoiceKey(bindingKey(spec.inherit)):storeChoiceKey(alias);
76
- const selectedOrigin=originAt({origins,origin,pointer},`${at}/${mode}`,mode==='default'?'soul-default':'soul-requirement');
77
- if(mode==='fixed') requirements.push({key:choiceKey,kind:'equals',value:validateStoreLocator(spec.fixed),origin:selectedOrigin});
78
- else if(mode==='default') candidates.push({key:choiceKey,kind:'soul-default',value:validateStoreLocator(spec.default),origin:selectedOrigin});
79
- else requirements.push({key:choiceKey,kind:'required',origin:selectedOrigin});
80
- stores[alias]={mode,choiceKey,origin:selectedOrigin};
81
- }
82
- if(!Array.isArray(payload.reads) || !Array.isArray(payload.owns)) fail('E_CONFIG','knowledge reads/owns must be arrays');
83
- const reads=[],owns=[],seenReads=new Set(),seenOwns=new Set();
84
- payload.reads.forEach((entry,index)=>{
85
- keys(entry,['store','node'],['store','node'],'knowledge read');
86
- const store=identifier(entry.store),node=identifier(entry.node),key=`${store}/${node}`;
87
- if(!Object.hasOwn(stores,store)) fail('E_CONFIG',`read references undeclared store: ${store}`);
88
- if(seenReads.has(key)) fail('E_CONFIG',`duplicate knowledge read: ${key}`);seenReads.add(key);
89
- reads.push({store,node,origin:originAt({origins,origin,pointer},`/reads/${index}`,'soul-requirement')});
90
- });
91
- payload.owns.forEach((entry,index)=>{
92
- keys(entry,['node','destination'],['node'],'knowledge ownership');
93
- const node=identifier(entry.node),destination=entry.destination===undefined?null:identifier(entry.destination);
94
- if(destination!==null && !Object.hasOwn(stores,destination)) fail('E_CONFIG',`owned node references undeclared destination: ${destination}`);
95
- const key=`${destination ?? '<write.default>'}/${node}`;
96
- if(seenOwns.has(key)) fail('E_CONFIG',`duplicate knowledge ownership: ${key}`);seenOwns.add(key);
97
- const itemOrigin=originAt({origins,origin,pointer},`/owns/${index}`,'soul-requirement');
98
- if(destination===null) requirements.push({key:bindingChoiceKey('write.default'),kind:'required',origin:itemOrigin});
99
- owns.push({node,destination,choiceKey:destination===null?bindingChoiceKey('write.default'):stores[destination].choiceKey,origin:itemOrigin});
100
- });
101
- return {contract:KNOWLEDGE_CONTRACT,version:KNOWLEDGE_CONTRACT_VERSION,owner,stores,reads,owns,requirements,candidates};
102
- }
103
-
104
- /** Convert already-parsed workspace/adoption/operator values to candidates.
105
- * This intentionally performs no precedence or recursive merging. */
106
- export function normalizeKnowledgeBindingCandidates({bindings,kind,origins={},origin=null,pointer='/bindings'}={}) {
107
- if(!candidateKinds.has(kind)) fail('E_CONFIG',`invalid knowledge candidate kind: ${kind}`);
108
- if(!obj(bindings)) fail('E_CONFIG','knowledge bindings must be an object');
109
- const candidates=[];
110
- for(const [name,value] of Object.entries(bindings)) {
111
- const key=bindingChoiceKey(name),at=`/${pointerKey(name)}`;
112
- candidates.push({key,kind,value:validateStoreLocator(value),origin:originAt({origins,origin,pointer},at,kind)});
113
- }
114
- return candidates;
115
- }
116
-
117
- function selected(choices,key) {
118
- if(!obj(choices) || !obj(choices[key]) || !Object.hasOwn(choices[key],'value') || choices[key].value===null) fail('E_CONFIG',`unresolved knowledge binding: ${key}`);
119
- return validateStoreLocator(choices[key].value);
120
- }
121
- function runtimeStore(locator) {return locator.kind==='directory'?{...locator,path:locator.path.slice(5)}:locator;}
122
-
123
- /** Render provider-owned nonsecret data after the kernel's shared resolver has
124
- * selected every field. Reads never imply a write destination. */
125
- export function bindKnowledgeDomain({model,choices}) {
126
- if(!obj(model) || model.contract!==KNOWLEDGE_CONTRACT || model.version!==KNOWLEDGE_CONTRACT_VERSION) fail('E_CONFIG','invalid normalized OKF model');
127
- const aliases={},stores={},provenance=[];
128
- const add=(locator,choiceKey)=>{
129
- const rendered=runtimeStore(locator);
130
- if(Object.hasOwn(stores,locator.id) && !sameJson(stores[locator.id],rendered)) fail('E_CONFIG',`store identity ${locator.id} resolves to conflicting locations`);
131
- stores[locator.id]=rendered;
132
- const selectedBy=choices[choiceKey]?.selectedBy;if(obj(selectedBy)) provenance.push(clone(selectedBy));
133
- return locator.id;
134
- };
135
- for(const [alias,plan] of Object.entries(model.stores)) aliases[alias]=add(selected(choices,plan.choiceKey),plan.choiceKey);
136
- const reads=model.reads.map(entry=>({store:aliases[entry.store],node:entry.node}));
137
- const readKeys=new Set();for(const entry of reads) {const key=`${entry.store}/${entry.node}`;if(readKeys.has(key)) fail('E_CONFIG',`duplicate resolved knowledge read: ${key}`);readKeys.add(key);}
138
- const owns=model.owns.map(entry=>{
139
- const locator=entry.destination===null?selected(choices,entry.choiceKey):selected(choices,model.stores[entry.destination].choiceKey);
140
- const store=add(locator,entry.choiceKey);
141
- return {store,node:entry.node,steward:model.owner};
142
- });
143
- const ownKeys=new Set();for(const entry of owns) {const key=`${entry.store}/${entry.node}`;if(ownKeys.has(key)) fail('E_OWNER',`duplicate resolved knowledge steward: ${key}`);ownKeys.add(key);}
144
- for(const entry of [...model.reads,...model.owns]) provenance.push(clone(entry.origin));
145
- const unique=[];for(const item of provenance) if(!unique.some(prior=>sameJson(prior,item))) unique.push(item);
146
- return {contract:KNOWLEDGE_CONTRACT,version:KNOWLEDGE_CONTRACT_VERSION,payload:{owner:model.owner,stores,reads,owns},credentialRefs:{},provenance:unique};
147
- }
148
-
149
- function absolutePath(value,label) {
150
- if(typeof value!=='string' || !isAbsolute(value) || resolve(value)!==value) fail('E_PATH',`${label} must be a normalized absolute path`);
151
- return value;
152
- }
153
- function boundStore(value) {
154
- if(!obj(value)) fail('E_CONFIG','bound knowledge store must be an object');
155
- const portable=value.kind==='directory'?{...value,path:`path:${value.path}`} : value;
156
- return runtimeStore(validateStoreLocator(portable));
157
- }
158
-
159
- /** Purely adapt a captured nonsecret provider domain to the existing OKF v1
160
- * documents. The host supplies durable custody paths; neither is derived from
161
- * a source home, and this function performs no filesystem access. */
162
- export function renderKnowledgeRuntime({domain,stateDir,descriptorFile}={}) {
163
- keys(domain,['owner','stores','reads','owns'],['owner','stores','reads','owns'],'bound knowledge domain');
164
- const owner=identifier(domain.owner),bases={};
165
- if(!obj(domain.stores) || !Object.keys(domain.stores).length) fail('E_CONFIG','bound knowledge domain requires stores');
166
- for(const [alias,value] of Object.entries(domain.stores)) {
167
- identifier(alias);const store=boundStore(value);
168
- if(store.id!==alias) fail('E_ID',`runtime alias must equal stable store identity: ${alias}`);
169
- bases[alias]=store;
170
- }
171
- const references=(entries,label,owned=false)=>{
172
- if(!Array.isArray(entries)) fail('E_CONFIG',`bound knowledge ${label} must be an array`);
173
- return entries.map(entry=>{
174
- keys(entry,owned?['store','node','steward']:['store','node'],owned?['store','node','steward']:['store','node'],`bound knowledge ${label}`);
175
- const store=identifier(entry.store),node=identifier(entry.node);
176
- if(!Object.hasOwn(bases,store)) fail('E_CONFIG',`${label} references unbound store: ${store}`);
177
- if(owned && identifier(entry.steward)!==owner) fail('E_OWNER',`bound knowledge steward mismatch: ${store}/${node}`);
178
- return `${store}/${node}`;
179
- });
180
- };
181
- const declaration=validateDeclaration({version:1,owner,reads:references(domain.reads,'read'),owns:references(domain.owns,'ownership',true)});
182
- return {descriptorFile:absolutePath(descriptorFile,'OKF descriptor file'),bindings:{version:1,stateDir:absolutePath(stateDir,'OKF stateDir'),bases},declaration};
183
- }
184
-
185
- /** Read-only custody/accepted-base check for an already rendered capture.
186
- * `accepted` must come from the existing base validation path. */
187
- export function checkKnowledgeRuntime({rendered,accepted,sourceHome,sourceWork}={}) {
188
- keys(rendered,['descriptorFile','bindings','declaration'],['descriptorFile','bindings','declaration'],'rendered OKF runtime');
189
- keys(rendered.bindings,['version','stateDir','bases'],['version','stateDir','bases'],'rendered OKF bindings');
190
- if(rendered.bindings.version!==1 || !obj(rendered.bindings.bases) || !Object.keys(rendered.bindings.bases).length) fail('E_CONFIG','invalid rendered OKF bindings');
191
- absolutePath(rendered.bindings.stateDir,'OKF stateDir');
192
- for(const [alias,value] of Object.entries(rendered.bindings.bases)) if(identifier(alias)!==boundStore(value).id) fail('E_ID',`runtime alias must equal stable store identity: ${alias}`);
193
- if(!obj(accepted)) fail('E_CONFIG','accepted OKF base metadata is required');
194
- const descriptorFile=absolutePath(rendered.descriptorFile,'OKF descriptor file');
195
- const bindings=validateBindings(rendered.bindings,descriptorFile,{sourceHome,sourceWork});
196
- const declaration=validateDeclaration(rendered.declaration);
197
- resolveNodes(declaration,bindings,accepted);
198
- return {descriptorFile,bindings,declaration};
199
- }
@@ -1,46 +0,0 @@
1
- import { isAbsolute, resolve } from 'node:path';
2
- import { fail, relPath } from './io.mjs';
3
- const obj=value=>value!==null && typeof value==='object' && !Array.isArray(value);
4
- const invalid=()=>fail('E_SOURCE','invalid nonsecret qualified source identity');
5
- function exact(value,fields) {if(!obj(value) || Object.keys(value).some(key=>!fields.includes(key)) || fields.some(key=>!Object.hasOwn(value,key))) invalid();}
6
-
7
- /** Provider-owned nonsecret locator contract; no I/O, source resolver or secret
8
- * lookup. Authentication remains in the native credential transport. */
9
- export function validateRepositoryLocator(value) {
10
- const bad=()=>fail('E_CONFIG','Git repository must not contain credentials, query, fragment or malformed paths');
11
- if(typeof value!=='string' || /[\s\\\x00-\x1f\x7f?#]/.test(value)) bad();
12
- const scp=/^(git)@([^/:]+):(.+)$/.exec(value);
13
- const spelling=scp?`ssh://${scp[1]}@${scp[2]}${scp[3].startsWith('/')?'':'/'}${scp[3]}`:value;
14
- if(!/^(https|ssh):\/\//.test(spelling)) bad();
15
- const pathStart=spelling.indexOf('/',spelling.indexOf('://')+3);
16
- if(pathStart<0) bad();
17
- for(const part of spelling.slice(pathStart+1).split('/')) {
18
- let decoded;try{decoded=decodeURIComponent(part);}catch{bad();}
19
- if(!part || decoded==='.' || decoded==='..' || /[/\\\x00-\x1f\x7f]/.test(decoded)) bad();
20
- }
21
- let url;try{url=new URL(spelling);}catch{bad();}
22
- if(!url.hostname || url.password || url.search || url.hash || (url.protocol==='https:' && url.username)) bad();
23
- if(scp) {if(value!==`${scp[1]}@${url.hostname}:${scp[3]}`) bad();}
24
- else if(url.href!==value) bad();
25
- return value;
26
- }
27
- export function qualifiedSoulIdentity(value) {
28
- try {
29
- if(value?.kind==='git-soul') {
30
- exact(value,['kind','repository','exportPath']);const r=value.repository;
31
- if(r?.kind==='provider-repository') {
32
- exact(r,['kind','provider','host','id']);
33
- if(typeof r.provider!=='string' || !/^[a-z][a-z0-9.-]*$/.test(r.provider) || typeof r.host!=='string' || !/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/.test(r.host) || typeof r.id!=='string' || !r.id || /[\x00-\x1f\x7f]/.test(r.id)) invalid();
34
- } else {
35
- exact(r,['kind','remote']);if(r.kind!=='canonical-remote' || typeof r.remote!=='string' || !r.remote.startsWith('git:')) invalid();
36
- validateRepositoryLocator(r.remote.slice(4));
37
- }
38
- } else if(value?.kind==='local-soul') {
39
- exact(value,['kind','source','exportPath']);
40
- if(typeof value.source!=='string' || !value.source.startsWith('path:') || /[\x00-\x1f\x7f]/.test(value.source)) invalid();
41
- const p=value.source.slice(5);if(!isAbsolute(p) || resolve(p)!==p) invalid();
42
- } else invalid();
43
- relPath(value.exportPath,value.kind==='local-soul');
44
- return JSON.parse(JSON.stringify(value));
45
- } catch {invalid();}
46
- }