@gcunharodrigues/wrxn 0.11.0 → 0.13.0
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/lib/install.cjs +3 -0
- package/manifest.json +15 -5
- package/migrations/007-auto-memory-transition.cjs +136 -0
- package/package.json +2 -2
- package/payload/.claude/hooks/memory-synth-spawn.cjs +101 -0
- package/payload/.claude/hooks/session-start.cjs +86 -8
- package/payload/.claude/hooks/synapse-engine.cjs +2 -3
- package/payload/.claude/settings.json +7 -0
- package/payload/.claude/skills/dream/SKILL.md +2 -31
- package/payload/.claude/skills/synapse/SKILL.md +5 -5
- package/payload/.claude/skills/synapse/references/brackets.md +1 -2
- package/payload/.claude/skills/synapse/references/layers.md +1 -2
- package/payload/.mcp.json +1 -1
- package/payload/.wrxn/dream.cjs +112 -66
- package/payload/.wrxn/memory-synth.cjs +761 -0
- package/payload/.wrxn/memory.config.json +12 -0
- package/payload/.wrxn/wiki.cjs +7 -19
- package/payload/.claude/skills/handoff/SKILL.md +0 -22
package/payload/.wrxn/dream.cjs
CHANGED
|
@@ -6,11 +6,14 @@
|
|
|
6
6
|
// (node stdlib only). The dream SKILL (the LLM, dream-02) PROPOSES; THIS adapter JUDGES (a
|
|
7
7
|
// deterministic gate) and records — "bad memory is worse than no memory".
|
|
8
8
|
//
|
|
9
|
-
// In THIS slice the adapter is driven by hand-fed proposal JSON (no LLM).
|
|
9
|
+
// In THIS slice the adapter is driven by hand-fed proposal JSON (no LLM). Three subcommands:
|
|
10
10
|
// check <proposal.json | batch.json> run the Validation gate.
|
|
11
11
|
// · a single Proposal object → a single Verdict { ok, reason? }.
|
|
12
12
|
// · an array / { proposals:[…] } / { abstain:true } → a batch result
|
|
13
13
|
// { abstained, accepted[], rejected[{index,slug,reason}] } (applies the ≤5 run cap + restraint).
|
|
14
|
+
// · --source <file> (optional, auto-memory-01): the session transcript blob. When present,
|
|
15
|
+
// every evidence quote must verifiably appear in it (normalized substring) or the proposal
|
|
16
|
+
// is rejected quote_not_in_source. Absent ⇒ no quote-verify (the trusted manual-dream path).
|
|
14
17
|
// stage <batch.json> record the VALIDATED (accepted) batch into the audit trail
|
|
15
18
|
// under .wrxn/dream/ as .jsonl (NEVER .md, so recon's prose ingestion never recalls a
|
|
16
19
|
// staged-but-unapproved proposal). Nothing is written to the wiki.
|
|
@@ -22,21 +25,18 @@
|
|
|
22
25
|
// can never reach recall even if its slug is force-approved. Additive + dedup-SKIP; a slug not
|
|
23
26
|
// staged (not_staged) or one that fails re-validation is recorded skipped with the reason, and
|
|
24
27
|
// the rest of the batch still writes. Then the outcome is appended to the .wrxn/dream/ audit log.
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// log it. This is the LONE update-exception: only the focus slot may be overwritten; the
|
|
28
|
-
// knowledge gate/commit above stay additive + dedup-skip. The focus slot is DISJOINT from the
|
|
29
|
-
// continuity baton (`.wrxn/continuity/latest.md`, single-writer = the handoff skill) — set-focus
|
|
30
|
-
// NEVER reads or writes that baton (different path, different writer: the continuity doctrine).
|
|
28
|
+
// · --source <file> (optional, auto-memory-01): re-verifies every quote at the write boundary,
|
|
29
|
+
// so a hallucinated proposal is blocked from recall even if its slug is force-approved.
|
|
31
30
|
//
|
|
32
|
-
//
|
|
31
|
+
// Flags: --root <dir> (override the install-root walk-up; mainly for tests).
|
|
32
|
+
// --source <file> (check|commit only) — the transcript blob for quote-verification (auto-memory-01).
|
|
33
33
|
//
|
|
34
34
|
// Proposal { kind:"concept"|"decision"|"gotcha"|"rule"; tier:"concepts"|"decisions"|"gotchas"|"_rules";
|
|
35
35
|
// slug; title; body /* starts "# " */; confidence /*0–1*/; rationale; evidence:[{quote,source?}] }
|
|
36
36
|
// Verdict { ok:boolean; reason?:string /* machine code on reject */ }
|
|
37
37
|
// NOTE: `_slots` is NOT a knowledge-gate tier — KIND_TIER stays {concepts,decisions,gotchas,_rules}; a
|
|
38
|
-
// knowledge proposal targeting `_slots` is rejected `unsupported_tier`. The slot
|
|
39
|
-
//
|
|
38
|
+
// knowledge proposal targeting `_slots` is rejected `unsupported_tier`. (The standing-focus slot and
|
|
39
|
+
// its set-focus op were retired in auto-memory-05; the auto-handoff baton carries "where we are".)
|
|
40
40
|
|
|
41
41
|
const fs = require('fs');
|
|
42
42
|
const path = require('path');
|
|
@@ -51,13 +51,7 @@ const BODY_MAX = 32000; // size cap (chars) — a durable page, not a transcript
|
|
|
51
51
|
const MAX_ACCEPTED = 5; // one run can't flood the wiki.
|
|
52
52
|
const DREAM_DIR = ['.wrxn', 'dream'];
|
|
53
53
|
const STAGED_FILE = 'staged.jsonl'; // the validated-but-unapproved batch (full proposals).
|
|
54
|
-
const AUDIT_FILE = 'audit.jsonl'; // the append-only outcome log (stage + commit
|
|
55
|
-
|
|
56
|
-
// The durable standing-focus slot — a FIXED wiki path (tier + slug). set-focus is its ONLY writer and
|
|
57
|
-
// overwrites it in place (the lone update-exception). `_slots` is deliberately ABSENT from KIND_TIER /
|
|
58
|
-
// TIERS above: it is not a knowledge-gate tier, so the additive + dedup-skip knowledge gate is untouched.
|
|
59
|
-
const FOCUS_TIER = '_slots';
|
|
60
|
-
const FOCUS_SLUG = 'current-focus';
|
|
54
|
+
const AUDIT_FILE = 'audit.jsonl'; // the append-only outcome log (stage + commit events).
|
|
61
55
|
|
|
62
56
|
// ── install-root resolution (mirrors wiki.cjs / enforce-managed-guard.cjs) ─────
|
|
63
57
|
function findInstallRoot(start) {
|
|
@@ -134,14 +128,6 @@ function wikiWritePage(root, tier, slug, description, body) {
|
|
|
134
128
|
return JSON.parse(execFileSync('node', args, { encoding: 'utf8' }));
|
|
135
129
|
}
|
|
136
130
|
|
|
137
|
-
// Overwrite a page in place via the wiki adapter's --force path (the indirection contract — never a
|
|
138
|
-
// direct .md write). wiki.cjs restricts --force to the `_slots` tier, so only the focus slot is reachable.
|
|
139
|
-
function wikiForceWritePage(root, tier, slug, description, body) {
|
|
140
|
-
guardArgv([slug, String(description || ''), String(body || '')]);
|
|
141
|
-
const args = [wikiAdapter(), 'write-page', tier, slug, '--description', String(description || ''), '--body', String(body || ''), '--force', '--root', root];
|
|
142
|
-
return JSON.parse(execFileSync('node', args, { encoding: 'utf8' }));
|
|
143
|
-
}
|
|
144
|
-
|
|
145
131
|
function normalizeTitle(t) {
|
|
146
132
|
return String(t == null ? '' : t).toLowerCase().replace(/\s+/g, ' ').trim();
|
|
147
133
|
}
|
|
@@ -235,11 +221,66 @@ function secretScan(text) {
|
|
|
235
221
|
return null;
|
|
236
222
|
}
|
|
237
223
|
|
|
224
|
+
// ── source quote-verification (auto-memory-01 + F1 substantive floor) ──────────
|
|
225
|
+
// The single mechanical control that lets a NON-human proposer (auto-dream) write durable memory
|
|
226
|
+
// without poisoning recall: when a --source transcript blob is supplied, every evidence quote must be a
|
|
227
|
+
// SUBSTANTIVE verbatim span that VERIFIABLY appears in it, else the proposal is a hallucination. Matching
|
|
228
|
+
// is normalized — lowercased + whitespace-collapsed — so transcript formatting (line wraps, indentation,
|
|
229
|
+
// case) never causes a false reject, while the substantive quote text must still be present contiguously
|
|
230
|
+
// (we do NOT strip punctuation: the AC scopes normalization to whitespace + case only). When no source
|
|
231
|
+
// is supplied (the manual dream skill — a trusted main-agent proposer) this is a no-op, so behavior is
|
|
232
|
+
// byte-identical to today. A non-string quote never reaches here: the evidence-presence check rejects it.
|
|
233
|
+
//
|
|
234
|
+
// F1 (security MED): a bare substring match is satisfied by a trivially-present quote — "the" is a
|
|
235
|
+
// substring of essentially every transcript — so the proposer needed only ANY real word to clear the
|
|
236
|
+
// gate, under-delivering the PRD's load-bearing "a hallucination can't poison recall" claim. So a quote
|
|
237
|
+
// must FIRST be substantive before its presence counts: the NORMALIZED quote must be ≥ QUOTE_MIN_CHARS
|
|
238
|
+
// chars AND ≥ QUOTE_MIN_TOKENS whitespace-delimited word tokens, else quote_not_substantive. The token
|
|
239
|
+
// floor rejects single/two-word fragments ("the", "it works"); the char floor backstops it against tiny
|
|
240
|
+
// 3-token spans ("a b c"). The bar is low enough never to false-reject a terse real decision quote
|
|
241
|
+
// ("use pino logs"), and a proposer grounding a real decision can always cite a fuller span.
|
|
242
|
+
function normalizeForMatch(s) {
|
|
243
|
+
return String(s == null ? '' : s).toLowerCase().replace(/\s+/g, ' ').trim();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// The substantive-quote floor (F1). Operates on the NORMALIZED quote so it is independent of transcript
|
|
247
|
+
// formatting/case. Tunable here; pinned in BOTH directions (reject "the"/"authentication"; admit
|
|
248
|
+
// "use pino logs") by test/dream.test.cjs.
|
|
249
|
+
const QUOTE_MIN_CHARS = 12;
|
|
250
|
+
const QUOTE_MIN_TOKENS = 3;
|
|
251
|
+
|
|
252
|
+
function isSubstantiveQuote(quote) {
|
|
253
|
+
const norm = normalizeForMatch(quote);
|
|
254
|
+
if (norm.length < QUOTE_MIN_CHARS) return false;
|
|
255
|
+
return norm.split(' ').filter(Boolean).length >= QUOTE_MIN_TOKENS;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Quote-verify (auto-memory-01 + F1): returns a reject reason or null. PRECEDENCE — substantiveness is a
|
|
259
|
+
// property of the quote ALONE, so it is a global precondition checked BEFORE any source-presence match:
|
|
260
|
+
// if ANY quote is non-substantive → quote_not_substantive; else if ANY quote is absent from the source →
|
|
261
|
+
// quote_not_in_source. (So quote_not_substantive is reported before quote_not_in_source.)
|
|
262
|
+
function verifyQuotes(p, source) {
|
|
263
|
+
if (!p.evidence.every((e) => isSubstantiveQuote(e.quote))) return 'quote_not_substantive';
|
|
264
|
+
const hay = normalizeForMatch(source);
|
|
265
|
+
if (!p.evidence.every((e) => hay.includes(normalizeForMatch(e.quote)))) return 'quote_not_in_source';
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
238
269
|
// ── the pure per-proposal gate ────────────────────────────────────────────────
|
|
239
|
-
// Deterministic given (proposal, io). `io` injects the dedup IO so the gate stays a pure,
|
|
240
|
-
// function (mirrors Phase-2 decideRecall): io.pathExists(tier,slug) /
|
|
241
|
-
//
|
|
242
|
-
|
|
270
|
+
// Deterministic given (proposal, io, source). `io` injects the dedup IO so the gate stays a pure,
|
|
271
|
+
// unit-testable function (mirrors Phase-2 decideRecall): io.pathExists(tier,slug) /
|
|
272
|
+
// io.titleExists(title,tier,slug). `source` (auto-memory-01) is the optional transcript blob: when a
|
|
273
|
+
// non-null string, every evidence quote must be substantive (quote_not_substantive) and verifiably appear
|
|
274
|
+
// in it (quote_not_in_source); when null (the manual dream path) the quote-verify is skipped — behavior is
|
|
275
|
+
// byte-identical to today.
|
|
276
|
+
// PRECEDENCE (deterministic, documented): routing validity (tier, kind↔tier) → confidence floor →
|
|
277
|
+
// evidence presence → SOURCE quote-verify [substantive floor (quote_not_substantive) BEFORE
|
|
278
|
+
// source-presence (quote_not_in_source)] → rationale → body → negative filters → secret-scan →
|
|
279
|
+
// identity → dedup. So the quote-verify reasons are reported AFTER the cheap structural checks the quote
|
|
280
|
+
// itself depends on (it needs evidence to exist) and the confidence floor, but BEFORE the negative
|
|
281
|
+
// filters, secret-scan, identity and the expensive dedup IO. Quote-verify composes with — never
|
|
282
|
+
// bypasses — every existing check: a proposal that passes quote-verify still faces all later gates.
|
|
283
|
+
function validateProposal(p, io, source) {
|
|
243
284
|
if (!p || typeof p !== 'object' || Array.isArray(p)) return { ok: false, reason: 'invalid_proposal' };
|
|
244
285
|
if (!TIERS.includes(p.tier)) return { ok: false, reason: 'unsupported_tier' };
|
|
245
286
|
if (KIND_TIER[p.kind] !== p.tier) return { ok: false, reason: 'kind_tier_mismatch' };
|
|
@@ -248,6 +289,14 @@ function validateProposal(p, io) {
|
|
|
248
289
|
!p.evidence.every((e) => e && typeof e.quote === 'string' && e.quote.trim().length > 0)) {
|
|
249
290
|
return { ok: false, reason: 'missing_evidence' };
|
|
250
291
|
}
|
|
292
|
+
// SOURCE quote-verify (auto-memory-01 + F1): runs ONLY when a --source blob is supplied. The evidence is
|
|
293
|
+
// known present + non-blank here, so each quote must be a substantive span (quote_not_substantive) AND
|
|
294
|
+
// normalize-substring-match the source (quote_not_in_source) or the proposal is a hallucination. Null
|
|
295
|
+
// source (manual dream) ⇒ skipped ⇒ byte-identical to today.
|
|
296
|
+
if (source != null) {
|
|
297
|
+
const qr = verifyQuotes(p, source);
|
|
298
|
+
if (qr) return { ok: false, reason: qr };
|
|
299
|
+
}
|
|
251
300
|
if (typeof p.rationale !== 'string' || p.rationale.trim().length === 0) return { ok: false, reason: 'missing_rationale' };
|
|
252
301
|
if (typeof p.body !== 'string' || !p.body.startsWith('# ')) return { ok: false, reason: 'body_missing_h1' };
|
|
253
302
|
if (p.body.length > BODY_MAX) return { ok: false, reason: 'body_too_large' };
|
|
@@ -266,14 +315,15 @@ function validateProposal(p, io) {
|
|
|
266
315
|
return { ok: true };
|
|
267
316
|
}
|
|
268
317
|
|
|
269
|
-
// Run-level gate: restraint (empty/abstain ⇒ write nothing) + the ≤5 accepted cap.
|
|
270
|
-
|
|
318
|
+
// Run-level gate: restraint (empty/abstain ⇒ write nothing) + the ≤5 accepted cap. `source` threads
|
|
319
|
+
// the optional --source blob to every per-proposal quote-verify (auto-memory-01); null ⇒ legacy path.
|
|
320
|
+
function validateRun(proposals, io, source) {
|
|
271
321
|
const accepted = [];
|
|
272
322
|
const rejected = [];
|
|
273
323
|
let acceptedCount = 0;
|
|
274
324
|
for (let i = 0; i < proposals.length; i++) {
|
|
275
325
|
const p = proposals[i];
|
|
276
|
-
const v = validateProposal(p, io);
|
|
326
|
+
const v = validateProposal(p, io, source);
|
|
277
327
|
if (v.ok) {
|
|
278
328
|
acceptedCount += 1;
|
|
279
329
|
if (acceptedCount > MAX_ACCEPTED) {
|
|
@@ -342,6 +392,30 @@ function readJson(file) {
|
|
|
342
392
|
}
|
|
343
393
|
}
|
|
344
394
|
|
|
395
|
+
// Read the optional --source transcript blob (auto-memory-01). Absent ⇒ null (the legacy path, no
|
|
396
|
+
// quote-verify). PRESENT ⇒ it MUST carry a real, readable file value: a value-less / empty / --leading
|
|
397
|
+
// token (F2) or an unreadable path ⇒ HARD fail (exit 2). A missing source must NEVER silently disable the
|
|
398
|
+
// gate — that would let a malformed argv (or a broken file) turn off the one defense against a
|
|
399
|
+
// hallucinated memory. We read argv DIRECTLY here (not flag()): flag('source') collapses "flag absent"
|
|
400
|
+
// and "flag present-but-valueless" both to undefined, but only the former may fall through to legacy.
|
|
401
|
+
// The blob is raw text (the transcript), read as-is, not parsed.
|
|
402
|
+
function readSource() {
|
|
403
|
+
const i = process.argv.indexOf('--source');
|
|
404
|
+
if (i === -1) return null; // the flag is truly absent → legacy path (the trusted manual-dream proposer)
|
|
405
|
+
const file = process.argv[i + 1];
|
|
406
|
+
// --source WAS asked for, so it must not silently revert to no-verify: a trailing token (undefined), an
|
|
407
|
+
// empty value, or another flag (e.g. `--root`) is a fail-CLOSED malformed argv, never "gate off" (F2).
|
|
408
|
+
if (file === undefined || file === '' || file.startsWith('--')) {
|
|
409
|
+
fail('--source was given without a readable file value — refusing to silently disable quote-verify');
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
return fs.readFileSync(file, 'utf8');
|
|
413
|
+
} catch (err) {
|
|
414
|
+
fail(`cannot read --source file "${file}": ${err.message}`);
|
|
415
|
+
return undefined;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
345
419
|
function appendLine(file, obj) {
|
|
346
420
|
fs.appendFileSync(file, JSON.stringify(obj) + '\n');
|
|
347
421
|
}
|
|
@@ -357,12 +431,13 @@ function runCheck() {
|
|
|
357
431
|
const input = readJson(positionalFile());
|
|
358
432
|
const root = installRoot();
|
|
359
433
|
const io = makeIo(root);
|
|
434
|
+
const source = readSource(); // null ⇒ legacy path (no quote-verify); string ⇒ verify every quote
|
|
360
435
|
if (isBatchInput(input)) {
|
|
361
436
|
const { proposals, abstain } = normalizeBatch(input);
|
|
362
437
|
if (abstain) return print({ abstained: true, accepted: [], rejected: [] });
|
|
363
|
-
return print(validateRun(proposals, io));
|
|
438
|
+
return print(validateRun(proposals, io, source));
|
|
364
439
|
}
|
|
365
|
-
return print(validateProposal(input, io)); // single proposal → single Verdict
|
|
440
|
+
return print(validateProposal(input, io, source)); // single proposal → single Verdict
|
|
366
441
|
}
|
|
367
442
|
|
|
368
443
|
function runStage() {
|
|
@@ -427,6 +502,7 @@ function runCommit() {
|
|
|
427
502
|
const root = installRoot();
|
|
428
503
|
const approved = approvedSlugs(input);
|
|
429
504
|
const io = makeIo(root);
|
|
505
|
+
const source = readSource(); // null ⇒ legacy re-gate; string ⇒ re-verify every quote at the write boundary
|
|
430
506
|
const staged = readStaged(root);
|
|
431
507
|
const written = [];
|
|
432
508
|
const skipped = [];
|
|
@@ -437,7 +513,7 @@ function runCommit() {
|
|
|
437
513
|
skipped.push({ slug: key, reason: 'not_staged' });
|
|
438
514
|
continue;
|
|
439
515
|
}
|
|
440
|
-
const v = validateProposal(p, io); // the re-gate — additive + dedup-skip + every quality/safety check
|
|
516
|
+
const v = validateProposal(p, io, source); // the re-gate — additive + dedup-skip + quote-verify + every quality/safety check
|
|
441
517
|
if (!v.ok) {
|
|
442
518
|
skipped.push({ slug: key, reason: v.reason });
|
|
443
519
|
continue;
|
|
@@ -458,34 +534,6 @@ function runCommit() {
|
|
|
458
534
|
return print({ written, skipped });
|
|
459
535
|
}
|
|
460
536
|
|
|
461
|
-
// set-focus — write/overwrite the durable standing-focus slot. The LONE update-exception: it is the
|
|
462
|
-
// only op that overwrites a wiki page, and only ever `_slots/current-focus.md`. It is NOT a knowledge
|
|
463
|
-
// proposal (no gate, no dedup) — the skill drafts a short standing-focus statement, the operator
|
|
464
|
-
// confirms, then this writes it via the wiki --force path. DISJOINT from the continuity baton:
|
|
465
|
-
// set-focus never reads or writes `.wrxn/continuity/latest.md` (single-writer = the handoff skill).
|
|
466
|
-
function runSetFocus() {
|
|
467
|
-
const input = readJson(positionalFile());
|
|
468
|
-
const root = installRoot();
|
|
469
|
-
const obj = input && typeof input === 'object' && !Array.isArray(input) ? input : {};
|
|
470
|
-
const title = obj.title ? String(obj.title) : 'Current focus';
|
|
471
|
-
const body = obj.body;
|
|
472
|
-
if (typeof body !== 'string' || body.trim().length === 0) {
|
|
473
|
-
fail('set-focus needs a non-empty "body" — the standing-focus statement to pin');
|
|
474
|
-
}
|
|
475
|
-
// Gate the focus slot too (security M1): it is an ungated --force write, so run the same content-safety
|
|
476
|
-
// checks the knowledge gate runs — the anti-superstition negative filters + the credential secret-scan
|
|
477
|
-
// — over the focus title+body, and refuse a `--`-leading flag-injection value. Reject ⇒ nothing written.
|
|
478
|
-
const scan = `${title}\n${body}`;
|
|
479
|
-
const neg = negativeFilter(scan);
|
|
480
|
-
if (neg) fail(`set-focus rejected — the focus body trips a negative filter (${neg}); state durable standing context, not a transient note`);
|
|
481
|
-
const sec = secretScan(scan);
|
|
482
|
-
if (sec) fail(`set-focus rejected — the focus body contains a credential (${sec}); never pin a session secret`);
|
|
483
|
-
if (title.startsWith('--') || body.startsWith('--')) fail('set-focus rejected — a --leading title/body is refused (flag-injection guard)');
|
|
484
|
-
const r = wikiForceWritePage(root, FOCUS_TIER, FOCUS_SLUG, title, body);
|
|
485
|
-
appendLine(path.join(dreamDir(root), AUDIT_FILE), { ts: new Date().toISOString(), op: 'set-focus', file: r.written });
|
|
486
|
-
return print({ focus: r.written });
|
|
487
|
-
}
|
|
488
|
-
|
|
489
537
|
function main() {
|
|
490
538
|
const cmd = process.argv[2];
|
|
491
539
|
switch (cmd) {
|
|
@@ -495,10 +543,8 @@ function main() {
|
|
|
495
543
|
return runStage();
|
|
496
544
|
case 'commit':
|
|
497
545
|
return runCommit();
|
|
498
|
-
case 'set-focus':
|
|
499
|
-
return runSetFocus();
|
|
500
546
|
default:
|
|
501
|
-
process.stdout.write('Usage: node .wrxn/dream.cjs <check|stage|commit
|
|
547
|
+
process.stdout.write('Usage: node .wrxn/dream.cjs <check|stage|commit> <file.json> [--root <dir>]\n');
|
|
502
548
|
process.exit(cmd ? 2 : 0);
|
|
503
549
|
}
|
|
504
550
|
}
|