fraim 2.0.346 → 2.0.348

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/registry/scripts/learning/completion-state.js +80 -4
  2. package/dist/registry/scripts/learning/corpus-audit.js +7 -7
  3. package/dist/registry/scripts/learning/entry-authoring.js +181 -0
  4. package/dist/registry/scripts/learning/entry-format.js +10 -1
  5. package/dist/registry/scripts/learning/outcome-assessment.js +162 -0
  6. package/dist/registry/scripts/learning/outcome-types.js +92 -0
  7. package/dist/registry/scripts/learning/owner-identity.js +11 -0
  8. package/dist/registry/scripts/learning/proposal-queue.js +81 -0
  9. package/dist/registry/scripts/learning/run-application.js +1003 -0
  10. package/dist/registry/scripts/learning/run-publication.js +364 -0
  11. package/dist/registry/scripts/learning/run-record.js +895 -0
  12. package/dist/registry/scripts/learning-usage/learning-source-record.js +131 -0
  13. package/dist/src/cli/commands/learning.js +64 -0
  14. package/dist/src/cli/doctor/checks/scripts-checks.js +53 -1
  15. package/dist/src/cli/fraim.js +2 -0
  16. package/dist/src/cli/learning/inventory.js +285 -0
  17. package/dist/src/cli/learning/queue-operations.js +199 -0
  18. package/dist/src/cli/learning/runtime.js +82 -0
  19. package/dist/src/cli/learning/usage.js +277 -0
  20. package/dist/src/core/fraim-config-schema.generated.js +0 -4
  21. package/dist/src/local-mcp-server/learning-context-builder.js +91 -377
  22. package/dist/src/local-mcp-server/learning-entry-format.js +25 -0
  23. package/dist/src/local-mcp-server/learning-usage-analysis.js +12 -410
  24. package/dist/src/local-mcp-server/learning-usage-eligibility.js +8 -14
  25. package/dist/src/local-mcp-server/learning-usage-projection.js +5 -22
  26. package/dist/src/local-mcp-server/learning-usage-store.js +93 -393
  27. package/dist/src/local-mcp-server/learning-usage-verify.js +9 -40
  28. package/dist/src/local-mcp-server/stdio-server.js +0 -48
  29. package/package.json +9 -1
  30. package/dist/src/local-mcp-server/learning-firing-parser.js +0 -247
  31. package/dist/src/local-mcp-server/learning-usage-attestation.js +0 -238
  32. package/dist/src/local-mcp-server/learning-usage-command.js +0 -518
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.matchesOutcomeScope = matchesOutcomeScope;
4
+ exports.assessmentKey = assessmentKey;
5
+ exports.assessmentContent = assessmentContent;
6
+ exports.outcomeAssessmentDigest = outcomeAssessmentDigest;
7
+ exports.validateOutcomeAssessmentInput = validateOutcomeAssessmentInput;
8
+ exports.validateOutcomeAssessmentRecord = validateOutcomeAssessmentRecord;
9
+ exports.summarizeLearningOutcomes = summarizeLearningOutcomes;
10
+ const node_crypto_1 = require("node:crypto");
11
+ const owner_identity_1 = require("./owner-identity");
12
+ const { isLearningEntryId } = require('./entry-format');
13
+ function matchesOutcomeScope(record, scope) {
14
+ return (scope.key === undefined || assessmentKey(record) === scope.key)
15
+ && (scope.principal === undefined || record.learning.learningSubject === scope.principal)
16
+ && (scope.owners === undefined || scope.owners.some(owner => (0, owner_identity_1.ownerKey)(owner) === (0, owner_identity_1.ownerKey)(record.learning.owner)));
17
+ }
18
+ const canonical = (value) => JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item)
19
+ ? Object.fromEntries(Object.keys(item).sort().map(key => [key, item[key]])) : item);
20
+ const text = (value) => typeof value === 'string' && !!value.trim() && !/[\x00-\x08\x0b\x0c\x0e-\x1f]/.test(value);
21
+ const hash = (value) => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
22
+ const relative = (value) => text(value) && !/[\\:\x00-\x1f]/.test(value) && value.split('/').every(part => !!part && part !== '.' && part !== '..');
23
+ const date = (value) => typeof value === 'string' && /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z)?$/.test(value)
24
+ && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value.slice(0, 10);
25
+ /** Semantic identity comes from Sleep; paths are provenance, never a title or move-sensitive join. */
26
+ function assessmentKey(input) {
27
+ return canonical([(0, owner_identity_1.ownerKey)(input.learning.owner), input.learning.learningSubject, input.learning.entryId, input.incidentId]);
28
+ }
29
+ function assessmentContent(input) {
30
+ return canonical({ learning: input.learning, incidentId: input.incidentId, observedAt: input.observedAt, outcome: input.outcome,
31
+ reportedHelp: input.reportedHelp, explicitlySkipped: input.explicitlySkipped,
32
+ sourceRefs: [...input.sourceRefs].sort((a, b) => canonical(a).localeCompare(canonical(b))), rationale: input.rationale,
33
+ guidanceExistedAt: input.guidanceExistedAt });
34
+ }
35
+ function outcomeAssessmentDigest(record) {
36
+ return (0, node_crypto_1.createHash)('sha256').update(canonical({ content: assessmentContent(record), revision: record.revision,
37
+ expectedPriorDigest: record.expectedPriorDigest, reviewer: record.reviewer })).digest('hex');
38
+ }
39
+ function validateOutcomeAssessmentInput(input) {
40
+ if (!input || !input.learning)
41
+ throw new Error('invalid-outcome-assessment');
42
+ (0, owner_identity_1.validateOwner)(input.learning.owner);
43
+ if (!relative(input.learning.relativePath) || !isLearningEntryId(input.learning.entryId)
44
+ || !text(input.learning.learningSubject) || !text(input.incidentId) || input.incidentId.length > 512
45
+ || !date(input.observedAt) || !date(input.guidanceExistedAt)
46
+ || !['success', 'miss', 'unknown'].includes(input.outcome) || typeof input.reportedHelp !== 'boolean'
47
+ || typeof input.explicitlySkipped !== 'boolean' || !text(input.rationale)
48
+ || (input.expectedPriorDigest !== null && !hash(input.expectedPriorDigest))
49
+ || !Array.isArray(input.sourceRefs) || !input.sourceRefs.length)
50
+ throw new Error('invalid-outcome-assessment');
51
+ if (input.reportedHelp && input.outcome !== 'success' || input.explicitlySkipped && input.outcome !== 'miss')
52
+ throw new Error('invalid-outcome-qualification');
53
+ if (Date.parse(input.guidanceExistedAt) > Date.parse(input.observedAt))
54
+ throw new Error('guidance-after-incident');
55
+ for (const source of input.sourceRefs) {
56
+ if (!source)
57
+ throw new Error('invalid-assessment-source');
58
+ (0, owner_identity_1.validateOwner)(source.owner);
59
+ if (!relative(source.relativePath) || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(source.sourceId)
60
+ || !hash(source.sha256))
61
+ throw new Error('invalid-assessment-source');
62
+ }
63
+ if (new Set(input.sourceRefs.map(source => canonical([(0, owner_identity_1.ownerKey)(source.owner), source.sourceId]))).size !== input.sourceRefs.length)
64
+ throw new Error('duplicate-assessment-source');
65
+ }
66
+ function validateOutcomeAssessmentRecord(record) {
67
+ validateOutcomeAssessmentInput(record);
68
+ if (!Number.isInteger(record.revision) || record.revision < 1 || !date(record.assessedAt)
69
+ || !text(record.reviewer) || !text(record.runId) || record.digest !== outcomeAssessmentDigest(record))
70
+ throw new Error('invalid-outcome-record');
71
+ }
72
+ function summarizeLearningOutcomes(records, learning) {
73
+ const selected = new Map();
74
+ for (const record of records) {
75
+ if ((0, owner_identity_1.ownerKey)(record.learning.owner) !== (0, owner_identity_1.ownerKey)(learning.owner) || record.learning.entryId !== learning.entryId
76
+ || record.learning.learningSubject !== learning.learningSubject)
77
+ continue;
78
+ validateOutcomeAssessmentRecord(record);
79
+ const key = assessmentKey(record), prior = selected.get(key);
80
+ if (!prior || record.revision > prior.revision)
81
+ selected.set(key, record);
82
+ else if (record.revision === prior.revision && record.digest !== prior.digest)
83
+ throw new Error('assessment-history-conflict');
84
+ }
85
+ if (!selected.size)
86
+ return undefined;
87
+ const current = [...selected.values()], observed = current.map(record => record.observedAt).sort();
88
+ return { success: current.filter(record => record.outcome === 'success').length,
89
+ reportedHelp: current.filter(record => record.outcome === 'success' && record.reportedHelp).length,
90
+ miss: current.filter(record => record.outcome === 'miss').length, unknown: current.filter(record => record.outcome === 'unknown').length,
91
+ firstObservedAt: observed[0], lastObservedAt: observed[observed.length - 1], assessedAt: current.map(record => record.assessedAt).sort().at(-1) };
92
+ }
@@ -8,6 +8,7 @@ exports.ownerPath = ownerPath;
8
8
  exports.validateOwner = validateOwner;
