ucn 5.3.3 → 5.3.5
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/.claude/skills/ucn/SKILL.md +23 -1
- package/cli/index.js +23 -12
- package/core/account.js +5 -1
- package/core/analysis.js +18 -0
- package/core/cache.js +106 -1
- package/core/callers.js +939 -122
- package/core/confidence.js +26 -14
- package/core/imports.js +18 -4
- package/core/index-ir.js +1 -1
- package/core/ir.js +1 -1
- package/core/output/analysis.js +20 -8
- package/core/output/provenance.js +41 -0
- package/core/output/public.js +2 -1
- package/core/output/shared.js +3 -0
- package/core/project.js +9 -0
- package/core/provenance-facts.js +287 -0
- package/core/provenance-overload.js +118 -0
- package/core/provenance.js +423 -0
- package/core/python-fixture-flow.js +216 -0
- package/core/receiver-types.js +26 -0
- package/core/rust-result-flow.js +206 -0
- package/core/verify.js +7 -3
- package/languages/adapter.js +2 -0
- package/languages/c-family.js +14 -1
- package/languages/csharp.js +21 -9
- package/languages/go.js +105 -86
- package/languages/java.js +23 -10
- package/languages/javascript.js +129 -18
- package/languages/python.js +202 -35
- package/languages/rust.js +173 -73
- package/languages/type-evidence.js +63 -0
- package/mcp/server.js +7 -2
- package/package.json +2 -2
|
@@ -39,7 +39,7 @@ answer, a notice stating the full size and limit, and the accounting/contract
|
|
|
39
39
|
lines needed to interpret what remains; the requested limit includes all
|
|
40
40
|
three. The text block is the whole response on every surface.
|
|
41
41
|
|
|
42
|
-
Persistent indexes live in a per-user, project-keyed cache rather than the analyzed repository. Set `UCN_CACHE_DIR` to override the cache root; CLI `--no-cache` bypasses persistence and `--clear-cache` removes the current project's cache. Legacy `<project>/.ucn-cache` directories are migrated on first use.
|
|
42
|
+
Persistent indexes live in a per-user, project-keyed cache rather than the analyzed repository. Set `UCN_CACHE_DIR` to override the cache root; CLI `--no-cache` bypasses persistence and `--clear-cache` removes the current project's cache. Legacy `<project>/.ucn-cache` directories are migrated on first use. Concurrent invocations on a cold cache share one build: the first process takes a build lock, the others wait for its cache (`Waiting for another ucn process building the index...` on stderr) instead of rebuilding; a lock left by a dead process is broken automatically.
|
|
43
43
|
|
|
44
44
|
Supported source families are JavaScript/TypeScript/TSX, Python, Go, Rust, Java, C, C++, C#, and HTML inline JavaScript/event handlers. C/C++ uses `compile_commands.json` when available to classify headers and resolve include paths. Recoverable preprocessor branches contribute AST-proven source facts, so a single selected configuration does not silently erase definitions or calls; disagreeing conditional macro identities stay visible as unverified. C++ resolution uses namespace ownership, static overload shape (including arrays), and macro-parameter requalification. C# resolution uses declared property/field receiver types plus overload and hiding discipline. This is portable AST analysis, not a compiler build; macros, templates, generated code, reflection, and external dependency semantics can remain unverified.
|
|
45
45
|
|
|
@@ -220,3 +220,25 @@ file set, the unit a refactor has to break.
|
|
|
220
220
|
- Use `search` or ordinary repository search for text, filenames, configuration, and unsupported syntax.
|
|
221
221
|
|
|
222
222
|
Read [references/commands.md](references/commands.md) for all public commands and flags. Read [references/trust-contract.md](references/trust-contract.md) before building automation that gates changes on UCN output.
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
### Inspect confirmation provenance
|
|
226
|
+
|
|
227
|
+
Caller and callee JSON carries compact `provenance`: `rule`, contributing `rules`
|
|
228
|
+
when there is more than one, `validation`, and the receiver's source and origin
|
|
229
|
+
line when recorded. Incomplete proofs include a `diagnostic`, also shown in text.
|
|
230
|
+
Callees retain compact `siteProvenance` for each distinct occurrence; their summary
|
|
231
|
+
uses the weakest confirmed site's rule. Exclusions expose counts by rule and
|
|
232
|
+
validation under `account.excluded.evidenceSummary`. Full declaration, import,
|
|
233
|
+
and member-lookup facts remain on engine results and in oracle reports.
|
|
234
|
+
|
|
235
|
+
A lone project owner of a method name does not identify an untyped receiver.
|
|
236
|
+
Such candidates stay unverified with `single-owner`; importing the class does not
|
|
237
|
+
change that. `provenance-incomplete` means the available facts cannot establish
|
|
238
|
+
this declaration. Inspect the site and diagnostic before editing it. Neither
|
|
239
|
+
reason is permission to discard a possible caller. `validation: unsupported`
|
|
240
|
+
means the witness collector does not yet cover that path (for example a wildcard
|
|
241
|
+
re-export or an external inherited member). Its existing classification is kept
|
|
242
|
+
in report-only mode; it is not a validated proof. Missing or inconsistent facts
|
|
243
|
+
on a supported lookup still route unverified. Full rule migration is tracked as
|
|
244
|
+
#356. Ordinal evidence weights are not probabilities.
|
package/cli/index.js
CHANGED
|
@@ -766,8 +766,18 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
766
766
|
// If cache was loaded but stale, force rebuild to avoid duplicates
|
|
767
767
|
let needsCacheSave = false;
|
|
768
768
|
if (!usedCache) {
|
|
769
|
-
|
|
770
|
-
|
|
769
|
+
const buildOpts = { quiet: flags.quiet, forceRebuild: cacheWasLoaded, followSymlinks: flags.followSymlinks, maxFiles: flags.maxFiles, workers: flags.workers };
|
|
770
|
+
if (flags.cache && !flags.maxFiles) {
|
|
771
|
+
// Cross-process build lock (fix #354): concurrent cold-cache
|
|
772
|
+
// invocations share one build instead of each rebuilding.
|
|
773
|
+
index.buildCached(buildOpts, {
|
|
774
|
+
onWait: () => { if (!flags.quiet) console.error('Waiting for another ucn process building the index...'); },
|
|
775
|
+
});
|
|
776
|
+
needsCacheSave = false; // buildCached saved (or loaded the other process's cache)
|
|
777
|
+
} else {
|
|
778
|
+
index.build(null, buildOpts);
|
|
779
|
+
needsCacheSave = flags.cache;
|
|
780
|
+
}
|
|
771
781
|
}
|
|
772
782
|
|
|
773
783
|
try {
|
|
@@ -986,13 +996,10 @@ function runInteractive(rootDir) {
|
|
|
986
996
|
if (flags.cache) {
|
|
987
997
|
const loaded = !flags.clearCache && index.loadCache();
|
|
988
998
|
iCacheFresh = loaded && !index.isCacheStale();
|
|
989
|
-
if (!iCacheFresh && loaded) {
|
|
990
|
-
index.build(null, { quiet: true, forceRebuild: true, workers: flags.workers });
|
|
991
|
-
} else if (!iCacheFresh) {
|
|
992
|
-
index.build(null, { quiet: true, workers: flags.workers });
|
|
993
|
-
}
|
|
994
999
|
if (!iCacheFresh) {
|
|
995
|
-
|
|
1000
|
+
index.buildCached({ quiet: true, forceRebuild: !!loaded, workers: flags.workers }, {
|
|
1001
|
+
onWait: () => console.log('Waiting for another ucn process building the index...'),
|
|
1002
|
+
});
|
|
996
1003
|
}
|
|
997
1004
|
} else {
|
|
998
1005
|
index.build(null, { quiet: true, workers: flags.workers });
|
|
@@ -1113,15 +1120,19 @@ Flags can be added per-command: show myFunc --sections=source,callers
|
|
|
1113
1120
|
// appeared in neighbouring answers (UCN5-044).
|
|
1114
1121
|
if (index.isCacheStale()) {
|
|
1115
1122
|
console.log('Source changed; rebuilding index...');
|
|
1116
|
-
|
|
1123
|
+
const rebuildOpts = {
|
|
1117
1124
|
quiet: true,
|
|
1118
1125
|
forceRebuild: true,
|
|
1119
1126
|
followSymlinks: flags.followSymlinks,
|
|
1120
1127
|
maxFiles: flags.maxFiles,
|
|
1121
1128
|
workers: flags.workers,
|
|
1122
|
-
}
|
|
1123
|
-
if (flags.cache) {
|
|
1124
|
-
|
|
1129
|
+
};
|
|
1130
|
+
if (flags.cache && !flags.maxFiles) {
|
|
1131
|
+
index.buildCached(rebuildOpts, {
|
|
1132
|
+
onWait: () => console.log('Waiting for another ucn process building the index...'),
|
|
1133
|
+
});
|
|
1134
|
+
} else {
|
|
1135
|
+
index.build(null, rebuildOpts);
|
|
1125
1136
|
}
|
|
1126
1137
|
console.log(`Index ready: ${index.files.size} files, ${index.symbols.size} unique symbol names`);
|
|
1127
1138
|
}
|
package/core/account.js
CHANGED
|
@@ -463,7 +463,11 @@ function buildAccount(index, name, parts) {
|
|
|
463
463
|
confirmed,
|
|
464
464
|
unverified,
|
|
465
465
|
nonCall,
|
|
466
|
-
excluded: { total: excludedTotal, byReason: excludedByReason
|
|
466
|
+
excluded: { total: excludedTotal, byReason: excludedByReason,
|
|
467
|
+
...(excludedEntries.some(e => e.provenance) && { evidence: excludedEntries
|
|
468
|
+
.filter(e => e.provenance).map(e => ({ file: relPath(index, e.file),
|
|
469
|
+
line: e.line, reason: e.reason, provenance: e.provenance })) }),
|
|
470
|
+
},
|
|
467
471
|
unparsed: groundSet.unparsed,
|
|
468
472
|
unsupported,
|
|
469
473
|
unreadableFiles: groundSet.unreadableFiles,
|
package/core/analysis.js
CHANGED
|
@@ -1158,6 +1158,8 @@ function impact(index, name, options = {}) {
|
|
|
1158
1158
|
evidenceScore: c.evidenceScore,
|
|
1159
1159
|
scoreKind: c.scoreKind,
|
|
1160
1160
|
resolution: c.resolution,
|
|
1161
|
+
...(c.provenance && { provenance: c.provenance }),
|
|
1162
|
+
...(c.siteProvenance && { siteProvenance: c.siteProvenance }),
|
|
1161
1163
|
...(c.tier && { tier: c.tier }),
|
|
1162
1164
|
...analysis
|
|
1163
1165
|
});
|
|
@@ -1190,6 +1192,8 @@ function impact(index, name, options = {}) {
|
|
|
1190
1192
|
evidenceScore: c.evidenceScore,
|
|
1191
1193
|
scoreKind: c.scoreKind,
|
|
1192
1194
|
resolution: c.resolution,
|
|
1195
|
+
...(c.provenance && { provenance: c.provenance }),
|
|
1196
|
+
...(c.siteProvenance && { siteProvenance: c.siteProvenance }),
|
|
1193
1197
|
tier: c.tier,
|
|
1194
1198
|
}));
|
|
1195
1199
|
// findCallers already applied binding, receiver, module ownership, and
|
|
@@ -1241,6 +1245,8 @@ function impact(index, name, options = {}) {
|
|
|
1241
1245
|
evidenceScore: call.evidenceScore,
|
|
1242
1246
|
scoreKind: call.scoreKind,
|
|
1243
1247
|
resolution: call.resolution,
|
|
1248
|
+
...(call.provenance && { provenance: call.provenance }),
|
|
1249
|
+
...(call.siteProvenance && { siteProvenance: call.siteProvenance }),
|
|
1244
1250
|
...(call.tier && { tier: call.tier }),
|
|
1245
1251
|
...analysis
|
|
1246
1252
|
});
|
|
@@ -1262,6 +1268,8 @@ function impact(index, name, options = {}) {
|
|
|
1262
1268
|
evidenceScore: u.evidenceScore,
|
|
1263
1269
|
scoreKind: u.scoreKind,
|
|
1264
1270
|
resolution: u.resolution,
|
|
1271
|
+
...(u.provenance && { provenance: u.provenance }),
|
|
1272
|
+
...(u.siteProvenance && { siteProvenance: u.siteProvenance }),
|
|
1265
1273
|
tier: 'unverified',
|
|
1266
1274
|
...(u.reason && { reason: u.reason }),
|
|
1267
1275
|
...(u.dispatchVia && { dispatchVia: u.dispatchVia }),
|
|
@@ -1711,6 +1719,8 @@ function about(index, name, options = {}) {
|
|
|
1711
1719
|
evidenceScore: c.evidenceScore,
|
|
1712
1720
|
scoreKind: c.scoreKind,
|
|
1713
1721
|
resolution: c.resolution,
|
|
1722
|
+
...(c.provenance && { provenance: c.provenance }),
|
|
1723
|
+
...(c.siteProvenance && { siteProvenance: c.siteProvenance }),
|
|
1714
1724
|
reachable: c.reachable,
|
|
1715
1725
|
}));
|
|
1716
1726
|
|
|
@@ -1735,6 +1745,8 @@ function about(index, name, options = {}) {
|
|
|
1735
1745
|
evidenceScore: c.evidenceScore,
|
|
1736
1746
|
scoreKind: c.scoreKind,
|
|
1737
1747
|
resolution: c.resolution,
|
|
1748
|
+
...(c.provenance && { provenance: c.provenance }),
|
|
1749
|
+
...(c.siteProvenance && { siteProvenance: c.siteProvenance }),
|
|
1738
1750
|
...(c.reason && { reason: c.reason }),
|
|
1739
1751
|
...(c.dispatchVia && { dispatchVia: c.dispatchVia }),
|
|
1740
1752
|
...(c.dispatchCandidates != null && { dispatchCandidates: c.dispatchCandidates }),
|
|
@@ -1786,6 +1798,8 @@ function about(index, name, options = {}) {
|
|
|
1786
1798
|
evidenceScore: c.evidenceScore,
|
|
1787
1799
|
scoreKind: c.scoreKind,
|
|
1788
1800
|
resolution: c.resolution,
|
|
1801
|
+
...(c.provenance && { provenance: c.provenance }),
|
|
1802
|
+
...(c.siteProvenance && { siteProvenance: c.siteProvenance }),
|
|
1789
1803
|
reachable: c.reachable,
|
|
1790
1804
|
...(c.returnType && { returnType: c.returnType }),
|
|
1791
1805
|
...(c.paramTypes && { paramTypes: c.paramTypes }),
|
|
@@ -2392,6 +2406,8 @@ function diffImpact(index, options = {}) {
|
|
|
2392
2406
|
evidenceScore: c.evidenceScore,
|
|
2393
2407
|
scoreKind: c.scoreKind,
|
|
2394
2408
|
resolution: c.resolution,
|
|
2409
|
+
...(c.provenance && { provenance: c.provenance }),
|
|
2410
|
+
...(c.siteProvenance && { siteProvenance: c.siteProvenance }),
|
|
2395
2411
|
...(c.tier && { tier: c.tier }),
|
|
2396
2412
|
})),
|
|
2397
2413
|
unverifiedCallers: unverified.map(u => ({
|
|
@@ -2404,6 +2420,8 @@ function diffImpact(index, options = {}) {
|
|
|
2404
2420
|
evidenceScore: u.evidenceScore,
|
|
2405
2421
|
scoreKind: u.scoreKind,
|
|
2406
2422
|
resolution: u.resolution,
|
|
2423
|
+
...(u.provenance && { provenance: u.provenance }),
|
|
2424
|
+
...(u.siteProvenance && { siteProvenance: u.siteProvenance }),
|
|
2407
2425
|
tier: 'unverified',
|
|
2408
2426
|
...(u.reason && { reason: u.reason }),
|
|
2409
2427
|
...(u.dispatchVia && { dispatchVia: u.dispatchVia }),
|
package/core/cache.js
CHANGED
|
@@ -256,6 +256,14 @@ function pruneUserCache({ force = false, now = Date.now() } = {}) {
|
|
|
256
256
|
root = data.root || null;
|
|
257
257
|
touched = Math.max(touched, Number(data.timestamp) || 0);
|
|
258
258
|
} catch (_) { /* malformed/incomplete cache expires below */ }
|
|
259
|
+
// fix #354: a project whose FIRST build is in progress has no
|
|
260
|
+
// index.json yet, only a live build.lock — another process's prune
|
|
261
|
+
// must not delete the lock (or the dir) out from under the builder.
|
|
262
|
+
const lockPath = path.join(dir, BUILD_LOCK_FILE);
|
|
263
|
+
if (!root && fs.existsSync(lockPath) && !_lockIsStale(lockPath)) {
|
|
264
|
+
entries.push({ dir, touched: now, bytes: null });
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
259
267
|
if (!root || !fs.existsSync(root) || now - touched > CACHE_TTL_MS) {
|
|
260
268
|
try {
|
|
261
269
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
@@ -699,7 +707,10 @@ function clearAllCaches() {
|
|
|
699
707
|
// v212 (fix #342): extendsGraph/extendedByGraph no longer persisted (rebuilt on load).
|
|
700
708
|
// v213 (fix #348): Python from-import records carry per-name `renames` so
|
|
701
709
|
// import bindings pair each alias with its own module.
|
|
702
|
-
|
|
710
|
+
// v215: receiver-type provenance records the originating AST fact (#355).
|
|
711
|
+
// v224 (#355 recovery): Go declaration origins; Rust aliases, wrapper patterns,
|
|
712
|
+
// copied bindings and qualified macro receivers; TS indexed array evidence.
|
|
713
|
+
const CACHE_FORMAT_VERSION = 224;
|
|
703
714
|
const USAGE_CACHE_FILE = 'usage-results.json';
|
|
704
715
|
|
|
705
716
|
/**
|
|
@@ -1494,7 +1505,101 @@ function _computeReachabilityFingerprint(index) {
|
|
|
1494
1505
|
return `${fileCount}:${symbolCount}:${sample}`;
|
|
1495
1506
|
}
|
|
1496
1507
|
|
|
1508
|
+
const BUILD_LOCK_FILE = 'build.lock';
|
|
1509
|
+
const BUILD_LOCK_STALE_MS = 15 * 60 * 1000;
|
|
1510
|
+
const BUILD_LOCK_WAIT_MS = 15 * 60 * 1000;
|
|
1511
|
+
const BUILD_LOCK_POLL_MS = 150;
|
|
1512
|
+
|
|
1513
|
+
function _sleepSync(ms) {
|
|
1514
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function _pidAlive(pid) {
|
|
1518
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
1519
|
+
try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; }
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
function _readLock(lockPath) {
|
|
1523
|
+
try { return JSON.parse(fs.readFileSync(lockPath, 'utf8')); } catch (_) { return null; }
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
function _lockIsStale(lockPath) {
|
|
1527
|
+
const info = _readLock(lockPath);
|
|
1528
|
+
if (!info) {
|
|
1529
|
+
// Unreadable or half-written: judge by mtime alone
|
|
1530
|
+
try { return Date.now() - fs.statSync(lockPath).mtimeMs > BUILD_LOCK_STALE_MS; } catch (_) { return true; }
|
|
1531
|
+
}
|
|
1532
|
+
if (info.pid && !_pidAlive(info.pid)) return true;
|
|
1533
|
+
return Date.now() - (info.startedAt || 0) > BUILD_LOCK_STALE_MS;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* Cross-process build lock (fix #354): N concurrent cold-cache invocations
|
|
1538
|
+
* used to rebuild the whole index N times at once (6 processes on a 474k-line
|
|
1539
|
+
* repo: 9s each alone, 363s each together — the stampede fires after every
|
|
1540
|
+
* version bump, because the bump invalidates every cache). One process takes
|
|
1541
|
+
* the lock and builds; the others wait, then load the cache it saved. A lock
|
|
1542
|
+
* whose holder is dead, or older than 15 minutes, is broken. Waiters give up
|
|
1543
|
+
* after 15 minutes and build themselves (a wedged holder must never make the
|
|
1544
|
+
* tool hang forever).
|
|
1545
|
+
*
|
|
1546
|
+
* @returns {{ built: boolean, waited: boolean }}
|
|
1547
|
+
*/
|
|
1548
|
+
function buildWithLock(index, buildOpts = {}, options = {}) {
|
|
1549
|
+
const cacheDir = getProjectCacheDir(index.root);
|
|
1550
|
+
const lockPath = path.join(cacheDir, BUILD_LOCK_FILE);
|
|
1551
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
1552
|
+
const deadline = Date.now() + BUILD_LOCK_WAIT_MS;
|
|
1553
|
+
let announced = false;
|
|
1554
|
+
for (;;) {
|
|
1555
|
+
let fd = null;
|
|
1556
|
+
try {
|
|
1557
|
+
fd = fs.openSync(lockPath, 'wx');
|
|
1558
|
+
} catch (e) {
|
|
1559
|
+
if (e.code !== 'EEXIST') throw e;
|
|
1560
|
+
}
|
|
1561
|
+
if (fd !== null) {
|
|
1562
|
+
try {
|
|
1563
|
+
fs.writeSync(fd, JSON.stringify({ pid: process.pid, startedAt: Date.now() }));
|
|
1564
|
+
fs.closeSync(fd);
|
|
1565
|
+
index.build(null, buildOpts);
|
|
1566
|
+
if (options.save !== false) {
|
|
1567
|
+
try { saveCache(index); } catch (_) { /* best-effort */ }
|
|
1568
|
+
}
|
|
1569
|
+
return { built: true, waited: announced };
|
|
1570
|
+
} finally {
|
|
1571
|
+
try { fs.unlinkSync(lockPath); } catch (_) { /* already gone */ }
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
if (_lockIsStale(lockPath)) {
|
|
1575
|
+
try { fs.unlinkSync(lockPath); } catch (_) { /* raced */ }
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
if (Date.now() > deadline) {
|
|
1579
|
+
index.build(null, buildOpts);
|
|
1580
|
+
if (options.save !== false) {
|
|
1581
|
+
try { saveCache(index); } catch (_) { /* best-effort */ }
|
|
1582
|
+
}
|
|
1583
|
+
return { built: true, waited: true };
|
|
1584
|
+
}
|
|
1585
|
+
if (!announced) {
|
|
1586
|
+
announced = true;
|
|
1587
|
+
if (typeof options.onWait === 'function') options.onWait(_readLock(lockPath));
|
|
1588
|
+
}
|
|
1589
|
+
_sleepSync(BUILD_LOCK_POLL_MS);
|
|
1590
|
+
if (!fs.existsSync(lockPath)) {
|
|
1591
|
+
// The holder finished: its cache is the answer unless it is
|
|
1592
|
+
// already stale again (or it never saved one)
|
|
1593
|
+
if (options.reload !== false && loadCache(index) && !isCacheStale(index)) {
|
|
1594
|
+
return { built: false, waited: true };
|
|
1595
|
+
}
|
|
1596
|
+
// fall through: take the lock ourselves
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1497
1601
|
module.exports = {
|
|
1602
|
+
buildWithLock,
|
|
1498
1603
|
saveCache, saveUsageCache, loadCache, loadCallsCache, isCacheStale, ensureCallsCacheLoaded,
|
|
1499
1604
|
getUserCacheRoot, getProjectCacheDir, getProjectCachePath,
|
|
1500
1605
|
getLegacyProjectCacheDir, migrateLegacyProjectCache, clearProjectCache,
|