fraim 2.0.360-next.0 → 2.0.360-next.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/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 +192 -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/fraim/db-service.js +1 -1
- 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/middleware/logger.js +1 -45
- 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');
|
|
@@ -279,14 +288,55 @@ async function verifyCapabilityDelivery(action, input, options) {
|
|
|
279
288
|
const { resolvePackHome } = load('cli/utils/pack-home');
|
|
280
289
|
const manager = resolvePackHome('manager', { migrateLegacy: false });
|
|
281
290
|
const org = resolvePackHome('org', { migrateLegacy: false });
|
|
291
|
+
const consumedLocalFiles = new Set();
|
|
282
292
|
const resolver = new LocalRegistryResolver({ workspaceRoot: options.projectPath,
|
|
283
293
|
managerLocalRoot: manager.contentRoot, managerCacheRoot: manager.contentRoot, orgCacheRoot: org.contentRoot,
|
|
284
294
|
remoteContentResolver: async () => { throw new Error('capability-not-available-locally'); },
|
|
295
|
+
onLocalDependency: (file, kind, content) => {
|
|
296
|
+
if (kind === 'directory')
|
|
297
|
+
snapshotDirectory(file);
|
|
298
|
+
else {
|
|
299
|
+
snapshot(file, content);
|
|
300
|
+
// A content-bearing callback records an actual read; existence
|
|
301
|
+
// probes and absent overlays must not become publication inputs.
|
|
302
|
+
if (content !== undefined)
|
|
303
|
+
consumedLocalFiles.add(node_path_1.default.resolve(file));
|
|
304
|
+
}
|
|
305
|
+
},
|
|
285
306
|
});
|
|
286
307
|
const originalResolve = resolver.resolveFile.bind(resolver);
|
|
287
308
|
const snapshots = new Map();
|
|
309
|
+
const snapshotDirectory = (file) => {
|
|
310
|
+
const absolute = node_path_1.default.resolve(file);
|
|
311
|
+
let realPath = null;
|
|
312
|
+
try {
|
|
313
|
+
if (!node_fs_1.default.statSync(absolute).isDirectory())
|
|
314
|
+
throw new Error('capability-local-content-changed');
|
|
315
|
+
realPath = node_fs_1.default.realpathSync(absolute);
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
if (error.code !== 'ENOENT')
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
const prior = snapshots.get(absolute);
|
|
322
|
+
if (prior && (prior.kind !== 'directory' || prior.realPath !== realPath))
|
|
323
|
+
throw new Error('capability-local-content-changed');
|
|
324
|
+
snapshots.set(absolute, { file: absolute, realPath, sha256: null, kind: 'directory' });
|
|
325
|
+
};
|
|
288
326
|
const snapshot = (file, expectedContent) => {
|
|
289
327
|
const absolute = node_path_1.default.resolve(file);
|
|
328
|
+
try {
|
|
329
|
+
node_fs_1.default.lstatSync(absolute);
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
if (error.code !== 'ENOENT' || expectedContent !== undefined)
|
|
333
|
+
throw error;
|
|
334
|
+
const prior = snapshots.get(absolute);
|
|
335
|
+
if (prior?.sha256)
|
|
336
|
+
throw new Error('capability-local-content-changed');
|
|
337
|
+
snapshots.set(absolute, { file: absolute, realPath: null, sha256: null });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
290
340
|
const body = node_fs_1.default.readFileSync(absolute, 'utf8');
|
|
291
341
|
if (expectedContent !== undefined && !sameDeliveryText(body, expectedContent))
|
|
292
342
|
throw new Error('capability-local-content-changed');
|
|
@@ -296,12 +346,23 @@ async function verifyCapabilityDelivery(action, input, options) {
|
|
|
296
346
|
throw new Error('capability-local-content-changed');
|
|
297
347
|
snapshots.set(absolute, current);
|
|
298
348
|
};
|
|
349
|
+
const snapshotRuleOverlays = (relative) => {
|
|
350
|
+
const normalized = relative.replace(/\\/g, '/');
|
|
351
|
+
if (node_path_1.default.isAbsolute(normalized) || normalized.split('/').some(part => !part || part === '..' || part === '.'))
|
|
352
|
+
throw new Error('invalid-capability-include');
|
|
353
|
+
// A new wrapper/fragment override can change delivery without touching
|
|
354
|
+
// the selected source. Bind absent overlay paths as well as its bytes.
|
|
355
|
+
if (normalized.startsWith('rules/')) {
|
|
356
|
+
for (const root of [node_path_1.default.join(options.projectPath, 'fraim/personalized-employee'), manager.contentRoot, org.contentRoot]) {
|
|
357
|
+
snapshot(node_path_1.default.join(root, normalized));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
};
|
|
299
361
|
const snapshotResolvedInclude = (relative, resolved) => {
|
|
300
362
|
if (resolved.source !== 'local')
|
|
301
363
|
return;
|
|
364
|
+
snapshotRuleOverlays(relative);
|
|
302
365
|
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
366
|
let candidates;
|
|
306
367
|
if (resolved.scope === 'manager')
|
|
307
368
|
candidates = [node_path_1.default.join(manager.contentRoot, normalized)];
|
|
@@ -309,17 +370,40 @@ async function verifyCapabilityDelivery(action, input, options) {
|
|
|
309
370
|
candidates = [node_path_1.default.join(org.contentRoot, normalized)];
|
|
310
371
|
else if (resolved.scope === 'project')
|
|
311
372
|
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));
|
|
373
|
+
else
|
|
374
|
+
candidates = resolved.localPath ? [resolved.localPath] : [];
|
|
375
|
+
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));
|
|
376
|
+
// Inherited overrides have expanded bodies. Their scoped raw source was
|
|
377
|
+
// snapshotted before resolution; imported children are traced separately.
|
|
378
|
+
const inheritedSource = normalized.startsWith('rules/') && resolved.scope && resolved.inherited ? candidates[0] : undefined;
|
|
379
|
+
const source = matchingSource ?? inheritedSource;
|
|
321
380
|
if (source)
|
|
322
|
-
snapshot(source, resolved.content);
|
|
381
|
+
snapshot(source, matchingSource ? resolved.content : undefined);
|
|
382
|
+
if (normalized.startsWith('rules/') && resolved.scope && source && node_path_1.default.resolve(source) !== node_path_1.default.resolve(target)) {
|
|
383
|
+
if (resolved.scope === 'project' && shared) {
|
|
384
|
+
sharedCallers.push({ file: source, expectedHash: snapshots.get(node_path_1.default.resolve(source)).sha256 });
|
|
385
|
+
}
|
|
386
|
+
else if (resolved.scope === 'manager' || resolved.scope === 'org') {
|
|
387
|
+
const level = resolved.scope === 'manager' ? 'Manager' : 'Org';
|
|
388
|
+
const sourceHome = (0, owner_identity_1.resolveOwnerHome)(options.projectPath, level, { runtimeModule: options.runtimeModule });
|
|
389
|
+
if (sourceHome.requiresPublish)
|
|
390
|
+
supportingPublications.set(source, sourceHome);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (shared && normalized.startsWith('rules/') && resolved.scope === undefined) {
|
|
394
|
+
// Shared delivery depends on the composition that actually shipped,
|
|
395
|
+
// not a local wrapper edit (or a stale installed copy). Verification
|
|
396
|
+
// checks these files at both merged and current HEAD; it never
|
|
397
|
+
// publishes supporting edits under the target rule's approval.
|
|
398
|
+
const registryFile = node_path_1.default.join(options.projectPath, 'registry', normalized);
|
|
399
|
+
if (!node_fs_1.default.existsSync(registryFile) || !sameDeliveryText(node_fs_1.default.readFileSync(registryFile, 'utf8'), resolved.content)) {
|
|
400
|
+
sharedCatalogAvailable = false;
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
snapshot(registryFile, resolved.content);
|
|
404
|
+
sharedCallers.push({ file: registryFile, expectedHash: (0, proposal_queue_1.hashContent)(node_fs_1.default.readFileSync(registryFile, 'utf8')) });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
323
407
|
};
|
|
324
408
|
snapshot(target, targetContent);
|
|
325
409
|
if (snapshots.get(node_path_1.default.resolve(target))?.sha256 !== localHash)
|
|
@@ -332,6 +416,45 @@ async function verifyCapabilityDelivery(action, input, options) {
|
|
|
332
416
|
return { published: false, reference: target, contentHash, reachable: false };
|
|
333
417
|
let reachable = true;
|
|
334
418
|
const sharedCallers = [];
|
|
419
|
+
const supportingPublications = new Map();
|
|
420
|
+
let sharedCatalogAvailable = true;
|
|
421
|
+
const captureRuleSelection = (callerPath) => {
|
|
422
|
+
const paths = load('core/utils/project-fraim-paths');
|
|
423
|
+
const workspace = paths.getWorkspaceFraimPath(options.projectPath);
|
|
424
|
+
const user = paths.getUserFraimDirPath();
|
|
425
|
+
snapshot(node_path_1.default.join(workspace, 'config.json'));
|
|
426
|
+
snapshot(node_path_1.default.join(user, 'config.json'));
|
|
427
|
+
const machineCatalog = node_path_1.default.join(user, 'domains.json');
|
|
428
|
+
const workspaceCatalog = node_path_1.default.join(workspace, 'domains.json');
|
|
429
|
+
const registryCatalog = node_path_1.default.join(options.projectPath, 'registry/domains.json');
|
|
430
|
+
const syncedCatalog = node_fs_1.default.existsSync(machineCatalog) ? machineCatalog : workspaceCatalog;
|
|
431
|
+
const baselineCatalog = node_fs_1.default.existsSync(syncedCatalog) ? syncedCatalog : registryCatalog;
|
|
432
|
+
for (const file of [machineCatalog, workspaceCatalog, registryCatalog])
|
|
433
|
+
snapshot(file);
|
|
434
|
+
for (const [level, root] of [['Project', node_path_1.default.join(workspace, 'personalized-employee')], ['Manager', manager.contentRoot], ['Org', org.contentRoot]]) {
|
|
435
|
+
const file = node_path_1.default.join(root, 'domains.json');
|
|
436
|
+
snapshot(file);
|
|
437
|
+
if (node_fs_1.default.existsSync(file)) {
|
|
438
|
+
const sourceHome = (0, owner_identity_1.resolveOwnerHome)(options.projectPath, level, { runtimeModule: options.runtimeModule });
|
|
439
|
+
if (sourceHome.requiresPublish)
|
|
440
|
+
supportingPublications.set(file, sourceHome);
|
|
441
|
+
if (shared && level === 'Project')
|
|
442
|
+
sharedCallers.push({ file, expectedHash: snapshots.get(node_path_1.default.resolve(file)).sha256 });
|
|
443
|
+
}
|
|
444
|
+
// An override created after verification must invalidate the proof.
|
|
445
|
+
snapshot(node_path_1.default.join(root, targetPath));
|
|
446
|
+
snapshot(node_path_1.default.join(root, callerPath));
|
|
447
|
+
}
|
|
448
|
+
if (shared) {
|
|
449
|
+
// An installed catalog alone cannot prove that the shared selection
|
|
450
|
+
// shipped in the reviewed repository revision and current remote HEAD.
|
|
451
|
+
if (!node_fs_1.default.existsSync(registryCatalog) || !node_fs_1.default.existsSync(baselineCatalog)
|
|
452
|
+
|| !sameDeliveryText(node_fs_1.default.readFileSync(registryCatalog, 'utf8'), node_fs_1.default.readFileSync(baselineCatalog, 'utf8')))
|
|
453
|
+
sharedCatalogAvailable = false;
|
|
454
|
+
else
|
|
455
|
+
sharedCallers.push({ file: registryCatalog, expectedHash: snapshots.get(node_path_1.default.resolve(registryCatalog)).sha256 });
|
|
456
|
+
}
|
|
457
|
+
};
|
|
335
458
|
for (const caller of input.callers) {
|
|
336
459
|
const callerHome = options.resolveHome(caller.owner);
|
|
337
460
|
(0, owner_identity_1.assertSameOwner)(caller.owner, callerHome.owner);
|
|
@@ -349,6 +472,7 @@ async function verifyCapabilityDelivery(action, input, options) {
|
|
|
349
472
|
sharedCallers.push({ file: callerFile, expectedHash: (0, proposal_queue_1.hashContent)(node_fs_1.default.readFileSync(callerFile, 'utf8')) });
|
|
350
473
|
let traversed = false;
|
|
351
474
|
resolver.resolveFile = async (relative, resolveOptions) => {
|
|
475
|
+
snapshotRuleOverlays(relative);
|
|
352
476
|
const resolved = await originalResolve(relative, resolveOptions);
|
|
353
477
|
snapshotResolvedInclude(relative, resolved);
|
|
354
478
|
if (relative === targetPath && resolved.scope === targetScope && sameDeliveryText(resolved.content, targetContent))
|
|
@@ -356,20 +480,72 @@ async function verifyCapabilityDelivery(action, input, options) {
|
|
|
356
480
|
return resolved;
|
|
357
481
|
};
|
|
358
482
|
const hydrated = await resolver.resolveIncludes(loadedCaller.content);
|
|
483
|
+
if (!traversed && callerPath.startsWith('jobs/') && targetPath.startsWith('rules/')) {
|
|
484
|
+
captureRuleSelection(callerPath);
|
|
485
|
+
const { assembleDomainRules } = load('core/utils/domain-rule-assembler');
|
|
486
|
+
const assembly = await assembleDomainRules({ jobPath: callerPath,
|
|
487
|
+
resolveFile: (relative) => resolver.resolveFile(relative, { includeMetadata: false, stripMcpHeader: true }) });
|
|
488
|
+
// Source traversal binds the approved raw bytes; assembly metadata
|
|
489
|
+
// proves this exact owner/path survived effective composition.
|
|
490
|
+
traversed = traversed && assembly.metadata.complete && assembly.metadata.selection === 'mapped'
|
|
491
|
+
&& [...assembly.metadata.artifacts, ...(assembly.metadata.includedArtifacts || [])]
|
|
492
|
+
.some((artifact) => artifact.path === targetPath && artifact.scope === targetScope);
|
|
493
|
+
}
|
|
359
494
|
if (!traversed || /\{\{include:[^}]+\}\}/.test(hydrated)) {
|
|
360
495
|
reachable = false;
|
|
361
496
|
break;
|
|
362
497
|
}
|
|
363
498
|
}
|
|
364
499
|
resolver.resolveFile = originalResolve;
|
|
500
|
+
if (shared) {
|
|
501
|
+
// Inheritance resolves raw parents internally, bypassing the public
|
|
502
|
+
// resolveFile interceptor. Include the Project rule bytes it consumed
|
|
503
|
+
// in both merged-commit and current-remote publication verification.
|
|
504
|
+
const projectRules = node_path_1.default.join(options.projectPath, 'fraim/personalized-employee/rules');
|
|
505
|
+
for (const file of consumedLocalFiles) {
|
|
506
|
+
const relative = node_path_1.default.relative(projectRules, file);
|
|
507
|
+
if (!relative || relative === '..' || relative.startsWith('..' + node_path_1.default.sep) || node_path_1.default.isAbsolute(relative))
|
|
508
|
+
continue;
|
|
509
|
+
sharedCallers.push({ file, expectedHash: snapshots.get(file).sha256 });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
365
512
|
if (!reachable || (0, proposal_queue_1.hashContent)(node_fs_1.default.readFileSync(target, 'utf8')) !== localHash)
|
|
366
513
|
return { published: false, reference: target, contentHash, reachable: false };
|
|
367
514
|
const localFiles = [...snapshots.values()].sort((a, b) => a.file.localeCompare(b.file));
|
|
368
515
|
const verifySnapshots = () => {
|
|
369
|
-
for (const item of localFiles)
|
|
516
|
+
for (const item of localFiles) {
|
|
517
|
+
if (item.kind === 'directory' && item.realPath !== null) {
|
|
518
|
+
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)
|
|
519
|
+
throw new Error('capability-local-content-changed');
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (item.sha256 === null) {
|
|
523
|
+
try {
|
|
524
|
+
node_fs_1.default.lstatSync(item.file);
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
if (error.code === 'ENOENT')
|
|
528
|
+
continue;
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
throw new Error('capability-local-content-changed');
|
|
532
|
+
}
|
|
370
533
|
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
534
|
throw new Error('capability-local-content-changed');
|
|
535
|
+
}
|
|
372
536
|
};
|
|
537
|
+
verifySnapshots();
|
|
538
|
+
if (!sharedCatalogAvailable)
|
|
539
|
+
return { published: false, reference: target, contentHash, reachable, localFiles, ...reviewEvidence };
|
|
540
|
+
for (const [file, sourceHome] of supportingPublications) {
|
|
541
|
+
if (!options.runtimeModulePath)
|
|
542
|
+
throw new Error('capability-publication-required');
|
|
543
|
+
// Rule approval never authorizes publishing supporting catalog or rule edits.
|
|
544
|
+
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);
|
|
545
|
+
verifySnapshots();
|
|
546
|
+
if (!supporting.published)
|
|
547
|
+
return { ...supporting, contentHash, reachable, localFiles, ...reviewEvidence };
|
|
548
|
+
}
|
|
373
549
|
if (shared) {
|
|
374
550
|
const publication = verifySharedProjectPublication(options.projectPath, target, localHash, input.publicationRef, sharedCallers);
|
|
375
551
|
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
|
}
|