@namzu/sdk 21.0.0 → 21.1.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/CHANGELOG.md +44 -0
- package/dist/public-runtime.d.ts +4 -0
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +15 -0
- package/dist/public-runtime.js.map +1 -1
- package/dist/run/command-gate.d.ts +107 -0
- package/dist/run/command-gate.d.ts.map +1 -0
- package/dist/run/command-gate.js +157 -0
- package/dist/run/command-gate.js.map +1 -0
- package/dist/run/index.d.ts +6 -0
- package/dist/run/index.d.ts.map +1 -1
- package/dist/run/index.js +3 -0
- package/dist/run/index.js.map +1 -1
- package/dist/run/memory-promoter.d.ts +70 -0
- package/dist/run/memory-promoter.d.ts.map +1 -0
- package/dist/run/memory-promoter.js +117 -0
- package/dist/run/memory-promoter.js.map +1 -0
- package/dist/run/workspace-fingerprint.d.ts +105 -0
- package/dist/run/workspace-fingerprint.d.ts.map +1 -0
- package/dist/run/workspace-fingerprint.js +147 -0
- package/dist/run/workspace-fingerprint.js.map +1 -0
- package/package.json +1 -1
- package/src/public-runtime.ts +31 -0
- package/src/run/command-gate.ts +234 -0
- package/src/run/index.ts +17 -0
- package/src/run/memory-promoter.ts +155 -0
- package/src/run/workspace-fingerprint.ts +193 -0
package/src/run/index.ts
CHANGED
|
@@ -7,6 +7,23 @@ export type { RunReporter } from './reporter.js'
|
|
|
7
7
|
|
|
8
8
|
export { DEFAULT_DRAIN_PAGE_SIZE, drainRuns } from './drain.js'
|
|
9
9
|
export type { DrainFailure, DrainRun, DrainRunsParams, DrainRunsResult } from './drain.js'
|
|
10
|
+
export {
|
|
11
|
+
DEFAULT_GATE_MAX_RETRIES,
|
|
12
|
+
DEFAULT_GATE_OUTPUT_CHARS,
|
|
13
|
+
DEFAULT_GATE_TIMEOUT_MS,
|
|
14
|
+
clipOutput,
|
|
15
|
+
createCommandGate,
|
|
16
|
+
} from './command-gate.js'
|
|
17
|
+
export type { CommandGateOptions, GateExec } from './command-gate.js'
|
|
18
|
+
export {
|
|
19
|
+
FINGERPRINT_MAX_BYTES,
|
|
20
|
+
FINGERPRINT_TIMEOUT_MS,
|
|
21
|
+
fingerprintWorkspace,
|
|
22
|
+
} from './workspace-fingerprint.js'
|
|
23
|
+
export type { FingerprintExec, WorkspaceFingerprintOptions } from './workspace-fingerprint.js'
|
|
10
24
|
|
|
11
25
|
export { checkLimitsDetailed, buildLimitConfig } from './LimitChecker.js'
|
|
12
26
|
export type { LimitCheckerState, LimitCheckResult } from './LimitChecker.js'
|
|
27
|
+
|
|
28
|
+
export { RUN_MEMORY_TAG, createMemoryPromoter } from './memory-promoter.js'
|
|
29
|
+
export type { MemoryPromoterOptions } from './memory-promoter.js'
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The default {@link PromoteMemory}: write what a run learned into a
|
|
3
|
+
* {@link MemoryStore}, or write nothing at all.
|
|
4
|
+
*
|
|
5
|
+
* `promoteMemory` is called once at settle with the compaction extractor's
|
|
6
|
+
* already-structured output — decisions, discoveries, user requirements,
|
|
7
|
+
* failures, environment facts — and **nothing shipped supplied the hook**.
|
|
8
|
+
* So the structure the compaction pass had spent tokens producing was
|
|
9
|
+
* serialized into one system message and dropped on the floor when the run
|
|
10
|
+
* ended, exactly as its own module comment says. This is the supplier, and
|
|
11
|
+
* it is mostly a filter: the hard part — extracting facts from a transcript
|
|
12
|
+
* — already happened.
|
|
13
|
+
*
|
|
14
|
+
* ## The filter, which is the only decision here
|
|
15
|
+
*
|
|
16
|
+
* **A run that learned nothing must leave nothing.** Not an empty record,
|
|
17
|
+
* not a record whose body says "no decisions" — nothing. A promoter that
|
|
18
|
+
* wrote a row per run would fill the store with the runs least worth
|
|
19
|
+
* remembering, and `search_memory` would then return them: the model reads
|
|
20
|
+
* that store on later runs, so noise here is not merely wasted disk, it is
|
|
21
|
+
* context spent on a run that did nothing.
|
|
22
|
+
*
|
|
23
|
+
* What counts as having learned something is the five KNOWLEDGE categories —
|
|
24
|
+
* decisions, discoveries, user requirements, failures, environment. Not
|
|
25
|
+
* `task`, which every run has because it is the prompt restated. Not
|
|
26
|
+
* `files`, which every run that opened anything has, and which says what was
|
|
27
|
+
* touched rather than what was learned. A run whose only trace is "it read
|
|
28
|
+
* six files" is the exact record this filter exists to refuse.
|
|
29
|
+
*
|
|
30
|
+
* ## What it does NOT do
|
|
31
|
+
*
|
|
32
|
+
* Deduplicate against what is already stored, merge with a previous run's
|
|
33
|
+
* record, or expire anything. Each is a policy with real trade-offs and a
|
|
34
|
+
* host that wants one owns it — `promoteMemory` is a callback precisely so
|
|
35
|
+
* that the runtime does not decide this. This is the obvious default, not
|
|
36
|
+
* the only possible one.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import type { MemoryStore } from '../types/memory/index.js'
|
|
40
|
+
import type { PromoteMemory, RunMemoryCandidate } from '../types/run/memory-promotion.js'
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The categories that make a run worth remembering.
|
|
44
|
+
*
|
|
45
|
+
* Ordered as they are rendered. `userRequirements` first because it is the
|
|
46
|
+
* most durable of the five — a constraint the user stated outlives the run
|
|
47
|
+
* that heard it, whereas a discovery about a codebase expires when the
|
|
48
|
+
* codebase moves.
|
|
49
|
+
*/
|
|
50
|
+
const KNOWLEDGE = [
|
|
51
|
+
['userRequirements', 'What the user requires'],
|
|
52
|
+
['decisions', 'Decisions'],
|
|
53
|
+
['discoveries', 'Discoveries'],
|
|
54
|
+
['failures', 'What did not work'],
|
|
55
|
+
['environment', 'Environment'],
|
|
56
|
+
] as const satisfies readonly (readonly [keyof RunMemoryCandidate, string])[]
|
|
57
|
+
|
|
58
|
+
/** Tag every record this promoter writes, so a host can find or prune them. */
|
|
59
|
+
export const RUN_MEMORY_TAG = 'run-memory'
|
|
60
|
+
|
|
61
|
+
export interface MemoryPromoterOptions {
|
|
62
|
+
/** Where records go. The same store `save_memory` writes through. */
|
|
63
|
+
readonly store: MemoryStore
|
|
64
|
+
/**
|
|
65
|
+
* Extra tags on every record, beyond {@link RUN_MEMORY_TAG}.
|
|
66
|
+
*
|
|
67
|
+
* A host running several agents against one store uses this to tell whose
|
|
68
|
+
* memory is whose; without it a later search cannot.
|
|
69
|
+
*/
|
|
70
|
+
readonly tags?: readonly string[]
|
|
71
|
+
/**
|
|
72
|
+
* Cap on entries rendered per category. Defaults to 20.
|
|
73
|
+
*
|
|
74
|
+
* The extractor already caps its lists, and this is the second cap for
|
|
75
|
+
* the same reason the first exists: a record nobody will read is a record
|
|
76
|
+
* that costs context every time it is retrieved.
|
|
77
|
+
*/
|
|
78
|
+
readonly maxPerCategory?: number
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Everything the candidate knows, as `[heading, items]`, empties dropped. */
|
|
82
|
+
function knowledge(
|
|
83
|
+
candidate: RunMemoryCandidate,
|
|
84
|
+
cap: number,
|
|
85
|
+
): readonly (readonly [string, readonly string[]])[] {
|
|
86
|
+
const out: (readonly [string, readonly string[]])[] = []
|
|
87
|
+
for (const [key, heading] of KNOWLEDGE) {
|
|
88
|
+
const items = candidate[key] as readonly string[]
|
|
89
|
+
if (items.length > 0) out.push([heading, items.slice(0, cap)])
|
|
90
|
+
}
|
|
91
|
+
return out
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A one-line summary naming what kind of knowledge the record holds. */
|
|
95
|
+
function summarize(sections: readonly (readonly [string, readonly string[]])[]): string {
|
|
96
|
+
return sections.map(([heading, items]) => `${heading.toLowerCase()} (${items.length})`).join(', ')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function render(
|
|
100
|
+
candidate: RunMemoryCandidate,
|
|
101
|
+
sections: readonly (readonly [string, readonly string[]])[],
|
|
102
|
+
): string {
|
|
103
|
+
const body = sections.map(
|
|
104
|
+
([heading, items]) => `## ${heading}\n\n${items.map((i) => `- ${i}`).join('\n')}`,
|
|
105
|
+
)
|
|
106
|
+
// The eviction counts, when there are any. Carried rather than hidden for
|
|
107
|
+
// the reason the candidate carries them: somebody reading this record
|
|
108
|
+
// should know they are reading a truncated account of the run, not a
|
|
109
|
+
// complete one.
|
|
110
|
+
const evicted = Object.entries(candidate.evicted).filter(([, n]) => n > 0)
|
|
111
|
+
if (evicted.length > 0) {
|
|
112
|
+
body.push(
|
|
113
|
+
`## Dropped during the run\n\n${evicted
|
|
114
|
+
.map(([category, n]) => `- ${category}: ${n} entr${n === 1 ? 'y' : 'ies'} evicted`)
|
|
115
|
+
.join('\n')}`,
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
if (candidate.files.length > 0) {
|
|
119
|
+
body.push(`## Files touched\n\n${candidate.files.map((f) => `- ${f}`).join('\n')}`)
|
|
120
|
+
}
|
|
121
|
+
return `# ${candidate.task}\n\n${body.join('\n\n')}\n`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Build a promoter that writes one record per run that learned something.
|
|
126
|
+
*
|
|
127
|
+
* Never throws out to the runtime — but it does not swallow either: the
|
|
128
|
+
* runtime already catches and logs a promoter's failure at settle, and
|
|
129
|
+
* catching here as well would hide a broken store from the one place that
|
|
130
|
+
* reports it.
|
|
131
|
+
*/
|
|
132
|
+
export function createMemoryPromoter(options: MemoryPromoterOptions): PromoteMemory {
|
|
133
|
+
const cap = options.maxPerCategory ?? 20
|
|
134
|
+
const tags = [RUN_MEMORY_TAG, ...(options.tags ?? [])]
|
|
135
|
+
|
|
136
|
+
return async (candidate: RunMemoryCandidate): Promise<void> => {
|
|
137
|
+
const sections = knowledge(candidate, cap)
|
|
138
|
+
// Nothing learned, nothing written. Not an empty record: a store full
|
|
139
|
+
// of rows describing runs that discovered nothing is a store whose
|
|
140
|
+
// search results are mostly noise, and the model reads that store.
|
|
141
|
+
if (sections.length === 0) return
|
|
142
|
+
|
|
143
|
+
await options.store.create({
|
|
144
|
+
title: candidate.task.trim() || `Run ${candidate.runId}`,
|
|
145
|
+
summary: summarize(sections),
|
|
146
|
+
content: render(candidate, sections),
|
|
147
|
+
tags,
|
|
148
|
+
format: 'markdown',
|
|
149
|
+
// The run id, so a record can be traced back to the run that formed
|
|
150
|
+
// it. Evidence rather than decoration: without it a surprising
|
|
151
|
+
// memory cannot be checked against what actually happened.
|
|
152
|
+
metadata: { runId: candidate.runId, source: RUN_MEMORY_TAG },
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A hash of everything a run could have changed in its working tree.
|
|
3
|
+
*
|
|
4
|
+
* It exists to answer one question, asked between two attempts at the same
|
|
5
|
+
* verification: **did anything happen since it last failed?** A verify-then-fix
|
|
6
|
+
* loop that re-runs the build after a turn which edited nothing spends a full
|
|
7
|
+
* command execution to learn what a comparison already knew, and does it once
|
|
8
|
+
* per remaining attempt — so a model that has stopped making progress burns
|
|
9
|
+
* the entire budget confirming the same failure.
|
|
10
|
+
*
|
|
11
|
+
* ## What is hashed, and why each part
|
|
12
|
+
*
|
|
13
|
+
* Three sources, because no one of them is complete:
|
|
14
|
+
*
|
|
15
|
+
* 1. **`git status --porcelain`** — which paths differ from the index at all.
|
|
16
|
+
* Cheap, and it catches additions, deletions and mode changes. On its own
|
|
17
|
+
* it is not enough: editing a tracked file that was ALREADY modified
|
|
18
|
+
* leaves the status output byte-identical.
|
|
19
|
+
* 2. **`git diff --binary HEAD`** — the content of every tracked change.
|
|
20
|
+
* `--binary` so an edit to a file git treats as binary is a real diff
|
|
21
|
+
* rather than the constant line `Binary files … differ`, which would make
|
|
22
|
+
* every edit to such a file invisible.
|
|
23
|
+
* 3. **Untracked file contents**, which no `git diff` covers. A new file is
|
|
24
|
+
* named by `status` but its CONTENT is not, so successive edits to a
|
|
25
|
+
* brand-new file would otherwise look like no change at all.
|
|
26
|
+
*
|
|
27
|
+
* ### Symlinks are recorded as their target, not read through
|
|
28
|
+
*
|
|
29
|
+
* Reading a link follows it, so a link repointed from one file to another
|
|
30
|
+
* with identical contents hashes the same — while the thing the workspace
|
|
31
|
+
* actually resolves has changed. The link's target path is the fact that
|
|
32
|
+
* moved, so that is what goes in.
|
|
33
|
+
*
|
|
34
|
+
* ## Failing open, on the cheap side
|
|
35
|
+
*
|
|
36
|
+
* Every uncertainty returns `null`, meaning *no fingerprint*, and a caller
|
|
37
|
+
* that cannot fingerprint re-runs its command. That is the correct direction:
|
|
38
|
+
* the cost of a wrong `null` is one command execution, and the cost of a
|
|
39
|
+
* wrong MATCH is a verification silently skipped — the loop would report
|
|
40
|
+
* "nothing changed" about a workspace that did change, and the model would be
|
|
41
|
+
* told to edit something it had already edited.
|
|
42
|
+
*
|
|
43
|
+
* So: a non-zero exit from any git invocation, a repository with no commits,
|
|
44
|
+
* a timeout, or output past the size cap all produce `null` rather than a
|
|
45
|
+
* partial hash. A truncated diff that hashed successfully would be the worst
|
|
46
|
+
* outcome available here, because two different workspaces truncated at the
|
|
47
|
+
* same point collide.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
import { createHash } from 'node:crypto'
|
|
51
|
+
import { lstat, readFile, readlink } from 'node:fs/promises'
|
|
52
|
+
import { join } from 'node:path'
|
|
53
|
+
|
|
54
|
+
import type { CommandOptions, CommandResult } from '../types/execution/index.js'
|
|
55
|
+
|
|
56
|
+
/** How a fingerprint runs git. Injected so a test needs no repository. */
|
|
57
|
+
export type FingerprintExec = (
|
|
58
|
+
command: string,
|
|
59
|
+
args: string[],
|
|
60
|
+
options?: CommandOptions,
|
|
61
|
+
) => Promise<CommandResult>
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The three filesystem reads an untracked entry needs.
|
|
65
|
+
*
|
|
66
|
+
* Injectable for one specific reason, written down because a seam that
|
|
67
|
+
* exists only for tests is usually a smell: **creating a symlink requires a
|
|
68
|
+
* privilege that is not granted by default on Windows**, so the symlink rule
|
|
69
|
+
* below — the one that says a repointed link changes the fingerprint even
|
|
70
|
+
* when the bytes behind it do not — cannot be exercised on a developer
|
|
71
|
+
* machine without it. A rule that can only be checked on some machines is a
|
|
72
|
+
* rule nobody checks.
|
|
73
|
+
*
|
|
74
|
+
* The default is `node:fs/promises` and every other test uses it against a
|
|
75
|
+
* real repository, so this is not a fixture standing in for production; it is
|
|
76
|
+
* one branch of one function reached without a privilege.
|
|
77
|
+
*/
|
|
78
|
+
export interface FingerprintFs {
|
|
79
|
+
lstat(path: string): Promise<{ isSymbolicLink(): boolean; isFile(): boolean }>
|
|
80
|
+
readlink(path: string): Promise<string>
|
|
81
|
+
readFile(path: string): Promise<Buffer>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const NODE_FS: FingerprintFs = { lstat, readlink, readFile }
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Cap on the bytes any single git invocation may produce.
|
|
88
|
+
*
|
|
89
|
+
* Past it the fingerprint is abandoned rather than hashed. A diff big enough
|
|
90
|
+
* to hit this is a diff nobody is going to iterate on anyway, and hashing a
|
|
91
|
+
* clipped one would let two different trees agree.
|
|
92
|
+
*/
|
|
93
|
+
export const FINGERPRINT_MAX_BYTES = 4 * 1024 * 1024
|
|
94
|
+
|
|
95
|
+
/** Default deadline per git invocation. */
|
|
96
|
+
export const FINGERPRINT_TIMEOUT_MS = 20_000
|
|
97
|
+
|
|
98
|
+
export interface WorkspaceFingerprintOptions {
|
|
99
|
+
/** Repository root, or any directory inside it. */
|
|
100
|
+
readonly cwd: string
|
|
101
|
+
/** How to run git. */
|
|
102
|
+
readonly exec: FingerprintExec
|
|
103
|
+
/** Per-invocation deadline. See {@link FINGERPRINT_TIMEOUT_MS}. */
|
|
104
|
+
readonly timeoutMs?: number
|
|
105
|
+
/** See {@link FINGERPRINT_MAX_BYTES}. */
|
|
106
|
+
readonly maxBytes?: number
|
|
107
|
+
/** Filesystem reads. See {@link FingerprintFs}. */
|
|
108
|
+
readonly fs?: FingerprintFs
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** One untracked path's contribution, or `null` when it could not be read. */
|
|
112
|
+
async function untrackedEntry(cwd: string, rel: string, fs: FingerprintFs): Promise<string | null> {
|
|
113
|
+
const abs = join(cwd, rel)
|
|
114
|
+
try {
|
|
115
|
+
const stats = await fs.lstat(abs)
|
|
116
|
+
if (stats.isSymbolicLink()) {
|
|
117
|
+
// The TARGET, not what is behind it. Following the link would hash a
|
|
118
|
+
// repointed link to the same value whenever the new target happens
|
|
119
|
+
// to hold the same bytes, and a repoint is a change to the workspace
|
|
120
|
+
// by any reading that matters.
|
|
121
|
+
return `L ${rel}\0${await fs.readlink(abs)}`
|
|
122
|
+
}
|
|
123
|
+
if (!stats.isFile()) return `? ${rel}`
|
|
124
|
+
const body = await fs.readFile(abs)
|
|
125
|
+
return `F ${rel}\0${createHash('sha256').update(body).digest('hex')}`
|
|
126
|
+
} catch {
|
|
127
|
+
// Vanished between the listing and the read, or unreadable. Neither is
|
|
128
|
+
// a fingerprint this function may guess at.
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A hash of the working tree's uncommitted state, or `null` when it cannot be
|
|
135
|
+
* established.
|
|
136
|
+
*
|
|
137
|
+
* **`null` is never "unchanged".** It means "I cannot tell", and the caller
|
|
138
|
+
* must treat it as a reason to do the work rather than to skip it.
|
|
139
|
+
*/
|
|
140
|
+
export async function fingerprintWorkspace(
|
|
141
|
+
options: WorkspaceFingerprintOptions,
|
|
142
|
+
): Promise<string | null> {
|
|
143
|
+
const { cwd, exec } = options
|
|
144
|
+
const timeoutMs = options.timeoutMs ?? FINGERPRINT_TIMEOUT_MS
|
|
145
|
+
const maxBytes = options.maxBytes ?? FINGERPRINT_MAX_BYTES
|
|
146
|
+
const fs = options.fs ?? NODE_FS
|
|
147
|
+
|
|
148
|
+
const git = async (args: string[]): Promise<string | null> => {
|
|
149
|
+
let result: CommandResult
|
|
150
|
+
try {
|
|
151
|
+
result = await exec('git', args, { cwd, timeoutMs })
|
|
152
|
+
} catch {
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
// A timeout surfaces here as a non-zero exit, and so does "not a
|
|
156
|
+
// repository" and "no commits yet". All three mean the same thing to
|
|
157
|
+
// this function: it has no basis for a comparison.
|
|
158
|
+
if (result.exitCode !== 0) return null
|
|
159
|
+
if (Buffer.byteLength(result.stdout, 'utf8') > maxBytes) return null
|
|
160
|
+
return result.stdout
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const status = await git(['status', '--porcelain'])
|
|
164
|
+
if (status === null) return null
|
|
165
|
+
|
|
166
|
+
const diff = await git(['diff', '--binary', 'HEAD'])
|
|
167
|
+
if (diff === null) return null
|
|
168
|
+
|
|
169
|
+
const untracked = await git(['ls-files', '--others', '--exclude-standard', '-z'])
|
|
170
|
+
if (untracked === null) return null
|
|
171
|
+
|
|
172
|
+
const parts = [`status ${status}`, `diff ${diff}`]
|
|
173
|
+
// Split on NUL, which is what `-z` is for: a path may contain a newline,
|
|
174
|
+
// and splitting on one would turn a single strange filename into two
|
|
175
|
+
// ordinary-looking ones.
|
|
176
|
+
//
|
|
177
|
+
// Sorted, because `ls-files` order is not part of any contract and a
|
|
178
|
+
// fingerprint that moved when the listing order did would report a change
|
|
179
|
+
// nobody made.
|
|
180
|
+
for (const rel of untracked.split('\0').filter(Boolean).sort()) {
|
|
181
|
+
const entry = await untrackedEntry(cwd, rel, fs)
|
|
182
|
+
if (entry === null) return null
|
|
183
|
+
parts.push(entry)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Length-prefixed rather than delimiter-joined. A diff can contain any
|
|
187
|
+
// byte, so any separator is a separator the content can forge — and two
|
|
188
|
+
// different trees that agreed after forgery would be reported as
|
|
189
|
+
// unchanged, which is the one wrong answer this file is arranged to avoid.
|
|
190
|
+
const hash = createHash('sha256')
|
|
191
|
+
for (const part of parts) hash.update(`${Buffer.byteLength(part, 'utf8')}:${part}`)
|
|
192
|
+
return hash.digest('hex')
|
|
193
|
+
}
|