@awebai/oats 0.30.0 → 0.30.2
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.
- package/bin/oats.mjs +1 -1
- package/docs/capabilities.md +3 -3
- package/docs/design/2026-09-23-workspace-module-contracts.md +2 -1
- package/docs/desktop-cli-api.md +23 -11
- package/docs/first-team.md +1 -1
- package/docs/implementation.md +2 -1
- package/docs/integrations.md +1 -1
- package/docs/knowledge-capability-authoring.md +1 -1
- package/docs/knowledge.md +4 -4
- package/docs/official-catalog.md +4 -4
- package/docs/packages.md +13 -13
- package/docs/plans/0.30-close-out.md +24 -2
- package/docs/release-lane.md +7 -2
- package/docs/release-notes/v0.30.1.md +123 -0
- package/docs/release-notes/v0.30.2.md +85 -0
- package/docs/souls-and-instances.md +6 -5
- package/docs/workspaces.md +7 -2
- package/lib/core.mjs +42 -28
- package/lib/instance-inspect.mjs +1 -1
- package/lib/instance-resolution.mjs +15 -8
- package/lib/materialize.mjs +33 -19
- package/lib/packages.mjs +1 -1
- package/lib/resolve.mjs +1 -1
- package/package-catalog.json +3 -3
- package/package.json +1 -3
- package/skills/oats-getting-started/SKILL.md +2 -2
- package/capabilities/oats-authoring/LICENSE +0 -21
- package/capabilities/oats-authoring/oats-package.json +0 -11
- package/capabilities/oats-authoring/oats.json +0 -12
- package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +0 -84
- package/capabilities/oats-authoring/skills/skill-craft/SKILL.md +0 -109
- package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +0 -116
- package/capabilities/oats-aweb/bin/oats-aweb-binding.mjs +0 -11
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +0 -1672
- package/capabilities/oats-aweb/injects/aweb.md +0 -47
- package/capabilities/oats-aweb/lib/binding-wire.mjs +0 -365
- package/capabilities/oats-aweb/lib/captured-execution.mjs +0 -91
- package/capabilities/oats-aweb/lib/captured-native.mjs +0 -91
- package/capabilities/oats-aweb/lib/grant-custody.mjs +0 -38
- package/capabilities/oats-aweb/lib/invocation-shape.mjs +0 -135
- package/capabilities/oats-aweb/lib/portable-binding.mjs +0 -146
- package/capabilities/oats-aweb/lib/session-readiness.mjs +0 -56
- package/capabilities/oats-aweb/lib/wake-receive.mjs +0 -56
- package/capabilities/oats-aweb/oats.json +0 -201
- package/capabilities/oats-aweb/skills/LICENSE +0 -21
- package/capabilities/oats-aweb/skills/VENDORED.md +0 -31
- package/capabilities/oats-aweb/skills/aweb-identity/SKILL.md +0 -201
- package/capabilities/oats-aweb/skills/aweb-messaging/SKILL.md +0 -161
- package/capabilities/oats-aweb/skills/aweb-messaging/references/messaging-scenarios.md +0 -61
- package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +0 -116
- package/capabilities/oats-aweb/skills/aweb-team-membership/references/team-membership-reference.md +0 -74
- package/capabilities/oats-aweb/skills/oats-aweb/SKILL.md +0 -286
- package/capabilities/oats-code-review/injects/reviewer.md +0 -26
- package/capabilities/oats-code-review/oats.json +0 -16
- package/capabilities/oats-code-review/skills/adversarial-review/SKILL.md +0 -66
- package/capabilities/oats-code-review/skills/review-dev-docs/SKILL.md +0 -30
- package/capabilities/oats-code-review/skills/security-review/SKILL.md +0 -56
- package/capabilities/oats-code-review/skills/simplification-review/SKILL.md +0 -34
- package/capabilities/oats-developer/injects/developer.md +0 -38
- package/capabilities/oats-developer/oats.json +0 -17
- package/capabilities/oats-developer/skills/execution-strategy/SKILL.md +0 -43
- package/capabilities/oats-developer/skills/maintain-dev-docs/SKILL.md +0 -47
- package/capabilities/oats-developer/skills/run-the-review-loop/SKILL.md +0 -65
- package/capabilities/oats-developer/skills/understand-the-spec/SKILL.md +0 -37
- package/capabilities/oats-developer/skills/worktrees/SKILL.md +0 -36
- package/capabilities/oats-engineering-expert/injects/expert.md +0 -37
- package/capabilities/oats-engineering-expert/oats.json +0 -17
- package/capabilities/oats-engineering-expert/skills/coordinate-developers/SKILL.md +0 -37
- package/capabilities/oats-engineering-expert/skills/coordinate-experts/SKILL.md +0 -52
- package/capabilities/oats-engineering-expert/skills/land-your-prs/SKILL.md +0 -50
- package/capabilities/oats-engineering-expert/skills/plan-and-spec/SKILL.md +0 -53
- package/capabilities/oats-engineering-expert/skills/verify-developer-work/SKILL.md +0 -49
- package/capabilities/oats-jira/bin/oats-jira.mjs +0 -40
- package/capabilities/oats-jira/injects/jira.md +0 -10
- package/capabilities/oats-jira/oats.json +0 -22
- package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +0 -179
- package/capabilities/oats-linear/bin/oats-linear-hook.mjs +0 -34
- package/capabilities/oats-linear/bin/oats-linear.mjs +0 -344
- package/capabilities/oats-linear/injects/linear.md +0 -8
- package/capabilities/oats-linear/oats.json +0 -24
- package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +0 -223
- package/capabilities/oats-okf/bin/oats-okf-binding.mjs +0 -14
- package/capabilities/oats-okf/bin/oats-okf.mjs +0 -213
- package/capabilities/oats-okf/injects/okf.md +0 -42
- package/capabilities/oats-okf/lib/binding-wire.mjs +0 -380
- package/capabilities/oats-okf/lib/captured-worker.mjs +0 -109
- package/capabilities/oats-okf/lib/config.mjs +0 -124
- package/capabilities/oats-okf/lib/consult.mjs +0 -518
- package/capabilities/oats-okf/lib/harvest-status.mjs +0 -88
- package/capabilities/oats-okf/lib/harvest-switch.mjs +0 -94
- package/capabilities/oats-okf/lib/inspection.mjs +0 -138
- package/capabilities/oats-okf/lib/invocation-context.mjs +0 -111
- package/capabilities/oats-okf/lib/invocation-shape.mjs +0 -135
- package/capabilities/oats-okf/lib/io.mjs +0 -118
- package/capabilities/oats-okf/lib/migration.mjs +0 -137
- package/capabilities/oats-okf/lib/okf-validate.mjs +0 -123
- package/capabilities/oats-okf/lib/portable-binding.mjs +0 -199
- package/capabilities/oats-okf/lib/source-contract.mjs +0 -46
- package/capabilities/oats-okf/lib/sources.mjs +0 -438
- package/capabilities/oats-okf/lib/stores.mjs +0 -473
- package/capabilities/oats-okf/lib/worker.mjs +0 -486
- package/capabilities/oats-okf/oats.json +0 -151
- package/capabilities/oats-okf/schemas/okf-base.schema.json +0 -46
- package/capabilities/oats-okf/schemas/okf-bindings.schema.json +0 -112
- package/capabilities/oats-okf/schemas/okf-portable-declaration.schema.json +0 -87
- package/capabilities/oats-okf/schemas/okf-portable-payload.schema.json +0 -113
- package/capabilities/oats-okf/schemas/okf-soul.schema.json +0 -37
- package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +0 -144
- package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +0 -86
- package/capabilities/oats-okf/skills/okf-instance-knowledge/SKILL.md +0 -104
- package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +0 -140
- package/capabilities/oats-okf-harvest/injects/harvester.md +0 -12
- package/capabilities/oats-okf-harvest/oats.json +0 -26
- package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +0 -168
- package/capabilities/oats-okf-harvest/skills/knowledge-theory/SKILL.md +0 -192
- package/capabilities/oats-okf-harvest/skills/okf-authoring/SKILL.md +0 -151
- package/capabilities/oats-okf-harvest/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
- package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +0 -170
- package/capabilities/oats-okf-maintenance/injects/maintainer.md +0 -12
- package/capabilities/oats-okf-maintenance/lib/provenance.mjs +0 -50
- package/capabilities/oats-okf-maintenance/oats.json +0 -21
- package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +0 -159
- package/capabilities/oats-okf-maintenance/skills/knowledge-theory/SKILL.md +0 -192
- package/capabilities/oats-okf-maintenance/skills/okf-authoring/SKILL.md +0 -151
- package/capabilities/oats-okf-maintenance/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
- package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +0 -134
- package/capabilities/oats-workspace-experts/injects/oats-experts.md +0 -26
- package/capabilities/oats-workspace-experts/oats.json +0 -9
|
@@ -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
|
-
}
|