ground-control-cli 0.2.0__py3-none-any.whl

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.
@@ -0,0 +1,171 @@
1
+ // The JavaScript and TypeScript reader's half that runs on Node, with the TypeScript compiler API.
2
+ //
3
+ // Reads on stdin {"typescript": <the compiler's package folder>, "name": <the file as shown>,
4
+ // "blocks": [{"text", "kind": js|jsx|ts|tsx, "offset": lines before the block}]} and writes on
5
+ // stdout {"symbols": [...], "calls": [...]} or {"error": "..."}. See javascript.py for what a
6
+ // symbol is.
7
+ "use strict";
8
+
9
+ const input = JSON.parse(require("fs").readFileSync(0, "utf8"));
10
+ const ts = require(input.typescript);
11
+ const K = ts.SyntaxKind;
12
+ const KINDS = { js: ts.ScriptKind.JS, jsx: ts.ScriptKind.JSX, ts: ts.ScriptKind.TS,
13
+ tsx: ts.ScriptKind.TSX };
14
+ const OPEN = "([{";
15
+ const CLOSE = ")]}";
16
+ const TRIVIA = new Set([K.WhitespaceTrivia, K.NewLineTrivia, K.SingleLineCommentTrivia,
17
+ K.MultiLineCommentTrivia, K.ShebangTrivia, K.ConflictMarkerTrivia]);
18
+
19
+ // The text from start to end on one line, comments dropped, joined the way the Python reader
20
+ // joins a header: one space where the source had a gap, none after an opener or before a closer.
21
+ function oneLine(sf, start, end) {
22
+ const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, sf.languageVariant,
23
+ sf.text.slice(start, end));
24
+ const parts = [];
25
+ const templates = [];
26
+ let braces = 0;
27
+ let prevEnd = null;
28
+ for (let t = scanner.scan(); t !== K.EndOfFileToken; t = scanner.scan()) {
29
+ if (TRIVIA.has(t)) continue;
30
+ if (t === K.TemplateHead) templates.push(braces);
31
+ else if (t === K.OpenBraceToken) braces++;
32
+ else if (t === K.CloseBraceToken) {
33
+ if (templates.length && templates[templates.length - 1] === braces) {
34
+ t = scanner.reScanTemplateToken(false);
35
+ if (t === K.TemplateTail) templates.pop();
36
+ } else braces--;
37
+ }
38
+ const text = scanner.getTokenText();
39
+ const at = scanner.getTokenPos ? scanner.getTokenPos() : scanner.getTokenStart();
40
+ const gap = prevEnd !== null && at !== prevEnd;
41
+ if (parts.length && gap && !OPEN.includes(parts[parts.length - 1]) && !CLOSE.includes(text)) {
42
+ parts.push(" ");
43
+ }
44
+ parts.push(text);
45
+ prevEnd = scanner.getTextPos();
46
+ }
47
+ return parts.join("").trim();
48
+ }
49
+
50
+ function child(node, sf, kinds) {
51
+ return node.getChildren(sf).find((c) => kinds.includes(c.kind));
52
+ }
53
+
54
+ function nameOf(node, sf) {
55
+ if (!node.name) return "default";
56
+ return ts.isIdentifier(node.name) || ts.isPrivateIdentifier(node.name)
57
+ ? node.name.text : node.name.getText(sf);
58
+ }
59
+
60
+ // From the type parameters or the parameters of a function to its body (or its `=>`).
61
+ function params(fn, sf) {
62
+ const open = child(fn, sf, [K.LessThanToken, K.OpenParenToken]);
63
+ const start = open ? open.getStart(sf) : fn.parameters[0].getStart(sf);
64
+ const end = ts.isArrowFunction(fn) ? fn.equalsGreaterThanToken.getStart(sf)
65
+ : fn.body.getStart(sf);
66
+ return oneLine(sf, start, end);
67
+ }
68
+
69
+ function isFunction(node) {
70
+ return node && (ts.isArrowFunction(node) || ts.isFunctionExpression(node));
71
+ }
72
+
73
+ function unwrap(node) {
74
+ while (node && (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) ||
75
+ ts.isSatisfiesExpression && ts.isSatisfiesExpression(node))) {
76
+ node = node.expression;
77
+ }
78
+ return node;
79
+ }
80
+
81
+ function readBlock(block, found) {
82
+ const sf = ts.createSourceFile(input.name, block.text, ts.ScriptTarget.Latest, true,
83
+ KINDS[block.kind]);
84
+ const line = (pos) => sf.getLineAndCharacterOfPosition(pos).line + 1 + block.offset;
85
+ const problems = sf.parseDiagnostics || [];
86
+ if (problems.length) {
87
+ const p = problems[0];
88
+ const message = ts.flattenDiagnosticMessageText(p.messageText, " ");
89
+ throw new Error(`${input.name}: TypeScript cannot parse it: line ${line(p.start)}: ${message}`);
90
+ }
91
+ const symbols = [];
92
+ const add = (kind, scope, signature, span) => {
93
+ symbols.push({ kind, name: scope.join("."), signature, first: line(span.getStart(sf)),
94
+ last: line(span.getEnd()) });
95
+ };
96
+
97
+ function members(cls, scope) {
98
+ for (const m of cls.members) {
99
+ if ((ts.isMethodDeclaration(m) || ts.isConstructorDeclaration(m) ||
100
+ ts.isGetAccessorDeclaration(m) || ts.isSetAccessorDeclaration(m)) && m.body) {
101
+ const name = ts.isConstructorDeclaration(m) ? "constructor" : nameOf(m, sf);
102
+ const from = ts.isGetAccessorDeclaration(m) || ts.isSetAccessorDeclaration(m)
103
+ ? child(m, sf, [K.GetKeyword, K.SetKeyword])
104
+ : ts.isConstructorDeclaration(m) ? child(m, sf, [K.ConstructorKeyword]) : m.name;
105
+ add("method", [...scope, name], oneLine(sf, from.getStart(sf), m.body.getStart(sf)), m);
106
+ } else if (ts.isPropertyDeclaration(m) && isFunction(unwrap(m.initializer))) {
107
+ const name = nameOf(m, sf);
108
+ add("method", [...scope, name], name + params(unwrap(m.initializer), sf), m);
109
+ }
110
+ }
111
+ }
112
+
113
+ function cls(node, name, scope, span) {
114
+ const brace = child(node, sf, [K.OpenBraceToken]);
115
+ const from = node.name ? node.name.getStart(sf) : brace.getStart(sf);
116
+ const header = node.name ? oneLine(sf, from, brace.getStart(sf)) : name;
117
+ add("class", [...scope, name], header, span);
118
+ members(node, [...scope, name]);
119
+ }
120
+
121
+ for (const st of sf.statements) {
122
+ if (ts.isFunctionDeclaration(st) && st.body) {
123
+ const name = nameOf(st, sf);
124
+ add("function", [name], name + params(st, sf), st);
125
+ } else if (ts.isClassDeclaration(st)) {
126
+ cls(st, nameOf(st, sf), [], st);
127
+ } else if (ts.isVariableStatement(st)) {
128
+ const list = st.declarationList.declarations;
129
+ for (const d of list) {
130
+ const value = unwrap(d.initializer);
131
+ if (!ts.isIdentifier(d.name) || !value) continue;
132
+ const span = list.length === 1 ? st : d;
133
+ if (isFunction(value)) add("function", [d.name.text], d.name.text + params(value, sf), span);
134
+ else if (ts.isClassExpression(value)) cls(value, d.name.text, [], span);
135
+ }
136
+ } else if (ts.isExportAssignment(st) && isFunction(unwrap(st.expression))) {
137
+ add("function", ["default"], "default" + params(unwrap(st.expression), sf), st);
138
+ }
139
+ }
140
+
141
+ const calls = [];
142
+ const visit = (node) => {
143
+ let callee = null;
144
+ if (ts.isCallExpression(node) || ts.isNewExpression(node)) callee = node.expression;
145
+ else if ((ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) &&
146
+ !/^[a-z]/.test(node.tagName.getText(sf))) callee = node.tagName; // not <div>
147
+ if (callee) {
148
+ const name = ts.isIdentifier(callee) ? callee.text
149
+ : ts.isPropertyAccessExpression(callee) ? callee.name.text : "";
150
+ if (name) calls.push({ callee: name, line: line(node.getStart(sf)) });
151
+ }
152
+ ts.forEachChild(node, visit);
153
+ };
154
+ visit(sf);
155
+
156
+ found.symbols.push(...symbols);
157
+ for (const c of calls) {
158
+ const inside = symbols.filter((s) => s.first <= c.line && c.line <= s.last);
159
+ inside.sort((a, b) => (a.last - a.first) - (b.last - b.first));
160
+ found.calls.push({ caller: inside.length ? inside[0].name : "<module>", ...c });
161
+ }
162
+ }
163
+
164
+ try {
165
+ const found = { symbols: [], calls: [] };
166
+ for (const block of input.blocks) readBlock(block, found);
167
+ found.calls.sort((a, b) => a.line - b.line);
168
+ process.stdout.write(JSON.stringify(found));
169
+ } catch (e) {
170
+ process.stdout.write(JSON.stringify({ error: e.message }));
171
+ }
@@ -0,0 +1,193 @@
1
+ """The JavaScript and TypeScript reader, with the TypeScript compiler API on Node.
2
+
3
+ It reads .js .mjs .cjs .jsx .ts .tsx, and the inline `<script>` blocks of .html files (those
4
+ without `src` whose type is JavaScript), each block parsed on its own with the file's lines.
5
+ Symbols are the module's function declarations, its `const`/`let`/`var` bound to a function or
6
+ an arrow function, its classes (declared or bound to a variable), and the methods of its classes:
7
+ methods, the constructor, get and set accessors, and properties bound to a function. A function
8
+ inside a function is part of the outer one, as in the Python reader. `export default` without a
9
+ name is named `default`.
10
+
11
+ The signature is the header as written, from the name to the body (to the `=>` for an arrow),
12
+ on one line, comments dropped; a function bound to a variable is its variable's name followed
13
+ by its parameters. The first line is the statement's, decorators and modifiers included.
14
+
15
+ The compiler is TypeScript 6.0.3, the method's own private copy: installed with
16
+ `npm install --prefix <the method's data folder> typescript@6.0.3` and loaded from there, never
17
+ from a global install. The data folder is METHOD_DATA if set, else ~/.method. `missing()`
18
+ says what to install when Node or that copy is not there.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import shutil
25
+ import subprocess
26
+ from html.parser import HTMLParser
27
+ from pathlib import Path
28
+
29
+ from .. import errors, settings
30
+
31
+ KINDS = {".js": "js", ".mjs": "js", ".cjs": "js", ".jsx": "jsx", ".ts": "ts", ".tsx": "tsx"}
32
+ HTML = ".html"
33
+ SCRIPT_TYPES = ("", "module", "text/javascript", "application/javascript")
34
+ HELPER = Path(__file__).with_name("javascript.js")
35
+ COMPILER = "typescript@6.0.3"
36
+
37
+
38
+ def data_dir() -> Path:
39
+ """The method's own data folder: METHOD_DATA if set, else ~/.method."""
40
+ return settings.method_data()
41
+
42
+
43
+ def compiler() -> Path:
44
+ """Where the method's copy of the TypeScript compiler is (or is to be) installed."""
45
+ return data_dir() / "node_modules" / "typescript"
46
+
47
+
48
+ def install_line() -> str:
49
+ """Return the npm line that installs the method's copy of the TypeScript compiler.
50
+
51
+ Returns:
52
+ An `npm install --prefix` line into the method's data folder.
53
+
54
+ Company: assumes none.
55
+ """
56
+ return f"npm install --prefix {data_dir()} {COMPILER}"
57
+
58
+
59
+ def _installed() -> bool:
60
+ try:
61
+ found = json.loads((compiler() / "package.json").read_text(encoding="utf-8"))
62
+ except (OSError, ValueError):
63
+ return False
64
+ return f"typescript@{found.get('version')}" == COMPILER
65
+
66
+
67
+ def missing() -> str:
68
+ """What to install for this reader to run, or "" when Node and the compiler are there."""
69
+ if not shutil.which("node"):
70
+ return (
71
+ f"install Node.js (https://nodejs.org), then the TypeScript compiler: {install_line()}"
72
+ )
73
+ if not _installed():
74
+ return f"install the TypeScript compiler: {install_line()}"
75
+ return ""
76
+
77
+
78
+ def tools_missing() -> list:
79
+ """The names of its missing tools: node, the method's typescript, or none."""
80
+ if not shutil.which("node"):
81
+ return ["node", "typescript"] if not _installed() else ["node"]
82
+ return [] if _installed() else ["typescript"]
83
+
84
+
85
+ class _Scripts(HTMLParser):
86
+ """The inline JavaScript blocks of an HTML page: (lines before the block, its text)."""
87
+
88
+ def __init__(self):
89
+ super().__init__(convert_charrefs=False)
90
+ self.blocks, self._open = [], None
91
+
92
+ def handle_starttag(self, tag, attrs):
93
+ if tag != "script":
94
+ return
95
+ found = {k.lower(): (v or "") for k, v in attrs}
96
+ inline = "src" not in found and found.get("type", "").strip().lower() in SCRIPT_TYPES
97
+ self._open = [None, []] if inline else None
98
+
99
+ def handle_data(self, data):
100
+ if self._open is not None:
101
+ if self._open[0] is None:
102
+ self._open[0] = self.getpos()[0] - 1
103
+ self._open[1].append(data)
104
+
105
+ def handle_endtag(self, tag):
106
+ if tag == "script" and self._open is not None:
107
+ offset, parts = self._open
108
+ if "".join(parts).strip():
109
+ self.blocks.append((offset, "".join(parts)))
110
+ self._open = None
111
+
112
+
113
+ def _blocks(path: Path) -> list:
114
+ text = path.read_text(encoding="utf-8", errors="replace")
115
+ if path.suffix.lower() != HTML:
116
+ return [{"text": text, "kind": KINDS[path.suffix.lower()], "offset": 0}]
117
+ scripts = _Scripts()
118
+ scripts.feed(text)
119
+ scripts.close()
120
+ return [{"text": t, "kind": "js", "offset": offset} for offset, t in scripts.blocks]
121
+
122
+
123
+ def _read(path: Path, name: str) -> dict:
124
+ need = missing()
125
+ if need:
126
+ raise errors.Refused(
127
+ f"{name}: reading it needs Node and the TypeScript compiler: {need}",
128
+ errors.TOOL_MISSING,
129
+ )
130
+ blocks = _blocks(path)
131
+ if not blocks:
132
+ return {"symbols": [], "calls": []}
133
+ request = {"typescript": str(compiler()), "name": name, "blocks": blocks}
134
+ out = subprocess.run(
135
+ [shutil.which("node"), str(HELPER)],
136
+ input=json.dumps(request),
137
+ capture_output=True,
138
+ text=True,
139
+ )
140
+ try:
141
+ found = json.loads(out.stdout)
142
+ except ValueError as e:
143
+ raise errors.Refused(
144
+ f"{name}: the TypeScript reader failed: {(out.stderr or out.stdout).strip()[-500:]}"
145
+ ) from e
146
+ if "error" in found:
147
+ raise errors.Refused(found["error"])
148
+ return found
149
+
150
+
151
+ def symbols(path: Path, name: str) -> list:
152
+ """Return the file's functions, classes and methods, as the Node helper reads them.
153
+
154
+ Args:
155
+ path: The file to read.
156
+ name: The file as it is shown.
157
+
158
+ Returns:
159
+ A Symbol for each; none for an HTML file without inline scripts.
160
+
161
+ Raises:
162
+ errors.Refused: If Node or the compiler is missing, or the helper fails or reports an
163
+ error.
164
+
165
+ Company: assumes none.
166
+ """
167
+ from . import Symbol
168
+
169
+ return [
170
+ Symbol(s["kind"], s["name"], name, s["signature"], s["first"], s["last"])
171
+ for s in _read(path, name)["symbols"]
172
+ ]
173
+
174
+
175
+ def calls(path: Path, name: str) -> list:
176
+ """Return each call in the file, `new` and JSX elements included.
177
+
178
+ Args:
179
+ path: The file to read.
180
+ name: The file as it is shown.
181
+
182
+ Returns:
183
+ A Call for each: the enclosing symbol (or `<module>`), the called name, the line.
184
+
185
+ Raises:
186
+ errors.Refused: If Node or the compiler is missing, or the helper fails or reports an
187
+ error.
188
+
189
+ Company: assumes none.
190
+ """
191
+ from . import Call
192
+
193
+ return [Call(c["caller"], c["callee"], name, c["line"]) for c in _read(path, name)["calls"]]
@@ -0,0 +1,143 @@
1
+ """The Python reader, with `ast`.
2
+
3
+ Symbols are the module's classes and functions, and the methods of its classes (nested classes
4
+ included). A function inside a function is part of the outer one, not a symbol of its own.
5
+ The qualified name joins the enclosing classes with `.`; the signature is the header as
6
+ written, from the name to the colon, on one line, comments dropped. The first line is the
7
+ first decorator's.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import ast
13
+ import io
14
+ import tokenize
15
+ from pathlib import Path
16
+
17
+ from .. import errors
18
+
19
+ DEFS = (ast.FunctionDef, ast.AsyncFunctionDef)
20
+ OPEN, CLOSE = "([{", ")]}"
21
+
22
+
23
+ def missing() -> str:
24
+ """Nothing to install: `ast` is part of Python."""
25
+ return ""
26
+
27
+
28
+ def tools_missing() -> list:
29
+ """None of its tools is missing: `ast` is part of Python."""
30
+ return []
31
+
32
+
33
+ def _tree(path: Path, name: str):
34
+ text = path.read_text(encoding="utf-8")
35
+ try:
36
+ return text, ast.parse(text, filename=name)
37
+ except SyntaxError as e:
38
+ raise errors.Refused(f"{name}: Python cannot parse it: line {e.lineno}: {e.msg}") from e
39
+
40
+
41
+ def _header(lines: list, node) -> str:
42
+ """From the name after `def`/`class` up to the colon that ends the header."""
43
+ first = node.lineno - 1
44
+ source = "".join(lines[first : node.body[0].lineno])
45
+ tokens = tokenize.generate_tokens(io.StringIO(source).readline)
46
+ parts, depth, prev_end, seen_keyword = [], 0, None, False
47
+ for tok in tokens:
48
+ if tok.type in (
49
+ tokenize.COMMENT,
50
+ tokenize.NL,
51
+ tokenize.NEWLINE,
52
+ tokenize.INDENT,
53
+ tokenize.DEDENT,
54
+ ):
55
+ continue
56
+ if not seen_keyword:
57
+ if tok.string in ("def", "class"):
58
+ seen_keyword = True
59
+ prev_end = tok.end
60
+ continue
61
+ if tok.string == ":" and depth == 0:
62
+ break
63
+ if tok.string and tok.string in OPEN:
64
+ depth += 1
65
+ elif tok.string and tok.string in CLOSE:
66
+ depth -= 1
67
+ gap = prev_end is not None and tok.start != prev_end
68
+ if parts and gap and parts[-1] not in OPEN and tok.string not in CLOSE:
69
+ parts.append(" ")
70
+ parts.append(tok.string)
71
+ prev_end = tok.end
72
+ return "".join(parts).strip()
73
+
74
+
75
+ def _walk(body, scope: list, inside_class: bool):
76
+ for node in body:
77
+ if isinstance(node, ast.ClassDef):
78
+ yield node, "class", scope + [node.name]
79
+ yield from _walk(node.body, scope + [node.name], True)
80
+ elif isinstance(node, DEFS):
81
+ yield node, "method" if inside_class else "function", scope + [node.name]
82
+ elif isinstance(node, (ast.If, ast.Try, ast.With, ast.AsyncWith)):
83
+ # definitions under `if TYPE_CHECKING:`, `try: ... except ImportError:` and the like
84
+ for field in ("body", "orelse", "finalbody"):
85
+ yield from _walk(getattr(node, field, []), scope, inside_class)
86
+ for handler in getattr(node, "handlers", []):
87
+ yield from _walk(handler.body, scope, inside_class)
88
+
89
+
90
+ def _first(node) -> int:
91
+ """The first line of the symbol: its first decorator, or its `def`/`class` line."""
92
+ return min([node.lineno] + [d.lineno for d in node.decorator_list])
93
+
94
+
95
+ def symbols(path: Path, name: str) -> list:
96
+ """Return the module's classes and functions, and the methods of its classes.
97
+
98
+ Args:
99
+ path: The file to read.
100
+ name: The file as it is shown.
101
+
102
+ Returns:
103
+ A Symbol for each, in the order they are written.
104
+
105
+ Raises:
106
+ errors.Refused: If Python cannot parse the file.
107
+
108
+ Company: assumes none.
109
+ """
110
+ from . import Symbol
111
+
112
+ text, tree = _tree(path, name)
113
+ lines = text.splitlines(keepends=True)
114
+ return [
115
+ Symbol(kind, ".".join(qualified), name, _header(lines, node), _first(node), node.end_lineno)
116
+ for node, kind, qualified in _walk(tree.body, [], False)
117
+ ]
118
+
119
+
120
+ def calls(path: Path, name: str) -> list:
121
+ """Each call in the file: the enclosing symbol (or `<module>`), the called name, the line."""
122
+ from . import Call
123
+
124
+ _, tree = _tree(path, name)
125
+ spans = [
126
+ (_first(node), node.end_lineno, ".".join(q)) for node, _, q in _walk(tree.body, [], False)
127
+ ]
128
+ found = []
129
+ for node in ast.walk(tree):
130
+ if not isinstance(node, ast.Call):
131
+ continue
132
+ func = node.func
133
+ callee = (
134
+ func.id
135
+ if isinstance(func, ast.Name)
136
+ else (func.attr if isinstance(func, ast.Attribute) else "")
137
+ )
138
+ if not callee:
139
+ continue
140
+ inside = [s for s in spans if s[0] <= node.lineno <= s[1]]
141
+ caller = min(inside, key=lambda s: s[1] - s[0])[2] if inside else "<module>"
142
+ found.append(Call(caller, callee, name, node.lineno))
143
+ return sorted(found, key=lambda c: c.line)
@@ -0,0 +1,67 @@
1
+ """Why a probe could not find its fact, as the service's facts name it (app/method/facts.py).
2
+
3
+ A probe says which of these stopped it, never what a command printed.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ NO_COMMAND = "no_command" # the repo names no command for this
9
+ COMMAND_FAILED = "command_failed" # the repo's command ran and failed
10
+ NO_READER = "no_reader" # no code reader reads the file
11
+ TOOL_MISSING = "tool_missing" # a reader's or git's own tooling is missing
12
+ UNREADABLE = "unreadable" # the reader could not parse the file
13
+ NOT_FOUND = "not_found" # the ref, file, PR or address is not there
14
+ TIMED_OUT = "timed_out" # it did not finish in the repo's time
15
+ REFUSED = "refused" # git or gh refused
16
+
17
+ ERRORS = (
18
+ NO_COMMAND,
19
+ COMMAND_FAILED,
20
+ NO_READER,
21
+ TOOL_MISSING,
22
+ UNREADABLE,
23
+ NOT_FOUND,
24
+ TIMED_OUT,
25
+ REFUSED,
26
+ )
27
+
28
+ # What git and gh print when the ref, the commit or the PR is not there.
29
+ NOT_THERE = (
30
+ "unknown revision",
31
+ "bad revision",
32
+ "bad object",
33
+ "not a valid object name",
34
+ "not a valid commit name",
35
+ "could not resolve",
36
+ "no pull requests found",
37
+ "not found",
38
+ )
39
+
40
+
41
+ class Refused(Exception):
42
+ """A code reader could not read a file; `error` is which of ERRORS stopped it.
43
+
44
+ The message says why, for the person on this machine; a probe returns only the error.
45
+
46
+ Company: assumes none.
47
+ """
48
+
49
+ def __init__(self, message: str, error: str = UNREADABLE):
50
+ """Keep the message and the error."""
51
+ super().__init__(message)
52
+ self.error = error
53
+
54
+
55
+ def failed(stderr: str) -> str:
56
+ """The error a failed git or gh command stands for: not_found or refused.
57
+
58
+ Args:
59
+ stderr: What the command printed on standard error, read here and never kept.
60
+
61
+ Returns:
62
+ NOT_FOUND when it says the thing asked for is not there; REFUSED otherwise.
63
+
64
+ Company: assumes none.
65
+ """
66
+ said = (stderr or "").lower()
67
+ return NOT_FOUND if any(n in said for n in NOT_THERE) else REFUSED