tickmarkr 1.93.0 → 1.97.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/dist/adapters/model-lints.d.ts +1 -1
- package/dist/adapters/model-lints.js +116 -52
- package/dist/brand.d.ts +3 -0
- package/dist/brand.js +3 -1
- package/dist/cli/commands/beat.d.ts +1 -0
- package/dist/cli/commands/beat.js +50 -0
- package/dist/cli/commands/status.js +325 -127
- package/dist/cli/commands/verify.js +52 -26
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +3 -1
- package/dist/compile/gsd.js +34 -1
- package/dist/compile/native.js +26 -1
- package/dist/drivers/herdr.d.ts +26 -1
- package/dist/drivers/herdr.js +77 -29
- package/dist/gates/acceptance.js +26 -4
- package/dist/gates/baseline.d.ts +10 -1
- package/dist/gates/baseline.js +35 -6
- package/dist/run/daemon.d.ts +5 -0
- package/dist/run/daemon.js +25 -4
- package/dist/run/merge.d.ts +6 -1
- package/dist/run/merge.js +46 -11
- package/fixtures/gsd-sample/07-live-check/07-01-PLAN.md +1 -1
- package/fixtures/gsd-sample/PROJECT.md +19 -0
- package/fixtures/payload-shape/p99-shaped.spec.md +68 -0
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +42 -18
- package/skills/tickmarkr-overseer/scripts/seat-send.sh +98 -5
|
@@ -57,7 +57,7 @@ export declare function catalogModelAdvisory(cfg: TickmarkrConfig, catalog: Cata
|
|
|
57
57
|
*/
|
|
58
58
|
export declare function hasWindowsConfig(cfg: TickmarkrConfig): boolean;
|
|
59
59
|
export declare function declaredModelWindow(cfg: TickmarkrConfig, adapter: string, model: string): number | undefined;
|
|
60
|
-
/** A numeric estimate exists only when every
|
|
60
|
+
/** A numeric estimate exists only when every context ref is measurable from the worker-visible tree. */
|
|
61
61
|
export declare function estimateTaskPayloadTokens(task: Task, repoRoot: string, feedback?: string): number | undefined;
|
|
62
62
|
export type RoutedAssignment = {
|
|
63
63
|
taskId: string;
|
|
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { DEFAULT_CONFIG, TIER_RANK } from "../config/config.js";
|
|
5
|
+
import { filesGlob } from "../graph/files-glob.js";
|
|
5
6
|
import { buildTaskPrompt } from "./prompt.js";
|
|
6
7
|
import { channelKey, MODEL_ID_RE } from "./types.js";
|
|
7
8
|
import { resolveCatalogModel } from "./catalog-remote.js";
|
|
@@ -194,80 +195,136 @@ export function hasWindowsConfig(cfg) {
|
|
|
194
195
|
export function declaredModelWindow(cfg, adapter, model) {
|
|
195
196
|
return cfg.tiers[adapter]?.windows?.[model];
|
|
196
197
|
}
|
|
198
|
+
/** Named paths per lint line: an uncapped enumeration reached 4,924 columns on the P99 graph. */
|
|
199
|
+
const MAX_NAMED_PATHS = 3;
|
|
197
200
|
const GLOB_CHARS = /[*?{[]/;
|
|
198
201
|
// T13's visibility oracle is the committed base tree: workers are created from HEAD, not from the
|
|
199
202
|
// author's checkout or index. Keep --full-tree load-bearing for callers below the repository root.
|
|
203
|
+
// `-l` prices every blob in the SAME call (a per-path `cat-file -s` spawn cannot price a pattern),
|
|
204
|
+
// and `-z` is load-bearing correctness, not economy: without it git quotes any non-ASCII path, so
|
|
205
|
+
// every Arabic/UTF-8 tracked file read as absent.
|
|
200
206
|
function baseTreeAtHead(dir) {
|
|
201
207
|
const git = (...args) => spawnSync("git", ["-C", dir, ...args], { encoding: "utf8", maxBuffer: 1 << 28 });
|
|
202
208
|
const top = git("rev-parse", "--show-toplevel");
|
|
203
|
-
const tree = git("ls-tree", "--full-tree", "-r", "
|
|
209
|
+
const tree = git("ls-tree", "--full-tree", "-r", "-l", "-z", "HEAD");
|
|
204
210
|
if (top.status !== 0 || tree.status !== 0 || typeof top.stdout !== "string" || typeof tree.stdout !== "string")
|
|
205
211
|
return undefined;
|
|
206
|
-
|
|
212
|
+
const sizes = new Map();
|
|
213
|
+
for (const record of tree.stdout.split("\0")) {
|
|
214
|
+
const tab = record.indexOf("\t");
|
|
215
|
+
if (tab < 0)
|
|
216
|
+
continue;
|
|
217
|
+
// `<mode> <type> <object> <size>\t<path>`; a submodule entry carries `-` as its size and no bytes.
|
|
218
|
+
const bytes = Number.parseInt(record.slice(0, tab).split(/\s+/)[3] ?? "", 10);
|
|
219
|
+
sizes.set(record.slice(tab + 1), Number.isSafeInteger(bytes) && bytes >= 0 ? bytes : 0);
|
|
220
|
+
}
|
|
221
|
+
return { root: top.stdout.trim(), sizes };
|
|
207
222
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Bytes a worker can read at that path in the base tree, or undefined when the tree carries nothing
|
|
225
|
+
* there. A files[]-shaped pattern and a directory both price as everything they cover — matching
|
|
226
|
+
* through src/graph/files-glob.ts so the estimate reads patterns exactly as the scope gate does.
|
|
227
|
+
*/
|
|
228
|
+
function measureBytes(tree, rel) {
|
|
213
229
|
const path = rel.replace(/\/+$/, "");
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
230
|
+
const exact = tree.sizes.get(path);
|
|
231
|
+
if (exact !== undefined)
|
|
232
|
+
return exact;
|
|
233
|
+
const match = GLOB_CHARS.test(path) ? filesGlob(path) : undefined;
|
|
234
|
+
let total = 0;
|
|
235
|
+
let covered = false;
|
|
236
|
+
for (const [tracked, bytes] of tree.sizes) {
|
|
237
|
+
if (!(match ? match(tracked) : tracked.startsWith(`${path}/`)))
|
|
238
|
+
continue;
|
|
239
|
+
total += bytes;
|
|
240
|
+
covered = true;
|
|
219
241
|
}
|
|
220
|
-
|
|
221
|
-
const size = spawnSync("git", ["-C", tree.root, "cat-file", "-s", `HEAD:${path}`], { encoding: "utf8" });
|
|
222
|
-
const bytes = Number.parseInt(typeof size.stdout === "string" ? size.stdout.trim() : "", 10);
|
|
223
|
-
return size.status === 0 && Number.isSafeInteger(bytes) && bytes >= 0
|
|
224
|
-
? { status: "measured", bytes }
|
|
225
|
-
: { status: "unreadable", reason: "base-tree-read-failed" };
|
|
242
|
+
return covered ? total : undefined;
|
|
226
243
|
}
|
|
227
|
-
function
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
244
|
+
function absentContextReason(root, path) {
|
|
245
|
+
if (spawnSync("git", ["-C", root, "check-ignore", "-q", "--", path]).status === 0)
|
|
246
|
+
return "gitignored";
|
|
247
|
+
return existsSync(join(root, path)) ? "untracked" : "absent";
|
|
248
|
+
}
|
|
249
|
+
function upstreamProducer(tasks) {
|
|
250
|
+
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
251
|
+
const cache = new Map();
|
|
252
|
+
const upstream = (taskId) => {
|
|
253
|
+
let entry = cache.get(taskId);
|
|
254
|
+
if (!entry) {
|
|
255
|
+
const seen = new Set();
|
|
256
|
+
const walk = (from) => {
|
|
257
|
+
for (const dep of byId.get(from)?.deps ?? [])
|
|
258
|
+
if (!seen.has(dep)) {
|
|
259
|
+
seen.add(dep);
|
|
260
|
+
walk(dep);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
walk(taskId);
|
|
264
|
+
entry = [...seen].map((id) => {
|
|
265
|
+
const files = byId.get(id)?.files ?? [];
|
|
266
|
+
return { id, covers: files.length ? filesGlob(files) : () => false };
|
|
267
|
+
});
|
|
268
|
+
cache.set(taskId, entry);
|
|
269
|
+
}
|
|
270
|
+
return entry;
|
|
237
271
|
};
|
|
238
|
-
|
|
239
|
-
|
|
272
|
+
return (taskId, path) => upstream(taskId).find((dep) => dep.covers(path))?.id;
|
|
273
|
+
}
|
|
274
|
+
function taskPayloadResolution(task, repoRoot, feedback, tree = baseTreeAtHead(repoRoot), producerOf = () => undefined) {
|
|
275
|
+
let bytes = buildTaskPrompt(task, feedback).length;
|
|
276
|
+
if (!tree) {
|
|
277
|
+
return { tokens: Math.ceil(bytes / CHARS_PER_TOKEN), unmeasurable: [{ path: ".", reason: "base-tree-unavailable" }] };
|
|
278
|
+
}
|
|
279
|
+
// files[] is WRITE SCOPE, not payload: a task's own output (`99-04-SUMMARY.md`) is absent from the
|
|
280
|
+
// base tree BY CONSTRUCTION and a scope entry is a set, not a document. Price what the worker can
|
|
281
|
+
// already read there and charge nothing for the rest. Billing an output path as a measurement
|
|
282
|
+
// failure is what made the window comparison unreachable on every real graph — 41 of 41 tasks on
|
|
283
|
+
// the P99 graph reported "payload unreadable" and none was ever compared to its model's window.
|
|
240
284
|
for (const path of task.files)
|
|
241
|
-
|
|
242
|
-
|
|
285
|
+
bytes += measureBytes(tree, path) ?? 0;
|
|
286
|
+
const unmeasurable = [];
|
|
287
|
+
for (const path of task.context) {
|
|
288
|
+
const measured = measureBytes(tree, path);
|
|
289
|
+
if (measured !== undefined) {
|
|
290
|
+
bytes += measured;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const producer = producerOf(task.id, path);
|
|
294
|
+
unmeasurable.push(producer
|
|
295
|
+
? { path, reason: "produced-upstream", producer }
|
|
296
|
+
: { path, reason: absentContextReason(tree.root, path) });
|
|
297
|
+
}
|
|
298
|
+
return { tokens: Math.ceil(bytes / CHARS_PER_TOKEN), unmeasurable };
|
|
243
299
|
}
|
|
244
|
-
/** A numeric estimate exists only when every
|
|
300
|
+
/** A numeric estimate exists only when every context ref is measurable from the worker-visible tree. */
|
|
245
301
|
export function estimateTaskPayloadTokens(task, repoRoot, feedback = "") {
|
|
246
302
|
const estimate = taskPayloadResolution(task, repoRoot, feedback);
|
|
247
|
-
return estimate.
|
|
303
|
+
return estimate.unmeasurable.length === 0 ? estimate.tokens : undefined;
|
|
248
304
|
}
|
|
249
|
-
function
|
|
250
|
-
const describe = ({
|
|
251
|
-
const prefix = `${field} ${JSON.stringify(path)}`;
|
|
252
|
-
if (reason === "untracked")
|
|
253
|
-
return `${prefix} is present in the checkout but not in the base tree`;
|
|
254
|
-
if (reason === "absent")
|
|
255
|
-
return `${prefix} is absent from the base tree and checkout`;
|
|
256
|
-
if (reason === "glob")
|
|
257
|
-
return `${prefix} is a glob and cannot be measured`;
|
|
258
|
-
if (reason === "directory")
|
|
259
|
-
return `${prefix} is a directory and cannot be measured as one file`;
|
|
305
|
+
function unreachableContextLint(taskId, dangling) {
|
|
306
|
+
const describe = ({ path, reason }) => {
|
|
260
307
|
if (reason === "base-tree-unavailable")
|
|
261
|
-
return
|
|
262
|
-
|
|
308
|
+
return "the base tree is unavailable, so no context ref can be checked";
|
|
309
|
+
const named = JSON.stringify(path);
|
|
310
|
+
if (reason === "gitignored")
|
|
311
|
+
return `${named} is gitignored — no commit can carry it into a worktree`;
|
|
312
|
+
if (reason === "untracked")
|
|
313
|
+
return `${named} is present in the checkout but not in the base tree`;
|
|
314
|
+
return `${named} is absent from the base tree and no task in this graph produces it`;
|
|
263
315
|
};
|
|
264
|
-
|
|
316
|
+
const rest = dangling.length - MAX_NAMED_PATHS;
|
|
317
|
+
return `${taskId}: context unreachable — ${dangling.slice(0, MAX_NAMED_PATHS).map(describe).join(", ")}`
|
|
318
|
+
+ `${rest > 0 ? `, +${rest} more` : ""}; the worker's "read these first" list dangles`
|
|
319
|
+
+ " and the context-window comparison is skipped";
|
|
265
320
|
}
|
|
266
321
|
/** Advisory only — absent windows config or undeclared model window ⇒ no lint. */
|
|
267
322
|
export function contextWindowLints(tasks, assignments, cfg, repoRoot) {
|
|
268
323
|
if (!hasAnyWindows(cfg))
|
|
269
324
|
return [];
|
|
270
325
|
const byId = new Map(assignments.map((a) => [a.taskId, a]));
|
|
326
|
+
const tree = baseTreeAtHead(repoRoot);
|
|
327
|
+
const producerOf = upstreamProducer(tasks);
|
|
271
328
|
const lints = [];
|
|
272
329
|
for (const t of tasks) {
|
|
273
330
|
const a = byId.get(t.id);
|
|
@@ -276,13 +333,20 @@ export function contextWindowLints(tasks, assignments, cfg, repoRoot) {
|
|
|
276
333
|
const window = declaredModelWindow(cfg, a.adapter, a.model);
|
|
277
334
|
if (window === undefined)
|
|
278
335
|
continue;
|
|
279
|
-
const estimate = taskPayloadResolution(t, repoRoot, "");
|
|
280
|
-
|
|
281
|
-
|
|
336
|
+
const estimate = taskPayloadResolution(t, repoRoot, "", tree, producerOf);
|
|
337
|
+
const dangling = estimate.unmeasurable.filter((u) => u.reason !== "produced-upstream");
|
|
338
|
+
if (dangling.length > 0) {
|
|
339
|
+
lints.push(unreachableContextLint(t.id, dangling));
|
|
282
340
|
continue;
|
|
283
341
|
}
|
|
342
|
+
// An upstream-produced ref is unmeasurable but REACHABLE — the run writes it before this task
|
|
343
|
+
// branches — so it earns no lint of its own: silence on the benign class is what leaves a real
|
|
344
|
+
// dangling ref visible. The estimate is then a lower bound, and a lower bound over the window is
|
|
345
|
+
// still an overflow, so it is reported with what it excludes named.
|
|
284
346
|
if (estimate.tokens > window) {
|
|
285
|
-
|
|
347
|
+
const pending = estimate.unmeasurable.length;
|
|
348
|
+
lints.push(`${t.id}: payload ~${estimate.tokens} tokens exceeds ${a.adapter}:${a.model} window ${window}`
|
|
349
|
+
+ (pending > 0 ? ` (lower bound — ${pending} context ref(s) are produced upstream and not yet measurable)` : ""));
|
|
286
350
|
}
|
|
287
351
|
}
|
|
288
352
|
return lints;
|
package/dist/brand.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export declare function paneDispatchCommand(scriptPath: string): string;
|
|
|
26
26
|
export declare const BRAND_RAMP: readonly [84, 78, 41, 35];
|
|
27
27
|
/** Brand green (ramp anchor 41) — the tickmark hue; also the ok/pass/authed verdict color. */
|
|
28
28
|
export declare const brand: (s: string) => string;
|
|
29
|
+
/** Compact product chip — black ink on the terminal theme's ANSI green. */
|
|
30
|
+
export declare const brandChip: (s: string) => string;
|
|
29
31
|
/** Ok verdicts (pass/authed/green) render in the brand green ramp — same hue as the tickmark. */
|
|
30
32
|
export declare const ok: (s: string) => string;
|
|
31
33
|
/** Fail verdicts (unauthed/red) — red, always paired with the ✗ shape. */
|
|
@@ -39,6 +41,7 @@ export declare const bold: (s: string) => string;
|
|
|
39
41
|
/** Every semantic color token, for sweeps: each is TTY-gated and NO_COLOR-aware. */
|
|
40
42
|
export declare const TOKENS: {
|
|
41
43
|
readonly brand: (s: string) => string;
|
|
44
|
+
readonly brandChip: (s: string) => string;
|
|
42
45
|
readonly ok: (s: string) => string;
|
|
43
46
|
readonly fail: (s: string) => string;
|
|
44
47
|
readonly warn: (s: string) => string;
|
package/dist/brand.js
CHANGED
|
@@ -108,6 +108,8 @@ const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === u
|
|
|
108
108
|
const sgr = (code) => (s) => visual() ? `\x1b[${code}m${s}${R}` : s;
|
|
109
109
|
/** Brand green (ramp anchor 41) — the tickmark hue; also the ok/pass/authed verdict color. */
|
|
110
110
|
export const brand = sgr(`38;5;${BRAND_RAMP[2]}`);
|
|
111
|
+
/** Compact product chip — black ink on the terminal theme's ANSI green. */
|
|
112
|
+
export const brandChip = sgr("30;42");
|
|
111
113
|
/** Ok verdicts (pass/authed/green) render in the brand green ramp — same hue as the tickmark. */
|
|
112
114
|
export const ok = brand;
|
|
113
115
|
/** Fail verdicts (unauthed/red) — red, always paired with the ✗ shape. */
|
|
@@ -119,7 +121,7 @@ export const dim = sgr("2");
|
|
|
119
121
|
/** Emphasis (titles, selection, the product name) — bold. */
|
|
120
122
|
export const bold = sgr("1");
|
|
121
123
|
/** Every semantic color token, for sweeps: each is TTY-gated and NO_COLOR-aware. */
|
|
122
|
-
export const TOKENS = { brand, ok, fail, warn, dim, bold };
|
|
124
|
+
export const TOKENS = { brand, brandChip, ok, fail, warn, dim, bold };
|
|
123
125
|
/**
|
|
124
126
|
* The glyph vocabulary — plain characters only; color layers on via tokens so
|
|
125
127
|
* shape survives NO_COLOR. Bracket toggles ([x]/[ ]) are forbidden on every surface.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function beat(argv: string[], cwd?: string): Promise<string>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { tickmarkrDir } from "../../graph/graph.js";
|
|
4
|
+
import { SUPERVISION_BEAT_MS, SUPERVISION_STALE_MS, SUPERVISION_TIERS, beatSupervision, supervisionStandDownPath, } from "../../run/supervision.js";
|
|
5
|
+
// SUP-04: the writer side of supervision, as a VERB. `beatSupervision` and `SUPERVISION_BEAT_MS` shipped
|
|
6
|
+
// with exactly one in-repo caller — the daemon, on one tier — so `status` printed
|
|
7
|
+
// `orchestrator ARMED / overseer ABSENT / watch ABSENT` while a real overseer worked the run: two thirds
|
|
8
|
+
// of a supervision claim were constants dressed as measurements. A seat that supervises from a shell
|
|
9
|
+
// (the overseer skill's watcher loop) has no way into a node module; a command does.
|
|
10
|
+
//
|
|
11
|
+
// ONE-SHOT AND ERROR-PROPAGATING, deliberately not `armSupervision`. That helper is the right shape for
|
|
12
|
+
// a long-lived node process and the wrong shape here twice over. Its recurring interval would outlive
|
|
13
|
+
// the invocation — in a long-lived dispatcher a later `--stand-down` would disarm only its own timer
|
|
14
|
+
// while an earlier one kept writing, flipping DISARMED back to ARMED ten seconds later: the exact
|
|
15
|
+
// fail-open this instrument exists to catch. And it swallows write failures by design (an unwritten
|
|
16
|
+
// beat ages out, which is the truth for a watcher that must not be crashed by its own instrument) —
|
|
17
|
+
// but a CLI that swallows them PRINTS a state nothing recorded, so `beat` says ARMED while `status`
|
|
18
|
+
// reads UNREADABLE. Here the writes are unguarded: a failure leaves the process with a non-zero exit
|
|
19
|
+
// and a message instead of a claim. The LOOP belongs to the caller, which is what makes the beat
|
|
20
|
+
// evidence: stop calling it and the tier ages into STALE on its own.
|
|
21
|
+
export async function beat(argv, cwd = process.cwd()) {
|
|
22
|
+
const standDown = argv.includes("--stand-down");
|
|
23
|
+
const named = argv.find((a) => !a.startsWith("--"));
|
|
24
|
+
if (!isTier(named)) {
|
|
25
|
+
throw new Error(`usage: tickmarkr beat <${SUPERVISION_TIERS.join("|")}> [--stand-down] — got ${named ? `\`${named}\`` : "no tier"}`);
|
|
26
|
+
}
|
|
27
|
+
if (standDown)
|
|
28
|
+
return standDownTier(cwd, named);
|
|
29
|
+
// Clear a stand-down marker left by an earlier session BEFORE beating: a valid marker outranks every
|
|
30
|
+
// beat that does not strictly follow it, and two writes landing in the same millisecond do not. An
|
|
31
|
+
// uncleared marker would render DISARMED while this verb claimed ARMED, so the removal is unguarded
|
|
32
|
+
// too — `force` makes the ordinary "no marker" case a no-op, and anything else is a real failure.
|
|
33
|
+
rmSync(supervisionStandDownPath(cwd, named), { force: true, recursive: true });
|
|
34
|
+
beatSupervision(cwd, named);
|
|
35
|
+
return `${named} ARMED — beat again every ${SUPERVISION_BEAT_MS / 1_000}s; the tier reads STALE ${SUPERVISION_STALE_MS / 1_000}s after the last beat`;
|
|
36
|
+
}
|
|
37
|
+
// Stand-down is a RECORDED act, not a silence: the marker tells a reader this watcher left on purpose,
|
|
38
|
+
// so the tier reads DISARMED rather than ageing out as a death. Published atomically — written aside,
|
|
39
|
+
// renamed over — because a torn marker is rejected by the reader, and a rejected stand-down reports a
|
|
40
|
+
// deliberate hand-off as a death.
|
|
41
|
+
function standDownTier(repoRoot, tier) {
|
|
42
|
+
tickmarkrDir(repoRoot); // the write path DOES create — markers land inside the gitignored state dir
|
|
43
|
+
const p = supervisionStandDownPath(repoRoot, tier);
|
|
44
|
+
const tmp = `${p}.${process.pid}.tmp`;
|
|
45
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
46
|
+
writeFileSync(tmp, JSON.stringify({ tier, pid: process.pid, disarmedAt: new Date().toISOString() }) + "\n");
|
|
47
|
+
renameSync(tmp, p);
|
|
48
|
+
return `${tier} DISARMED — hand-off recorded; status reads it stood down, not dead`;
|
|
49
|
+
}
|
|
50
|
+
const isTier = (v) => SUPERVISION_TIERS.includes(v ?? "");
|