quantum-cortex 1.0.0__py3-none-any.whl
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.
- cortex_c2/__init__.py +161 -0
- cortex_c2/consolidation.py +146 -0
- cortex_c2/router_ablation.py +254 -0
- cortex_c2/router_v2.py +109 -0
- cortex_c2b/__init__.py +724 -0
- cortex_c2b/companion_demo.py +203 -0
- cortex_c2b/crypto.py +102 -0
- cortex_c2b/external_arm.py +591 -0
- cortex_c2b/hm_lm.py +983 -0
- cortex_c2b/hm_protocol.py +308 -0
- cortex_c2b/lifecycle.py +454 -0
- cortex_c2b/lm_bridge.py +338 -0
- cortex_c2b/organ_use.py +254 -0
- cortex_c2b/read_path.py +218 -0
- cortex_c2b/reconstruction_probe.py +471 -0
- cortex_c2b/write_path.py +176 -0
- cortex_data/__init__.py +439 -0
- cortex_data/relay.py +174 -0
- cortex_eval/__init__.py +20 -0
- cortex_eval/ablation_mqar.py +187 -0
- cortex_eval/confirmation_run.py +133 -0
- cortex_eval/domain_map.py +127 -0
- cortex_eval/external_baseline.py +148 -0
- cortex_eval/ingest_confirmation.py +194 -0
- cortex_eval/mqar.py +122 -0
- cortex_eval/multiseed.py +148 -0
- cortex_eval/recurrent_base.py +197 -0
- cortex_eval/recurrent_ladder.py +159 -0
- cortex_eval/resumable_ladder.py +231 -0
- cortex_eval/resumable_rb.py +207 -0
- cortex_eval/run_mqar.py +127 -0
- cortex_eval/serial_position.py +116 -0
- quantum_cortex-1.0.0.dist-info/METADATA +209 -0
- quantum_cortex-1.0.0.dist-info/RECORD +41 -0
- quantum_cortex-1.0.0.dist-info/WHEEL +5 -0
- quantum_cortex-1.0.0.dist-info/licenses/AUTHORS.md +30 -0
- quantum_cortex-1.0.0.dist-info/licenses/LICENSE +73 -0
- quantum_cortex-1.0.0.dist-info/licenses/LICENSE-DOCS.md +34 -0
- quantum_cortex-1.0.0.dist-info/licenses/NOTICE +50 -0
- quantum_cortex-1.0.0.dist-info/top_level.txt +5 -0
- train.py +885 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""cortex_c2b.hm_protocol — the H.M. dissociation protocol (ADR-007 Slice D, the milestone).
|
|
2
|
+
|
|
3
|
+
The refutable claim of the whole memory story: skills and semantic knowledge live
|
|
4
|
+
in the WEIGHTS; episodes live in the JOURNAL (C2b). Cut the journal at inference
|
|
5
|
+
and episodic recall must collapse while skills hold. If the dissociation does not
|
|
6
|
+
hold, the memory story is wrong — and the loss is published.
|
|
7
|
+
|
|
8
|
+
Implements `docs/benchmarks/hm-protocol.md` EXACTLY. Frozen thresholds (never
|
|
9
|
+
adjusted after seeing numbers; a change needs a dated amendment BEFORE a run):
|
|
10
|
+
δ = 0.50 hm_recall_on − hm_recall_off ≥ δ (the gap is the claim)
|
|
11
|
+
ε_S = 0.01 hm_skill_delta ≤ ε_S (1% relative) (skills intact)
|
|
12
|
+
floor = chance + 0.05 (recall_off at the floor)
|
|
13
|
+
negative control: hallucinated recall ≥ 0.10 INVALIDATES the run
|
|
14
|
+
|
|
15
|
+
Session A plants 200 synthetic facts (5 schemas, balanced) through the normal
|
|
16
|
+
C2b write path (surprise gate active — novel facts pass). Session B, in a fresh
|
|
17
|
+
context, probes each fact (direct query + one paraphrase) with the journal ON
|
|
18
|
+
and OFF. A negative-control set of 50 never-planted facts must yield ~0 recall
|
|
19
|
+
in both modes.
|
|
20
|
+
|
|
21
|
+
Slice D binds the protocol to the C2b organ built in A–C: the journal (store),
|
|
22
|
+
the write path (gate), the read path (cue index). The "model" side — skills
|
|
23
|
+
and the recall reader — is abstracted behind `SkillProbe` and `Reader` so the
|
|
24
|
+
protocol can run today against the journal alone (recall_off ≡ no journal ⇒
|
|
25
|
+
parametric floor by construction) and later against the full model whose
|
|
26
|
+
weights hold skills. Both arms, both numbers, PASS or FAIL, published.
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import hashlib
|
|
31
|
+
import json
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from datetime import datetime, timezone
|
|
34
|
+
|
|
35
|
+
import numpy as np
|
|
36
|
+
|
|
37
|
+
from cortex_c2b import Journal, CUE_DIM, MODE_DURABLE, POLICY_STOP, lifecycle_declaration
|
|
38
|
+
from cortex_c2b.write_path import WritePath
|
|
39
|
+
from cortex_c2b.read_path import JournalPath
|
|
40
|
+
|
|
41
|
+
# ---- frozen thresholds (hm-protocol.md) ------------------------------------
|
|
42
|
+
DELTA = 0.50
|
|
43
|
+
EPSILON_S = 0.01
|
|
44
|
+
FLOOR_MARGIN = 0.05
|
|
45
|
+
NEGCTRL_INVALID = 0.10
|
|
46
|
+
N_FACTS = 200
|
|
47
|
+
N_NEGCTRL = 50
|
|
48
|
+
SCHEMAS = ("person", "place", "event", "object", "decision")
|
|
49
|
+
|
|
50
|
+
_CONS = "bdfgklmnprstvz"
|
|
51
|
+
_VOW = "aeiou"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------- #
|
|
55
|
+
# The generator: open, seeded, config-hashed, leakage-proof #
|
|
56
|
+
# ---------------------------------------------------------------------------- #
|
|
57
|
+
def _entity_name(rng) -> str:
|
|
58
|
+
"""`{consonant-vowel trigrams}-{4 hex}` — never a dictionary word, never a real name."""
|
|
59
|
+
syl = "".join(rng.choice(list(_CONS)) + rng.choice(list(_VOW)) for _ in range(2))
|
|
60
|
+
tri = syl[:1].upper() + syl[1:]
|
|
61
|
+
hexs = "".join(rng.choice(list("0123456789abcdef")) for _ in range(4))
|
|
62
|
+
return f"{tri}-{hexs}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_TEMPLATES = {
|
|
66
|
+
"person": ("{e} works as a {a}", "what is {e}'s job?", "{e} — occupation?"),
|
|
67
|
+
"place": ("{e} is a town in {a}", "where is {e}?", "{e} lies in which region?"),
|
|
68
|
+
"event": ("{e} took place in {a}", "when did {e} happen?", "{e} — the year?"),
|
|
69
|
+
"object": ("{e} is made of {a}", "what is {e} made of?", "{e} — its material?"),
|
|
70
|
+
"decision": ("{e} was decided by {a}", "who decided {e}?", "{e} — decided by whom?"),
|
|
71
|
+
}
|
|
72
|
+
_ATTRS = {
|
|
73
|
+
"person": ["glazier", "cartographer", "cooper", "luthier", "falconer", "archivist", "tanner", "milliner"],
|
|
74
|
+
"place": ["Kalvaria", "Osterhold", "Brenmoor", "Vilkas", "Sandrelle", "Tornquay", "Ebbelund", "Marrowick"],
|
|
75
|
+
"event": ["1873", "1904", "1931", "1958", "1967", "1982", "1996", "2003"],
|
|
76
|
+
"object": ["pewter", "boxwood", "vellum", "basalt", "linen", "amber", "cobalt", "horn"],
|
|
77
|
+
"decision": ["the guild council", "the harbour master", "the abbess", "the river warden",
|
|
78
|
+
"the quarry board", "the toll keeper", "the choir master", "the ferry syndic"],
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class Fact:
|
|
84
|
+
entity: str
|
|
85
|
+
schema: str
|
|
86
|
+
attr: str
|
|
87
|
+
statement: str
|
|
88
|
+
query: str
|
|
89
|
+
paraphrase: str
|
|
90
|
+
cue: np.ndarray = field(repr=False)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _cue_for(text: str, dim: int = CUE_DIM) -> np.ndarray:
|
|
94
|
+
"""Deterministic pseudo-embedding of a text (hash-seeded unit vector). Stands in
|
|
95
|
+
for the model's cue encoder so the protocol runs against the journal alone;
|
|
96
|
+
the same function embeds statement, query and paraphrase, so recall is a
|
|
97
|
+
retrieval test, not a language test — exactly the journal's job."""
|
|
98
|
+
h = int(hashlib.sha256(text.encode()).hexdigest()[:16], 16)
|
|
99
|
+
v = np.random.default_rng(h).standard_normal(dim).astype(np.float32)
|
|
100
|
+
return v / (np.linalg.norm(v) + 1e-8)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
address_cue = _cue_for
|
|
104
|
+
"""The organ level cue convention, under its public name (whitepaper 3.6, added
|
|
105
|
+
2026-09-30): the entity is the address. Any unit vector of CUE_DIM floats is a
|
|
106
|
+
valid cue; this one stands in for a learned encoder and addresses no paraphrase."""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def value_of_statement(statement: str) -> tuple[str, str, str] | None:
|
|
110
|
+
"""The organ's own reading of a line: (schema, entity, attribute) recovered from a
|
|
111
|
+
planted statement by the generator's templates, or None when no template matches.
|
|
112
|
+
Used by the `+value` decode policies (ADR-008 amendment 2026-09-18): the value the
|
|
113
|
+
model answers is then the cited line's, read by the organ, never generated."""
|
|
114
|
+
import re
|
|
115
|
+
for schema, (st, _, _) in _TEMPLATES.items():
|
|
116
|
+
pat = "^" + re.escape(st).replace(r"\{e\}", "(?P<e>.+?)").replace(r"\{a\}", "(?P<a>.+)") + "$"
|
|
117
|
+
m = re.match(pat, statement)
|
|
118
|
+
if m:
|
|
119
|
+
return schema, m.group("e"), m.group("a")
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def generate_facts(n: int = N_FACTS, seed: int = 0) -> tuple[list[Fact], str]:
|
|
124
|
+
"""Balanced across 5 schemas; returns (facts, config_hash of the generator)."""
|
|
125
|
+
rng = np.random.default_rng(seed)
|
|
126
|
+
facts = []
|
|
127
|
+
per = n // len(SCHEMAS)
|
|
128
|
+
for schema in SCHEMAS:
|
|
129
|
+
st, q, p = _TEMPLATES[schema]
|
|
130
|
+
for _ in range(per):
|
|
131
|
+
e = _entity_name(rng)
|
|
132
|
+
a = rng.choice(_ATTRS[schema])
|
|
133
|
+
statement = st.format(e=e, a=a)
|
|
134
|
+
facts.append(Fact(entity=e, schema=schema, attr=a, statement=statement,
|
|
135
|
+
query=q.format(e=e), paraphrase=p.format(e=e),
|
|
136
|
+
cue=_cue_for(f"{schema}|{e}"))) # the entity is the address
|
|
137
|
+
cfg = hashlib.sha256(json.dumps({"n": n, "seed": seed, "schemas": SCHEMAS,
|
|
138
|
+
"templates": _TEMPLATES}, sort_keys=True).encode()).hexdigest()[:16]
|
|
139
|
+
return facts, cfg
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------- #
|
|
143
|
+
# The reader: recall = retrieve the planted attribute for a probed entity #
|
|
144
|
+
# ---------------------------------------------------------------------------- #
|
|
145
|
+
class Reader:
|
|
146
|
+
"""Journal ON: query the cue index for the entity's address, read the payload,
|
|
147
|
+
exact-match the attribute. Journal OFF: no journal ⇒ the parametric floor
|
|
148
|
+
(a weights-only model that never saw the episode can only guess: chance)."""
|
|
149
|
+
|
|
150
|
+
def __init__(self, journal_path: JournalPath | None, chance: float):
|
|
151
|
+
self.jp = journal_path
|
|
152
|
+
self.chance = chance
|
|
153
|
+
|
|
154
|
+
def recall(self, fact: Fact, rng) -> bool:
|
|
155
|
+
if self.jp is None: # journal OFF
|
|
156
|
+
# parametric floor: a weights-only model that never saw the episode can
|
|
157
|
+
# only guess ONCE per fact (its guess is the same for the direct query and
|
|
158
|
+
# the paraphrase — both address the same entity); seeded per fact.
|
|
159
|
+
g = np.random.default_rng(int(hashlib.sha256(fact.entity.encode()).hexdigest()[:8], 16))
|
|
160
|
+
return bool(g.random() < self.chance)
|
|
161
|
+
hits = self.jp.retrieve(fact.cue, k=1)
|
|
162
|
+
if not hits:
|
|
163
|
+
return False
|
|
164
|
+
entry, payload, score = hits[0]
|
|
165
|
+
return fact.attr in payload.decode("utf-8", errors="ignore")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class SkillProbe:
|
|
169
|
+
"""Skills live in the weights. Against the journal alone the skill score is
|
|
170
|
+
journal-independent by construction (the journal is not consulted): we
|
|
171
|
+
return a fixed reference so `hm_skill_delta` is exactly 0 — the protocol's
|
|
172
|
+
contract, to be replaced by the model's real suites when the LM is wired."""
|
|
173
|
+
|
|
174
|
+
def __init__(self, reference: float = 0.7):
|
|
175
|
+
self.reference = reference
|
|
176
|
+
|
|
177
|
+
def score(self, journal_on: bool) -> float:
|
|
178
|
+
return self.reference
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------------------- #
|
|
182
|
+
# The protocol #
|
|
183
|
+
# ---------------------------------------------------------------------------- #
|
|
184
|
+
def run_hm_protocol(n_facts: int = N_FACTS, n_negctrl: int = N_NEGCTRL, seed: int = 0,
|
|
185
|
+
admission_threshold: float = 0.15, journal_path=None, key: bytes | None = None,
|
|
186
|
+
plaintext: bool = False, policy: str = POLICY_STOP) -> dict:
|
|
187
|
+
"""Spec amendment 2026-09-12 (ADR-007 D8/D9, dated before any run that uses it):
|
|
188
|
+
the protocol reads the journal's storage state and reports `persistent` and
|
|
189
|
+
`claimable`. Without `journal_path` the journal lives in memory: session B
|
|
190
|
+
shares the process with session A, and the pass is reported with
|
|
191
|
+
`persistent: false` -- an organ level measurement, not a claim of survival
|
|
192
|
+
across a restart. With `journal_path`, session B REOPENS the journal from the
|
|
193
|
+
disk alone (a new object, the index recomputed); `persistent` is true only if
|
|
194
|
+
the journal stayed durable through both sessions. A measurement run uses the
|
|
195
|
+
`stop` policy: a storage failure aborts the run instead of continuing in
|
|
196
|
+
memory unnoticed. Thresholds are untouched."""
|
|
197
|
+
facts, cfg = generate_facts(n_facts, seed)
|
|
198
|
+
neg, _ = generate_facts(n_negctrl, seed + 10_000) # never planted
|
|
199
|
+
rng = np.random.default_rng(seed + 1)
|
|
200
|
+
|
|
201
|
+
# chance level: pick an attribute at random among the schema's vocabulary
|
|
202
|
+
chance = float(np.mean([1.0 / len(_ATTRS[s]) for s in SCHEMAS]))
|
|
203
|
+
floor = chance + FLOOR_MARGIN
|
|
204
|
+
|
|
205
|
+
# --- Session A: plant every fact through the NORMAL gated write path -----
|
|
206
|
+
def open_journal():
|
|
207
|
+
if journal_path is None:
|
|
208
|
+
return Journal()
|
|
209
|
+
return Journal(journal_path, key=key, plaintext=plaintext, policy=policy)
|
|
210
|
+
journal = open_journal()
|
|
211
|
+
wp = WritePath(journal, admission_threshold=admission_threshold, seed=seed)
|
|
212
|
+
jp = JournalPath(journal)
|
|
213
|
+
admitted = 0
|
|
214
|
+
for f in facts:
|
|
215
|
+
rep = wp.write(f.cue, f.statement.encode(), f.schema, now=float(admitted))
|
|
216
|
+
if rep.admitted:
|
|
217
|
+
jp.on_write(rep.entry); admitted += 1
|
|
218
|
+
durable_a = journal.mode == MODE_DURABLE
|
|
219
|
+
|
|
220
|
+
# --- Session B: fresh context (new Reader objects, nothing from A but the journal) --
|
|
221
|
+
if journal_path is not None:
|
|
222
|
+
del wp, jp # nothing of session A but the disk
|
|
223
|
+
journal = open_journal() # reopened: structure, bytes, index recomputed
|
|
224
|
+
jp = JournalPath(journal)
|
|
225
|
+
storage = lifecycle_declaration(journal)["storage"]
|
|
226
|
+
persistent = bool(journal_path is not None and durable_a and journal.mode == MODE_DURABLE)
|
|
227
|
+
reader_on = Reader(jp, chance)
|
|
228
|
+
reader_off = Reader(None, chance) # journal cut
|
|
229
|
+
def recall_rate(reader, fs):
|
|
230
|
+
hits = 0
|
|
231
|
+
for f in fs:
|
|
232
|
+
# direct query + one paraphrase — both address the same entity cue
|
|
233
|
+
hits += int(reader.recall(f, rng) or reader.recall(f, rng))
|
|
234
|
+
return hits / len(fs)
|
|
235
|
+
recall_on = recall_rate(reader_on, facts)
|
|
236
|
+
recall_off = recall_rate(reader_off, facts)
|
|
237
|
+
negctrl_on = recall_rate(reader_on, neg) # never planted → the JOURNAL must not hallucinate
|
|
238
|
+
negctrl_off = recall_rate(reader_off, neg) # a guessing floor "recalls" ~chance by construction
|
|
239
|
+
# Amendment 2026-09-08 (dated BEFORE this run's verdict is used; see hm-protocol.md §5):
|
|
240
|
+
# the negative control is read ABOVE CHANCE — guessing is not hallucinating.
|
|
241
|
+
negctrl = max(negctrl_on, max(0.0, negctrl_off - chance))
|
|
242
|
+
|
|
243
|
+
# --- Skills arm ---------------------------------------------------------
|
|
244
|
+
sp = SkillProbe()
|
|
245
|
+
s_on, s_off = sp.score(True), sp.score(False)
|
|
246
|
+
skill_delta = abs(s_on - s_off) / max(s_on, 1e-9)
|
|
247
|
+
|
|
248
|
+
# --- Verdict (frozen thresholds) ----------------------------------------
|
|
249
|
+
valid = negctrl < NEGCTRL_INVALID
|
|
250
|
+
gap = recall_on - recall_off
|
|
251
|
+
dissociation_pass = bool(valid and skill_delta <= EPSILON_S and gap >= DELTA and recall_off <= floor)
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
"benchmark": "hm_protocol", "spec": "docs/benchmarks/hm-protocol.md (frozen thresholds)",
|
|
255
|
+
"timestamp_utc": datetime.now(timezone.utc).isoformat(),
|
|
256
|
+
"generator": {"n_facts": n_facts, "n_negctrl": n_negctrl, "seed": seed, "config_hash": cfg,
|
|
257
|
+
"schemas": SCHEMAS, "admitted": admitted},
|
|
258
|
+
"thresholds": {"delta": DELTA, "epsilon_s": EPSILON_S, "floor": round(floor, 4),
|
|
259
|
+
"chance": round(chance, 4), "negctrl_invalid": NEGCTRL_INVALID},
|
|
260
|
+
"hm_recall_on": round(recall_on, 4),
|
|
261
|
+
"hm_recall_off": round(recall_off, 4),
|
|
262
|
+
"hm_gap": round(gap, 4),
|
|
263
|
+
"hm_negctrl_rate": round(negctrl, 4),
|
|
264
|
+
"hm_negctrl_on": round(negctrl_on, 4),
|
|
265
|
+
"hm_negctrl_off_raw": round(negctrl_off, 4),
|
|
266
|
+
"hm_negctrl_off_above_chance": round(max(0.0, negctrl_off - chance), 4),
|
|
267
|
+
"hm_skill_delta": round(skill_delta, 4),
|
|
268
|
+
"run_valid": valid,
|
|
269
|
+
"hm_dissociation_pass": int(dissociation_pass),
|
|
270
|
+
"persistent": persistent,
|
|
271
|
+
"claimable": int(dissociation_pass and persistent),
|
|
272
|
+
"storage": {"on_disk": journal_path is not None, "mode": storage["mode"], "policy": storage["policy"],
|
|
273
|
+
"sealed": key is not None, "plaintext_scope": storage["plaintext_scope"]},
|
|
274
|
+
"verdict": (("PASS \u2014 the dissociation holds: episodes live in the journal, skills are journal-independent"
|
|
275
|
+
if dissociation_pass else
|
|
276
|
+
("INVALID \u2014 hallucinated recall on the negative control" if not valid else
|
|
277
|
+
"FAIL \u2014 the dissociation does not hold (recorded; triggers a dated revision of the memory story)"))
|
|
278
|
+
+ ("" if persistent else
|
|
279
|
+
" \u2014 persistent: false (journal in memory or in memory mode; sessions A and B within one process): not claimable")),
|
|
280
|
+
"reserve": "Slice D runs the protocol against the C2b organ with a hash-seeded cue encoder and a "
|
|
281
|
+
"reference skill probe; the language-model arm (real skills suites, learned cue encoder) "
|
|
282
|
+
"lands when the LM is wired to the journal. Recall here is a retrieval test — the journal's job.",
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
if __name__ == "__main__":
|
|
287
|
+
import argparse
|
|
288
|
+
from pathlib import Path
|
|
289
|
+
ap = argparse.ArgumentParser(description="the H.M. dissociation protocol against the C2b organ")
|
|
290
|
+
ap.add_argument("--persistent", action="store_true",
|
|
291
|
+
help="sealed journal on disk, session B reopened from the disk alone (claimable)")
|
|
292
|
+
ap.add_argument("--seed", type=int, default=0)
|
|
293
|
+
ap.add_argument("--out", default=None,
|
|
294
|
+
help="retain the full result as a file (e.g. metrics/mqar/hm-protocol-persistent-<date>.json); "
|
|
295
|
+
"a number read from a console is not a source")
|
|
296
|
+
args = ap.parse_args()
|
|
297
|
+
if args.persistent:
|
|
298
|
+
import tempfile
|
|
299
|
+
from cortex_c2b.crypto import generate_key
|
|
300
|
+
with tempfile.TemporaryDirectory() as d: # sealed on disk, session B reopened from the disk alone
|
|
301
|
+
r = run_hm_protocol(seed=args.seed, journal_path=f"{d}/hm-journal.jsonl", key=generate_key())
|
|
302
|
+
else:
|
|
303
|
+
r = run_hm_protocol(seed=args.seed)
|
|
304
|
+
if args.out:
|
|
305
|
+
out = Path(args.out); out.parent.mkdir(parents=True, exist_ok=True)
|
|
306
|
+
out.write_text(json.dumps(r, indent=2) + "\n", encoding="utf-8")
|
|
307
|
+
print(f"retained: {out}")
|
|
308
|
+
print(json.dumps({k: v for k, v in r.items() if k != "generator"}, indent=2))
|