9
9
  exports.ownerKey = ownerKey;
10
10
  exports.assertSameOwner = assertSameOwner;
11
+ exports.assertLearningTargetPath = assertLearningTargetPath;
11
12
  exports.resolveOwnerHome = resolveOwnerHome;
12
13
  exports.ownerArtifactPath = ownerArtifactPath;
13
14
  exports.configuredArtifactPath = configuredArtifactPath;
@@ -34,6 +35,16 @@ function assertSameOwner(expected, actual) {
34
35
  if (ownerKey(expected) !== ownerKey(actual))
35
36
  throw new Error('owner-path-conflict: the approved home differs from the configured path');
36
37
  }
38
+ /** Shared authored-family identity check. Handwritten entries can omit Domain metadata. */
39
+ function assertLearningTargetPath(ref, domain) {
40
+ const subject = ref.owner.level === 'Org' ? 'org' : ref.learningSubject;
41
+ if (!subject || !/^[a-zA-Z0-9@._+-]+$/.test(subject))
42
+ throw new Error('learning-subject-required');
43
+ const escaped = subject.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
44
+ const match = ref.relativePath.match(new RegExp(`^learnings/${escaped}-((?:[a-z][a-z-]*-)?)(preferences|mistake-patterns|validated-patterns|manager-coaching)(\\.cold)?\\.md$`));
45
+ if (!match || (domain !== undefined && match[1] !== (domain === 'global' ? '' : domain + '-')))
46
+ throw new Error('domain-target-mismatch: learning subject, domain and family must match its file');
47
+ }
37
48
  /** Pure read: resolve existing configured homes without migration or identity configuration. */
