fraim 2.0.360-next.0 → 2.0.360-next.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.
- package/dist/registry/domains.json +159 -0
- package/dist/registry/scripts/domains/catalog.js +112 -0
- package/dist/registry/scripts/learning/entry-authoring.js +63 -25
- package/dist/registry/scripts/learning/run-application.js +12 -2
- package/dist/registry/scripts/learning/run-publication.js +169 -16
- package/dist/registry/scripts/learning-usage/local-runtime.js +1 -3
- package/dist/src/cli/learning/runtime.js +2 -0
- package/dist/src/cli/utils/remote-sync.js +7 -0
- package/dist/src/cli/utils/script-sync-utils.js +4 -0
- package/dist/src/config/learning-domains.js +24 -108
- package/dist/src/core/ai-mentor.js +7 -0
- package/dist/src/core/utils/domain-rule-assembler.js +206 -0
- package/dist/src/core/utils/local-registry-resolver.js +135 -32
- package/dist/src/local-mcp-server/agent-token-prices.js +25 -1
- package/dist/src/local-mcp-server/learning-context-builder.js +94 -36
- package/dist/src/local-mcp-server/stdio-server.js +122 -39
- package/dist/src/mcp/tool-schemas.js +2 -2
- package/dist/src/services/mcp-service.js +35 -16
- package/dist/src/services/registry-service.js +4 -0
- package/package.json +3 -2
- package/dist/registry/scripts/learning/learning-domains.json +0 -80
|
@@ -76,7 +76,7 @@ function verifySharedProjectPublication(projectPath, file, expectedHash, publica
|
|
|
76
76
|
return { published: true, reference: pr.url };
|
|
77
77
|
}
|
|
78
78
|
/** Existing publishers run in a bounded process. Cloud confirmation reads the actual pack without syncing it. */
|
|
79
|
-
function publishRunFile(projectPath, file, home, operationKey, expectedHash, runtimeModulePath, entryProof) {
|
|
79
|
+
function publishRunFile(projectPath, file, home, operationKey, expectedHash, runtimeModulePath, entryProof, verifyOnly = false) {
|
|
80
80
|
const relative = home.level === 'Project' && node_path_1.default.resolve(file) === node_path_1.default.join(home.root, 'config.json')
|
|
81
81
|
? 'config.json' : node_path_1.default.relative(home.contentRoot, file).replace(/\\/g, '/');
|
|
82
82
|
if ((0, owner_identity_1.ownerArtifactPath)(home, relative) !== node_path_1.default.resolve(file))
|
|
@@ -95,6 +95,13 @@ function publishRunFile(projectPath, file, home, operationKey, expectedHash, run
|
|
|
95
95
|
throw new Error('project-publisher-unavailable');
|
|
96
96
|
const branchSuffix = `sleep-${operationKey}`;
|
|
97
97
|
if (home.backend === 'git' && home.gitUrl) {
|
|
98
|
+
if (verifyOnly) {
|
|
99
|
+
const head = fetchRemoteHead(home.root);
|
|
100
|
+
const relative = node_path_1.default.relative(home.root, file).replace(/\\/g, '/');
|
|
101
|
+
const delivered = head && (0, node_child_process_1.spawnSync)('git', ['show', `${head}:${relative}`], { cwd: home.root, encoding: 'utf8', timeout: 30000, windowsHide: true });
|
|
102
|
+
return { published: Boolean(delivered && delivered.status === 0 && sameDeliveryText(delivered.stdout, node_fs_1.default.readFileSync(file, 'utf8'))
|
|
103
|
+
&& (0, proposal_queue_1.hashContent)(node_fs_1.default.readFileSync(file, 'utf8')) === expectedHash), reference: home.gitUrl };
|
|
104
|
+
}
|
|
98
105
|
const branch = `fraim-${layer}-update-${branchSuffix}`;
|
|
99
106
|
const found = (0, node_child_process_1.spawnSync)('gh', ['pr', 'view', branch, '--repo', home.gitUrl, '--json', 'state,url,mergeCommit'], { encoding: 'utf8', timeout: 30000, windowsHide: true });
|
|
100
107
|
if (found.status === 0) {
|
|
@@ -125,7 +132,7 @@ function publishRunFile(projectPath, file, home, operationKey, expectedHash, run
|
|
|
125
132
|
const fs=require('node:fs');
|
|
126
133
|
const crypto=require('node:crypto');
|
|
127
134
|
const hash=value=>crypto.createHash('sha256').update(value).digest('hex');
|
|
128
|
-
const [publisherPath,userConfigPath,file,relative,layer,branchSuffix,expectedHash,backend,entryProofJson,entryFormatPath]=process.argv.slice(1);
|
|
135
|
+
const [publisherPath,userConfigPath,file,relative,layer,branchSuffix,expectedHash,backend,entryProofJson,entryFormatPath,verifyOnly]=process.argv.slice(1);
|
|
129
136
|
const entryProof=JSON.parse(entryProofJson);
|
|
130
137
|
const {learningEntryBlocks}=require(entryFormatPath);
|
|
131
138
|
const matchesLearningPublication=${matchesLearningPublication.toString()};
|
|
@@ -146,14 +153,16 @@ async function confirmedCloud(){
|
|
|
146
153
|
if(backend==='fraim-cloud'){
|
|
147
154
|
const existing=await confirmedCloud();
|
|
148
155
|
if(existing.published){console.log('FRAIM_PUBLISH_RESULT='+JSON.stringify(existing));return;}
|
|
156
|
+
if(verifyOnly==='true'){console.log('FRAIM_PUBLISH_RESULT='+JSON.stringify(existing));return;}
|
|
149
157
|
}
|
|
158
|
+
if(verifyOnly==='true'){console.log('FRAIM_PUBLISH_RESULT='+JSON.stringify({published:false,reference:file}));return;}
|
|
150
159
|
const publish=layer==='manager'?publisher.publishManagerArtifacts:publisher.publishOrgArtifacts;
|
|
151
160
|
const result=await publish([{relativePath:relative,content:fs.readFileSync(file,'utf8')}],{branchSuffix});
|
|
152
161
|
const confirmed=backend==='fraim-cloud'?await confirmedCloud():{published:false,reference:result.prUrl||result.branch||file};
|
|
153
162
|
if(hash(fs.readFileSync(file,'utf8'))!==expectedHash)throw Error('publication-content-conflict');
|
|
154
163
|
console.log('FRAIM_PUBLISH_RESULT='+JSON.stringify(confirmed));
|
|
155
164
|
})().catch(()=>{process.exitCode=1;});`;
|
|
156
|
-
const result = (0, node_child_process_1.spawnSync)(process.execPath, [...process.execArgv, '-e', script, publisherPath, userConfigPath, file, relative, layer, branchSuffix, expectedHash, home.backend, JSON.stringify(entryProof || null), require.resolve('./entry-format')], { cwd: projectPath, encoding: 'utf8', timeout: 120000, windowsHide: true });
|
|
165
|
+
const result = (0, node_child_process_1.spawnSync)(process.execPath, [...process.execArgv, '-e', script, publisherPath, userConfigPath, file, relative, layer, branchSuffix, expectedHash, home.backend, JSON.stringify(entryProof || null), require.resolve('./entry-format'), String(verifyOnly)], { cwd: projectPath, encoding: 'utf8', timeout: 120000, windowsHide: true });
|
|
157
166
|
const encoded = String(result.stdout || '').match(/^FRAIM_PUBLISH_RESULT=(.+)$/m)?.[1];
|
|
158
167
|
if (result.status !== 0 || !encoded)
|
|
159
168
|
throw new Error('publication-pending: delivery or read-back has not been confirmed');
|
|
@@ -282,11 +291,41 @@ async function verifyCapabilityDelivery(action, input, options) {
|
|
|
282
291
|
const resolver = new LocalRegistryResolver({ workspaceRoot: options.projectPath,
|
|
283
292
|
managerLocalRoot: manager.contentRoot, managerCacheRoot: manager.contentRoot, orgCacheRoot: org.contentRoot,
|
|
284
293
|
remoteContentResolver: async () => { throw new Error('capability-not-available-locally'); },
|
|
294
|
+
onLocalDependency: (file, kind, content) => kind === 'directory' ? snapshotDirectory(file) : snapshot(file, content),
|
|
285
295
|
});
|
|
286
296
|
const originalResolve = resolver.resolveFile.bind(resolver);
|
|
287
297
|
const snapshots = new Map();
|
|
298
|
+
const snapshotDirectory = (file) => {
|
|
299
|
+
const absolute = node_path_1.default.resolve(file);
|
|
300
|
+
let realPath = null;
|
|
301
|
+
try {
|
|
302
|
+
if (!node_fs_1.default.statSync(absolute).isDirectory())
|
|
303
|
+
throw new Error('capability-local-content-changed');
|
|
304
|
+
realPath = node_fs_1.default.realpathSync(absolute);
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
if (error.code !== 'ENOENT')
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
const prior = snapshots.get(absolute);
|
|
311
|
+
if (prior && (prior.kind !== 'directory' || prior.realPath !== realPath))
|
|
312
|
+
throw new Error('capability-local-content-changed');
|
|
313
|
+
snapshots.set(absolute, { file: absolute, realPath, sha256: null, kind: 'directory' });
|
|
314
|
+
};
|
|
288
315
|
const snapshot = (file, expectedContent) => {
|
|
289
316
|
const absolute = node_path_1.default.resolve(file);
|
|
317
|
+
try {
|
|
318
|
+
node_fs_1.default.lstatSync(absolute);
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
if (error.code !== 'ENOENT' || expectedContent !== undefined)
|
|
322
|
+
throw error;
|
|
323
|
+
const prior = snapshots.get(absolute);
|
|
324
|
+
if (prior?.sha256)
|
|
325
|
+
throw new Error('capability-local-content-changed');
|
|
326
|
+
snapshots.set(absolute, { file: absolute, realPath: null, sha256: null });
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
290
329
|
const body = node_fs_1.default.readFileSync(absolute, 'utf8');
|
|
291
330
|
if (expectedContent !== undefined && !sameDeliveryText(body, expectedContent))
|
|
292
331
|
throw new Error('capability-local-content-changed');
|
|
@@ -296,12 +335,23 @@ async function verifyCapabilityDelivery(action, input, options) {
|
|
|
296
335
|
throw new Error('capability-local-content-changed');
|
|
297
336
|
snapshots.set(absolute, current);
|
|
298
337
|
};
|
|
338
|
+
const snapshotRuleOverlays = (relative) => {
|
|
339
|
+
const normalized = relative.replace(/\\/g, '/');
|
|
340
|
+
if (node_path_1.default.isAbsolute(normalized) || normalized.split('/').some(part => !part || part === '..' || part === '.'))
|
|
341
|
+
throw new Error('invalid-capability-include');
|
|
342
|
+
// A new wrapper/fragment override can change delivery without touching
|
|
343
|
+
// the selected source. Bind absent overlay paths as well as its bytes.
|
|
344
|
+
if (normalized.startsWith('rules/')) {
|
|
345
|
+
for (const root of [node_path_1.default.join(options.projectPath, 'fraim/personalized-employee'), manager.contentRoot, org.contentRoot]) {
|
|
346
|
+
snapshot(node_path_1.default.join(root, normalized));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
};
|
|
299
350
|
const snapshotResolvedInclude = (relative, resolved) => {
|
|
300
351
|
if (resolved.source !== 'local')
|
|
301
352
|
return;
|
|
353
|
+
snapshotRuleOverlays(relative);
|
|
302
354
|
const normalized = relative.replace(/\\/g, '/');
|
|
303
|
-
if (node_path_1.default.isAbsolute(normalized) || normalized.split('/').some(part => !part || part === '..' || part === '.'))
|
|
304
|
-
throw new Error('invalid-capability-include');
|
|
305
355
|
let candidates;
|
|
306
356
|
if (resolved.scope === 'manager')
|
|
307
357
|
candidates = [node_path_1.default.join(manager.contentRoot, normalized)];
|
|
@@ -309,17 +359,40 @@ async function verifyCapabilityDelivery(action, input, options) {
|
|
|
309
359
|
candidates = [node_path_1.default.join(org.contentRoot, normalized)];
|
|
310
360
|
else if (resolved.scope === 'project')
|
|
311
361
|
candidates = [node_path_1.default.join(options.projectPath, 'fraim/personalized-employee', normalized)];
|
|
312
|
-
else
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
const source = candidates.find(file => node_fs_1.default.existsSync(file) && node_fs_1.default.statSync(file).isFile() && sameDeliveryText(node_fs_1.default.readFileSync(file, 'utf8'), resolved.content));
|
|
362
|
+
else
|
|
363
|
+
candidates = resolved.localPath ? [resolved.localPath] : [];
|
|
364
|
+
const matchingSource = candidates.find(file => node_fs_1.default.existsSync(file) && node_fs_1.default.statSync(file).isFile() && sameDeliveryText(node_fs_1.default.readFileSync(file, 'utf8'), resolved.content));
|
|
365
|
+
// Inherited overrides have expanded bodies. Their scoped raw source was
|
|
366
|
+
// snapshotted before resolution; imported children are traced separately.
|
|
367
|
+
const inheritedSource = normalized.startsWith('rules/') && resolved.scope && resolved.inherited ? candidates[0] : undefined;
|
|
368
|
+
const source = matchingSource ?? inheritedSource;
|
|
321
369
|
if (source)
|
|
322
|
-
snapshot(source, resolved.content);
|
|
370
|
+
snapshot(source, matchingSource ? resolved.content : undefined);
|
|
371
|
+
if (normalized.startsWith('rules/') && resolved.scope && source && node_path_1.default.resolve(source) !== node_path_1.default.resolve(target)) {
|
|
372
|
+
if (resolved.scope === 'project' && shared) {
|
|
373
|
+
sharedCallers.push({ file: source, expectedHash: snapshots.get(node_path_1.default.resolve(source)).sha256 });
|
|
374
|
+
}
|
|
375
|
+
else if (resolved.scope === 'manager' || resolved.scope === 'org') {
|
|
376
|
+
const level = resolved.scope === 'manager' ? 'Manager' : 'Org';
|
|
377
|
+
const sourceHome = (0, owner_identity_1.resolveOwnerHome)(options.projectPath, level, { runtimeModule: options.runtimeModule });
|
|
378
|
+
if (sourceHome.requiresPublish)
|
|
379
|
+
supportingPublications.set(source, sourceHome);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (shared && normalized.startsWith('rules/') && resolved.scope === undefined) {
|
|
383
|
+
// Shared delivery depends on the composition that actually shipped,
|
|
384
|
+
// not a local wrapper edit (or a stale installed copy). Verification
|
|
385
|
+
// checks these files at both merged and current HEAD; it never
|
|
386
|
+
// publishes supporting edits under the target rule's approval.
|
|
387
|
+
const registryFile = node_path_1.default.join(options.projectPath, 'registry', normalized);
|
|
388
|
+
if (!node_fs_1.default.existsSync(registryFile) || !sameDeliveryText(node_fs_1.default.readFileSync(registryFile, 'utf8'), resolved.content)) {
|
|
389
|
+
sharedCatalogAvailable = false;
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
snapshot(registryFile, resolved.content);
|
|
393
|
+
sharedCallers.push({ file: registryFile, expectedHash: (0, proposal_queue_1.hashContent)(node_fs_1.default.readFileSync(registryFile, 'utf8')) });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
323
396
|
};
|
|
324
397
|
snapshot(target, targetContent);
|
|
325
398
|
if (snapshots.get(node_path_1.default.resolve(target))?.sha256 !== localHash)
|
|
@@ -332,6 +405,45 @@ async function verifyCapabilityDelivery(action, input, options) {
|
|
|
332
405
|
return { published: false, reference: target, contentHash, reachable: false };
|
|
333
406
|
let reachable = true;
|
|
334
407
|
const sharedCallers = [];
|
|
408
|
+
const supportingPublications = new Map();
|
|
409
|
+
let sharedCatalogAvailable = true;
|
|
410
|
+
const captureRuleSelection = (callerPath) => {
|
|
411
|
+
const paths = load('core/utils/project-fraim-paths');
|
|
412
|
+
const workspace = paths.getWorkspaceFraimPath(options.projectPath);
|
|
413
|
+
const user = paths.getUserFraimDirPath();
|
|
414
|
+
snapshot(node_path_1.default.join(workspace, 'config.json'));
|
|
415
|
+
snapshot(node_path_1.default.join(user, 'config.json'));
|
|
416
|
+
const machineCatalog = node_path_1.default.join(user, 'domains.json');
|
|
417
|
+
const workspaceCatalog = node_path_1.default.join(workspace, 'domains.json');
|
|
418
|
+
const registryCatalog = node_path_1.default.join(options.projectPath, 'registry/domains.json');
|
|
419
|
+
const syncedCatalog = node_fs_1.default.existsSync(machineCatalog) ? machineCatalog : workspaceCatalog;
|
|
420
|
+
const baselineCatalog = node_fs_1.default.existsSync(syncedCatalog) ? syncedCatalog : registryCatalog;
|
|
421
|
+
for (const file of [machineCatalog, workspaceCatalog, registryCatalog])
|
|
422
|
+
snapshot(file);
|
|
423
|
+
for (const [level, root] of [['Project', node_path_1.default.join(workspace, 'personalized-employee')], ['Manager', manager.contentRoot], ['Org', org.contentRoot]]) {
|
|
424
|
+
const file = node_path_1.default.join(root, 'domains.json');
|
|
425
|
+
snapshot(file);
|
|
426
|
+
if (node_fs_1.default.existsSync(file)) {
|
|
427
|
+
const sourceHome = (0, owner_identity_1.resolveOwnerHome)(options.projectPath, level, { runtimeModule: options.runtimeModule });
|
|
428
|
+
if (sourceHome.requiresPublish)
|
|
429
|
+
supportingPublications.set(file, sourceHome);
|
|
430
|
+
if (shared && level === 'Project')
|
|
431
|
+
sharedCallers.push({ file, expectedHash: snapshots.get(node_path_1.default.resolve(file)).sha256 });
|
|
432
|
+
}
|
|
433
|
+
// An override created after verification must invalidate the proof.
|
|
434
|
+
snapshot(node_path_1.default.join(root, targetPath));
|
|
435
|
+
snapshot(node_path_1.default.join(root, callerPath));
|
|
436
|
+
}
|
|
437
|
+
if (shared) {
|
|
438
|
+
// An installed catalog alone cannot prove that the shared selection
|
|
439
|
+
// shipped in the reviewed repository revision and current remote HEAD.
|
|
440
|
+
if (!node_fs_1.default.existsSync(registryCatalog) || !node_fs_1.default.existsSync(baselineCatalog)
|
|
441
|
+
|| !sameDeliveryText(node_fs_1.default.readFileSync(registryCatalog, 'utf8'), node_fs_1.default.readFileSync(baselineCatalog, 'utf8')))
|
|
442
|
+
sharedCatalogAvailable = false;
|
|
443
|
+
else
|
|
444
|
+
sharedCallers.push({ file: registryCatalog, expectedHash: snapshots.get(node_path_1.default.resolve(registryCatalog)).sha256 });
|
|
445
|
+
}
|
|
446
|
+
};
|
|
335
447
|
for (const caller of input.callers) {
|
|
336
448
|
const callerHome = options.resolveHome(caller.owner);
|
|
337
449
|
(0, owner_identity_1.assertSameOwner)(caller.owner, callerHome.owner);
|
|
@@ -349,6 +461,7 @@ async function verifyCapabilityDelivery(action, input, options) {
|
|
|
349
461
|
sharedCallers.push({ file: callerFile, expectedHash: (0, proposal_queue_1.hashContent)(node_fs_1.default.readFileSync(callerFile, 'utf8')) });
|
|
350
462
|
let traversed = false;
|
|
351
463
|
resolver.resolveFile = async (relative, resolveOptions) => {
|
|
464
|
+
snapshotRuleOverlays(relative);
|
|
352
465
|
const resolved = await originalResolve(relative, resolveOptions);
|
|
353
466
|
snapshotResolvedInclude(relative, resolved);
|
|
354
467
|
if (relative === targetPath && resolved.scope === targetScope && sameDeliveryText(resolved.content, targetContent))
|
|
@@ -356,6 +469,17 @@ async function verifyCapabilityDelivery(action, input, options) {
|
|
|
356
469
|
return resolved;
|
|
357
470
|
};
|
|
358
471
|
const hydrated = await resolver.resolveIncludes(loadedCaller.content);
|
|
472
|
+
if (!traversed && callerPath.startsWith('jobs/') && targetPath.startsWith('rules/')) {
|
|
473
|
+
captureRuleSelection(callerPath);
|
|
474
|
+
const { assembleDomainRules } = load('core/utils/domain-rule-assembler');
|
|
475
|
+
const assembly = await assembleDomainRules({ jobPath: callerPath,
|
|
476
|
+
resolveFile: (relative) => resolver.resolveFile(relative, { includeMetadata: false, stripMcpHeader: true }) });
|
|
477
|
+
// Source traversal binds the approved raw bytes; assembly metadata
|
|
478
|
+
// proves this exact owner/path survived effective composition.
|
|
479
|
+
traversed = traversed && assembly.metadata.complete && assembly.metadata.selection === 'mapped'
|
|
480
|
+
&& [...assembly.metadata.artifacts, ...(assembly.metadata.includedArtifacts || [])]
|
|
481
|
+
.some((artifact) => artifact.path === targetPath && artifact.scope === targetScope);
|
|
482
|
+
}
|
|
359
483
|
if (!traversed || /\{\{include:[^}]+\}\}/.test(hydrated)) {
|
|
360
484
|
reachable = false;
|
|
361
485
|
break;
|
|
@@ -366,10 +490,39 @@ async function verifyCapabilityDelivery(action, input, options) {
|
|
|
366
490
|
return { published: false, reference: target, contentHash, reachable: false };
|
|
367
491
|
const localFiles = [...snapshots.values()].sort((a, b) => a.file.localeCompare(b.file));
|
|
368
492
|
const verifySnapshots = () => {
|
|
369
|
-
for (const item of localFiles)
|
|
493
|
+
for (const item of localFiles) {
|
|
494
|
+
if (item.kind === 'directory' && item.realPath !== null) {
|
|
495
|
+
if (!node_fs_1.default.existsSync(item.file) || !node_fs_1.default.statSync(item.file).isDirectory() || node_fs_1.default.realpathSync(item.file) !== item.realPath)
|
|
496
|
+
throw new Error('capability-local-content-changed');
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (item.sha256 === null) {
|
|
500
|
+
try {
|
|
501
|
+
node_fs_1.default.lstatSync(item.file);
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
if (error.code === 'ENOENT')
|
|
505
|
+
continue;
|
|
506
|
+
throw error;
|
|
507
|
+
}
|
|
508
|
+
throw new Error('capability-local-content-changed');
|
|
509
|
+
}
|
|
370
510
|
if (node_fs_1.default.realpathSync(item.file) !== item.realPath || (0, proposal_queue_1.hashContent)(node_fs_1.default.readFileSync(item.file, 'utf8')) !== item.sha256)
|
|
371
511
|
throw new Error('capability-local-content-changed');
|
|
512
|
+
}
|
|
372
513
|
};
|
|
514
|
+
verifySnapshots();
|
|
515
|
+
if (!sharedCatalogAvailable)
|
|
516
|
+
return { published: false, reference: target, contentHash, reachable, localFiles, ...reviewEvidence };
|
|
517
|
+
for (const [file, sourceHome] of supportingPublications) {
|
|
518
|
+
if (!options.runtimeModulePath)
|
|
519
|
+
throw new Error('capability-publication-required');
|
|
520
|
+
// Rule approval never authorizes publishing supporting catalog or rule edits.
|
|
521
|
+
const supporting = publishRunFile(options.projectPath, file, sourceHome, input.operationKey || 'catalog-delivery-check', snapshots.get(node_path_1.default.resolve(file)).sha256, options.runtimeModulePath, undefined, true);
|
|
522
|
+
verifySnapshots();
|
|
523
|
+
if (!supporting.published)
|
|
524
|
+
return { ...supporting, contentHash, reachable, localFiles, ...reviewEvidence };
|
|
525
|
+
}
|
|
373
526
|
if (shared) {
|
|
374
527
|
const publication = verifySharedProjectPublication(options.projectPath, target, localHash, input.publicationRef, sharedCallers);
|
|
375
528
|
verifySnapshots();
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.WORKSPACE_FRAIM_DIRNAME =
|
|
6
|
+
exports.WORKSPACE_FRAIM_DIRNAME = void 0;
|
|
7
7
|
exports.getWorkspaceFraimDir = getWorkspaceFraimDir;
|
|
8
8
|
exports.getWorkspaceConfigPath = getWorkspaceConfigPath;
|
|
9
9
|
exports.getWorkspaceFraimPath = getWorkspaceFraimPath;
|
|
@@ -21,8 +21,6 @@ exports.isPathInsideOrEqual = isPathInsideOrEqual;
|
|
|
21
21
|
const fs_1 = require("fs");
|
|
22
22
|
const os_1 = __importDefault(require("os"));
|
|
23
23
|
const path_1 = require("path");
|
|
24
|
-
/** Share domain data with the MCP runtime and proposal authoring helpers. */
|
|
25
|
-
exports.LEARNING_DOMAINS = Object.freeze(JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../learning/learning-domains.json'), 'utf8')).domains);
|
|
26
24
|
exports.WORKSPACE_FRAIM_DIRNAME = 'fraim';
|
|
27
25
|
function getWorkspaceFraimDir(projectRoot = process.cwd()) {
|
|
28
26
|
return (0, path_1.join)(projectRoot, exports.WORKSPACE_FRAIM_DIRNAME);
|
|
@@ -44,6 +44,7 @@ const projectFraimPaths = __importStar(require("../../core/utils/project-fraim-p
|
|
|
44
44
|
const packHome = __importStar(require("../utils/pack-home"));
|
|
45
45
|
const userConfig = __importStar(require("../utils/user-config"));
|
|
46
46
|
const localRegistryResolver = __importStar(require("../../core/utils/local-registry-resolver"));
|
|
47
|
+
const domainRuleAssembler = __importStar(require("../../core/utils/domain-rule-assembler"));
|
|
47
48
|
const learningContextBuilder = __importStar(require("../../local-mcp-server/learning-context-builder"));
|
|
48
49
|
const learningUsageStore = __importStar(require("../../local-mcp-server/learning-usage-store"));
|
|
49
50
|
const MODULE_MAP = {
|
|
@@ -51,6 +52,7 @@ const MODULE_MAP = {
|
|
|
51
52
|
'cli/utils/pack-home': packHome,
|
|
52
53
|
'cli/utils/user-config': userConfig,
|
|
53
54
|
'core/utils/local-registry-resolver': localRegistryResolver,
|
|
55
|
+
'core/utils/domain-rule-assembler': domainRuleAssembler,
|
|
54
56
|
'local-mcp-server/learning-context-builder': learningContextBuilder,
|
|
55
57
|
'local-mcp-server/learning-usage-store': learningUsageStore,
|
|
56
58
|
};
|
|
@@ -228,6 +228,13 @@ async function syncTypedFilesToUserDir(files, type, subdir, label) {
|
|
|
228
228
|
* Extracted from syncFromRemote so it can run without a project root.
|
|
229
229
|
*/
|
|
230
230
|
async function syncScriptsToUserDir(files) {
|
|
231
|
+
const catalog = files.find(file => file.type === 'config' && file.path === 'domains.json');
|
|
232
|
+
if (catalog) {
|
|
233
|
+
// Only this known root artifact is accepted; never clean the user root.
|
|
234
|
+
JSON.parse(catalog.content);
|
|
235
|
+
(0, fs_1.mkdirSync)((0, script_sync_utils_1.getUserFraimDir)(), { recursive: true });
|
|
236
|
+
(0, fs_1.writeFileSync)((0, path_1.join)((0, script_sync_utils_1.getUserFraimDir)(), 'domains.json'), catalog.content, 'utf8');
|
|
237
|
+
}
|
|
231
238
|
return syncTypedFilesToUserDir(files, 'script', 'scripts', 'script');
|
|
232
239
|
}
|
|
233
240
|
/**
|
|
@@ -105,6 +105,10 @@ function copyScriptToUserDirectory(sourcePath, registryScriptsPath) {
|
|
|
105
105
|
*/
|
|
106
106
|
function syncScriptsToUserDirectory(registryPath) {
|
|
107
107
|
ensureUserFraimDirectories();
|
|
108
|
+
const domainCatalog = path_1.default.join(registryPath, 'domains.json');
|
|
109
|
+
if (fs_1.default.existsSync(domainCatalog)) {
|
|
110
|
+
fs_1.default.copyFileSync(domainCatalog, path_1.default.join(getUserFraimDir(), 'domains.json'));
|
|
111
|
+
}
|
|
108
112
|
const registryScriptsPath = path_1.default.join(registryPath, 'scripts');
|
|
109
113
|
const allScripts = getRegistryScripts(registryPath);
|
|
110
114
|
let syncedCount = 0;
|
|
@@ -1,119 +1,35 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Learning domains (issue #806).
|
|
4
|
-
*
|
|
5
|
-
* A learning's DOMAIN is the fourth axis of the learning system, alongside:
|
|
6
|
-
* - tier (L0 raw / L1 personal / L2 org),
|
|
7
|
-
* - category (mistake-patterns / preferences / validated-patterns / manager-coaching),
|
|
8
|
-
* - scope (machine-global `~/.fraim` vs repo-local `fraim/`).
|
|
9
|
-
*
|
|
10
|
-
* A learning is either `global` (applies to every job) or scoped to exactly one
|
|
11
|
-
* coarse job-domain. At `get_fraim_job`, the loader injects global learnings plus
|
|
12
|
-
* the current job's-domain learnings only, so a marketing job no longer loads
|
|
13
|
-
* engineering learnings.
|
|
14
|
-
*
|
|
15
|
-
* The shared registry/scripts/learning/learning-domains.json owns the domain list, the
|
|
16
|
-
* registry-category -> domain map, and the per-domain authoring voice (R3).
|
|
17
|
-
* This module provides the pure resolver used at the injection sites, following the
|
|
18
|
-
* config-module convention of `src/config/portfolio-slug-overrides.ts`.
|
|
19
|
-
*/
|
|
20
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.
|
|
3
|
+
exports.jobCategoryFromRegistryPath = exports.STORED_LEARNING_DOMAINS = exports.HISTORICAL_LEARNING_DOMAINS = exports.LEARNING_DOMAINS = exports.LEARNING_DOMAIN_MAP_VERSION = void 0;
|
|
22
4
|
exports.isLearningDomain = isLearningDomain;
|
|
23
|
-
exports.jobCategoryFromRegistryPath = jobCategoryFromRegistryPath;
|
|
24
5
|
exports.resolveJobDomain = resolveJobDomain;
|
|
25
|
-
exports.domainFromJobFrontmatter = domainFromJobFrontmatter;
|
|
26
6
|
exports.resolveDomainForJob = resolveDomainForJob;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
7
|
+
/** Learning domains use the same folder authority as rule delivery. */
|
|
8
|
+
const catalog_1 = require("../../registry/scripts/domains/catalog");
|
|
9
|
+
let catalog = { schemaVersion: 2, mapVersion: 0, domains: {} };
|
|
10
|
+
try {
|
|
11
|
+
const candidate = require('../../registry/domains.json');
|
|
12
|
+
if (candidate?.schemaVersion !== 2 || !candidate.domains || typeof candidate.domains !== 'object' || Array.isArray(candidate.domains))
|
|
13
|
+
throw new Error('Invalid domain catalog');
|
|
14
|
+
catalog = candidate;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
console.warn('Learning domains unavailable: restore or sync registry/domains.json, then refresh instructions. Global learning files remain available.');
|
|
18
|
+
}
|
|
19
|
+
exports.LEARNING_DOMAIN_MAP_VERSION = catalog.mapVersion;
|
|
20
|
+
exports.LEARNING_DOMAINS = Object.freeze(Object.keys(catalog.domains).filter(domain => domain !== 'global'));
|
|
21
|
+
/** Retired storage names remain readable/editable; never use these as current job domains. */
|
|
22
|
+
exports.HISTORICAL_LEARNING_DOMAINS = Object.freeze(['compliance', 'company-building', 'operations']);
|
|
23
|
+
exports.STORED_LEARNING_DOMAINS = Object.freeze([...exports.LEARNING_DOMAINS, ...exports.HISTORICAL_LEARNING_DOMAINS]);
|
|
33
24
|
function isLearningDomain(value) {
|
|
34
25
|
return typeof value === 'string' && exports.LEARNING_DOMAINS.includes(value);
|
|
35
26
|
}
|
|
36
|
-
|
|
37
|
-
* Extract the registry job category from a job's registry path.
|
|
38
|
-
*
|
|
39
|
-
* Handles both `jobs/ai-employee/<category>/<name>.md` (and `ai-manager`,
|
|
40
|
-
* `personalized-employee`) and the flatter `jobs/<category>/<name>.md`, with or
|
|
41
|
-
* without a leading `registry/` prefix and with either slash style. Returns null
|
|
42
|
-
* when the input is not a job path (for example a bare job name with no directory).
|
|
43
|
-
*
|
|
44
|
-
* Mirrors the jobs branch of `UsageCollector.parseComponentName` so both derive
|
|
45
|
-
* the category the same way.
|
|
46
|
-
*/
|
|
47
|
-
function jobCategoryFromRegistryPath(registryPath) {
|
|
48
|
-
if (!registryPath || typeof registryPath !== 'string')
|
|
49
|
-
return null;
|
|
50
|
-
let clean = registryPath.replace(/\\/g, '/').replace(/^registry\//, '');
|
|
51
|
-
if (clean.startsWith('/'))
|
|
52
|
-
clean = clean.slice(1);
|
|
53
|
-
const parts = clean.split('/');
|
|
54
|
-
if (parts[0] !== 'jobs')
|
|
55
|
-
return null;
|
|
56
|
-
if (parts[1] === 'ai-employee' || parts[1] === 'ai-manager' || parts[1] === 'personalized-employee') {
|
|
57
|
-
return parts.length > 3 ? parts[2] : null; // jobs/ai-employee/<category>/<name>.md
|
|
58
|
-
}
|
|
59
|
-
return parts.length > 2 ? parts[1] : null; // jobs/<category>/<name>.md
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Resolve the learning domain for a job from its registry path.
|
|
63
|
-
*
|
|
64
|
-
* Returns a `LearningDomain` for domain-scoped categories, or `null` for
|
|
65
|
-
* cross-cutting categories, unmapped categories, and unparseable inputs. `null`
|
|
66
|
-
* means "load global learnings only" at the injection site: global is never
|
|
67
|
-
* hidden and an unknown category never guesses a wrong domain. The completeness
|
|
68
|
-
* test is what distinguishes an intentional global from a missing mapping.
|
|
69
|
-
*/
|
|
27
|
+
exports.jobCategoryFromRegistryPath = catalog_1.jobDomainFromRegistryPath;
|
|
70
28
|
function resolveJobDomain(registryPath) {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
return null;
|
|
74
|
-
return exports.JOB_DOMAIN_MAP[category] ?? null;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Read an explicit learning domain a job declares in its own frontmatter, if any.
|
|
78
|
-
*
|
|
79
|
-
* This is the extensibility hook (issue #806 review): a job can self-declare
|
|
80
|
-
* `domain` so a newly-taught job with a new category is handled without editing
|
|
81
|
-
* `JOB_DOMAIN_MAP`. Supports the JSON frontmatter block FRAIM jobs use
|
|
82
|
-
* (`---\n{ ..., "domain": "people-ops" }\n---`) and a plain `domain: <value>`
|
|
83
|
-
* line. Returns null when there is no frontmatter, no `domain`, or an unknown
|
|
84
|
-
* domain - callers then fall back to the category map.
|
|
85
|
-
*/
|
|
86
|
-
function domainFromJobFrontmatter(jobFileContent) {
|
|
87
|
-
if (!jobFileContent || typeof jobFileContent !== 'string')
|
|
88
|
-
return null;
|
|
89
|
-
const fm = jobFileContent.match(/^?---\r?\n([\s\S]*?)\r?\n---/);
|
|
90
|
-
if (!fm)
|
|
91
|
-
return null;
|
|
92
|
-
const block = fm[1];
|
|
93
|
-
const trimmed = block.trim();
|
|
94
|
-
if (trimmed.startsWith('{')) {
|
|
95
|
-
try {
|
|
96
|
-
const parsed = JSON.parse(trimmed);
|
|
97
|
-
return isLearningDomain(parsed?.domain) ? parsed.domain : null;
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
// fall through to line parsing
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
const line = block.match(/^\s*domain:\s*["']?([a-z][a-z-]*)["']?\s*$/m);
|
|
104
|
-
return line && isLearningDomain(line[1]) ? line[1] : null;
|
|
29
|
+
const selected = (0, catalog_1.selectCatalogDomain)(catalog, registryPath);
|
|
30
|
+
return selected.selection === 'mapped' && isLearningDomain(selected.domain) ? selected.domain : null;
|
|
105
31
|
}
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
* category map; otherwise the map applies; a null/unparseable path yields null
|
|
110
|
-
* (load global only). Pure and side-effect-free: callers do the I/O (reading the
|
|
111
|
-
* registry path and job file) and pass the results here, which keeps this
|
|
112
|
-
* decision logic directly testable with mocked inputs.
|
|
113
|
-
*/
|
|
114
|
-
function resolveDomainForJob(registryPath, jobFileContent) {
|
|
115
|
-
if (!registryPath)
|
|
116
|
-
return null;
|
|
117
|
-
const declared = jobFileContent ? domainFromJobFrontmatter(jobFileContent) : null;
|
|
118
|
-
return declared ?? resolveJobDomain(registryPath);
|
|
32
|
+
/** Job declarations are deliberately ignored, including personalized overrides. */
|
|
33
|
+
function resolveDomainForJob(registryPath, _jobFileContent) {
|
|
34
|
+
return registryPath ? resolveJobDomain(registryPath) : null;
|
|
119
35
|
}
|
|
@@ -55,6 +55,13 @@ class AIMentor {
|
|
|
55
55
|
}
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
58
|
+
/** Delivery identity and first entry come from the executable job, never request categories. */
|
|
59
|
+
async getRuleDeliveryContext(jobName) {
|
|
60
|
+
const job = await this.getOrLoadJob(jobName);
|
|
61
|
+
if (!job?.path)
|
|
62
|
+
return null;
|
|
63
|
+
return { jobPath: job.path.replace(/\\/g, '/').replace(/^.*\/registry\//, ''), initialPhase: job.metadata.initialPhase || Object.keys(job.metadata.phases || {})[0] || Array.from(job.phases.keys())[0] || 'starting' };
|
|
64
|
+
}
|
|
58
65
|
async resolveIncludes(content, basePath) {
|
|
59
66
|
return (0, include_resolver_1.resolveIncludes)(content, this.resolver, basePath, this.skillDedup);
|
|
60
67
|
}
|