fraim 2.0.345 → 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 +123 -4
  2. package/dist/registry/scripts/learning/corpus-audit.js +205 -0
  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 +148 -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 +141 -477
  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 +11 -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,162 @@
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.summarizeLearningOutcomes = void 0;
7
+ exports.recordOutcomeAssessment = recordOutcomeAssessment;
8
+ exports.readOutcomeAssessments = readOutcomeAssessments;
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const learning_usage_store_1 = require("../../../src/local-mcp-server/learning-usage-store");
12
+ const owner_identity_1 = require("./owner-identity");
13
+ const proposal_queue_1 = require("./proposal-queue");
14
+ const completion_state_1 = require("./completion-state");
15
+ const artifact_paths_1 = require("../artifact-paths");
16
+ const learning_source_record_1 = require("../learning-usage/learning-source-record");
17
+ const corpus_audit_1 = require("./corpus-audit");
18
+ const outcome_types_1 = require("./outcome-types");
19
+ var outcome_types_2 = require("./outcome-types");
20
+ Object.defineProperty(exports, "summarizeLearningOutcomes", { enumerable: true, get: function () { return outcome_types_2.summarizeLearningOutcomes; } });
21
+ const { learningEntryBlocks, learningEntryMetadata, learningEntryIdentity } = require('./entry-format');
22
+ function resolveAssessmentHomes(run, input, options) {
23
+ const homes = new Map();
24
+ for (const owner of [run.owner, input.learning.owner, ...input.sourceRefs.map(source => source.owner)]) {
25
+ const home = options.resolveHome(owner);
26
+ (0, owner_identity_1.assertSameOwner)(owner, home.owner);
27
+ homes.set((0, owner_identity_1.ownerKey)(owner), home);
28
+ }
29
+ return homes;
30
+ }
31
+ function resolveAssessmentSources(input, homes) {
32
+ return input.sourceRefs.map(ref => {
33
+ const home = homes.get((0, owner_identity_1.ownerKey)(ref.owner));
34
+ const originalFile = (0, owner_identity_1.configuredArtifactPath)(home, node_path_1.default.join((0, owner_identity_1.ownerArtifactRoot)(home), ref.relativePath));
35
+ let file = originalFile;
36
+ if (!node_fs_1.default.existsSync(file)) {
37
+ const paths = (0, artifact_paths_1.resolveArtifactPaths)((0, owner_identity_1.ownerArtifactRoot)(home), home.contentRoot);
38
+ const archive = (0, completion_state_1.sourcePathKey)(node_path_1.default.dirname(file)) === (0, completion_state_1.sourcePathKey)(paths.raw) ? paths.archiveRaw
39
+ : (0, completion_state_1.sourcePathKey)(node_path_1.default.dirname(file)) === (0, completion_state_1.sourcePathKey)(paths.retrospectives) ? paths.archiveRetrospectives : undefined;
40
+ if (archive)
41
+ file = (0, owner_identity_1.configuredArtifactPath)(home, node_path_1.default.join(archive, node_path_1.default.basename(file)));
42
+ }
43
+ return { ref, home, file, archived: file !== originalFile };
44
+ });
45
+ }
46
+ function verifyAssessmentLearning(input, home, target) {
47
+ if (!node_fs_1.default.existsSync(target) || node_fs_1.default.lstatSync(target).isSymbolicLink())
48
+ throw new Error('assessment-learning-missing');
49
+ const entries = learningEntryBlocks(node_fs_1.default.readFileSync(target, 'utf8')).map(block => ({ ...block, ...learningEntryIdentity(block.text) }));
50
+ if (entries.some(block => block.ambiguous))
51
+ throw new Error('assessment-learning-missing-or-ambiguous');
52
+ const blocks = entries.filter(block => block.id === input.learning.entryId);
53
+ if (blocks.length !== 1)
54
+ throw new Error('assessment-learning-missing-or-ambiguous');
55
+ const domain = blocks[0].text.match(/^\*\*Domain\*\*:\s*([^\r\n]+)$/m)?.[1].trim();
56
+ (0, owner_identity_1.assertLearningTargetPath)(input.learning, domain);
57
+ const occurrences = (0, corpus_audit_1.collectLearningCorpus)([home], [input.learning.learningSubject]).entries.filter(entry => {
58
+ if (entry.ref.entryId !== input.learning.entryId)
59
+ return false;
60
+ try {
61
+ (0, owner_identity_1.assertLearningTargetPath)({ ...entry.ref, learningSubject: input.learning.learningSubject });
62
+ return true;
63
+ }
64
+ catch {
65
+ return false;
66
+ } // Shared org guidance is a distinct subject in a Project home.
67
+ });
68
+ if (occurrences.length !== 1)
69
+ throw new Error('assessment-learning-identity-ambiguous');
70
+ const metadata = learningEntryMetadata(blocks[0].text);
71
+ const introduced = metadata['First synthesized'] || metadata['First recorded'];
72
+ if (!introduced || !Number.isFinite(Date.parse(introduced)))
73
+ throw new Error('guidance-date-unavailable');
74
+ if (Date.parse(introduced) > Date.parse(input.observedAt) || Date.parse(introduced) > Date.parse(input.guidanceExistedAt))
75
+ throw new Error('guidance-after-incident');
76
+ }
77
+ function verifyAssessmentSources(sources, state) {
78
+ for (const source of sources) {
79
+ if (!node_fs_1.default.existsSync(source.file) || node_fs_1.default.lstatSync(source.file).isSymbolicLink())
80
+ throw new Error('assessment-source-unavailable');
81
+ const body = node_fs_1.default.readFileSync(source.file, 'utf8');
82
+ // Normal archival only fills this existing frontmatter field; source identity and all evidence bytes remain bound.
83
+ const originalBody = source.archived ? body.replace(/^(synthesized:)[^\r\n]*/m, '$1') : body;
84
+ if ((0, proposal_queue_1.hashContent)(body) !== source.ref.sha256 && (0, proposal_queue_1.hashContent)(originalBody) !== source.ref.sha256)
85
+ throw new Error('assessment-source-hash-conflict');
86
+ const parsed = (0, learning_source_record_1.parseLearningSourceRecord)(body, source.file, (0, owner_identity_1.ownerArtifactRoot)(source.home), source.home.contentRoot);
87
+ if (parsed.status !== 'valid')
88
+ throw new Error(`invalid-assessment-source: ${parsed.reason}`);
89
+ if (parsed.record.sourceId !== source.ref.sourceId
90
+ || !(0, completion_state_1.sourceAuthorAllowed)(state, parsed.record.author, source.home.owner))
91
+ throw new Error('invalid-assessment-source');
92
+ }
93
+ }
94
+ function replayAssessment(run, state, pending, prior) {
95
+ if (pending?.digest === prior.digest)
96
+ run.commitOutcomeAssessment(prior.digest, state.sequence);
97
+ return prior;
98
+ }
99
+ function commitAssessment(transaction) {
100
+ const { run, input, state, pending, options, home, target, sources } = transaction;
101
+ // File locks also coordinate callers with different Manager coordination paths.
102
+ // Refresh after acquiring them, before recording any recoverable intent.
103
+ const prior = (0, learning_usage_store_1.readAssessedOutcomes)(undefined, { key: (0, outcome_types_1.assessmentKey)(input) })[0];
104
+ if (prior && (0, outcome_types_1.assessmentContent)(prior) === (0, outcome_types_1.assessmentContent)(input))
105
+ return replayAssessment(run, state, pending, prior);
106
+ const reservations = (0, completion_state_1.readRunReservations)([run.directory, ...options.runDirectories], run.file);
107
+ if (reservations.assessmentKeys.includes((0, outcome_types_1.assessmentKey)(input)))
108
+ throw new Error('assessment-reserved-by-pending-run');
109
+ if ((prior?.digest ?? null) !== input.expectedPriorDigest)
110
+ throw new Error('stale-assessment-conflict');
111
+ verifyAssessmentLearning(input, home, target);
112
+ verifyAssessmentSources(sources, state);
113
+ const record = pending ?? { ...structuredClone(input), revision: (prior?.revision ?? 0) + 1,
114
+ assessedAt: new Date().toISOString(), reviewer: state.reviewer, runId: state.runId, digest: '' };
115
+ record.digest = (0, outcome_types_1.outcomeAssessmentDigest)(record);
116
+ if (!pending)
117
+ run.prepareOutcomeAssessment(record, state.sequence, sources.map(source => source.file));
118
+ options.afterStage?.('assessment-prepared');
119
+ const persisted = (0, learning_usage_store_1.recordAssessedOutcome)(record);
120
+ options.afterStage?.('assessment-stored');
121
+ run.commitOutcomeAssessment(record.digest, run.read().sequence);
122
+ options.afterStage?.('assessment-committed');
123
+ return persisted;
124
+ }
125
+ function withAssessmentFileLocks(transaction) {
126
+ const files = [...new Set([transaction.target, ...transaction.sources.map(source => source.file)])].sort();
127
+ const locked = (index) => index < files.length
128
+ ? (0, proposal_queue_1.withFileLock)(files[index], () => locked(index + 1)) : commitAssessment(transaction);
129
+ return locked(0);
130
+ }
131
+ /** Commit semantic judgments supplied by Sleep; no prose, title or retrospective-ID matching. */
132
+ function recordOutcomeAssessment(run, input, expectedSequence, options) {
133
+ (0, outcome_types_1.validateOutcomeAssessmentInput)(input);
134
+ (0, owner_identity_1.assertLearningTargetPath)(input.learning);
135
+ return (0, proposal_queue_1.withFileLock)(options.coordinationFile || node_path_1.default.join(run.directory, 'owner-application'), () => {
136
+ const state = run.read();
137
+ if (!Number.isInteger(expectedSequence) || state.sequence !== expectedSequence)
138
+ throw new Error('stale-sequence');
139
+ if (state.finalization || state.checkpoint?.receipt.finalizationState === 'pending')
140
+ throw new Error('finalization-started');
141
+ const homes = resolveAssessmentHomes(run, input, options);
142
+ if (!(0, completion_state_1.sourceAuthorAllowed)(state, input.learning.learningSubject, input.learning.owner))
143
+ throw new Error('assessment-subject-conflict');
144
+ const prior = (0, learning_usage_store_1.readAssessedOutcomes)(undefined, { key: (0, outcome_types_1.assessmentKey)(input) })[0];
145
+ const pending = state.pendingOutcomeAssessments?.find(record => (0, outcome_types_1.assessmentKey)(record) === (0, outcome_types_1.assessmentKey)(input));
146
+ if (pending && (0, outcome_types_1.assessmentContent)(pending) !== (0, outcome_types_1.assessmentContent)(input))
147
+ throw new Error('pending-assessment-conflict');
148
+ // Exact replay remains valid after sources have archived and detailed evidence expires.
149
+ if (prior && (0, outcome_types_1.assessmentContent)(prior) === (0, outcome_types_1.assessmentContent)(input))
150
+ return replayAssessment(run, state, pending, prior);
151
+ if ((prior?.digest ?? null) !== input.expectedPriorDigest)
152
+ throw new Error('stale-assessment-conflict');
153
+ const home = homes.get((0, owner_identity_1.ownerKey)(input.learning.owner));
154
+ const target = (0, owner_identity_1.ownerArtifactPath)(home, input.learning.relativePath);
155
+ const sources = resolveAssessmentSources(input, homes);
156
+ return withAssessmentFileLocks({ run, input, state, pending, options, home, target, sources });
157
+ });
158
+ }
159
+ /** Durable usage history outlives transient run journals, receipts and original source retention. */
160
+ function readOutcomeAssessments(homes, principal) {
161
+ return (0, learning_usage_store_1.readAssessedOutcomes)(undefined, { owners: homes.map(home => home.owner), principal });
162
+ }
@@ -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
+ }
@@ -0,0 +1,148 @@
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.ownerArtifactRoot = void 0;
7
+ exports.ownerPath = ownerPath;
8
+ exports.validateOwner = validateOwner;
9
+ exports.ownerKey = ownerKey;
10
+ exports.assertSameOwner = assertSameOwner;
11
+ exports.assertLearningTargetPath = assertLearningTargetPath;
12
+ exports.resolveOwnerHome = resolveOwnerHome;
13
+ exports.ownerArtifactPath = ownerArtifactPath;
14
+ exports.configuredArtifactPath = configuredArtifactPath;
15
+ const node_fs_1 = __importDefault(require("node:fs"));
16
+ const node_path_1 = __importDefault(require("node:path"));
17
+ /** A recorded home path is immutable; moving storage requires a newly reviewed run. */
18
+ function ownerPath(root) {
19
+ const absolute = node_path_1.default.resolve(root).replace(/\\/g, '/');
20
+ return process.platform === 'win32' ? absolute.toLowerCase() : absolute;
21
+ }
22
+ function validateOwner(owner) {
23
+ if (!owner || !['Project', 'Manager', 'Org'].includes(owner.level)
24
+ || Object.keys(owner).some(key => !['level', 'path'].includes(key))
25
+ || typeof owner.path !== 'string' || !node_path_1.default.isAbsolute(owner.path)
26
+ || /[\x00-\x1f]/.test(owner.path) || owner.path !== ownerPath(owner.path)) {
27
+ throw new Error('owner-unresolved: a canonical absolute home path is required');
28
+ }
29
+ }
30
+ function ownerKey(owner) {
31
+ validateOwner(owner);
32
+ return JSON.stringify([owner.level, owner.path]);
33
+ }
34
+ function assertSameOwner(expected, actual) {
35
+ if (ownerKey(expected) !== ownerKey(actual))
36
+ throw new Error('owner-path-conflict: the approved home differs from the configured path');
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
+ }
48
+ /** Pure read: resolve existing configured homes without migration or identity configuration. */
49
+ function resolveOwnerHome(projectPath, level, options) {
50
+ const load = (relative) => options.runtimeModule(projectPath, relative);
51
+ const paths = load('core/utils/project-fraim-paths');
52
+ let home;
53
+ if (level === 'Project') {
54
+ const root = paths.getWorkspaceFraimPath(projectPath);
55
+ home = { root, artifactRoot: node_path_1.default.resolve(projectPath), contentRoot: node_path_1.default.join(root, 'personalized-employee'), requiresPublish: false, backend: 'project' };
56
+ }
57
+ else {
58
+ if (!['Manager', 'Org'].includes(level))
59
+ throw new Error('owner-unresolved: invalid owner level');
60
+ const configFile = node_path_1.default.join(paths.getUserFraimDirPath(), 'config.json');
61
+ let config;
62
+ try {
63
+ config = node_fs_1.default.existsSync(configFile) ? JSON.parse(node_fs_1.default.readFileSync(configFile, 'utf8')) : {};
64
+ }
65
+ catch {
66
+ throw new Error('home-unavailable: user configuration is invalid');
67
+ }
68
+ if (!config || typeof config !== 'object' || Array.isArray(config))
69
+ throw new Error('home-unavailable: user configuration is invalid');
70
+ const raw = level === 'Manager' ? config.managerStorage : config.organization;
71
+ if (raw !== undefined && (!raw || typeof raw !== 'object' || Array.isArray(raw)
72
+ || !['git', 'local-folder', 'fraim-cloud', 'single-machine'].includes(raw.backend)
73
+ || (raw.backend === 'git' && (typeof raw.gitUrl !== 'string' || !raw.gitUrl.trim()))
74
+ || (raw.backend === 'local-folder' && (typeof raw.localPath !== 'string' || !raw.localPath.trim())))) {
75
+ throw new Error('home-unavailable: invalid configured storage home');
76
+ }
77
+ const resolved = load('cli/utils/pack-home').resolvePackHome(level === 'Manager' ? 'manager' : 'org', { migrateLegacy: false });
78
+ home = { root: resolved.root, contentRoot: resolved.contentRoot, requiresPublish: resolved.requiresPublish, backend: resolved.backend, ...(resolved.gitUrl ? { gitUrl: resolved.gitUrl } : {}) };
79
+ }
80
+ const owner = { level, path: ownerPath(home.root) };
81
+ if (options.expectedOwner)
82
+ assertSameOwner(options.expectedOwner, owner);
83
+ if (!node_fs_1.default.existsSync(home.root) || !node_fs_1.default.statSync(home.root).isDirectory()
84
+ || (home.backend === 'git' && !node_fs_1.default.existsSync(node_path_1.default.join(home.root, '.git')))) {
85
+ throw new Error('home-unavailable: configured home is not available locally');
86
+ }
87
+ return { ...home, level, owner };
88
+ }
89
+ /** Resolve a relative artifact without permitting lexical or realpath escapes, including junctions. */
90
+ function ownerArtifactPath(home, relativePath) {
91
+ assertSameOwner(home.owner, { level: home.level, path: ownerPath(home.root) });
92
+ if (!relativePath || node_path_1.default.isAbsolute(relativePath) || relativePath.includes('\\')
93
+ || relativePath.includes(':') || relativePath.split('/').some(part => !part || part === '.' || part === '..')) {
94
+ throw new Error('invalid-artifact-path');
95
+ }
96
+ // Shared project capabilities live in the repository registry, outside its personalized pack.
97
+ const shared = home.level === 'Project' && relativePath.startsWith('registry/');
98
+ const containmentRoot = shared ? node_path_1.default.dirname(home.root) : home.root;
99
+ // The workspace configuration has one explicit reference; arbitrary parent paths stay forbidden.
100
+ const workspaceConfig = home.level === 'Project' && relativePath === 'config.json';
101
+ const contentRoot = shared ? containmentRoot : workspaceConfig ? home.root : home.contentRoot;
102
+ const target = node_path_1.default.resolve(contentRoot, relativePath);
103
+ const existingAncestor = (file) => {
104
+ let current = file;
105
+ while (!node_fs_1.default.existsSync(current)) {
106
+ const parent = node_path_1.default.dirname(current);
107
+ if (parent === current)
108
+ throw new Error('home-unavailable');
109
+ current = parent;
110
+ }
111
+ return current;
112
+ };
113
+ const realRoot = node_fs_1.default.realpathSync(containmentRoot);
114
+ const ancestor = node_fs_1.default.realpathSync(existingAncestor(target));
115
+ const relative = node_path_1.default.relative(realRoot, ancestor);
116
+ if (relative === '..' || relative.startsWith('..' + node_path_1.default.sep) || node_path_1.default.isAbsolute(relative))
117
+ throw new Error('invalid-artifact-path: escaped owner home');
118
+ // Also reject escaping a content root via a symlink to a sibling in the pack.
119
+ if (node_fs_1.default.existsSync(contentRoot)) {
120
+ const contentRelative = node_path_1.default.relative(node_fs_1.default.realpathSync(contentRoot), ancestor);
121
+ if (contentRelative === '..' || contentRelative.startsWith('..' + node_path_1.default.sep) || node_path_1.default.isAbsolute(contentRelative))
122
+ throw new Error('invalid-artifact-path: escaped content root');
123
+ }
124
+ return target;
125
+ }
126
+ /** Explicit artifact base; never infer a project from its personalized directory. */
127
+ const ownerArtifactRoot = (home) => home.artifactRoot ?? home.root;
128
+ exports.ownerArtifactRoot = ownerArtifactRoot;
129
+ /** Configured artifacts may sit outside the personalized pack, but never outside their owner. */
130
+ function configuredArtifactPath(home, file) {
131
+ assertSameOwner(home.owner, { level: home.level, path: ownerPath(home.root) });
132
+ const root = node_path_1.default.resolve((0, exports.ownerArtifactRoot)(home));
133
+ const target = node_path_1.default.resolve(file);
134
+ const relative = node_path_1.default.relative(root, target);
135
+ if (!relative || relative === '..' || relative.startsWith('..' + node_path_1.default.sep) || node_path_1.default.isAbsolute(relative))
136
+ throw new Error('invalid-artifact-path: escaped owner home');
137
+ for (let current = target; ownerPath(current) !== ownerPath(root); current = node_path_1.default.dirname(current)) {
138
+ try {
139
+ if (node_fs_1.default.lstatSync(current).isSymbolicLink())
140
+ throw new Error('invalid-artifact-path: linked artifact');
141
+ }
142
+ catch (error) {
143
+ if (error.code !== 'ENOENT')
144
+ throw error;
145
+ }
146
+ }
147
+ return target;
148
+ }
@@ -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
+ }