38
49
  function resolveOwnerHome(projectPath, level, options) {
39
50
  const load = (relative) => options.runtimeModule(projectPath, relative);
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.hashContent = void 0;
7
+ exports.atomicWrite = atomicWrite;
8
+ exports.withFileLock = withFileLock;
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const node_crypto_1 = require("node:crypto");
12
+ const node_os_1 = require("node:os");
13
+ const hashContent = (text) => (0, node_crypto_1.createHash)('sha256').update(text).digest('hex');
14
+ exports.hashContent = hashContent;
15
+ function atomicWrite(file, text) {
16
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
17
+ const temporary = file + '.' + (0, node_crypto_1.randomUUID)() + '.tmp';
18
+ try {
19
+ node_fs_1.default.writeFileSync(temporary, text, { encoding: 'utf8', flag: 'wx' });
20
+ const fd = node_fs_1.default.openSync(temporary, 'r+');
21
+ try {
22
+ node_fs_1.default.fsyncSync(fd);
23
+ }
24
+ finally {
25
+ node_fs_1.default.closeSync(fd);
26
+ }
27
+ node_fs_1.default.renameSync(temporary, file);
28
+ }
29
+ finally {
30
+ if (node_fs_1.default.existsSync(temporary))
31
+ node_fs_1.default.unlinkSync(temporary);
32
+ }
33
+ }
34
+ function withFileLock(file, fn) {
35
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
36
+ let fd;
37
+ try {
38
+ fd = node_fs_1.default.openSync(file + '.lock', 'wx');
39
+ }
40
+ catch {
41
+ // Serialize recovery so two contenders cannot unlink each other's new lock.
42
+ let recovery;
43
+ try {
44
+ recovery = node_fs_1.default.openSync(file + '.lock.recovery', 'wx');
45
+ }
46
+ catch {
47
+ throw new Error('busy: lock recovery is already in progress');
48
+ }
49
+ try {
50
+ const owner = JSON.parse(node_fs_1.default.readFileSync(file + '.lock', 'utf8'));
51
+ let stale = false;
52
+ if (owner.host === (0, node_os_1.hostname)() && Number.isInteger(owner.pid) && owner.pid > 0) {
53
+ try {
54
+ process.kill(owner.pid, 0);
55
+ }
56
+ catch (error) {
57
+ stale = error.code === 'ESRCH';
58
+ }
59
+ }
60
+ if (!stale)
61
+ throw new Error('busy');
62
+ node_fs_1.default.unlinkSync(file + '.lock');
63
+ fd = node_fs_1.default.openSync(file + '.lock', 'wx');
64
+ }
65
+ catch {
66
+ throw new Error('busy: reconcile outstanding lock before retry');
67
+ }
68
+ finally {
69
+ node_fs_1.default.closeSync(recovery);
70
+ node_fs_1.default.unlinkSync(file + '.lock.recovery');
71
+ }
72
+ }
73
+ node_fs_1.default.writeFileSync(fd, JSON.stringify({ pid: process.pid, host: (0, node_os_1.hostname)(), at: new Date().toISOString() }));
74
+ try {
75
+ return fn();
76
+ }
77
+ finally {
78
+ node_fs_1.default.closeSync(fd);
79
+ node_fs_1.default.unlinkSync(file + '.lock');
80
+ }
81
+ }