@deksden-com/dd-flow-cli 0.8.0 → 0.9.0-beta.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 +16 -0
- package/README.md +30 -3
- package/dist/build-info.json +4 -4
- package/dist/cli/help.js +13 -61
- package/dist/cli/run-cli.js +55 -24
- package/dist/domain/stage-catalog.js +2 -2
- package/dist/schemas/agent-profile.schema.json +17 -0
- package/dist/schemas/code-review-decision.schema.json +5 -3
- package/dist/schemas/merge-result.schema.json +15 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +1 -1
- package/dist/services/cli-operation-classifier.js +1 -1
- package/dist/services/code-checks.js +125 -208
- package/dist/services/eval-snapshots.js +10 -1
- package/dist/services/harness-adapter.js +59 -0
- package/dist/services/hooks.js +87 -237
- package/dist/services/ids.js +18 -1
- package/dist/services/lifecycle-command.js +288 -0
- package/dist/services/merge-server.js +124 -0
- package/dist/services/runs.js +7 -2
- package/dist/services/sessions.js +18 -27
- package/dist/services/stage-pause.js +13 -0
- package/dist/services/vnext-code-review.js +70 -16
- package/dist/services/vnext-code.js +45 -18
- package/dist/services/vnext-execution-profile.js +6 -3
- package/dist/services/vnext-merge.js +330 -0
- package/dist/services/vnext-plan.js +13 -5
- package/dist/services/vnext-specify.js +5 -2
- package/dist/services/vnext-workspace-policy.js +8 -2
- package/dist/services/work-registry.js +35 -7
- package/dist/storage/database.js +66 -0
- package/package.json +2 -1
|
@@ -6,11 +6,22 @@ import { findRecentMatchingHookEvent, claimStageStartHookEvent, claimWorkStartHo
|
|
|
6
6
|
import { validateSchema } from "./schema-validation.js";
|
|
7
7
|
import { resolveProjectRoot, resolveRunReferences, writeJsonAtomic } from "../storage/paths.js";
|
|
8
8
|
import { refreshRunSessionProjection } from "./run-projection.js";
|
|
9
|
-
import { runCodeChecks } from "./code-checks.js";
|
|
10
|
-
import { nextWorkId } from "./ids.js";
|
|
9
|
+
import { readCodeCheckProfile, runCodeChecks } from "./code-checks.js";
|
|
10
|
+
import { nextWorkId, nextWorkIds } from "./ids.js";
|
|
11
11
|
import { appendFlowRunTimelineEvent } from "./runs.js";
|
|
12
|
+
import { flowCommand } from "./stage-pause.js";
|
|
12
13
|
const workColumns = "work_id, project_id, run_id, parent_work_id, task, launch_policy, result_schema, payload_json, depends_on_json, status, result, created_at, started_at, updated_at, completed_at";
|
|
13
14
|
export function ensureWorkRegistry(context) { context.db.exec("SELECT 1 FROM works LIMIT 1"); context.db.exec("SELECT 1 FROM work_sessions LIMIT 1"); }
|
|
15
|
+
export function createChildWork(context, input) {
|
|
16
|
+
const parent = requireWork(context, input.parentWorkId);
|
|
17
|
+
if (parent.status !== "running")
|
|
18
|
+
throw new AppError("invalid_work_state", "Child Work requires a running parent", 2, { parent_work_id: parent.work_id, status: parent.status });
|
|
19
|
+
const id = nextWorkId(context, parent.project_id, input.slug);
|
|
20
|
+
const now = context.now();
|
|
21
|
+
context.db.run(`INSERT INTO works (${workColumns}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, '[]', 'created', NULL, ?, NULL, ?, NULL)`, [id, parent.project_id, parent.run_id, parent.work_id, input.task, input.launchPolicy, input.resultSchema ?? null, input.payload ? JSON.stringify(input.payload) : null, now, now]);
|
|
22
|
+
refreshRunWorkProjection(context, parent.project_id, parent.run_id);
|
|
23
|
+
return requireWork(context, id);
|
|
24
|
+
}
|
|
14
25
|
/** Validate a proposed batch before PLAN accepts it, without registering Work. */
|
|
15
26
|
export function validateWorkBatchFile(file) {
|
|
16
27
|
const parsed = readJson(file);
|
|
@@ -54,7 +65,8 @@ export function addWorkBatch(context, input) {
|
|
|
54
65
|
const now = context.now();
|
|
55
66
|
context.db.exec("BEGIN IMMEDIATE");
|
|
56
67
|
try {
|
|
57
|
-
const
|
|
68
|
+
const allocated = nextWorkIds(context, parent.project_id, items.map((item) => item.key));
|
|
69
|
+
const ids = new Map(items.map((item, index) => [item.key, allocated[index]]));
|
|
58
70
|
const resolve = (value) => ids.get(value) ?? value;
|
|
59
71
|
const proposed = items.map((item) => ({ ...item, id: ids.get(item.key), parentId: item.parent ? resolve(item.parent) : parent.work_id, dependencies: (item.depends_on ?? []).map(resolve) }));
|
|
60
72
|
assertNoCycles(proposed.map((item) => ({ id: item.id, dependencies: item.dependencies })));
|
|
@@ -128,7 +140,13 @@ export function mutateWorkDeps(context, input) {
|
|
|
128
140
|
export function deleteWork(context, id) { const work = requireWork(context, id); const canonical = work.work_id; if (work.status !== "created" || work.started_at || context.db.get("SELECT 1 FROM works WHERE parent_work_id = ? LIMIT 1", [canonical]) || context.db.get("SELECT 1 FROM works WHERE depends_on_json LIKE ? LIMIT 1", [`%${canonical}%`]))
|
|
129
141
|
throw new AppError("invalid_work_state", "Only an unstarted unreferenced Work may be deleted", 2); context.db.run("DELETE FROM works WHERE work_id = ?", [canonical]); refreshRunWorkProjection(context, work.project_id, work.run_id); return { ok: true, deleted: canonical }; }
|
|
130
142
|
export function shortWorkId(id) { return /^WRK-\d{3,}(?:-|$)/.exec(id)?.[0]?.replace(/-$/, "") ?? id; }
|
|
131
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Return the engine-bound command, never a PATH-dependent `dd-flow` token.
|
|
145
|
+
* Fan-out workers run in independently spawned harnesses where the adapter's
|
|
146
|
+
* PATH is not inherited; using the shared lifecycle command keeps them on the
|
|
147
|
+
* same captured runtime as their parent stage.
|
|
148
|
+
*/
|
|
149
|
+
export function workStartCommand(context, work) { const run = requireRun(context, work.project_id, work.run_id); return `${flowCommand(context)} work start ${work.work_id} --project-root ${JSON.stringify(run.project_root)} --json`; }
|
|
132
150
|
export function startWork(context, id, input) {
|
|
133
151
|
ensureWorkRegistry(context);
|
|
134
152
|
const work = requireWork(context, id);
|
|
@@ -227,7 +245,7 @@ export function startStageCoordinatorWork(context, input) {
|
|
|
227
245
|
const run = requireRun(context, work.project_id, work.run_id);
|
|
228
246
|
if (input.projectRoot && resolveProjectRoot(input.projectRoot) !== resolveProjectRoot(run.project_root))
|
|
229
247
|
throw new AppError("project_mismatch", "stage coordinator project root does not match its RUN", 1, { work_id: work.work_id });
|
|
230
|
-
const identity = claimStageStartHookEvent(context, { projectId: work.project_id, eventKey: input.hookEventId, runId: work.run_id, stage: input.stage, projectRoot: run.project_root });
|
|
248
|
+
const identity = claimStageStartHookEvent(context, { projectId: work.project_id, eventKey: input.hookEventId, runId: work.run_id, stage: input.stage, projectRoot: run.project_root, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
231
249
|
const binding = startBoundWork(context, work, run, identity, input.hookEventId);
|
|
232
250
|
const sessionId = String(binding.session_binding.session_id ?? "");
|
|
233
251
|
context.db.run("UPDATE sessions SET current_stage = ?, updated_at = ? WHERE project_id = ? AND session_id = ?", [input.stage, context.now(), work.project_id, sessionId]);
|
|
@@ -247,6 +265,8 @@ export function startBoundWork(context, work, run, identity, hookEventId) {
|
|
|
247
265
|
context.db.exec("BEGIN IMMEDIATE");
|
|
248
266
|
try {
|
|
249
267
|
bindSession(context, work, run, identity, now);
|
|
268
|
+
if (work.parent_work_id && work.launch_policy === "reuse_allowed")
|
|
269
|
+
context.db.run("UPDATE work_sessions SET status = 'completed', completed_at = ?, updated_at = ? WHERE session_id = ? AND status = 'running' AND work_id <> ?", [now, now, identity.sessionId, work.work_id]);
|
|
250
270
|
const claimed = context.db.run("UPDATE works SET status = 'running', started_at = ?, updated_at = ? WHERE work_id = ? AND status = 'created'", [now, now, work.work_id]);
|
|
251
271
|
if (claimed.changes !== 1)
|
|
252
272
|
throw new AppError("conflict", "Work was claimed concurrently", 1, { work_id: work.work_id });
|
|
@@ -265,6 +285,10 @@ export function startBoundWork(context, work, run, identity, hookEventId) {
|
|
|
265
285
|
return { ok: true, work_id: work.work_id, work_session_id: linkId, worker_prompt_markdown: prompt, prompt_path: promptPath, session_binding: { source: "PreToolUse", session_id: identity.sessionId } };
|
|
266
286
|
}
|
|
267
287
|
export function finishWork(context, id, result, progress) { return settle(context, id, "completed", result, progress); }
|
|
288
|
+
/** Structured fan-in closes a parent after its Session was handed to a child. */
|
|
289
|
+
export function finishFanInWork(context, id, result) { const work = requireWork(context, id); if (work.status !== "running")
|
|
290
|
+
throw new AppError("invalid_work_state", "Fan-in Work is not running", 2, { work_id: work.work_id, status: work.status }); if (context.db.get("SELECT 1 FROM works WHERE parent_work_id = ? AND status IN ('created','running') LIMIT 1", [work.work_id]))
|
|
291
|
+
throw new AppError("active_child_work", "Fan-in Work still has active children", 2, { work_id: work.work_id }); const now = context.now(); context.db.run("UPDATE works SET status = 'completed', result = ?, completed_at = ?, updated_at = ? WHERE work_id = ?", [result, now, now, work.work_id]); context.db.run("UPDATE work_sessions SET status = 'completed', completed_at = COALESCE(completed_at, ?), updated_at = ? WHERE work_id = ? AND status = 'running'", [now, now, work.work_id]); refreshRunWorkProjection(context, work.project_id, work.run_id); appendFlowRunTimelineEvent(context, work.project_id, work.run_id, { type: "work_completed", work_id: work.work_id, fan_in: true }); return { ok: true, work_id: work.work_id, status: "completed", fan_in: true }; }
|
|
268
292
|
export function failWork(context, id, reason) { return settle(context, id, "failed", reason); }
|
|
269
293
|
export function cancelWork(context, id, reason) { return settle(context, id, "cancelled", reason); }
|
|
270
294
|
export function retryWork(context, id, reason) { const work = requireWork(context, id); id = work.work_id; if (work.status !== "failed")
|
|
@@ -290,10 +314,14 @@ async function settle(context, id, status, result, progress) {
|
|
|
290
314
|
validateWorkResult(work, result, run.project_root, run.workspace_root, run.id, link?.result_path ?? null);
|
|
291
315
|
const packet = codePacket(work);
|
|
292
316
|
if (packet) {
|
|
317
|
+
// CODE packets are projected from a PLAN that already validated this
|
|
318
|
+
// profile. Check it even when the Work has only raw focused checks, so a
|
|
319
|
+
// worker cannot complete after downgrading the frozen project contract.
|
|
320
|
+
readCodeCheckProfile(run.workspace_root);
|
|
293
321
|
coordinationDrift = plannedAreaDrift(packet, result);
|
|
294
322
|
const artifactDir = path.relative(requireRunHome(run), path.dirname(link.result_path));
|
|
295
323
|
receipts = await runCodeChecks(context, { projectId: work.project_id, runId: work.run_id, runHome: requireRunHome(run), workspaceRoot: run.workspace_root, workId: work.work_id, artifactDir, scope: "work", checks: packet.checks.filter((check) => check.run_at === "work"), ...(progress ? { progress } : {}) });
|
|
296
|
-
const failed = receipts.filter((receipt) => receipt.status
|
|
324
|
+
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
297
325
|
if (failed.length)
|
|
298
326
|
throw new AppError("work_checks_failed", "Work remains running because required checks failed", 2, { work_id: id, failures: failed, all_receipts: receipts });
|
|
299
327
|
}
|
|
@@ -393,7 +421,7 @@ function validateCodeWorkResult(work, value, projectRoot) {
|
|
|
393
421
|
}
|
|
394
422
|
}
|
|
395
423
|
function renderWorkerPrompt(context, work, run, dependencies, resultPath) {
|
|
396
|
-
const command =
|
|
424
|
+
const command = flowCommand(context);
|
|
397
425
|
const packet = codePacket(work);
|
|
398
426
|
const codeContext = packet ? ["<semantic_spine>", JSON.stringify(packet.semantic_spine, null, 2), "</semantic_spine>", "", ...(packet.repair ? ["<repair_context>", JSON.stringify(packet.repair, null, 2), "Read the failed receipt and its linked stdout/stderr before editing. Preserve the accepted origin context and fix only the evidenced failure.", "</repair_context>", ""] : []), "<accepted_requirements>", JSON.stringify(packet.requirements, null, 2), "</accepted_requirements>", "", "<acceptance_context>", "The criteria below are end-to-end context. Complete this Work's semantic contribution and declared checks; another ordered Work may own a different acceptance surface.", JSON.stringify(packet.acceptance, null, 2), "</acceptance_context>", "", "<required_read>", "These are mandatory starting sources, not a read allowlist. Read any additional project files needed to implement the Work correctly.", ...packet.required_read.map((item) => `- ${resolveRunReferences(item, work.run_id, requireRunHome(run))}`), "</required_read>", "", "<discovery_boundary>", "These are likely discovery areas, not a hard boundary. Expand project-local investigation when required and report material additions.", ...packet.discovery_boundary.map((item) => `- ${item}`), "</discovery_boundary>", "", "<planned_write_areas>", "SOFT COORDINATION HINT ONLY. These paths help the coordinator avoid concurrent collisions. They do not grant or deny write permission and do not limit the files needed for this Work. You may create or change any project file under workspace_root that is necessary and in semantic scope; report every actual changed path.", ...(packet.planned_write_areas.length ? packet.planned_write_areas.map((item) => `- ${item}`) : ["- none predicted; derive the necessary files from the task"]), "</planned_write_areas>", "", ...(packet.provides_checks.length ? ["<provided_checks>", ...packet.provides_checks.map((item) => `- ${item.id}: materialize ${item.command}${item.definition ? ` as ${item.definition}` : ""}; it is not usable until this Work finishes.`), "Update the declared project command or alias before Work finish. The CLI verifies the materialization and then executes the check.", "</provided_checks>", ""] : []), "<verification>", ...packet.checks.map((item) => `- ${item.id} at ${item.run_at}: ${item.command} — ${item.purpose}`), "The CLI executes work-scoped checks and retains their receipts. Report semantic evidence only; do not rerun declared checks manually.", "</verification>", "", "<stop_conditions>", ...packet.stop_conditions.map((item) => `- ${item}`), "</stop_conditions>", ""] : [];
|
|
399
427
|
if (packet)
|
package/dist/storage/database.js
CHANGED
|
@@ -58,13 +58,21 @@ export function ensureVnextWorkStorage(db) {
|
|
|
58
58
|
work_id TEXT,
|
|
59
59
|
scope TEXT NOT NULL,
|
|
60
60
|
declaration_id TEXT NOT NULL,
|
|
61
|
+
check_refs_json TEXT NOT NULL DEFAULT '[]',
|
|
62
|
+
gate TEXT NOT NULL DEFAULT 'work',
|
|
61
63
|
command TEXT NOT NULL,
|
|
64
|
+
input_hash TEXT NOT NULL DEFAULT '',
|
|
65
|
+
verification_epoch TEXT NOT NULL DEFAULT '',
|
|
62
66
|
status TEXT NOT NULL,
|
|
63
67
|
exit_code INTEGER,
|
|
64
68
|
stdout_path TEXT NOT NULL,
|
|
65
69
|
stderr_path TEXT NOT NULL,
|
|
66
70
|
receipt_path TEXT NOT NULL,
|
|
67
71
|
workspace_fingerprint TEXT NOT NULL,
|
|
72
|
+
before_fingerprint TEXT NOT NULL DEFAULT '',
|
|
73
|
+
after_fingerprint TEXT NOT NULL DEFAULT '',
|
|
74
|
+
profile_hash TEXT,
|
|
75
|
+
mutation_paths_json TEXT NOT NULL DEFAULT '[]',
|
|
68
76
|
artifacts_json TEXT NOT NULL,
|
|
69
77
|
started_at TEXT NOT NULL,
|
|
70
78
|
finished_at TEXT NOT NULL,
|
|
@@ -474,6 +482,56 @@ function migrate(db) {
|
|
|
474
482
|
CREATE INDEX IF NOT EXISTS idx_check_receipts_run
|
|
475
483
|
ON check_receipts(project_id, run_id, started_at);
|
|
476
484
|
|
|
485
|
+
CREATE TABLE IF NOT EXISTS merge_requests (
|
|
486
|
+
merge_request_id TEXT PRIMARY KEY,
|
|
487
|
+
project_id TEXT NOT NULL,
|
|
488
|
+
run_id TEXT NOT NULL,
|
|
489
|
+
executor_work_id TEXT NOT NULL,
|
|
490
|
+
protocol_ids_json TEXT NOT NULL,
|
|
491
|
+
source_workspace TEXT NOT NULL,
|
|
492
|
+
source_branch TEXT NOT NULL,
|
|
493
|
+
source_commit TEXT NOT NULL,
|
|
494
|
+
target_workspace TEXT NOT NULL,
|
|
495
|
+
target_branch TEXT NOT NULL,
|
|
496
|
+
enqueue_target_head TEXT,
|
|
497
|
+
execution_target_head TEXT,
|
|
498
|
+
integration_commit TEXT,
|
|
499
|
+
execution_route TEXT NOT NULL,
|
|
500
|
+
status TEXT NOT NULL,
|
|
501
|
+
dispatch_owner TEXT,
|
|
502
|
+
dispatch_lease_token TEXT,
|
|
503
|
+
dispatch_lease_expires_at TEXT,
|
|
504
|
+
lock_acquired_at TEXT,
|
|
505
|
+
checkpoint TEXT NOT NULL DEFAULT 'queued',
|
|
506
|
+
profile_hash TEXT,
|
|
507
|
+
adapter_receipt_json TEXT,
|
|
508
|
+
result_json TEXT,
|
|
509
|
+
last_error_json TEXT,
|
|
510
|
+
created_at TEXT NOT NULL,
|
|
511
|
+
updated_at TEXT NOT NULL,
|
|
512
|
+
completed_at TEXT,
|
|
513
|
+
FOREIGN KEY(project_id, run_id) REFERENCES runs(project_id, id),
|
|
514
|
+
FOREIGN KEY(executor_work_id) REFERENCES works(work_id)
|
|
515
|
+
);
|
|
516
|
+
CREATE INDEX IF NOT EXISTS idx_merge_requests_fifo
|
|
517
|
+
ON merge_requests(project_id, status, created_at, merge_request_id);
|
|
518
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_merge_requests_run
|
|
519
|
+
ON merge_requests(project_id, run_id);
|
|
520
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_merge_requests_active_project
|
|
521
|
+
ON merge_requests(project_id)
|
|
522
|
+
WHERE status IN ('active', 'waiting_user', 'action_required', 'recovery_required');
|
|
523
|
+
|
|
524
|
+
CREATE TABLE IF NOT EXISTS merge_servers (
|
|
525
|
+
server_id TEXT PRIMARY KEY,
|
|
526
|
+
profile_id TEXT NOT NULL,
|
|
527
|
+
status TEXT NOT NULL,
|
|
528
|
+
pid INTEGER,
|
|
529
|
+
started_at TEXT NOT NULL,
|
|
530
|
+
heartbeat_at TEXT NOT NULL,
|
|
531
|
+
stopped_at TEXT,
|
|
532
|
+
journal_path TEXT NOT NULL
|
|
533
|
+
);
|
|
534
|
+
|
|
477
535
|
CREATE TABLE IF NOT EXISTS flow_run_flag_mutations (
|
|
478
536
|
project_id TEXT NOT NULL,
|
|
479
537
|
run_id TEXT NOT NULL,
|
|
@@ -685,6 +743,14 @@ function migrate(db) {
|
|
|
685
743
|
ensureColumn(db, "check_receipts", "declaration_id", "ALTER TABLE check_receipts ADD COLUMN declaration_id TEXT NOT NULL DEFAULT 'CHK-LEGACY'");
|
|
686
744
|
ensureColumn(db, "check_receipts", "workspace_fingerprint", "ALTER TABLE check_receipts ADD COLUMN workspace_fingerprint TEXT NOT NULL DEFAULT ''");
|
|
687
745
|
ensureColumn(db, "check_receipts", "artifacts_json", "ALTER TABLE check_receipts ADD COLUMN artifacts_json TEXT NOT NULL DEFAULT '[]'");
|
|
746
|
+
ensureColumn(db, "check_receipts", "check_refs_json", "ALTER TABLE check_receipts ADD COLUMN check_refs_json TEXT NOT NULL DEFAULT '[]'");
|
|
747
|
+
ensureColumn(db, "check_receipts", "gate", "ALTER TABLE check_receipts ADD COLUMN gate TEXT NOT NULL DEFAULT 'work'");
|
|
748
|
+
ensureColumn(db, "check_receipts", "input_hash", "ALTER TABLE check_receipts ADD COLUMN input_hash TEXT NOT NULL DEFAULT ''");
|
|
749
|
+
ensureColumn(db, "check_receipts", "verification_epoch", "ALTER TABLE check_receipts ADD COLUMN verification_epoch TEXT NOT NULL DEFAULT ''");
|
|
750
|
+
ensureColumn(db, "check_receipts", "before_fingerprint", "ALTER TABLE check_receipts ADD COLUMN before_fingerprint TEXT NOT NULL DEFAULT ''");
|
|
751
|
+
ensureColumn(db, "check_receipts", "after_fingerprint", "ALTER TABLE check_receipts ADD COLUMN after_fingerprint TEXT NOT NULL DEFAULT ''");
|
|
752
|
+
ensureColumn(db, "check_receipts", "profile_hash", "ALTER TABLE check_receipts ADD COLUMN profile_hash TEXT");
|
|
753
|
+
ensureColumn(db, "check_receipts", "mutation_paths_json", "ALTER TABLE check_receipts ADD COLUMN mutation_paths_json TEXT NOT NULL DEFAULT '[]'");
|
|
688
754
|
ensureColumn(db, "hook_events", "event_key", "ALTER TABLE hook_events ADD COLUMN event_key TEXT");
|
|
689
755
|
ensureColumn(db, "hook_events", "match_key", "ALTER TABLE hook_events ADD COLUMN match_key TEXT");
|
|
690
756
|
ensureColumn(db, "hook_events", "claimed_at", "ALTER TABLE hook_events ADD COLUMN claimed_at TEXT");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0-beta.0",
|
|
4
4
|
"description": "Mechanical runtime CLI for dd-flow workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"ajv": "^8.20.0",
|
|
30
|
+
"shell-quote": "^1.10.0",
|
|
30
31
|
"smol-toml": "^1.6.1",
|
|
31
32
|
"yaml": "^2.9.0"
|
|
32
33
|
},
|