@dokebi/cli 0.4.2 → 0.4.27
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/AI.md +279 -57
- package/README.md +35 -10
- package/catalog.json +2520 -258
- package/describe.schema.json +748 -0
- package/dist/api-app.d.ts +2 -0
- package/dist/api-app.js +35 -0
- package/dist/app-urls.js +3 -2
- package/dist/bin.js +638 -37
- package/dist/capability-guide.d.ts +30 -0
- package/dist/capability-guide.js +122 -0
- package/dist/catalog.d.ts +16 -2
- package/dist/catalog.js +19 -7
- package/dist/check/asset-rules.d.ts +6 -0
- package/dist/check/asset-rules.js +163 -0
- package/dist/check/browser-sources.d.ts +20 -0
- package/dist/check/browser-sources.js +89 -0
- package/dist/check/console-rules.d.ts +6 -0
- package/dist/check/console-rules.js +40 -0
- package/dist/check/error-rules.d.ts +21 -0
- package/dist/check/error-rules.js +212 -0
- package/dist/check/form-rules.d.ts +12 -0
- package/dist/check/form-rules.js +69 -6
- package/dist/check/formatter-rules.d.ts +2 -0
- package/dist/check/formatter-rules.js +71 -0
- package/dist/check/i18n-rules.d.ts +27 -0
- package/dist/check/i18n-rules.js +353 -0
- package/dist/check/index.d.ts +15 -1
- package/dist/check/index.js +89 -11
- package/dist/check/mail-rules.d.ts +4 -0
- package/dist/check/mail-rules.js +72 -0
- package/dist/check/module-rules.d.ts +13 -0
- package/dist/check/module-rules.js +104 -0
- package/dist/check/packages.d.ts +8 -0
- package/dist/check/packages.js +16 -0
- package/dist/check/pool-rules.d.ts +8 -0
- package/dist/check/pool-rules.js +164 -0
- package/dist/check/problems.d.ts +6 -1
- package/dist/check/problems.js +100 -2
- package/dist/check/project-rules.d.ts +23 -3
- package/dist/check/project-rules.js +265 -23
- package/dist/check/seal-rules.d.ts +6 -0
- package/dist/check/seal-rules.js +68 -0
- package/dist/check/security-rules.d.ts +10 -0
- package/dist/check/security-rules.js +450 -0
- package/dist/check/shared-state-rules.d.ts +15 -0
- package/dist/check/shared-state-rules.js +117 -0
- package/dist/check/source-rules.d.ts +26 -3
- package/dist/check/source-rules.js +249 -28
- package/dist/check/structure-rules.d.ts +23 -0
- package/dist/check/structure-rules.js +502 -0
- package/dist/check/timer-rules.d.ts +7 -0
- package/dist/check/timer-rules.js +543 -0
- package/dist/check/typescript.d.ts +35 -5
- package/dist/check/typescript.js +163 -17
- package/dist/check/vite-rules.d.ts +2 -0
- package/dist/check/vite-rules.js +136 -0
- package/dist/check/wait-rules.d.ts +16 -0
- package/dist/check/wait-rules.js +337 -0
- package/dist/commands.d.ts +7 -0
- package/dist/commands.js +381 -36
- package/dist/config-diff.d.ts +42 -0
- package/dist/config-diff.js +289 -0
- package/dist/config-edit.d.ts +35 -2
- package/dist/config-edit.js +145 -13
- package/dist/config-init.d.ts +15 -0
- package/dist/config-init.js +61 -0
- package/dist/config-paths.d.ts +22 -0
- package/dist/config-paths.js +82 -0
- package/dist/config-secrets.d.ts +46 -0
- package/dist/config-secrets.js +187 -0
- package/dist/config-show.d.ts +82 -0
- package/dist/config-show.js +563 -0
- package/dist/config-values.d.ts +8 -0
- package/dist/config-values.js +68 -0
- package/dist/configuration-scaffold.d.ts +42 -0
- package/dist/configuration-scaffold.js +258 -4
- package/dist/configuration.d.ts +63 -3
- package/dist/configuration.js +178 -4
- package/dist/deploy-scripts.d.ts +4 -0
- package/dist/deploy-scripts.js +361 -0
- package/dist/deploy-templates.d.ts +46 -0
- package/dist/deploy-templates.js +379 -0
- package/dist/deploy.d.ts +46 -0
- package/dist/deploy.js +592 -0
- package/dist/deployment.d.ts +2 -0
- package/dist/deployment.js +111 -0
- package/dist/describe-db.d.ts +22 -0
- package/dist/describe-db.js +176 -0
- package/dist/describe.d.ts +92 -0
- package/dist/describe.js +417 -0
- package/dist/dev-compose.d.ts +20 -0
- package/dist/dev-compose.js +154 -0
- package/dist/dev-guard.js +44 -0
- package/dist/dev-host.d.ts +13 -0
- package/dist/dev-host.js +55 -0
- package/dist/dev-infra.d.ts +72 -4
- package/dist/dev-infra.js +237 -17
- package/dist/dev-watch.d.ts +34 -0
- package/dist/dev-watch.js +192 -0
- package/dist/diagnostics.d.ts +10 -7
- package/dist/diagnostics.js +22 -4
- package/dist/drift.d.ts +50 -0
- package/dist/drift.js +218 -0
- package/dist/env-file.d.ts +25 -1
- package/dist/env-file.js +64 -6
- package/dist/errors.d.ts +7 -1
- package/dist/example-config.d.ts +2 -0
- package/dist/example-config.js +3 -0
- package/dist/first-admin.d.ts +43 -0
- package/dist/first-admin.js +175 -0
- package/dist/formatter-ignore.d.ts +37 -0
- package/dist/formatter-ignore.js +344 -0
- package/dist/generator.d.ts +45 -3
- package/dist/generator.js +205 -15
- package/dist/guide.d.ts +1 -1
- package/dist/guide.js +42 -21
- package/dist/install-module.d.ts +20 -2
- package/dist/install-module.js +45 -8
- package/dist/install-plan.js +5 -3
- package/dist/layout.d.ts +12 -1
- package/dist/layout.js +19 -6
- package/dist/log-writer-control.d.ts +2 -0
- package/dist/log-writer-control.js +17 -2
- package/dist/log-writer.js +43 -10
- package/dist/logging-config.d.ts +8 -3
- package/dist/logging-config.js +19 -5
- package/dist/manifest.d.ts +29 -1
- package/dist/manifest.js +84 -6
- package/dist/merge-install-defaults.d.ts +0 -1
- package/dist/merge-install-defaults.js +13 -47
- package/dist/npm-client.d.ts +40 -4
- package/dist/npm-client.js +152 -15
- package/dist/npm-paths.js +18 -4
- package/dist/openapi.d.ts +27 -0
- package/dist/openapi.js +649 -0
- package/dist/option-fixes.d.ts +39 -0
- package/dist/option-fixes.js +130 -0
- package/dist/output.d.ts +1 -1
- package/dist/output.js +5 -5
- package/dist/plugin-lifecycle.d.ts +34 -2
- package/dist/plugin-lifecycle.js +195 -16
- package/dist/plugin-options.d.ts +19 -4
- package/dist/plugin-options.js +138 -6
- package/dist/plugins-command.js +44 -7
- package/dist/process-group.d.ts +39 -0
- package/dist/process-group.js +132 -0
- package/dist/production-doctor.d.ts +3 -0
- package/dist/production-doctor.js +176 -0
- package/dist/project-guide.d.ts +19 -1
- package/dist/project-guide.js +119 -20
- package/dist/project-inspector.d.ts +15 -0
- package/dist/project-inspector.js +497 -33
- package/dist/project-state.d.ts +5 -0
- package/dist/project-state.js +8 -0
- package/dist/project.d.ts +203 -19
- package/dist/project.js +3006 -333
- package/dist/recipe-facts.d.ts +127 -0
- package/dist/recipe-facts.js +563 -0
- package/dist/recipe-login.d.ts +63 -0
- package/dist/recipe-login.js +464 -0
- package/dist/recipe-mail.d.ts +21 -0
- package/dist/recipe-mail.js +214 -0
- package/dist/recipe-members.d.ts +35 -0
- package/dist/recipe-members.js +178 -0
- package/dist/recipe-record.d.ts +82 -7
- package/dist/recipe-record.js +226 -22
- package/dist/recipe-reset.d.ts +2 -0
- package/dist/recipe-reset.js +117 -0
- package/dist/recipe-site.d.ts +48 -0
- package/dist/recipe-site.js +396 -0
- package/dist/recipe-step.d.ts +42 -0
- package/dist/recipe-step.js +89 -0
- package/dist/recipe.d.ts +56 -20
- package/dist/recipe.js +703 -131
- package/dist/registry.d.ts +98 -2
- package/dist/registry.js +171 -5
- package/dist/reloadable.d.ts +35 -0
- package/dist/reloadable.js +257 -0
- package/dist/remote-verify.d.ts +31 -0
- package/dist/remote-verify.js +204 -0
- package/dist/server.d.ts +43 -4
- package/dist/server.js +601 -52
- package/dist/settings.d.ts +48 -0
- package/dist/settings.js +168 -0
- package/dist/site-auth.d.ts +39 -0
- package/dist/site-auth.js +195 -0
- package/dist/template-fixes.d.ts +26 -0
- package/dist/template-fixes.js +285 -0
- package/dist/test-reporter.d.ts +49 -0
- package/dist/test-reporter.js +190 -0
- package/dist/transaction.d.ts +47 -0
- package/dist/transaction.js +241 -7
- package/dist/verify-tests.d.ts +98 -0
- package/dist/verify-tests.js +449 -0
- package/dist/verify.d.ts +103 -3
- package/dist/verify.js +272 -21
- package/dist/view-choice.d.ts +58 -0
- package/dist/view-choice.js +131 -0
- package/dist/vite-watch.js +4 -2
- package/package.json +8 -6
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
// The wait-count rule of `dokebi check` (@dokebi/db AI.md §1.2 and §6.4, AI-M34), over the
|
|
2
|
+
// TypeScript AST:
|
|
3
|
+
// wait-count-refusal (warning) server code under src/ (not views/, not tests/) that compares
|
|
4
|
+
// how many callers wait for a database connection with a number: pg's
|
|
5
|
+
// `pool.waitingCount`, mariadb's `pool.taskQueueSize()`, the MongoDB
|
|
6
|
+
// driver's `waitQueueSize` and mysql2's `pool._connectionQueue.length`.
|
|
7
|
+
// The number of waiters says nothing about overload (a pool of 10 serving
|
|
8
|
+
// 5 ms statements clears 100 waiters in about 50 ms), so refusing work on
|
|
9
|
+
// it refuses work the database can still do (M34: 60% of 200 concurrent
|
|
10
|
+
// requests refused by an idle app). Bound the wait by its deadline instead
|
|
11
|
+
// (waitTimeoutMs, then 503 with Retry-After) and shed per tenant on time.
|
|
12
|
+
// in-process-queue (warning) server code that parks callers in its own queue: the resolve
|
|
13
|
+
// of a `new Promise` executor pushed into a collection (`.push`,
|
|
14
|
+
// `.unshift`, `.set`, `.add`) to be called later. @dokebi/db AI.md says
|
|
15
|
+
// never build in-process queues or mutexes around database writes: such a
|
|
16
|
+
// queue has no bound or deadline (M35: requests waited up to 63 s, no 503),
|
|
17
|
+
// is per worker, and drops its place on a restart. Let the pool's wait
|
|
18
|
+
// deadline bound the wait and shed one tenant by time instead.
|
|
19
|
+
// fixed-owner-cap (warning) a per-owner count of requests in progress (a Map entry that goes
|
|
20
|
+
// up and down by one) compared with a fixed limit before a refusal, without
|
|
21
|
+
// asking whether a connection is free: it refuses work while the database is
|
|
22
|
+
// idle (M37: a cap of 12 refused up to 1.6 % of a burst at 9 % database CPU).
|
|
23
|
+
// Refuse an owner only while the pool is full (@dokebi/db AI.md 6.4).
|
|
24
|
+
// A `// dokebi-check: allow wait-count-refusal <reason>` (or `in-process-queue`) comment keeps a
|
|
25
|
+
// deliberate one (a metric or a log line that only reports the number; a queue of a library
|
|
26
|
+
// adapter written in the app).
|
|
27
|
+
import { field } from './typescript.js';
|
|
28
|
+
/** Properties (or methods) that count the callers waiting for a pool connection. */
|
|
29
|
+
const WAIT_COUNTS = new Set(['waitingCount', 'taskQueueSize', 'waitQueueSize']);
|
|
30
|
+
export const WAIT_FIX = 'bound the wait for a connection by its deadline instead (the pool option waitTimeoutMs: past it the caller gets 503 DB_UNAVAILABLE with Retry-After) and shed one tenant by the time its requests already hold, never by how many callers wait; a metric or log line that only reports the number takes // dokebi-check: allow wait-count-refusal <reason>';
|
|
31
|
+
export const QUEUE_FIX = 'do not hold callers in a queue of your own: send the database work at once and let the pool bound the wait (waitTimeoutMs, then 503 DB_UNAVAILABLE with Retry-After), shedding one tenant by time it already holds; ordered background work goes to @dokebi/outbox. A deliberate one takes // dokebi-check: allow in-process-queue <reason>';
|
|
32
|
+
/** Callers parked in a collection: the resolve of a `new Promise` executor stored to call later. */
|
|
33
|
+
export function queueRules(file, kinds, report) {
|
|
34
|
+
if (!/\bnew\s+Promise\b/.test(file.text))
|
|
35
|
+
return;
|
|
36
|
+
const STORE = new Set(['push', 'unshift', 'set', 'add']);
|
|
37
|
+
const text = (node) => file.text.slice(node.getStart(), node.end);
|
|
38
|
+
const functional = (node) => node !== undefined &&
|
|
39
|
+
(node.kind === kinds.ArrowFunction || node.kind === kinds.FunctionExpression);
|
|
40
|
+
const uses = (node, name) => {
|
|
41
|
+
let found = false;
|
|
42
|
+
const walk = (child) => {
|
|
43
|
+
if (child.kind === kinds.Identifier && text(child) === name)
|
|
44
|
+
found = true;
|
|
45
|
+
if (!found)
|
|
46
|
+
child.forEachChild(walk);
|
|
47
|
+
return undefined;
|
|
48
|
+
};
|
|
49
|
+
walk(node);
|
|
50
|
+
return found;
|
|
51
|
+
};
|
|
52
|
+
const visit = (node) => {
|
|
53
|
+
if (node.kind === kinds.NewExpression &&
|
|
54
|
+
text(field(node, 'expression')) === 'Promise') {
|
|
55
|
+
const executor = node.arguments?.[0];
|
|
56
|
+
const first = functional(executor)
|
|
57
|
+
? executor.parameters[0]
|
|
58
|
+
: undefined;
|
|
59
|
+
const resolve = first ? field(first, 'name') : undefined;
|
|
60
|
+
if (executor && resolve?.kind === kinds.Identifier) {
|
|
61
|
+
const name = text(resolve);
|
|
62
|
+
// Names that hold the resolve (`const pending = { input, resolve }`, `const fresh =
|
|
63
|
+
// [pending]`), followed until nothing new: a store of any of them parks the caller (M36).
|
|
64
|
+
const held = new Set([name]);
|
|
65
|
+
const holds = (node) => [...held].some((item) => uses(node, item));
|
|
66
|
+
for (let grew = true; grew;) {
|
|
67
|
+
grew = false;
|
|
68
|
+
const scan = (child) => {
|
|
69
|
+
if (child.kind === kinds.VariableDeclaration) {
|
|
70
|
+
const own = field(child, 'name');
|
|
71
|
+
const value = field(child, 'initializer');
|
|
72
|
+
if (own?.kind === kinds.Identifier && value && !held.has(text(own)) && holds(value)) {
|
|
73
|
+
held.add(text(own));
|
|
74
|
+
grew = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
child.forEachChild(scan);
|
|
78
|
+
return undefined;
|
|
79
|
+
};
|
|
80
|
+
scan(executor);
|
|
81
|
+
}
|
|
82
|
+
const walk = (child) => {
|
|
83
|
+
if (child.kind === kinds.CallExpression) {
|
|
84
|
+
const callee = field(child, 'expression');
|
|
85
|
+
const method = callee?.kind === kinds.PropertyAccessExpression ? field(callee, 'name') : undefined;
|
|
86
|
+
const args = child.arguments ?? [];
|
|
87
|
+
if (method && STORE.has(text(method)) && args.some((arg) => holds(arg))) {
|
|
88
|
+
report(child, `the resolve of a new Promise is stored with ${text(method)}() to be called later: callers wait in a queue of this worker, without a bound or a deadline, and lose their place on a restart (@dokebi/db AI.md: no in-process queues around database writes; M35)`, QUEUE_FIX);
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
child.forEachChild(walk);
|
|
93
|
+
return undefined;
|
|
94
|
+
};
|
|
95
|
+
walk(executor);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
node.forEachChild(visit);
|
|
99
|
+
return undefined;
|
|
100
|
+
};
|
|
101
|
+
file.forEachChild(visit);
|
|
102
|
+
}
|
|
103
|
+
export function waitRules(file, kinds, report) {
|
|
104
|
+
// Most files never look at a pool's queue: skip the walk.
|
|
105
|
+
if (!/\b(?:waitingCount|taskQueueSize|waitQueueSize|_connectionQueue)\b/.test(file.text))
|
|
106
|
+
return;
|
|
107
|
+
const COMPARISONS = new Set([
|
|
108
|
+
kinds.GreaterThanToken,
|
|
109
|
+
kinds.GreaterThanEqualsToken,
|
|
110
|
+
kinds.LessThanToken,
|
|
111
|
+
kinds.LessThanEqualsToken,
|
|
112
|
+
]);
|
|
113
|
+
const name = (node) => node && (node.kind === kinds.Identifier || node.kind === kinds.PrivateIdentifier)
|
|
114
|
+
? file.text.slice(node.getStart(), node.end)
|
|
115
|
+
: undefined;
|
|
116
|
+
const unwrap = (node) => {
|
|
117
|
+
let current = node;
|
|
118
|
+
while (current?.kind === kinds.ParenthesizedExpression)
|
|
119
|
+
current = field(current, 'expression');
|
|
120
|
+
return current;
|
|
121
|
+
};
|
|
122
|
+
/** The waiting count this side reads, or undefined. */
|
|
123
|
+
const waitCount = (side) => {
|
|
124
|
+
let value = unwrap(side);
|
|
125
|
+
// `pool.taskQueueSize()`: the method of mariadb.
|
|
126
|
+
if (value?.kind === kinds.CallExpression)
|
|
127
|
+
value = field(value, 'expression');
|
|
128
|
+
if (value?.kind !== kinds.PropertyAccessExpression)
|
|
129
|
+
return undefined;
|
|
130
|
+
const member = name(field(value, 'name'));
|
|
131
|
+
if (member && WAIT_COUNTS.has(member))
|
|
132
|
+
return member;
|
|
133
|
+
// mysql2: `pool._connectionQueue.length`.
|
|
134
|
+
const inner = field(value, 'expression');
|
|
135
|
+
if (member === 'length' &&
|
|
136
|
+
inner?.kind === kinds.PropertyAccessExpression &&
|
|
137
|
+
name(field(inner, 'name')) === '_connectionQueue')
|
|
138
|
+
return '_connectionQueue.length';
|
|
139
|
+
return undefined;
|
|
140
|
+
};
|
|
141
|
+
const visit = (node) => {
|
|
142
|
+
if (node.kind === kinds.BinaryExpression) {
|
|
143
|
+
const operator = field(node, 'operatorToken');
|
|
144
|
+
if (operator && COMPARISONS.has(operator.kind)) {
|
|
145
|
+
const counted = waitCount(field(node, 'left')) ?? waitCount(field(node, 'right'));
|
|
146
|
+
if (counted)
|
|
147
|
+
report(node, `${counted} compared with a number: the count of callers waiting for a connection is not overload, so a decision on it refuses work the database can still do (@dokebi/db AI.md 1.2 and 6.4)`, WAIT_FIX);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
node.forEachChild(visit);
|
|
151
|
+
return undefined;
|
|
152
|
+
};
|
|
153
|
+
file.forEachChild(visit);
|
|
154
|
+
}
|
|
155
|
+
export const OWNER_CAP_FIX = "use the owner share of @dokebi/db instead of a count of your own: const release = context.getDependency(db).share(main).enter(owner) before the database work, release() after it; it refuses only while the pool is full (D-0156). By hand: refuse an owner only while the pool is full: admit every request while a connection is free (pg: max - pool.totalCount + pool.idleCount > 0) or the pool became full less than a connect time ago (connections being opened count as open), and when it is full refuse with 503 and Retry-After only an owner that already holds an equal share (max divided by the owners with requests in progress) (@dokebi/db AI.md 6.4, an owner's share). A budget that bounds something other than connections (the report CPU budget of @dokebi/db AI.md 6.9, a plan's quota) takes // dokebi-check: allow fixed-owner-cap <reason>";
|
|
156
|
+
/**
|
|
157
|
+
* A per-owner count of requests in progress compared with a fixed limit (AI-M37): a Map whose
|
|
158
|
+
* entries go up by one and down by one in the same file, read into a variable that an `if`
|
|
159
|
+
* compares with `>=` or `>` before a throw or a return, with no free or idle connection in the
|
|
160
|
+
* condition. It refuses work while the database is idle.
|
|
161
|
+
*/
|
|
162
|
+
export function ownerCapRules(file, kinds, report) {
|
|
163
|
+
// Most files keep no counters in a Map: skip the walks.
|
|
164
|
+
if (!/\.get\([^)]*\)/.test(file.text) || !/[-+]\s*1\b/.test(file.text))
|
|
165
|
+
return;
|
|
166
|
+
const text = (node) => file.text.slice(node.getStart(), node.end);
|
|
167
|
+
const operator = (node) => {
|
|
168
|
+
const token = field(node, 'operatorToken');
|
|
169
|
+
return token ? text(token) : '';
|
|
170
|
+
};
|
|
171
|
+
// Maps read with `.get(`: the ones some `+ 1` and some `- 1` expression reads or writes.
|
|
172
|
+
const up = new Set();
|
|
173
|
+
const down = new Set();
|
|
174
|
+
const mapOf = (node) => {
|
|
175
|
+
if (node.kind !== kinds.CallExpression)
|
|
176
|
+
return undefined;
|
|
177
|
+
const callee = field(node, 'expression');
|
|
178
|
+
if (callee?.kind !== kinds.PropertyAccessExpression)
|
|
179
|
+
return undefined;
|
|
180
|
+
const method = field(callee, 'name');
|
|
181
|
+
const object = field(callee, 'expression');
|
|
182
|
+
return method && object && text(method) === 'get' ? text(object) : undefined;
|
|
183
|
+
};
|
|
184
|
+
const mapsIn = (node) => {
|
|
185
|
+
const found = [];
|
|
186
|
+
const walk = (child) => {
|
|
187
|
+
const map = mapOf(child);
|
|
188
|
+
if (map)
|
|
189
|
+
found.push(map);
|
|
190
|
+
child.forEachChild(walk);
|
|
191
|
+
return undefined;
|
|
192
|
+
};
|
|
193
|
+
walk(node);
|
|
194
|
+
return found;
|
|
195
|
+
};
|
|
196
|
+
// Variables holding `map.get(key)` (with `?? 0` or `|| 0`).
|
|
197
|
+
const counters = new Map();
|
|
198
|
+
const scan = (node) => {
|
|
199
|
+
if (node.kind === kinds.BinaryExpression) {
|
|
200
|
+
const right = field(node, 'right');
|
|
201
|
+
const left = field(node, 'left');
|
|
202
|
+
const op = operator(node);
|
|
203
|
+
if ((op === '+' || op === '-') && right && /^1$/.test(text(right)) && left) {
|
|
204
|
+
for (const map of mapsIn(left))
|
|
205
|
+
(op === '+' ? up : down).add(map);
|
|
206
|
+
// `current + 1` where current holds map.get(key): counted below, once counters are known.
|
|
207
|
+
if (left.kind === kinds.Identifier)
|
|
208
|
+
(op === '+' ? up : down).add(`#var:${text(left)}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else if (node.kind === kinds.VariableDeclaration) {
|
|
212
|
+
const name = field(node, 'name');
|
|
213
|
+
const value = field(node, 'initializer');
|
|
214
|
+
if (name?.kind === kinds.Identifier && value) {
|
|
215
|
+
const [map] = mapsIn(value);
|
|
216
|
+
if (map &&
|
|
217
|
+
/^\(?\s*[\w.#]+\.get\([^)]*\)\s*(?:(?:\?\?|\|\|)\s*\d+)?\s*\)?(?:\s*\+\s*1)?$/.test(text(value)))
|
|
218
|
+
counters.set(text(name), map);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
node.forEachChild(scan);
|
|
222
|
+
return undefined;
|
|
223
|
+
};
|
|
224
|
+
file.forEachChild(scan);
|
|
225
|
+
for (const [name, map] of counters) {
|
|
226
|
+
if (up.has(`#var:${name}`))
|
|
227
|
+
up.add(map);
|
|
228
|
+
if (down.has(`#var:${name}`))
|
|
229
|
+
down.add(map);
|
|
230
|
+
}
|
|
231
|
+
const inFlight = (name) => {
|
|
232
|
+
const map = counters.get(name);
|
|
233
|
+
return map !== undefined && up.has(map) && down.has(map);
|
|
234
|
+
};
|
|
235
|
+
const exits = (node) => {
|
|
236
|
+
if (!node)
|
|
237
|
+
return false;
|
|
238
|
+
let found = false;
|
|
239
|
+
const walk = (child) => {
|
|
240
|
+
if (child.kind === kinds.ThrowStatement || child.kind === kinds.ReturnStatement)
|
|
241
|
+
found = true;
|
|
242
|
+
if (!found)
|
|
243
|
+
child.forEachChild(walk);
|
|
244
|
+
return undefined;
|
|
245
|
+
};
|
|
246
|
+
walk(node);
|
|
247
|
+
return found;
|
|
248
|
+
};
|
|
249
|
+
// Conditions about the pool's free connections, directly or through a variable made from them
|
|
250
|
+
// (`const free = max - pool.totalCount + pool.idleCount; const full = free <= 0`).
|
|
251
|
+
const POOL = /idle|free|totalCount|totalConnections/i;
|
|
252
|
+
const poolNames = new Set();
|
|
253
|
+
for (let grew = true; grew;) {
|
|
254
|
+
grew = false;
|
|
255
|
+
const collect = (node) => {
|
|
256
|
+
if (node.kind === kinds.VariableDeclaration) {
|
|
257
|
+
const name = field(node, 'name');
|
|
258
|
+
const value = field(node, 'initializer');
|
|
259
|
+
if (name?.kind === kinds.Identifier &&
|
|
260
|
+
value &&
|
|
261
|
+
!poolNames.has(text(name)) &&
|
|
262
|
+
aboutPool(value)) {
|
|
263
|
+
poolNames.add(text(name));
|
|
264
|
+
grew = true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
node.forEachChild(collect);
|
|
268
|
+
return undefined;
|
|
269
|
+
};
|
|
270
|
+
file.forEachChild(collect);
|
|
271
|
+
}
|
|
272
|
+
function aboutPool(node) {
|
|
273
|
+
if (POOL.test(text(node)))
|
|
274
|
+
return true;
|
|
275
|
+
let found = false;
|
|
276
|
+
const walk = (child) => {
|
|
277
|
+
if (child.kind === kinds.Identifier && poolNames.has(text(child)))
|
|
278
|
+
found = true;
|
|
279
|
+
if (!found)
|
|
280
|
+
child.forEachChild(walk);
|
|
281
|
+
return undefined;
|
|
282
|
+
};
|
|
283
|
+
walk(node);
|
|
284
|
+
return found;
|
|
285
|
+
}
|
|
286
|
+
/** The comparison of an in-flight count with a limit in this condition, if any. */
|
|
287
|
+
const comparedIn = (condition) => {
|
|
288
|
+
let compared;
|
|
289
|
+
const walk = (child) => {
|
|
290
|
+
if (child.kind === kinds.BinaryExpression) {
|
|
291
|
+
const op = operator(child);
|
|
292
|
+
const left = field(child, 'left');
|
|
293
|
+
const right = field(child, 'right');
|
|
294
|
+
if ((op === '>=' || op === '>') && left?.kind === kinds.Identifier && inFlight(text(left)))
|
|
295
|
+
compared = text(left);
|
|
296
|
+
if ((op === '<=' || op === '<') &&
|
|
297
|
+
right?.kind === kinds.Identifier &&
|
|
298
|
+
inFlight(text(right)))
|
|
299
|
+
compared = text(right);
|
|
300
|
+
}
|
|
301
|
+
if (!compared)
|
|
302
|
+
child.forEachChild(walk);
|
|
303
|
+
return undefined;
|
|
304
|
+
};
|
|
305
|
+
walk(condition);
|
|
306
|
+
return compared;
|
|
307
|
+
};
|
|
308
|
+
// `guarded`: the code runs only when a condition about free connections decided it — inside
|
|
309
|
+
// either branch of such an `if`, or after one whose branch leaves the function (M38: the
|
|
310
|
+
// comparison nested in `if (free <= 0) { … }` is the guidance's own pattern).
|
|
311
|
+
const visit = (node, guarded) => {
|
|
312
|
+
if (node.kind === kinds.IfStatement) {
|
|
313
|
+
const condition = field(node, 'expression');
|
|
314
|
+
const pool = condition !== undefined && aboutPool(condition);
|
|
315
|
+
const compared = condition && !pool && !guarded ? comparedIn(condition) : undefined;
|
|
316
|
+
if (compared && exits(field(node, 'thenStatement')))
|
|
317
|
+
report(node, `${compared} counts one owner's requests in progress and is compared with a fixed limit: it refuses requests while connections are free and the database is idle (@dokebi/db AI.md 6.4, an owner's share; M37)`, OWNER_CAP_FIX);
|
|
318
|
+
node.forEachChild((child) => visit(child, guarded || pool));
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
const statements = node.statements;
|
|
322
|
+
if (node.kind !== file.kind && Array.isArray(statements)) {
|
|
323
|
+
let after = guarded;
|
|
324
|
+
for (const statement of statements) {
|
|
325
|
+
visit(statement, after);
|
|
326
|
+
if (statement.kind === kinds.IfStatement &&
|
|
327
|
+
aboutPool(field(statement, 'expression')) &&
|
|
328
|
+
exits(field(statement, 'thenStatement')))
|
|
329
|
+
after = true;
|
|
330
|
+
}
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
node.forEachChild((child) => visit(child, guarded));
|
|
334
|
+
return undefined;
|
|
335
|
+
};
|
|
336
|
+
file.forEachChild((child) => visit(child, false));
|
|
337
|
+
}
|
package/dist/commands.d.ts
CHANGED
|
@@ -11,6 +11,13 @@ export interface CommandHelp {
|
|
|
11
11
|
export declare const COMMANDS: readonly CommandHelp[];
|
|
12
12
|
/** True when the usage line of `dokebi <command>` names `flag` (absent `no-flag:` rules). */
|
|
13
13
|
export declare function commandHasFlag(command: string, flag: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The commands whose usage line takes --dry-run, in table order (AI-M04 F8: `--help` said only
|
|
16
|
+
* "changing commands", and an agent tried `start --dry-run`). Derived, so it cannot drift.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DRY_RUN_COMMANDS: readonly string[];
|
|
19
|
+
/** DRY_RUN_COMMANDS on one line, subcommands grouped: `app add/remove/link/unlink`. */
|
|
20
|
+
export declare function dryRunList(): string;
|
|
14
21
|
/** `dokebi <command> --help`: what each flag does, and one example (D-0065, AI-M01). */
|
|
15
22
|
export declare const COMMAND_DETAILS: Readonly<Record<string, {
|
|
16
23
|
readonly flags: readonly (readonly [string, string])[];
|