cortad 0.3.11 → 0.4.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/README.md +13 -9
- package/lib/auto-reach.mjs +6 -5
- package/lib/contact-text.mjs +3 -4
- package/lib/env-files.mjs +0 -85
- package/lib/files.mjs +5 -0
- package/lib/fresh.mjs +1 -1
- package/lib/mcp.mjs +3 -3
- package/lib/proc.mjs +3 -1
- package/lib/proxy.mjs +14 -3
- package/lib/pyhook/sitecustomize.py +1 -500
- package/lib/reach.mjs +7 -7
- package/lib/read-text.mjs +0 -1
- package/lib/replay.mjs +6 -11
- package/lib/start.mjs +13 -34
- package/lib/switches.mjs +9 -69
- package/lib/trace.cjs +2 -320
- package/local.mjs +167 -517
- package/package.json +2 -17
- package/skill/SKILL.md +12 -10
- package/skill/references/results.md +5 -5
- package/lib/backing.mjs +0 -230
- package/lib/container.mjs +0 -60
- package/lib/data.mjs +0 -143
- package/lib/elastic.mjs +0 -88
- package/lib/hosted.mjs +0 -201
- package/lib/identities.mjs +0 -689
- package/lib/keeping.mjs +0 -181
- package/lib/ledger.mjs +0 -20
- package/lib/listing.mjs +0 -12
- package/lib/mint.mjs +0 -350
- package/lib/mongo.mjs +0 -135
- package/lib/pg.mjs +0 -118
- package/lib/qdrant.mjs +0 -48
- package/lib/redis.mjs +0 -130
- package/lib/service.mjs +0 -96
- package/lib/stores.mjs +0 -433
package/README.md
CHANGED
|
@@ -6,11 +6,16 @@ Runs Cortad's test conversations against the AI app on your machine, and gives y
|
|
|
6
6
|
npx cortad <code>
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
Run it in your app's folder with the code from cortad.com
|
|
9
|
+
Run it in your app's folder with the code from cortad.com and your own start command after `--`: Cortad runs that command as you typed it, with its hook inside and nothing else added, connects the app, and adds Cortad to Claude Code, Codex, Cursor and Copilot on this machine. Then send your AI one message the way a user would; that one request is what Cortad learns the endpoint from. Runs start when you press Run in the browser or ask your agent for one. Ctrl-C disconnects.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
npx cortad <code> -- npm run dev your command, as you start it, from the terminal that holds its settings
|
|
13
|
+
npx cortad <code> --port 3000 an app already running: its replies are seen, its model calls are not
|
|
14
|
+
```
|
|
10
15
|
|
|
11
16
|
## Your coding agent
|
|
12
17
|
|
|
13
|
-
After that, your agent has
|
|
18
|
+
After that, your agent has the tools `status`, `reach`, `run`, `run_status`, `findings`, `verify`, `dispute`, `feedback`, `field_connect`, `field`, `world`, `world_status`, `test`, `connect`, `intelligence`, `incidents`, `outcomes`, `subscribe` and `query`. The same work as commands:
|
|
14
19
|
|
|
15
20
|
```
|
|
16
21
|
npx cortad status
|
|
@@ -22,7 +27,7 @@ npx cortad verify <findingId>
|
|
|
22
27
|
|
|
23
28
|
`npx cortad wait` returns when the next run ends, with what it found; an agent starts it in the background so the run's end wakes it.
|
|
24
29
|
|
|
25
|
-
A run needs your app up. If nothing on this machine is holding it, the command starts it with the key in `~/.cortad
|
|
30
|
+
A run needs your app up. If nothing on this machine is holding it, the command starts it with the key and the start command kept in `~/.cortad`, and stops it ten minutes after the last run.
|
|
26
31
|
|
|
27
32
|
To make checking with Cortad part of the repository:
|
|
28
33
|
|
|
@@ -36,19 +41,18 @@ The hooks run after an edit and on your next message: the first names the prompt
|
|
|
36
41
|
## Flags
|
|
37
42
|
|
|
38
43
|
```
|
|
44
|
+
-- <command> how your app starts; everything after -- is run as typed (--start "cmd" says the same)
|
|
45
|
+
--port 3000 use an app that is already running, as it is
|
|
46
|
+
--port 3000 -- <command> start a container and watch the port it publishes; the model base URL to set inside it is printed
|
|
39
47
|
--explain show what would be sent and started, then exit
|
|
40
|
-
--port 3000 use an app that is already running
|
|
41
|
-
--start "make dev" how your app starts
|
|
42
|
-
--data .env.staging copy the databases this settings file names, not the ones your app's settings name
|
|
43
48
|
--verbose print your app's output
|
|
44
49
|
```
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
Nothing about your environment is guessed or changed. Your app gets this terminal's environment and its own request limits raised for the session, by name; its env files are read only to hide their values in what leaves this machine. Nothing is copied or started beside it: test conversations write where your app writes, after your yes in the browser.
|
|
47
52
|
|
|
48
53
|
## Files it creates
|
|
49
54
|
|
|
50
|
-
- `~/.cortad/<project>/` the key, the last tree digest, which process holds your app up, your app's output in `app.log`, the run it asked for last, in `news.txt` the line a finished run left for your agent, removed once the agent has it, and in `heard.txt` the ids of the runs your agent has read
|
|
51
|
-
- `~/.cortad/identity.key` the seed for the session's test accounts
|
|
55
|
+
- `~/.cortad/<project>/` the key, the start command, the last tree digest, which process holds your app up, your app's output in `app.log`, the sign-in your own request carried, the run it asked for last, in `news.txt` the line a finished run left for your agent, removed once the agent has it, and in `heard.txt` the ids of the runs your agent has read
|
|
52
56
|
- `$TMPDIR/cortad-<pid>/` removed on exit
|
|
53
57
|
|
|
54
58
|
macOS and Linux, Node 20 or later. No dependencies. What is sent and what stays on your machine: `npx cortad --explain`, and cortad.com/privacy.
|
package/lib/auto-reach.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// The agent's `reach`, sent by the connect command: one request to each endpoint the ask names, or to
|
|
2
|
+
// every endpoint status lists as not reached, from this machine to the app on its port. The ask
|
|
3
|
+
// reaches this process through two files beside the runner record, since the sign-in the person's own
|
|
4
|
+
// request carried lives here and nowhere else. A round with no ask sends each due endpoint once and
|
|
5
|
+
// again only when its file changes; the connect command runs none, so nothing reaches the app on
|
|
6
|
+
// Cortad's own account.
|
|
6
7
|
import { existsSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
7
8
|
import { join } from "node:path";
|
|
8
9
|
import { notUp, picked, reachText, REACH_MS, sendOurs, testedLine } from "./reach.mjs";
|
package/lib/contact-text.mjs
CHANGED
|
@@ -125,11 +125,10 @@ function unreachedLine(m) {
|
|
|
125
125
|
return ` ${m.method} ${m.path}${m.file ? ` ${at(m.file, m.line)}` : ""}${said ? ` ${said}` : ""}`;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
//
|
|
128
|
+
// The agent sends each one message, or asks Cortad to send the request it read where there is one.
|
|
129
129
|
function reachLine(missing) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return `Cortad sends each of them a test request${except.length ? `, except any ${except.join(" or ")}` : ""}.`;
|
|
130
|
+
const ready = missing.filter((m) => m.request && !m.unsure && !m.excluded);
|
|
131
|
+
return `Send each one message as a user would, signed in where your app needs it${ready.length ? `, or call reach to send the request Cortad read for ${ready.length === missing.length ? "them" : `${num(ready.length)} of them`}` : ""}.`;
|
|
133
132
|
}
|
|
134
133
|
|
|
135
134
|
// `data`: what the command did with the app's stores and services, said right under the run it is
|
package/lib/env-files.mjs
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { basename, dirname, join, sep } from "node:path";
|
|
3
|
-
import { listingUrl } from "./listing.mjs";
|
|
4
|
-
|
|
5
1
|
// An env file read the way dotenv and python-dotenv read one: comments and `export` dropped, a quoted
|
|
6
2
|
// value kept whole across lines, an unquoted one cut at " #", ${NAME} filled from the names above it
|
|
7
3
|
// and then from `outer`. A bare $ is kept as written, as dotenv keeps it: "pa$$word" and a bcrypt hash
|
|
@@ -41,84 +37,3 @@ function closes(body, quote) {
|
|
|
41
37
|
}
|
|
42
38
|
return 0;
|
|
43
39
|
}
|
|
44
|
-
|
|
45
|
-
const read = (file, outer) => { try { return parseEnv(readFileSync(file, "utf8"), outer); } catch { return {}; } };
|
|
46
|
-
const PLACEHOLDERS = /\.(?:example|sample)$/;
|
|
47
|
-
|
|
48
|
-
// The folders from the app's own up to the repository root, nearest first.
|
|
49
|
-
const upFrom = (appDir, root) => {
|
|
50
|
-
const dirs = [];
|
|
51
|
-
for (let d = appDir; ; d = dirname(d)) {
|
|
52
|
-
dirs.push(d);
|
|
53
|
-
if (d === root || !d.startsWith(root + sep) || dirname(d) === d) return dirs;
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
// What a start by hand gives the app from its env files: the `.env` nearest the app, as `source .env`
|
|
58
|
-
// does for an app with no loader of its own. A name another env file there sets to another value is
|
|
59
|
-
// left to the app's own loader, which reads its files by its own mode and order: put in the process,
|
|
60
|
-
// it outranks that loader (the process wins in dotenv, python-dotenv, pydantic-settings, Next and
|
|
61
|
-
// Vite). Nitham's app reads .env.test then .env.local; the test file's key, handed over this way,
|
|
62
|
-
// was refused on every call it made under Cortad and on none started by hand.
|
|
63
|
-
export function appEnv(envFiles, appDir, root, outer = {}) {
|
|
64
|
-
const base = upFrom(appDir, root).map((d) => join(d, ".env")).find((f) => envFiles.includes(f));
|
|
65
|
-
if (!base) return {};
|
|
66
|
-
const values = read(base, outer);
|
|
67
|
-
const beside = envFiles.filter((f) => f !== base && !PLACEHOLDERS.test(f) && (dirname(f) === dirname(base) || dirname(f) === appDir));
|
|
68
|
-
for (const file of beside) {
|
|
69
|
-
for (const [name, value] of Object.entries(read(file, outer))) if (name in values && values[name] !== value) delete values[name];
|
|
70
|
-
}
|
|
71
|
-
return values;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// The real env files in the app's folders, lowest first, the way dotenv-flow, Next and Vite rank them
|
|
75
|
-
// in development: .env, then the development file, then .env.local, and the file of the mode the shell
|
|
76
|
-
// names above all. A test, staging or stage file of another mode goes under them all and still counts:
|
|
77
|
-
// Nitham's app reads .env.test in its local mode, and a store only it names is a store the app writes
|
|
78
|
-
// into. Placeholders are left out.
|
|
79
|
-
const RANK = { ".env": 1, ".env.development": 2, ".env.dev": 2, ".env.local": 3, ".env.development.local": 4 };
|
|
80
|
-
const MODE_FILE = /^\.env\.(test|staging|stage)$/;
|
|
81
|
-
export function rankedFiles(envFiles, appDir, root, mode = "") {
|
|
82
|
-
const dirs = upFrom(appDir, root).reverse();
|
|
83
|
-
const want = mode.toLowerCase() === "staging" ? "stage" : mode.toLowerCase();
|
|
84
|
-
const rank = (f) => {
|
|
85
|
-
const name = basename(f);
|
|
86
|
-
const m = MODE_FILE.exec(name);
|
|
87
|
-
if (m) return (m[1] === "staging" ? "stage" : m[1]) === want ? 5 : 0;
|
|
88
|
-
return RANK[name] ?? -1;
|
|
89
|
-
};
|
|
90
|
-
return envFiles.filter((f) => dirs.includes(dirname(f)) && !PLACEHOLDERS.test(f) && rank(f) >= 0)
|
|
91
|
-
.sort((a, b) => dirs.indexOf(dirname(a)) - dirs.indexOf(dirname(b)) || rank(a) - rank(b));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// The values the app's settings could settle on, ranked as above: for reading which stores and ports
|
|
95
|
-
// the app names, and for a shell that cannot read the files itself. `outer` names the mode and fills ${NAME}.
|
|
96
|
-
export function localEnv(envFiles, appDir, root, outer = {}) {
|
|
97
|
-
const mode = outer.ENV ?? outer.APP_ENV ?? outer.NODE_ENV ?? outer.ENVIRONMENT ?? "";
|
|
98
|
-
return Object.assign({}, ...rankedFiles(envFiles, appDir, root, mode).map((f) => read(f, outer)));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Each real env file in the app's folders, read whole: a key is only ever sent with the address the
|
|
102
|
-
// same file gives it, or one the app's development files give every key.
|
|
103
|
-
export function fileValues(envFiles, appDir, root, outer = {}) {
|
|
104
|
-
const dirs = upFrom(appDir, root);
|
|
105
|
-
return envFiles.filter((f) => dirs.includes(dirname(f)) && !PLACEHOLDERS.test(f)).map((f) => read(f, outer));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// The values a development start reads whatever its mode: .env, the development files and .env.local,
|
|
109
|
-
// never a test, staging or stage file.
|
|
110
|
-
export function devEnv(envFiles, appDir, root, outer = {}) {
|
|
111
|
-
return Object.assign({}, ...rankedFiles(envFiles, appDir, root).filter((f) => !MODE_FILE.test(basename(f))).map((f) => read(f, outer)));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// The key and address pairs a provider's model listing is asked with: the shell's key, else each
|
|
115
|
-
// distinct key a file holds. A key goes only to the address the shell or its own file names, else the
|
|
116
|
-
// one the development files name, else the provider's own; where only a test or staging file names
|
|
117
|
-
// an address, which the app may not use, the key is not sent at all.
|
|
118
|
-
export function keyTries(name, canonical, { shell, files, dev }) {
|
|
119
|
-
const prefix = name.replace(/_API_KEY$/, "");
|
|
120
|
-
const named = (v) => Boolean(v[`${prefix}_BASE_URL`] || v[`${prefix}_API_BASE`]);
|
|
121
|
-
const addressFor = (own) => (named(shell) || named(own) || named(dev) || !files.some(named) ? listingUrl(name, canonical, { ...dev, ...own, ...shell }) : null);
|
|
122
|
-
return [...new Map((shell[name] ? [shell] : files.filter((v) => v[name]))
|
|
123
|
-
.map((v) => ({ key: v[name], url: addressFor(v) })).filter((t) => t.url).map((t) => [`${t.url} ${t.key}`, t])).values()];
|
|
124
|
-
}
|
package/lib/files.mjs
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Which of the repository's files are code, and which are tests: where a problem is placed at its
|
|
2
|
+
// line (lib/replay.mjs), and which files count as the app a run plays (lib/fresh.mjs).
|
|
3
|
+
export const CODE = /\.(?:[cm]?[jt]sx?|py|rb|go|php)$/;
|
|
4
|
+
// A test opens its own scratch file; that is not where the app keeps its data.
|
|
5
|
+
export const TEST = /(?:^|\/)(?:tests?|__tests__|spec|e2e|fixtures?)\/|[._-](?:test|spec)\.[^/]+$|(?:^|\/)(?:test_[^/]+|conftest)\.py$/;
|
package/lib/fresh.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import { readFileSync, statSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
-
import { TEST } from "./
|
|
10
|
+
import { TEST } from "./files.mjs";
|
|
11
11
|
import { homeOf, readApp } from "./home.mjs";
|
|
12
12
|
import { readRunner } from "./runner.mjs";
|
|
13
13
|
import { writesDirOf, writtenPaths } from "./writes.mjs";
|
package/lib/mcp.mjs
CHANGED
|
@@ -10,7 +10,7 @@ export const PROTOCOL = "2025-06-18";
|
|
|
10
10
|
// Codex keeps only the first 512 characters self-contained and Claude Code shares about 4 KB across
|
|
11
11
|
// every server, so the first paragraph stands alone and the whole stays under 1,500 characters.
|
|
12
12
|
export const instructions = (spec = "cortad") => [
|
|
13
|
-
"Cortad tests the AI in this repository: simulated users written from the app's code talk to the app running on this machine, and every reply is checked.
|
|
13
|
+
"Cortad tests the AI in this repository: simulated users written from the app's code talk to the app running on this machine, and every reply is checked. You connect the app with its own start command after `--`, then send each AI endpoint one message as a user would; Cortad learns the endpoint from that request. status says whether the app is up, what Cortad's audit of the AI code found, and what a run would play and cost. A run starts when the person presses Run in the browser or asks you.",
|
|
14
14
|
`Tell the person what status found in three or four short, plain lines, leaving out file paths, endpoints, status codes and setting names unless they ask. Before you stop, start \`npx -y ${spec} wait\` as a background command: it returns when the next run or verify ends, with what it found. After a run, call findings and tell the person what their users ran into and what you would change, worst first, three problems at most; the person then says which fix to make, and nothing is edited before their go. On their go, one fix at a time: verify replays the failing conversations and says whether the failure is gone, and a further round plays only when verify is called again. findings has each failure whole and findings numbers every rate. connect gives the steps to connect the live app. intelligence says what is happening to it and incidents what went wrong and why; outcomes, subscribe and query are the layer's other tools. dispute records a check that reads this app wrong.`,
|
|
15
15
|
].join("\n\n");
|
|
16
16
|
export const INSTRUCTIONS = instructions();
|
|
@@ -25,8 +25,8 @@ export const TOOLS = [
|
|
|
25
25
|
{ name: "status", title: "Cortad status", annotations: readOnly,
|
|
26
26
|
description: "What Cortad found in this repository: whether the app is up and on which port, its audit of the prompts and of the code around the model, each endpoint's answer to Cortad's test request or the file and line it failed at, the plan with runs left, and the latest run. Applies at the start of a session, right after a connect, and before a commit that changes prompts, tools, models or retrieval. With show, one section in full, in pages.",
|
|
27
27
|
inputSchema: { type: "object", properties: { show: { type: "string", enum: SHOW, description: "A section to list in full: rules, standards, journeys, endpoints, trials, records or reviews; or machine, what Cortad does on this machine." }, page: { type: "integer", minimum: 1, description: "The page of that list. Default 1." } }, additionalProperties: false } },
|
|
28
|
-
{ name: "reach", title: "Send Cortad
|
|
29
|
-
description: "Sends Cortad
|
|
28
|
+
{ name: "reach", title: "Send a request Cortad read to an endpoint",
|
|
29
|
+
description: "Sends the request Cortad read for an endpoint, to the ones status lists as failed or not yet answered, or to the paths named, from this machine to the app on its port, signed in as your own request was where one was seen. Each endpoint comes back with its HTTP status, its time and, when it is not 2xx, the start of what the app answered. A message you send the app yourself does the same.",
|
|
30
30
|
inputSchema: { type: "object", properties: {
|
|
31
31
|
paths: { type: "array", items: { type: "string" }, description: "Only these endpoints, by path. Default: every endpoint status lists as failed or not yet answered." },
|
|
32
32
|
}, additionalProperties: false } },
|
package/lib/proc.mjs
CHANGED
|
@@ -58,7 +58,9 @@ export async function listenerOn(port) {
|
|
|
58
58
|
// already in use", Go's "0.0.0.0:8080: bind: address already in use", Node's "EADDRINUSE: address
|
|
59
59
|
// already in use :::3000" or "... 127.0.0.1:3000", Rails' "... for "127.0.0.1" port 3000". 0 when
|
|
60
60
|
// it names none.
|
|
61
|
-
|
|
61
|
+
// The port in the app's own words for a taken one, however its runtime spells the address: Node's
|
|
62
|
+
// "EADDRINUSE: address already in use :::3100", uvicorn's "('127.0.0.1', 58889): [errno 48] address already in use".
|
|
63
|
+
export const portInError = (said) => Number(/[:\s(,](\d{4,5})\)?:?[^\n]{0,40}?address already in use/i.exec(said)?.[1] ?? /(?:EADDRINUSE|address already in use)[^\n]{0,60}?[:\s](\d{4,5})\b/i.exec(said)?.[1] ?? 0);
|
|
62
64
|
|
|
63
65
|
// Whoever holds a port: its pid, the program and its first argument (never the rest of its command
|
|
64
66
|
// line, which can carry a key), and the folder it runs in.
|
package/lib/proxy.mjs
CHANGED
|
@@ -34,12 +34,23 @@ const asked = (raw) => (/^[^=&\s]+=[^&]*(?:&[^=&\s]+=[^&]*)*$/.test(raw) ? [...n
|
|
|
34
34
|
// ponytail: a worker slower than this gets a request said unseen; the wait per app if one is seen.
|
|
35
35
|
const UNSEEN_MS = 3000;
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
// On the port asked for when it is free, else on any: a container's settings name the model port once.
|
|
38
|
+
const listen = async (server, port = 0) => {
|
|
39
|
+
const at = await new Promise((resolve) => {
|
|
40
|
+
server.once("error", () => resolve(null));
|
|
41
|
+
server.listen(port, "127.0.0.1", () => resolve(server.address().port));
|
|
42
|
+
});
|
|
43
|
+
if (at !== null) return at;
|
|
44
|
+
server.removeAllListeners("error");
|
|
45
|
+
server.listen(0, "127.0.0.1");
|
|
46
|
+
await once(server, "listening");
|
|
47
|
+
return server.address().port;
|
|
48
|
+
};
|
|
38
49
|
|
|
39
50
|
// `file`: the trace file the command reads. `target()`: where the app answers now, { host, port };
|
|
40
51
|
// it moves when the app is started again on another port. `onSeen`: the first model call came through.
|
|
41
52
|
// `onUnseen(door)`: a request the app answered in words with no model call begun, said once, as "POST /chat".
|
|
42
|
-
export async function openProxy({ file, rulesFile, target, onSeen = () => {}, onUnseen = () => {} }) {
|
|
53
|
+
export async function openProxy({ file, rulesFile, target, modelPort: wantedModelPort = 0, onSeen = () => {}, onUnseen = () => {} }) {
|
|
43
54
|
const row = (value) => { try { appendFileSync(file, JSON.stringify(scrubbed(value)) + "\n", { mode: 0o600 }); } catch { /* the command is gone */ } };
|
|
44
55
|
const rec = makeRecorder({ row, rulesFile });
|
|
45
56
|
// The requests in flight through the front that a model call can be made for.
|
|
@@ -156,7 +167,7 @@ export async function openProxy({ file, rulesFile, target, onSeen = () => {}, on
|
|
|
156
167
|
const model = createServer((req, res) => { relay(req, res).catch(() => res.destroy()); });
|
|
157
168
|
|
|
158
169
|
const frontPort = await listen(front);
|
|
159
|
-
const modelPort = await listen(model);
|
|
170
|
+
const modelPort = await listen(model, wantedModelPort);
|
|
160
171
|
row({ hello: "proxy", pid: process.pid });
|
|
161
172
|
const via = (upstream) => {
|
|
162
173
|
if (!slots.includes(upstream)) slots.push(upstream);
|