fraim 2.0.298 → 2.0.299
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/src/cli/utils/user-config.js +20 -0
- package/dist/src/config/persona-capability-bundles.js +10 -1
- package/dist/src/config/persona-hiring.js +10 -0
- package/dist/src/core/ai-mentor.js +9 -2
- package/dist/src/core/handoff-contracts.js +93 -4
- package/dist/src/core/resolve-phase-edge.js +3 -1
- package/dist/src/local-mcp-server/learning-context-builder.js +11 -7
- package/dist/src/local-mcp-server/learning-usage-attestation.js +11 -4
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.getLayerBackend = getLayerBackend;
|
|
7
|
+
exports.isLayerMisconfigured = isLayerMisconfigured;
|
|
7
8
|
exports.setLayerBackend = setLayerBackend;
|
|
8
9
|
exports.readUserFraimConfig = readUserFraimConfig;
|
|
9
10
|
exports.writeUserFraimConfig = writeUserFraimConfig;
|
|
@@ -29,6 +30,25 @@ function getLayerBackend(layer) {
|
|
|
29
30
|
return getManagerStorageConfig();
|
|
30
31
|
return null;
|
|
31
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Returns true when the layer has a config entry whose backend is recognized
|
|
35
|
+
* but the required pointer field is missing (git with no gitUrl, local-folder
|
|
36
|
+
* with no localPath). Callers use this to distinguish "not configured" from
|
|
37
|
+
* "misconfigured" so the UI can surface a warning instead of silently falling
|
|
38
|
+
* back to single-machine. (issue #1465, R3)
|
|
39
|
+
*/
|
|
40
|
+
function isLayerMisconfigured(layer) {
|
|
41
|
+
const raw = layer === 'org'
|
|
42
|
+
? readUserFraimConfig().organization
|
|
43
|
+
: readUserFraimConfig().managerStorage;
|
|
44
|
+
if (!raw || typeof raw !== 'object')
|
|
45
|
+
return false;
|
|
46
|
+
if (raw.backend === 'git' && !raw.gitUrl)
|
|
47
|
+
return true;
|
|
48
|
+
if (raw.backend === 'local-folder' && !raw.localPath)
|
|
49
|
+
return true;
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
32
52
|
/** Write the backend config for a named layer (issue #834). */
|
|
33
53
|
function setLayerBackend(layer, config) {
|
|
34
54
|
if (layer === 'org') {
|
|
@@ -113,11 +113,20 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
113
113
|
defaultHireMode: 'job',
|
|
114
114
|
lockCopy: 'Hire SEChar to unlock security setup, review, and findings-command work for this request.'
|
|
115
115
|
},
|
|
116
|
+
travis: {
|
|
117
|
+
personaKey: 'travis',
|
|
118
|
+
bundleId: 'persona-travis-core',
|
|
119
|
+
catalogMetadata: buildCatalogMetadata('travis', ['travel-planning', 'travel-disruption-rebooking', 'travel-fare-watch']),
|
|
120
|
+
protectedJobs: ['travel-planning', 'travel-disruption-rebooking', 'travel-fare-watch'],
|
|
121
|
+
protectedAliases: ['travel-agent', 'travel', 'trip-planning', 'rebooking', 'fare-watch', 'flight-watch'],
|
|
122
|
+
defaultHireMode: 'job',
|
|
123
|
+
lockCopy: 'Hire TRAVis to unlock trip planning, fare watching, travel disruption handling, and rebooking support for this request.'
|
|
124
|
+
},
|
|
116
125
|
ashley: {
|
|
117
126
|
personaKey: 'ashley',
|
|
118
127
|
bundleId: 'persona-ashley-core',
|
|
119
128
|
catalogMetadata: buildCatalogMetadata('ashley', ['chief-of-staff-briefing', 'executive-assistant', 'analyze-transcript']),
|
|
120
|
-
protectedJobs: ['chief-of-staff-briefing', 'calendar-triage', 'meeting-preparation', 'executive-assistant', 'analyze-transcript'
|
|
129
|
+
protectedJobs: ['chief-of-staff-briefing', 'calendar-triage', 'meeting-preparation', 'executive-assistant', 'analyze-transcript'],
|
|
121
130
|
protectedAliases: ['executive-assistant', 'operations-assistant'],
|
|
122
131
|
defaultHireMode: 'job',
|
|
123
132
|
lockCopy: 'Hire AshLey to unlock executive-assistant work for this request.'
|
|
@@ -199,6 +199,15 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
199
199
|
jobPriceCents: 890,
|
|
200
200
|
fulltimePriceCents: 4990,
|
|
201
201
|
},
|
|
202
|
+
travis: {
|
|
203
|
+
displayName: 'TRAVis',
|
|
204
|
+
role: 'Travel Agent',
|
|
205
|
+
emoji: '\u{1F9ED}',
|
|
206
|
+
gradient: 'linear-gradient(135deg, #0f766e 0%, #0ea5e9 100%)',
|
|
207
|
+
blurb: 'Plans trips, watches fares, handles travel disruptions, and helps with rebooking options while keeping provider limits and booking boundaries clear.',
|
|
208
|
+
jobPriceCents: 890,
|
|
209
|
+
fulltimePriceCents: 4990,
|
|
210
|
+
},
|
|
202
211
|
ashley: {
|
|
203
212
|
displayName: 'AshLey',
|
|
204
213
|
role: 'Executive Assistant',
|
|
@@ -251,6 +260,7 @@ exports.PERSONA_AVATAR_CATALOG = {
|
|
|
251
260
|
ricardo: { seed: 'RICARDO-recruiter', bg: 'ede9fe', style: 'notionists' },
|
|
252
261
|
hari: { seed: 'HARI-hr', bg: 'ccfbf1', style: 'notionists' },
|
|
253
262
|
careena: { seed: 'CAREENA-career-coach', bg: 'e0f2fe', style: 'notionists' },
|
|
263
|
+
travis: { seed: 'TRAVIS-travel-agent', bg: 'ccfbf1', style: 'notionists' },
|
|
254
264
|
sade: { seed: 'SADE-salesforce', bg: 'bae6fd', style: 'notionists' },
|
|
255
265
|
sam: { seed: 'SAM-sales-account', bg: 'd1fae5', style: 'notionists' },
|
|
256
266
|
casey: { seed: 'CASEY-cx', bg: 'a5f3fc', style: 'notionists' },
|
|
@@ -83,7 +83,7 @@ class AIMentor {
|
|
|
83
83
|
const validOutcomes = (0, resolve_phase_edge_1.discriminantKeys)(onSuccess)
|
|
84
84
|
.map((key) => `"${key}"`)
|
|
85
85
|
.join(' | ');
|
|
86
|
-
return `\n\n---\n\n## Report Back\nWhen this phase is done, call \`seekMentoring({ jobName: "${jobName}", issueNumber: "<issue_number>", currentPhase: "${phaseId}", status: "complete", findings: {
|
|
86
|
+
return `\n\n---\n\n## Report Back\nWhen this phase is done, call \`seekMentoring({ jobName: "${jobName}", issueNumber: "<issue_number>", currentPhase: "${phaseId}", status: "complete", findings: { phaseOutcome: "<outcome>" } })\` with one of: ${validOutcomes}.${failureNote}`;
|
|
87
87
|
}
|
|
88
88
|
/** Phase-authority content injected for all phased workflows. Loaded from orchestration/phase-authority.md. */
|
|
89
89
|
async getPhaseAuthorityContent() {
|
|
@@ -173,7 +173,14 @@ class AIMentor {
|
|
|
173
173
|
nextPhaseId = (0, resolve_phase_edge_1.resolvePhaseEdge)(phaseFlow.onSuccess, (0, resolve_phase_edge_1.resolveDiscriminant)(findings, evidence));
|
|
174
174
|
}
|
|
175
175
|
let message = '';
|
|
176
|
-
if (nextPhaseId) {
|
|
176
|
+
if (nextPhaseId === resolve_phase_edge_1.WAITING_FOR_NEXT_INVOCATION) {
|
|
177
|
+
message += [
|
|
178
|
+
'### Waiting For Next Invocation',
|
|
179
|
+
'',
|
|
180
|
+
'Send a concise status update that this recurring assignment is waiting for its next scheduled check, then stop.'
|
|
181
|
+
].join('\n');
|
|
182
|
+
}
|
|
183
|
+
else if (nextPhaseId) {
|
|
177
184
|
message += `Moving to the next phase: **${nextPhaseId}**.\n\n`;
|
|
178
185
|
let nextInstructions = workflow.phases.get(nextPhaseId);
|
|
179
186
|
if (nextInstructions) {
|
|
@@ -96,6 +96,42 @@ function validateAddressFeedbackApproval(evidence) {
|
|
|
96
96
|
}
|
|
97
97
|
return null;
|
|
98
98
|
}
|
|
99
|
+
function isAbsoluteHttpUrl(url) {
|
|
100
|
+
if (typeof url !== 'string' || !url)
|
|
101
|
+
return false;
|
|
102
|
+
try {
|
|
103
|
+
const parsed = new URL(url);
|
|
104
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function validateReviewArtifactEntry(artifact, index) {
|
|
111
|
+
const errors = [];
|
|
112
|
+
const label = `evidence.reviewHandoff.artifacts[${index}]`;
|
|
113
|
+
if (!artifact || typeof artifact !== 'object' || Array.isArray(artifact)) {
|
|
114
|
+
errors.push(`${label} must be an object`);
|
|
115
|
+
return errors;
|
|
116
|
+
}
|
|
117
|
+
const entry = artifact;
|
|
118
|
+
if ('kind' in entry && !('type' in entry)) {
|
|
119
|
+
errors.push(`${label} uses kind but must use type — use type, not kind for artifact type (e.g. "markdown", "docx", "source_file")`);
|
|
120
|
+
}
|
|
121
|
+
else if (!entry.type || typeof entry.type !== 'string' || !entry.type.trim()) {
|
|
122
|
+
errors.push(`${label}.type must be a non-empty string (e.g. "markdown", "docx", "source_file")`);
|
|
123
|
+
}
|
|
124
|
+
if (!entry.label || typeof entry.label !== 'string' || !entry.label.trim()) {
|
|
125
|
+
errors.push(`${label}.label must be a non-empty string`);
|
|
126
|
+
}
|
|
127
|
+
if ('url' in entry) {
|
|
128
|
+
errors.push(`${label} must not include url — artifact_set artifacts are local files; use path`);
|
|
129
|
+
}
|
|
130
|
+
if (!entry.path || typeof entry.path !== 'string' || !entry.path.trim()) {
|
|
131
|
+
errors.push(`${label}.path must be a non-empty local path`);
|
|
132
|
+
}
|
|
133
|
+
return errors;
|
|
134
|
+
}
|
|
99
135
|
/**
|
|
100
136
|
* Validates evidence.reviewHandoff. Returns null if valid, or an array of
|
|
101
137
|
* human-readable error strings describing what is wrong.
|
|
@@ -103,9 +139,16 @@ function validateAddressFeedbackApproval(evidence) {
|
|
|
103
139
|
* Required minimum shape:
|
|
104
140
|
* {
|
|
105
141
|
* reviewRequired: boolean,
|
|
106
|
-
* reviewTarget: object | null,
|
|
107
142
|
* artifacts: array,
|
|
143
|
+
* // when reviewRequired is true:
|
|
144
|
+
* reviewTarget: { type: "pull_request", label: string, url: string } | { type: "artifact_set", label: string },
|
|
145
|
+
* reviewActions: [{ kind: "approve", ... }, { kind: "request_changes", ... }],
|
|
108
146
|
* }
|
|
147
|
+
*
|
|
148
|
+
* Hub normalizers (server.ts + script.js) both read reviewTarget.type, not .kind.
|
|
149
|
+
* Using reviewTarget.kind produces a payload that passes validation but is silently
|
|
150
|
+
* un-renderable. This validator enforces the renderable contract so malformed payloads
|
|
151
|
+
* are rejected before phase advancement.
|
|
109
152
|
*/
|
|
110
153
|
function validateReviewHandoff(value) {
|
|
111
154
|
if (value === undefined || value === null) {
|
|
@@ -119,7 +162,42 @@ function validateReviewHandoff(value) {
|
|
|
119
162
|
if (typeof obj.reviewRequired !== 'boolean') {
|
|
120
163
|
errors.push(`evidence.reviewHandoff.reviewRequired must be a boolean (got ${obj.reviewRequired === undefined ? 'missing' : typeof obj.reviewRequired})`);
|
|
121
164
|
}
|
|
165
|
+
// artifacts must always be present and be an array
|
|
166
|
+
if (!('artifacts' in obj) || !Array.isArray(obj.artifacts)) {
|
|
167
|
+
errors.push('evidence.reviewHandoff.artifacts must be an array');
|
|
168
|
+
}
|
|
122
169
|
if (obj.reviewRequired === true) {
|
|
170
|
+
const target = obj.reviewTarget;
|
|
171
|
+
if (target === undefined || target === null) {
|
|
172
|
+
errors.push('evidence.reviewHandoff.reviewTarget must be a non-null object with type equal to "pull_request" or "artifact_set" when reviewRequired is true');
|
|
173
|
+
}
|
|
174
|
+
else if (typeof target !== 'object' || Array.isArray(target)) {
|
|
175
|
+
errors.push('evidence.reviewHandoff.reviewTarget must be an object with type equal to "pull_request" or "artifact_set"');
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
const t = target;
|
|
179
|
+
if ('kind' in t && !('type' in t)) {
|
|
180
|
+
errors.push(`evidence.reviewHandoff.reviewTarget uses kind but must use type — use type, not kind (e.g. { type: "pull_request", label: "...", url: "https://..." })`);
|
|
181
|
+
}
|
|
182
|
+
else if (t.type !== 'pull_request' && t.type !== 'artifact_set') {
|
|
183
|
+
errors.push(`evidence.reviewHandoff.reviewTarget.type must be "pull_request" or "artifact_set" (got ${t.type === undefined ? 'missing' : JSON.stringify(t.type)})`);
|
|
184
|
+
}
|
|
185
|
+
else if (t.type === 'pull_request') {
|
|
186
|
+
if (!isAbsoluteHttpUrl(t.url)) {
|
|
187
|
+
errors.push(`evidence.reviewHandoff.reviewTarget.url must be an absolute http or https URL for a pull_request target (got ${t.url === undefined ? 'missing' : JSON.stringify(t.url)})`);
|
|
188
|
+
}
|
|
189
|
+
if (Array.isArray(obj.artifacts) && obj.artifacts.length > 0) {
|
|
190
|
+
errors.push('evidence.reviewHandoff.artifacts must be empty for a pull_request target — the PR is the complete review surface; set artifacts: []');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else if (t.type === 'artifact_set') {
|
|
194
|
+
if (Array.isArray(obj.artifacts)) {
|
|
195
|
+
obj.artifacts.forEach((art, i) => {
|
|
196
|
+
errors.push(...validateReviewArtifactEntry(art, i));
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
123
201
|
if (!Array.isArray(obj.reviewActions) || obj.reviewActions.length === 0) {
|
|
124
202
|
errors.push('evidence.reviewHandoff.reviewActions must be a non-empty array when reviewRequired is true');
|
|
125
203
|
}
|
|
@@ -135,6 +213,12 @@ function validateReviewHandoff(value) {
|
|
|
135
213
|
}
|
|
136
214
|
}
|
|
137
215
|
}
|
|
216
|
+
else if (obj.reviewRequired === false && Array.isArray(obj.artifacts)) {
|
|
217
|
+
// For reviewRequired:false, validate any supplied artifact entries (no approval semantics required)
|
|
218
|
+
obj.artifacts.forEach((art, i) => {
|
|
219
|
+
errors.push(...validateReviewArtifactEntry(art, i));
|
|
220
|
+
});
|
|
221
|
+
}
|
|
138
222
|
return errors.length > 0 ? errors : null;
|
|
139
223
|
}
|
|
140
224
|
/**
|
|
@@ -303,8 +387,12 @@ const REVIEW_HANDOFF_SCHEMA = `\`\`\`javascript
|
|
|
303
387
|
evidence: {
|
|
304
388
|
reviewHandoff: {
|
|
305
389
|
reviewRequired: true,
|
|
306
|
-
|
|
307
|
-
|
|
390
|
+
// Pull request review (PR is the complete review surface -- set artifacts: []):
|
|
391
|
+
reviewTarget: { type: "pull_request", label: "Pull request", url: "https://github.com/org/repo/pull/N" },
|
|
392
|
+
artifacts: [],
|
|
393
|
+
// -- OR -- artifact set review (local generated files):
|
|
394
|
+
// reviewTarget: { type: "artifact_set", label: "Review bundle" },
|
|
395
|
+
// artifacts: [{ type: "markdown", label: "<label>", path: "<local-path>" }],
|
|
308
396
|
summary: "<optional summary>",
|
|
309
397
|
reviewActions: [
|
|
310
398
|
{ kind: "approve", label: "Approve" },
|
|
@@ -312,7 +400,8 @@ evidence: {
|
|
|
312
400
|
]
|
|
313
401
|
}
|
|
314
402
|
}
|
|
315
|
-
|
|
403
|
+
\`\`\`
|
|
404
|
+
Note: reviewTarget uses "type" (not "kind"). Action entries use "kind" -- these are different fields.`;
|
|
316
405
|
const NEXT_JOB_RECOMMENDATIONS_SCHEMA = `\`\`\`javascript
|
|
317
406
|
evidence: {
|
|
318
407
|
nextJobRecommendations: [
|
|
@@ -15,13 +15,15 @@
|
|
|
15
15
|
* change inert for every job that authors no map.
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.FEEDBACK_PHASE_ID = void 0;
|
|
18
|
+
exports.FEEDBACK_PHASE_ID = exports.WAITING_FOR_NEXT_INVOCATION = void 0;
|
|
19
19
|
exports.resolvePhaseEdge = resolvePhaseEdge;
|
|
20
20
|
exports.resolveDiscriminant = resolveDiscriminant;
|
|
21
21
|
exports.discriminantKeys = discriminantKeys;
|
|
22
22
|
exports.derivePredecessorPhase = derivePredecessorPhase;
|
|
23
23
|
/** The default discriminant, and the mandatory key on every authored map. */
|
|
24
24
|
const DEFAULT_DISCRIMINANT = 'default';
|
|
25
|
+
/** Reserved transition target that parks a recurring job until the scheduler invokes it again. */
|
|
26
|
+
exports.WAITING_FOR_NEXT_INVOCATION = 'waiting-for-next-invocation';
|
|
25
27
|
/**
|
|
26
28
|
* Resolve an edge to the next phase id.
|
|
27
29
|
*
|
|
@@ -1131,11 +1131,15 @@ function resolveTeamContextFile(workspaceRoot, key) {
|
|
|
1131
1131
|
if (key === 'org' || key === 'orgRules') {
|
|
1132
1132
|
const cachePath = (0, path_1.join)((0, pack_home_1.resolvePackHome)('org').contentRoot, relativePath);
|
|
1133
1133
|
if ((0, fs_1.existsSync)(cachePath)) {
|
|
1134
|
+
const fraimDir = (0, project_fraim_paths_1.getUserFraimDirPath)();
|
|
1135
|
+
const orgDisplayPath = cachePath.startsWith(fraimDir)
|
|
1136
|
+
? (0, project_fraim_paths_1.getUserFraimDisplayPath)(cachePath.slice(fraimDir.length).replace(/\\/g, '/').replace(/^\//, ''))
|
|
1137
|
+
: cachePath.replace(/\\/g, '/');
|
|
1134
1138
|
return {
|
|
1135
1139
|
present: true,
|
|
1136
1140
|
readPath: cachePath,
|
|
1137
1141
|
writePath: '',
|
|
1138
|
-
displayPath:
|
|
1142
|
+
displayPath: orgDisplayPath,
|
|
1139
1143
|
scope,
|
|
1140
1144
|
managedByOrgSync: true
|
|
1141
1145
|
};
|
|
@@ -1146,16 +1150,16 @@ function resolveTeamContextFile(workspaceRoot, key) {
|
|
|
1146
1150
|
const managerCachePath = (0, path_1.join)(managerHome.contentRoot, relativePath);
|
|
1147
1151
|
if ((0, fs_1.existsSync)(managerCachePath)) {
|
|
1148
1152
|
const fraimDir = (0, project_fraim_paths_1.getUserFraimDirPath)();
|
|
1149
|
-
const
|
|
1150
|
-
? managerCachePath.slice(fraimDir.length).replace(/\\/g, '/').replace(/^\//, '')
|
|
1151
|
-
:
|
|
1153
|
+
const mgrDisplayPath = managerCachePath.startsWith(fraimDir)
|
|
1154
|
+
? (0, project_fraim_paths_1.getUserFraimDisplayPath)(managerCachePath.slice(fraimDir.length).replace(/\\/g, '/').replace(/^\//, ''))
|
|
1155
|
+
: managerCachePath.replace(/\\/g, '/');
|
|
1152
1156
|
return {
|
|
1153
1157
|
present: true,
|
|
1154
1158
|
readPath: managerCachePath,
|
|
1155
|
-
writePath: '',
|
|
1156
|
-
displayPath:
|
|
1159
|
+
writePath: managerHome.requiresPublish ? '' : managerCachePath,
|
|
1160
|
+
displayPath: mgrDisplayPath,
|
|
1157
1161
|
scope,
|
|
1158
|
-
managedByManagerSync: true
|
|
1162
|
+
...(managerHome.requiresPublish ? { managedByManagerSync: true } : {})
|
|
1159
1163
|
};
|
|
1160
1164
|
}
|
|
1161
1165
|
}
|
|
@@ -25,15 +25,22 @@ const learning_context_builder_1 = require("./learning-context-builder");
|
|
|
25
25
|
var learning_firing_parser_2 = require("./learning-firing-parser");
|
|
26
26
|
Object.defineProperty(exports, "parseFiringSection", { enumerable: true, get: function () { return learning_firing_parser_2.parseFiringSection; } });
|
|
27
27
|
Object.defineProperty(exports, "extractLegacyTitle", { enumerable: true, get: function () { return learning_firing_parser_2.extractLegacyTitle; } });
|
|
28
|
+
const ENTRY_INDEX_READS = [
|
|
29
|
+
{ scope: 'manager', level: 'machine' },
|
|
30
|
+
{ scope: 'manager', level: 'project' },
|
|
31
|
+
{ scope: 'org', level: 'machine' },
|
|
32
|
+
{ scope: 'reverse', level: 'machine' },
|
|
33
|
+
{ scope: 'reverse', level: 'project' },
|
|
34
|
+
];
|
|
28
35
|
function buildEntryIndex(workspaceRoot, userId) {
|
|
29
36
|
const byId = new Map();
|
|
30
37
|
const byTitle = new Map();
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
for (const scope
|
|
38
|
+
// Keep this list in parity with the preserved-entry levels that the offer
|
|
39
|
+
// path can surface. Storage resolution stays inside readPreservedLearnings.
|
|
40
|
+
for (const { scope, level } of ENTRY_INDEX_READS) {
|
|
34
41
|
let entries = [];
|
|
35
42
|
try {
|
|
36
|
-
entries = (0, learning_context_builder_1.readPreservedLearnings)(workspaceRoot, userId, scope);
|
|
43
|
+
entries = (0, learning_context_builder_1.readPreservedLearnings)(workspaceRoot, userId, scope, level);
|
|
37
44
|
}
|
|
38
45
|
catch {
|
|
39
46
|
continue;
|