fraim 2.0.357-next.2 → 2.0.357-next.4

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.
@@ -780,9 +780,9 @@ function completeRun(run, summary, homes, options, afterStage) {
780
780
  const parsed = (0, learning_source_record_1.parseLearningSourceRecord)(body, source.path, (0, owner_identity_1.ownerArtifactRoot)(home), home.contentRoot);
781
781
  if (parsed.status !== 'valid' || parsed.record.kind !== source.kind || !(0, completion_state_1.sourceAuthorAllowed)(state, parsed.record.author, home.owner) || parsed.record.state !== 'pending' || parsed.record.synthesized)
782
782
  throw new Error('invalid-learning-source');
783
- // References may use immutable identity, home-relative locator or
783
+ // References may use immutable identity, content-relative or home-relative path, or
784
784
  // the selected absolute path. None may archive an unfinished dependency.
785
- const refs = new Set([parsed.record.usageIdentity, parsed.record.sourceId, parsed.record.usageSourceRef,
785
+ const refs = new Set([parsed.record.usageIdentity, parsed.record.sourceId,
786
786
  node_path_1.default.relative(home.contentRoot, source.path).replace(/\\/g, '/'), node_path_1.default.relative((0, owner_identity_1.ownerArtifactRoot)(home), source.path).replace(/\\/g, '/')]);
787
787
  if (actions.some(action => action.sourceRefs.some(ref => refs.has(ref.replace(/\\/g, '/')) || (node_path_1.default.isAbsolute(ref) && (0, completion_state_2.sourcePathKey)(ref) === (0, completion_state_2.sourcePathKey)(source.path)))))
788
788
  throw new Error('action-processing-pending');
@@ -49,7 +49,7 @@ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, con
49
49
  for (const line of fm[1].split(/\r?\n/)) {
50
50
  const match = /^([A-Za-z][A-Za-z0-9]*):[ \t]*(.*)$/.exec(line);
51
51
  if (!match) {
52
- if (/^\s*["']?(artifactKind|author|date|job|runId|sourceId|usageSourceRef|synthesized)["']?\s*:/.test(line))
52
+ if (/^\s*["']?(artifactKind|author|date|job|runId|sourceId|synthesized)["']?\s*:/.test(line))
53
53
  malformedField = true;
54
54
  continue;
55
55
  }
@@ -76,7 +76,7 @@ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, con
76
76
  return invalid('Unsupported metadata key spelling');
77
77
  if (duplicates.size)
78
78
  return invalid(`Duplicate metadata: ${[...duplicates].join(', ')}`);
79
- for (const field of ['author', 'date', 'job', 'runId', 'sourceId', 'usageSourceRef', 'synthesized']) {
79
+ for (const field of ['author', 'date', 'job', 'runId', 'sourceId', 'synthesized']) {
80
80
  if (!Object.prototype.hasOwnProperty.call(fields, field))
81
81
  return invalid(`Missing metadata: ${field}`);
82
82
  }
@@ -110,21 +110,14 @@ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, con
110
110
  return invalid('Source is outside its configured learning location');
111
111
  if (state === 'archived' && !fields.synthesized)
112
112
  return invalid('Archive has no successful processing date');
113
- const reference = fields.usageSourceRef;
114
113
  const relative = node_path_1.default.relative(homeRoot, pendingPath).replace(/\\/g, '/');
115
114
  if (!relative || relative.split('/').some(part => !part || part === '.' || part === '..') || node_path_1.default.isAbsolute(relative))
116
115
  return invalid('Source escapes its owning root');
117
- if (!reference || /[\x00-\x1f]/.test(reference) || reference.includes('\\'))
118
- return invalid('Invalid usageSourceRef');
119
116
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(fields.sourceId)) {
120
117
  return invalid('Sources require an immutable sourceId UUID');
121
118
  }
122
- if (node_path_1.default.posix.isAbsolute(reference) || node_path_1.default.win32.isAbsolute(reference) || reference.includes(':')
123
- || reference.split('/').some(part => !part || part === '.' || part === '..') || reference !== relative) {
124
- return invalid('usageSourceRef must name the owner-relative pending path');
125
- }
126
119
  return { status: 'valid', record: { artifactKind: fields.artifactKind, kind, author: fields.author,
127
- date: fields.date, job: fields.job, runId: fields.runId, usageSourceRef: fields.usageSourceRef,
120
+ date: fields.date, job: fields.job, runId: fields.runId,
128
121
  usageIdentity: `learning-source:${fields.sourceId}`,
129
122
  sourceId: fields.sourceId,
130
123
  synthesized: fields.synthesized, pendingPath, archivePath, state } };
@@ -8,7 +8,10 @@ exports.ProviderClient = void 0;
8
8
  const axios_1 = __importDefault(require("axios"));
9
9
  class ProviderClient {
10
10
  constructor(fraimKey, serverUrl) {
11
- this.serverUrl = serverUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
11
+ this.serverUrl = serverUrl || process.env.FRAIM_REMOTE_URL ||
12
+ ((process.env.FRAIM_RELEASE_CHANNEL || '').toUpperCase() === 'PPE'
13
+ ? 'https://app-ashley-fraim-ppe.azurewebsites.net'
14
+ : 'https://fraim.wellnessatwork.me');
12
15
  this.client = axios_1.default.create({
13
16
  baseURL: this.serverUrl,
14
17
  headers: {
@@ -67,7 +67,7 @@ exports.overrideCommand = new commander_1.Command('override')
67
67
  serverUrl = `http://localhost:${localPort}`;
68
68
  }
69
69
  else {
70
- serverUrl = config.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
70
+ serverUrl = config.remoteUrl || process.env.FRAIM_REMOTE_URL || ((process.env.FRAIM_RELEASE_CHANNEL || '').toUpperCase() === 'PPE' ? 'https://app-ashley-fraim-ppe.azurewebsites.net' : 'https://fraim.wellnessatwork.me');
71
71
  }
72
72
  const apiKey = isLocal ? 'local-dev' : (config.apiKey || process.env.FRAIM_API_KEY);
73
73
  const headers = { 'Content-Type': 'application/json' };
@@ -125,7 +125,7 @@ const runSync = async (options) => {
125
125
  // Order: user-level config → env var → project config (only if in a project)
126
126
  const remoteUrl = useLocal
127
127
  ? resolveExplicitLocalSyncUrl() || `http://localhost:${process.env.FRAIM_MCP_PORT || (0, git_utils_1.getPort)()}`
128
- : process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
128
+ : process.env.FRAIM_REMOTE_URL || ((process.env.FRAIM_RELEASE_CHANNEL || '').toUpperCase() === 'PPE' ? 'https://app-ashley-fraim-ppe.azurewebsites.net' : 'https://fraim.wellnessatwork.me');
129
129
  let apiKey = useLocal ? 'local-dev' : loadUserApiKey() || process.env.FRAIM_API_KEY;
130
130
  const hasProject = (0, project_fraim_paths_1.workspaceFraimExists)(projectRoot);
131
131
  if (!apiKey && hasProject) {
@@ -49,7 +49,10 @@ exports.BASE_MCP_SERVERS = [
49
49
  // Include API key for IDE configs (Codex, VSCode, etc.)
50
50
  // The stdio-server will use this if set, otherwise falls back to ~/.fraim/config.json
51
51
  FRAIM_API_KEY: fraimKey,
52
- FRAIM_REMOTE_URL: 'https://fraim.wellnessatwork.me'
52
+ FRAIM_REMOTE_URL: (process.env.FRAIM_RELEASE_CHANNEL || '').toUpperCase() === 'PPE'
53
+ ? 'https://app-ashley-fraim-ppe.azurewebsites.net'
54
+ : 'https://fraim.wellnessatwork.me',
55
+ ...(process.env.FRAIM_RELEASE_CHANNEL ? { FRAIM_RELEASE_CHANNEL: process.env.FRAIM_RELEASE_CHANNEL } : {})
53
56
  }
54
57
  };
55
58
  }
@@ -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.MANAGER_MANAGED_HEADER = exports.MANAGER_SYNC_METADATA_FILE = exports.MANAGER_HOME_DIRNAME = void 0;
6
+ exports.MANAGER_SYNC_METADATA_FILE = exports.MANAGER_HOME_DIRNAME = void 0;
7
7
  exports.getManagerHomeDir = getManagerHomeDir;
8
8
  exports.readManagerSyncMetadata = readManagerSyncMetadata;
9
9
  exports.getManagerSyncAgeHours = getManagerSyncAgeHours;
@@ -30,7 +30,6 @@ const local_folder_sync_1 = require("./local-folder-sync");
30
30
  const user_config_1 = require("./user-config");
31
31
  exports.MANAGER_HOME_DIRNAME = 'manager';
32
32
  exports.MANAGER_SYNC_METADATA_FILE = '.manager-sync-metadata.json';
33
- exports.MANAGER_MANAGED_HEADER = '<!-- FRAIM_MANAGER_SYNC_MANAGED_CONTENT -->';
34
33
  // Issue #1043 adds 'employees' so a published employee record is walked out of
35
34
  // a git snapshot. isManagerPackRelativePath still decides what is admitted.
36
35
  const MANAGER_PACK_DIRS = ['context', 'rules', 'learnings', 'jobs', 'skills', 'templates', 'scripts', 'employees'];
@@ -86,24 +85,6 @@ function getManagerSyncAgeHours() {
86
85
  return null;
87
86
  return Math.max(0, (Date.now() - syncedAt) / 3_600_000);
88
87
  }
89
- function decorateManagedManagerFile(content, backend) {
90
- const normalized = content.replace(/^\uFEFF/, '');
91
- if (normalized.startsWith(exports.MANAGER_MANAGED_HEADER))
92
- return normalized;
93
- const writePath = backend === 'git'
94
- ? 'open a pull request against your manager storage repo'
95
- : backend === 'local-folder'
96
- ? 'edit the file in your synced folder and let the sync client upload it'
97
- : 'publish the change through your FRAIM manager storage flow';
98
- const marker = [
99
- exports.MANAGER_MANAGED_HEADER,
100
- '> [!IMPORTANT]',
101
- '> Synced from your personal manager storage. Local edits are overwritten on the next sync.',
102
- `> To change this content, ${writePath}.`,
103
- ''
104
- ].join('\n');
105
- return `${marker}\n${normalized}`;
106
- }
107
88
  function collectGitPackFilesRecursive(dir, relBase, files) {
108
89
  if (!fs_1.default.existsSync(dir))
109
90
  return;
@@ -137,7 +118,7 @@ async function fetchCloudPack(remoteUrl, apiKey) {
137
118
  version: String(response.data?.version ?? '0')
138
119
  };
139
120
  }
140
- function materializeCloudHome(files, metadata) {
121
+ function materializeCloudHome(files) {
141
122
  const cacheDir = getManagerHomeDir();
142
123
  const stagingDir = `${cacheDir}.staging-${process.pid}`;
143
124
  fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
@@ -150,11 +131,8 @@ function materializeCloudHome(files, metadata) {
150
131
  continue;
151
132
  const destination = path_1.default.join(stagingDir, file.relativePath);
152
133
  fs_1.default.mkdirSync(path_1.default.dirname(destination), { recursive: true });
153
- // Only markdown gets the managed-content header; scripts/JSON ride verbatim.
154
- const content = (0, capability_pack_1.shouldDecorateAsMarkdown)(file.relativePath)
155
- ? decorateManagedManagerFile(file.content, metadata.backend)
156
- : file.content;
157
- fs_1.default.writeFileSync(destination, content);
134
+ // Sync transports content; provenance lives in the sidecar metadata.
135
+ fs_1.default.writeFileSync(destination, file.content);
158
136
  }
159
137
  // Provenance is written outside the home by writeHomeMetadata.
160
138
  fs_1.default.rmSync(cacheDir, { recursive: true, force: true });
@@ -165,7 +143,7 @@ async function discoverCloudManagerStorage(options) {
165
143
  const apiKey = options?.apiKey || (0, user_config_1.readUserFraimConfig)().apiKey;
166
144
  if (!apiKey || apiKey === 'local-dev' || apiKey === 'test-mode-key')
167
145
  return null;
168
- const remoteUrl = options?.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
146
+ const remoteUrl = options?.remoteUrl || process.env.FRAIM_REMOTE_URL || ((process.env.FRAIM_RELEASE_CHANNEL || '').toUpperCase() === 'PPE' ? 'https://app-ashley-fraim-ppe.azurewebsites.net' : 'https://fraim.wellnessatwork.me');
169
147
  const pack = await fetchCloudPack(remoteUrl, apiKey);
170
148
  if (pack.files.length === 0)
171
149
  return null;
@@ -239,7 +217,7 @@ async function syncManagerCache(options) {
239
217
  writeHomeMetadata(metadata);
240
218
  return { status: 'synced', metadata };
241
219
  }
242
- const remoteUrl = options?.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
220
+ const remoteUrl = options?.remoteUrl || process.env.FRAIM_REMOTE_URL || ((process.env.FRAIM_RELEASE_CHANNEL || '').toUpperCase() === 'PPE' ? 'https://app-ashley-fraim-ppe.azurewebsites.net' : 'https://fraim.wellnessatwork.me');
243
221
  const apiKey = options?.apiKey || (0, user_config_1.readUserFraimConfig)().apiKey;
244
222
  if (!apiKey)
245
223
  throw new Error('No FRAIM API key available for the fraim-cloud manager backend.');
@@ -252,7 +230,7 @@ async function syncManagerCache(options) {
252
230
  };
253
231
  // The cloud has no user-visible folder, so this managed directory is the
254
232
  // home for that backend, not a cache beside one.
255
- materializeCloudHome(pack.files, metadata);
233
+ materializeCloudHome(pack.files);
256
234
  writeHomeMetadata(metadata);
257
235
  return { status: 'synced', metadata };
258
236
  }
@@ -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.ORG_MANAGED_HEADER = exports.ORG_SYNC_METADATA_FILE = exports.ORG_HOME_DIRNAME = exports.shouldDecorateAsMarkdown = void 0;
6
+ exports.ORG_SYNC_METADATA_FILE = exports.ORG_HOME_DIRNAME = void 0;
7
7
  exports.isSafePackPath = isSafePackPath;
8
8
  exports.isSafeOrgCapabilityPackPath = isSafeOrgCapabilityPackPath;
9
9
  exports.getOrgHomeDir = getOrgHomeDir;
@@ -30,11 +30,8 @@ const user_config_1 = require("./user-config");
30
30
  const pack_home_1 = require("./pack-home");
31
31
  const local_folder_sync_1 = require("./local-folder-sync");
32
32
  const capability_pack_1 = require("../../core/capability-pack");
33
- var capability_pack_2 = require("../../core/capability-pack");
34
- Object.defineProperty(exports, "shouldDecorateAsMarkdown", { enumerable: true, get: function () { return capability_pack_2.shouldDecorateAsMarkdown; } });
35
33
  exports.ORG_HOME_DIRNAME = 'org';
36
34
  exports.ORG_SYNC_METADATA_FILE = '.org-sync-metadata.json';
37
- exports.ORG_MANAGED_HEADER = '<!-- FRAIM_ORG_SYNC_MANAGED_CONTENT -->';
38
35
  /**
39
36
  * Subdirectories of the org pack that sync into the cache (spec R7.1).
40
37
  * Issue #1043 adds 'employees' so an org-shared employee record reaches the
@@ -126,24 +123,6 @@ function getOrgSyncAgeHours() {
126
123
  return null;
127
124
  return Math.max(0, (Date.now() - syncedAt) / 3_600_000);
128
125
  }
129
- function decorateManagedOrgFile(content, backend) {
130
- const normalized = content.replace(/^/, '');
131
- if (normalized.startsWith(exports.ORG_MANAGED_HEADER))
132
- return normalized;
133
- const writePath = backend === 'git'
134
- ? 'open a pull request against your organization repo'
135
- : backend === 'local-folder'
136
- ? 'edit the file in your synced folder and let the sync client upload it'
137
- : 'update it through the organization-onboarding flow';
138
- const marker = [
139
- exports.ORG_MANAGED_HEADER,
140
- '> [!IMPORTANT]',
141
- '> Synced from your organization\'s shared context. Local edits are overwritten on the next sync.',
142
- `> To change this content, ${writePath}.`,
143
- ''
144
- ].join('\n');
145
- return `${marker}\n${normalized}`;
146
- }
147
126
  function collectGitPackFilesRecursive(dir, relBase, files) {
148
127
  if (!fs_1.default.existsSync(dir))
149
128
  return;
@@ -178,7 +157,7 @@ async function fetchCloudPack(remoteUrl, apiKey) {
178
157
  };
179
158
  }
180
159
  /** Replace the cache contents atomically: stage fully, then swap. */
181
- function materializeCloudHome(files, metadata) {
160
+ function materializeCloudHome(files) {
182
161
  const cacheDir = getOrgHomeDir();
183
162
  const stagingDir = `${cacheDir}.staging-${process.pid}`;
184
163
  fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
@@ -189,11 +168,8 @@ function materializeCloudHome(files, metadata) {
189
168
  continue;
190
169
  const destination = path_1.default.join(stagingDir, file.relativePath);
191
170
  fs_1.default.mkdirSync(path_1.default.dirname(destination), { recursive: true });
192
- // Only markdown gets the managed-content header; JSON and future scripts ride verbatim.
193
- const content = (0, capability_pack_1.shouldDecorateAsMarkdown)(file.relativePath)
194
- ? decorateManagedOrgFile(file.content, metadata.backend)
195
- : file.content;
196
- fs_1.default.writeFileSync(destination, content);
171
+ // Sync transports content; provenance lives in the sidecar metadata.
172
+ fs_1.default.writeFileSync(destination, file.content);
197
173
  }
198
174
  // Provenance is written outside the home by writeHomeMetadata.
199
175
  fs_1.default.rmSync(cacheDir, { recursive: true, force: true });
@@ -303,7 +279,7 @@ async function syncOrgCache(options) {
303
279
  };
304
280
  // The cloud has no user-visible folder, so this managed directory is the
305
281
  // home for that backend, not a cache beside one.
306
- materializeCloudHome(pack.files, metadata);
282
+ materializeCloudHome(pack.files);
307
283
  writeHomeMetadata(metadata);
308
284
  return { status: 'synced', metadata };
309
285
  }
@@ -21,7 +21,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.CAPABILITY_DIRS = void 0;
22
22
  exports.isCapabilityPackPath = isCapabilityPackPath;
23
23
  exports.isScriptPath = isScriptPath;
24
- exports.shouldDecorateAsMarkdown = shouldDecorateAsMarkdown;
25
24
  exports.CAPABILITY_DIRS = ['jobs', 'skills', 'rules', 'templates', 'scripts', 'employees'];
26
25
  /**
27
26
  * Returns true when `rel` is a valid capability-pack member path:
@@ -97,11 +96,3 @@ function isScriptPath(rel) {
97
96
  return false;
98
97
  return rel.startsWith('scripts/');
99
98
  }
100
- /**
101
- * True when a sync pack file should receive the managed-content header on
102
- * materialization. Only markdown files get the header; scripts and JSON ride
103
- * verbatim. Shared between manager-pack-sync and org-pack-sync.
104
- */
105
- function shouldDecorateAsMarkdown(relativePath) {
106
- return relativePath.toLowerCase().endsWith('.md');
107
- }
@@ -180,27 +180,32 @@ class FraimDbService {
180
180
  async initializeIndexes() {
181
181
  if (!this.db)
182
182
  throw new Error('DB not connected during index initialization');
183
- await this.keysCollection.createIndex({ key: 1 }, { unique: true });
183
+ // Cosmos DB for MongoDB rejects adding unique indexes to collections that already exist
184
+ // (even empty ones auto-created by the driver). All unique createIndex calls use .catch(() => {})
185
+ // so startup never crashes on a fresh database. On established databases (prod, dev) the indexes
186
+ // already exist and createIndex is a no-op; on a fresh PPE/test database the unique constraint
187
+ // is silently skipped — the application's own upsert/insert logic still enforces logical uniqueness.
188
+ await this.keysCollection.createIndex({ key: 1 }, { unique: true }).catch(() => { });
184
189
  await this.keysCollection.createIndex({ userId: 1 });
185
190
  await this.keysCollection.createIndex({ stripeSubscriptionId: 1 });
186
191
  await this.keysCollection.createIndex({ status: 1 });
187
192
  await this.keysCollection.createIndex({ expiresAt: 1 });
188
- await this.sessionsCollection.createIndex({ sessionId: 1 }, { unique: true });
193
+ await this.sessionsCollection.createIndex({ sessionId: 1 }, { unique: true }).catch(() => { });
189
194
  await this.sessionsCollection.createIndex({ userId: 1 });
190
195
  await this.sessionsCollection.createIndex({ lastActive: -1 });
191
196
  // Issue #2093 — getActiveSessionByApiKey no longer bounds its match by recency, so a
192
197
  // long-lived user's full session history is a candidate. This compound index keeps that
193
198
  // lookup an indexed scan with an ordered result instead of a fetch-then-sort in memory.
194
199
  await this.sessionsCollection.createIndex({ userId: 1, lastActive: -1 });
195
- await this.signupsCollection.createIndex({ email: 1 }, { unique: true });
200
+ await this.signupsCollection.createIndex({ email: 1 }, { unique: true }).catch(() => { });
196
201
  await this.signupsCollection.createIndex({ timestamp: -1 });
197
202
  await this.salesCollection.createIndex({ email: 1 });
198
203
  await this.salesCollection.createIndex({ timestamp: -1 });
199
- await this.accessTokensCollection.createIndex({ token: 1 }, { unique: true });
204
+ await this.accessTokensCollection.createIndex({ token: 1 }, { unique: true }).catch(() => { });
200
205
  // TTL index — createIndex is a no-op if the index already exists with the same options.
201
206
  // Avoid drop+recreate: Cosmos DB PITR-restored accounts block index drops on restored collections.
202
207
  await this.accessTokensCollection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
203
- await this.pendingVerificationsCollection.createIndex({ token: 1 }, { unique: true });
208
+ await this.pendingVerificationsCollection.createIndex({ token: 1 }, { unique: true }).catch(() => { });
204
209
  // Issue #563 — org artifacts are unique per (orgId, relativePath); audit reads newest-first.
205
210
  // Index creation must never crash startup: Azure Cosmos DB (Mongo API) can
206
211
  // reject a unique compound index, and connect() re-throws on failure. Logical
@@ -224,10 +229,10 @@ class FraimDbService {
224
229
  await this.pendingVerificationsCollection.createIndex({ email: 1, codeHash: 1 });
225
230
  // TTL index — same pattern: create is idempotent, drop is not safe on restored accounts.
226
231
  await this.pendingVerificationsCollection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
227
- await this.webhookEventsCollection.createIndex({ eventId: 1 }, { unique: true });
232
+ await this.webhookEventsCollection.createIndex({ eventId: 1 }, { unique: true }).catch(() => { });
228
233
  await this.webhookEventsCollection.createIndex({ type: 1 });
229
234
  await this.webhookEventsCollection.createIndex({ receivedAt: -1 });
230
- await this.partnerDiscountsCollection.createIndex({ type: 1, match: 1 }, { unique: true });
235
+ await this.partnerDiscountsCollection.createIndex({ type: 1, match: 1 }, { unique: true }).catch(() => { });
231
236
  await this.partnerDiscountsCollection.createIndex({ active: 1 });
232
237
  try {
233
238
  await this.managerTeamsCollection.createIndex({ managerId: 1, memberId: 1 }, { unique: true });
@@ -49,7 +49,7 @@ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, con
49
49
  for (const line of fm[1].split(/\r?\n/)) {
50
50
  const match = /^([A-Za-z][A-Za-z0-9]*):[ \t]*(.*)$/.exec(line);
51
51
  if (!match) {
52
- if (/^\s*["']?(artifactKind|author|date|job|runId|sourceId|usageSourceRef|synthesized)["']?\s*:/.test(line))
52
+ if (/^\s*["']?(artifactKind|author|date|job|runId|sourceId|synthesized)["']?\s*:/.test(line))
53
53
  malformedField = true;
54
54
  continue;
55
55
  }
@@ -76,7 +76,7 @@ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, con
76
76
  return invalid('Unsupported metadata key spelling');
77
77
  if (duplicates.size)
78
78
  return invalid(`Duplicate metadata: ${[...duplicates].join(', ')}`);
79
- for (const field of ['author', 'date', 'job', 'runId', 'sourceId', 'usageSourceRef', 'synthesized']) {
79
+ for (const field of ['author', 'date', 'job', 'runId', 'sourceId', 'synthesized']) {
80
80
  if (!Object.prototype.hasOwnProperty.call(fields, field))
81
81
  return invalid(`Missing metadata: ${field}`);
82
82
  }
@@ -110,21 +110,14 @@ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, con
110
110
  return invalid('Source is outside its configured learning location');
111
111
  if (state === 'archived' && !fields.synthesized)
112
112
  return invalid('Archive has no successful processing date');
113
- const reference = fields.usageSourceRef;
114
113
  const relative = node_path_1.default.relative(homeRoot, pendingPath).replace(/\\/g, '/');
115
114
  if (!relative || relative.split('/').some(part => !part || part === '.' || part === '..') || node_path_1.default.isAbsolute(relative))
116
115
  return invalid('Source escapes its owning root');
117
- if (!reference || /[\x00-\x1f]/.test(reference) || reference.includes('\\'))
118
- return invalid('Invalid usageSourceRef');
119
116
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(fields.sourceId)) {
120
117
  return invalid('Sources require an immutable sourceId UUID');
121
118
  }
122
- if (node_path_1.default.posix.isAbsolute(reference) || node_path_1.default.win32.isAbsolute(reference) || reference.includes(':')
123
- || reference.split('/').some(part => !part || part === '.' || part === '..') || reference !== relative) {
124
- return invalid('usageSourceRef must name the owner-relative pending path');
125
- }
126
119
  return { status: 'valid', record: { artifactKind: fields.artifactKind, kind, author: fields.author,
127
- date: fields.date, job: fields.job, runId: fields.runId, usageSourceRef: fields.usageSourceRef,
120
+ date: fields.date, job: fields.job, runId: fields.runId,
128
121
  usageIdentity: `learning-source:${fields.sourceId}`,
129
122
  sourceId: fields.sourceId,
130
123
  synthesized: fields.synthesized, pendingPath, archivePath, state } };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.357-next.2",
3
+ "version": "2.0.357-next.4",
4
4
  "description": "FRAIM core CLI and MCP package.",
5
5
  "main": "index.js",
6
6
  "bin": {