@dokebi/cli 0.4.1 → 0.4.26

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.
Files changed (188) hide show
  1. package/AI.md +262 -57
  2. package/README.md +35 -10
  3. package/catalog.json +2519 -257
  4. package/describe.schema.json +748 -0
  5. package/dist/api-app.d.ts +2 -0
  6. package/dist/api-app.js +35 -0
  7. package/dist/app-urls.js +3 -2
  8. package/dist/bin.js +527 -35
  9. package/dist/capability-guide.d.ts +30 -0
  10. package/dist/capability-guide.js +122 -0
  11. package/dist/catalog.d.ts +16 -2
  12. package/dist/catalog.js +19 -7
  13. package/dist/check/asset-rules.d.ts +6 -0
  14. package/dist/check/asset-rules.js +163 -0
  15. package/dist/check/browser-sources.d.ts +20 -0
  16. package/dist/check/browser-sources.js +89 -0
  17. package/dist/check/console-rules.d.ts +6 -0
  18. package/dist/check/console-rules.js +40 -0
  19. package/dist/check/error-rules.d.ts +21 -0
  20. package/dist/check/error-rules.js +212 -0
  21. package/dist/check/form-rules.d.ts +12 -0
  22. package/dist/check/form-rules.js +69 -6
  23. package/dist/check/formatter-rules.d.ts +2 -0
  24. package/dist/check/formatter-rules.js +71 -0
  25. package/dist/check/i18n-rules.d.ts +27 -0
  26. package/dist/check/i18n-rules.js +353 -0
  27. package/dist/check/index.d.ts +15 -1
  28. package/dist/check/index.js +89 -11
  29. package/dist/check/mail-rules.d.ts +4 -0
  30. package/dist/check/mail-rules.js +72 -0
  31. package/dist/check/module-rules.d.ts +13 -0
  32. package/dist/check/module-rules.js +104 -0
  33. package/dist/check/packages.d.ts +8 -0
  34. package/dist/check/packages.js +16 -0
  35. package/dist/check/pool-rules.d.ts +8 -0
  36. package/dist/check/pool-rules.js +164 -0
  37. package/dist/check/problems.d.ts +6 -1
  38. package/dist/check/problems.js +100 -2
  39. package/dist/check/project-rules.d.ts +23 -3
  40. package/dist/check/project-rules.js +265 -23
  41. package/dist/check/seal-rules.d.ts +6 -0
  42. package/dist/check/seal-rules.js +68 -0
  43. package/dist/check/security-rules.d.ts +10 -0
  44. package/dist/check/security-rules.js +450 -0
  45. package/dist/check/shared-state-rules.d.ts +2 -0
  46. package/dist/check/shared-state-rules.js +114 -0
  47. package/dist/check/source-rules.d.ts +26 -3
  48. package/dist/check/source-rules.js +249 -28
  49. package/dist/check/structure-rules.d.ts +23 -0
  50. package/dist/check/structure-rules.js +502 -0
  51. package/dist/check/timer-rules.d.ts +7 -0
  52. package/dist/check/timer-rules.js +543 -0
  53. package/dist/check/typescript.d.ts +35 -5
  54. package/dist/check/typescript.js +163 -17
  55. package/dist/check/vite-rules.d.ts +2 -0
  56. package/dist/check/vite-rules.js +136 -0
  57. package/dist/check/wait-rules.d.ts +16 -0
  58. package/dist/check/wait-rules.js +337 -0
  59. package/dist/commands.d.ts +7 -0
  60. package/dist/commands.js +307 -31
  61. package/dist/config-diff.d.ts +42 -0
  62. package/dist/config-diff.js +289 -0
  63. package/dist/config-edit.d.ts +35 -2
  64. package/dist/config-edit.js +145 -13
  65. package/dist/config-init.d.ts +15 -0
  66. package/dist/config-init.js +61 -0
  67. package/dist/config-paths.d.ts +22 -0
  68. package/dist/config-paths.js +82 -0
  69. package/dist/config-show.d.ts +82 -0
  70. package/dist/config-show.js +563 -0
  71. package/dist/config-values.d.ts +8 -0
  72. package/dist/config-values.js +68 -0
  73. package/dist/configuration-scaffold.d.ts +42 -0
  74. package/dist/configuration-scaffold.js +258 -4
  75. package/dist/configuration.d.ts +63 -3
  76. package/dist/configuration.js +178 -4
  77. package/dist/deployment.d.ts +2 -0
  78. package/dist/deployment.js +111 -0
  79. package/dist/describe-db.d.ts +22 -0
  80. package/dist/describe-db.js +176 -0
  81. package/dist/describe.d.ts +92 -0
  82. package/dist/describe.js +417 -0
  83. package/dist/dev-compose.d.ts +20 -0
  84. package/dist/dev-compose.js +154 -0
  85. package/dist/dev-guard.js +44 -0
  86. package/dist/dev-host.d.ts +13 -0
  87. package/dist/dev-host.js +55 -0
  88. package/dist/dev-infra.d.ts +72 -4
  89. package/dist/dev-infra.js +237 -17
  90. package/dist/dev-watch.d.ts +34 -0
  91. package/dist/dev-watch.js +192 -0
  92. package/dist/diagnostics.d.ts +10 -7
  93. package/dist/diagnostics.js +22 -4
  94. package/dist/drift.d.ts +50 -0
  95. package/dist/drift.js +218 -0
  96. package/dist/env-file.d.ts +25 -1
  97. package/dist/env-file.js +64 -6
  98. package/dist/errors.d.ts +7 -1
  99. package/dist/example-config.d.ts +2 -0
  100. package/dist/example-config.js +3 -0
  101. package/dist/first-admin.d.ts +43 -0
  102. package/dist/first-admin.js +175 -0
  103. package/dist/formatter-ignore.d.ts +37 -0
  104. package/dist/formatter-ignore.js +344 -0
  105. package/dist/generator.d.ts +45 -3
  106. package/dist/generator.js +205 -15
  107. package/dist/guide.d.ts +1 -1
  108. package/dist/guide.js +42 -21
  109. package/dist/install-module.d.ts +29 -3
  110. package/dist/install-module.js +142 -13
  111. package/dist/install-plan.js +5 -3
  112. package/dist/layout.d.ts +12 -1
  113. package/dist/layout.js +19 -6
  114. package/dist/log-writer-control.d.ts +2 -0
  115. package/dist/log-writer-control.js +106 -27
  116. package/dist/log-writer.js +43 -10
  117. package/dist/logging-config.d.ts +8 -3
  118. package/dist/logging-config.js +19 -5
  119. package/dist/manifest.d.ts +29 -1
  120. package/dist/manifest.js +84 -6
  121. package/dist/merge-install-defaults.d.ts +0 -1
  122. package/dist/merge-install-defaults.js +13 -47
  123. package/dist/npm-client.d.ts +59 -4
  124. package/dist/npm-client.js +220 -16
  125. package/dist/npm-paths.js +18 -4
  126. package/dist/openapi.d.ts +27 -0
  127. package/dist/openapi.js +649 -0
  128. package/dist/option-fixes.d.ts +39 -0
  129. package/dist/option-fixes.js +130 -0
  130. package/dist/output.d.ts +1 -1
  131. package/dist/output.js +5 -5
  132. package/dist/plugin-lifecycle.d.ts +34 -2
  133. package/dist/plugin-lifecycle.js +206 -20
  134. package/dist/plugin-options.d.ts +19 -4
  135. package/dist/plugin-options.js +138 -6
  136. package/dist/plugins-command.js +44 -7
  137. package/dist/process-group.d.ts +39 -0
  138. package/dist/process-group.js +132 -0
  139. package/dist/project-guide.d.ts +19 -1
  140. package/dist/project-guide.js +119 -20
  141. package/dist/project-inspector.d.ts +15 -0
  142. package/dist/project-inspector.js +481 -33
  143. package/dist/project-state.d.ts +5 -0
  144. package/dist/project-state.js +8 -0
  145. package/dist/project.d.ts +203 -19
  146. package/dist/project.js +3016 -336
  147. package/dist/recipe-facts.d.ts +127 -0
  148. package/dist/recipe-facts.js +563 -0
  149. package/dist/recipe-login.d.ts +63 -0
  150. package/dist/recipe-login.js +464 -0
  151. package/dist/recipe-mail.d.ts +21 -0
  152. package/dist/recipe-mail.js +214 -0
  153. package/dist/recipe-members.d.ts +35 -0
  154. package/dist/recipe-members.js +178 -0
  155. package/dist/recipe-record.d.ts +82 -7
  156. package/dist/recipe-record.js +226 -22
  157. package/dist/recipe-reset.d.ts +2 -0
  158. package/dist/recipe-reset.js +117 -0
  159. package/dist/recipe-site.d.ts +48 -0
  160. package/dist/recipe-site.js +396 -0
  161. package/dist/recipe-step.d.ts +42 -0
  162. package/dist/recipe-step.js +89 -0
  163. package/dist/recipe.d.ts +56 -20
  164. package/dist/recipe.js +703 -131
  165. package/dist/registry.d.ts +98 -2
  166. package/dist/registry.js +171 -5
  167. package/dist/reloadable.d.ts +35 -0
  168. package/dist/reloadable.js +257 -0
  169. package/dist/server.d.ts +50 -4
  170. package/dist/server.js +790 -103
  171. package/dist/settings.d.ts +48 -0
  172. package/dist/settings.js +168 -0
  173. package/dist/site-auth.d.ts +39 -0
  174. package/dist/site-auth.js +195 -0
  175. package/dist/template-fixes.d.ts +26 -0
  176. package/dist/template-fixes.js +285 -0
  177. package/dist/test-reporter.d.ts +49 -0
  178. package/dist/test-reporter.js +190 -0
  179. package/dist/transaction.d.ts +67 -0
  180. package/dist/transaction.js +428 -46
  181. package/dist/verify-tests.d.ts +98 -0
  182. package/dist/verify-tests.js +449 -0
  183. package/dist/verify.d.ts +113 -4
  184. package/dist/verify.js +326 -28
  185. package/dist/view-choice.d.ts +58 -0
  186. package/dist/view-choice.js +131 -0
  187. package/dist/vite-watch.js +4 -2
  188. package/package.json +8 -6
@@ -1,7 +1,11 @@
1
1
  // Source rules of `dokebi check` (D-0072): one walk over each file's TypeScript 7 AST. Each rule
2
2
  // matches only shapes that are wrong whatever the surrounding code is (no heuristics on names):
3
3
  // empty-catch `catch {}`, `.catch(() => {})`, `.then(ok, () => {})`, an empty
4
- // `on('error', …)` listener (D-0047)
4
+ // `on('error', …)` listener, and the same handlers that only give back
5
+ // nothing (`() => undefined`, `() => null`, `() => void 0`) (D-0047)
6
+ // missing-cause a catch block that throws a new error and never reads the one it
7
+ // caught (`catch { throw new X() }`): the original error is lost
8
+ // (D-0047, D-0118)
5
9
  // floating-promise an expression statement whose call returns a Promise (the checker's
6
10
  // type) that is neither awaited, returned, `void`ed nor `.catch`ed;
7
11
  // node:test's test()/describe() are exempt (their promise never rejects)
@@ -9,9 +13,27 @@
9
13
  // values, parameter properties, decorators, `import x =`, `export =`, `<T>x`
10
14
  // private-import an `@dokebi/*` path outside the package exports (packages.ts)
11
15
  // missing-package an `@dokebi/*` package that is not installed, absent or unknown
16
+ // and, when their plugin is installed, the security rules of security-rules.ts (D-0076 §6), and
17
+ // when src/shared/errors.ts declares the app's codes, unknown-error-code (error-rules.ts, D-0077),
18
+ // and in server code background-timer (timer-rules.ts), console-output (console-rules.ts,
19
+ // D-0151) and the structure warnings
20
+ // responsibility-size and memory-filtered-page (structure-rules.ts, D-0105), and
21
+ // module-point-not-installed (module-rules.ts, D-0130).
22
+ // Browser code outside the program (.vue scripts, views/ .js; browser-sources.ts, D-0120) gets
23
+ // only empty-catch, missing-cause and floating-promise, typed by its own program.
12
24
  import { relative, sep } from 'node:path';
13
- import { MARKABLE } from './problems.js';
25
+ import { assetRouteRules } from './asset-rules.js';
26
+ import { consoleRules } from './console-rules.js';
27
+ import { ERRORS_FILE, errorCodeRules, isAppCode, readErrorList, } from './error-rules.js';
28
+ import { mailRules } from './mail-rules.js';
29
+ import { modulePoints } from './module-rules.js';
30
+ import { poolRules } from './pool-rules.js';
31
+ import { BROWSER_RULES, MARKABLE, WARNING_RULES } from './problems.js';
32
+ import { securityRules } from './security-rules.js';
33
+ import { layerOf, structureRules } from './structure-rules.js';
34
+ import { isServerCode, timerRules } from './timer-rules.js';
14
35
  import { field, nodes, property, } from './typescript.js';
36
+ import { ownerCapRules, queueRules, waitRules } from './wait-rules.js';
15
37
  const LISTEN = new Set([
16
38
  'on',
17
39
  'once',
@@ -22,7 +44,7 @@ const LISTEN = new Set([
22
44
  ]);
23
45
  const SNIPPET = 60;
24
46
  /** `// dokebi-check: allow <rule> <reason>` (also inside a block comment). */
25
- const MARKER = /dokebi-check:\s*allow\s+([a-z-]+)[ \t]*(?:[:\-—–][ \t]*)?([^\n]*?)[ \t]*(?:\*\/|$)/gm;
47
+ const MARKER = /dokebi-check:\s*allow\s+([a-z0-9-]+)[ \t]*(?:[:\-—–][ \t]*)?([^\n]*?)[ \t]*(?:\*\/|$)/gm;
26
48
  export function readMarkers(text) {
27
49
  const markers = [];
28
50
  let lineStart = 0;
@@ -43,16 +65,31 @@ export function readMarkers(text) {
43
65
  return markers;
44
66
  }
45
67
  /** Walks one file: all syntactic problems, and the statements the checker must judge. */
46
- export async function inspectFile(file, kinds, root, imports) {
47
- const path = relative(root, file.fileName.split('/').join(sep)).split(sep).join('/');
68
+ export async function inspectFile(file, kinds, root, imports,
69
+ /** Installed plugin ids: the security rules run only for the plugin they protect. */
70
+ installed = new Set(),
71
+ /** The codes of src/shared/errors.ts (D-0077 D); unknown-error-code runs only with them. */
72
+ errorList,
73
+ /**
74
+ * D-0120: a virtual browser source — the file problems name, and the only rules that run
75
+ * (the per-plugin, server and import rules do not apply to browser code).
76
+ */
77
+ browser) {
78
+ const path = browser?.path ?? relative(root, file.fileName.split('/').join(sep)).split(sep).join('/');
48
79
  const text = file.text;
49
80
  const markers = readMarkers(text);
50
81
  const lineOf = (position) => file.getLineAndCharacterOfPosition(position).line + 1;
51
82
  const problems = [];
52
83
  const candidates = [];
84
+ const untypedImports = [];
53
85
  const specifiers = [];
86
+ /** console-output (D-0151): a marker before the first statement covers the whole file. */
87
+ const firstStatement = nodes(file, 'statements')[0];
88
+ const headerEnd = firstStatement ? lineOf(firstStatement.getStart()) : Number.POSITIVE_INFINITY;
54
89
  /** The problem, or nothing when a marker with a reason sits on or just above its lines. */
55
90
  const report = (node, rule, message, fix) => {
91
+ if (browser && !browser.rules.has(rule))
92
+ return undefined;
56
93
  const first = lineOf(node.getStart());
57
94
  const last = lineOf(Math.max(node.getStart(), node.end - 1));
58
95
  let note = '';
@@ -60,12 +97,21 @@ export async function inspectFile(file, kinds, root, imports) {
60
97
  for (const marker of markers)
61
98
  if (marker.rule === rule &&
62
99
  marker.line <= last &&
63
- (marker.line >= first || (marker.line === first - 1 && marker.ownLine))) {
100
+ (marker.line >= first ||
101
+ (marker.line === first - 1 && marker.ownLine) ||
102
+ (rule === 'console-output' && marker.ownLine && marker.line < headerEnd))) {
64
103
  if (marker.reason)
65
104
  return undefined;
66
105
  note = ' (the dokebi-check marker has no reason, so it does not count)';
67
106
  }
68
- const problem = { rule, file: path, line: first, message: message + note, fix };
107
+ const problem = {
108
+ rule,
109
+ ...(WARNING_RULES.has(rule) ? { severity: 'warning' } : {}),
110
+ file: path,
111
+ line: first,
112
+ message: message + note,
113
+ fix,
114
+ };
69
115
  return problem;
70
116
  };
71
117
  const push = (problem) => {
@@ -80,12 +126,57 @@ export async function inspectFile(file, kinds, root, imports) {
80
126
  ? String(property(node, 'text'))
81
127
  : undefined;
82
128
  const hasModifier = (node, kind) => nodes(node, 'modifiers').some((modifier) => modifier.kind === kind);
83
- /** An arrow or function expression whose body is a block without statements. */
84
- const emptyFunction = (node) => {
129
+ const unwrap = (node) => {
130
+ let current = node;
131
+ while (current?.kind === kinds.ParenthesizedExpression)
132
+ current = field(current, 'expression');
133
+ return current;
134
+ };
135
+ /**
136
+ * An expression that does nothing: `undefined`, `null`, or `void` of a name or a literal
137
+ * (`void 0`, `void error`). A call (`void log(error)`) or any other value is not one: a
138
+ * fallback value such as `false` is the handler's decision, not a drop.
139
+ */
140
+ const nothing = (node) => {
141
+ const value = unwrap(node);
142
+ if (!value)
143
+ return false;
144
+ if (value.kind === kinds.NullKeyword)
145
+ return true;
146
+ if (value.kind === kinds.Identifier)
147
+ return property(value, 'text') === 'undefined';
148
+ if (value.kind !== kinds.VoidExpression)
149
+ return false;
150
+ const operand = unwrap(field(value, 'expression'));
151
+ return (operand !== undefined &&
152
+ (operand.kind === kinds.Identifier ||
153
+ operand.kind === kinds.NullKeyword ||
154
+ operand.kind === kinds.TrueKeyword ||
155
+ operand.kind === kinds.FalseKeyword ||
156
+ operand.kind === kinds.NumericLiteral ||
157
+ operand.kind === kinds.StringLiteral ||
158
+ operand.kind === kinds.NoSubstitutionTemplateLiteral));
159
+ };
160
+ /**
161
+ * An arrow or function expression that drops what it gets (D-0047): an empty block, a block
162
+ * that only returns nothing (`return;`, `return undefined;`, `return null;`), or an arrow whose
163
+ * body is nothing (`() => undefined`, `() => null`, `() => void 0`).
164
+ */
165
+ const emptyFunction = (argument) => {
166
+ const node = unwrap(argument);
85
167
  if (!node || (node.kind !== kinds.ArrowFunction && node.kind !== kinds.FunctionExpression))
86
168
  return false;
87
169
  const body = field(node, 'body');
88
- return body?.kind === kinds.Block && nodes(body, 'statements').length === 0;
170
+ if (body?.kind !== kinds.Block)
171
+ return nothing(body);
172
+ const statements = nodes(body, 'statements');
173
+ if (statements.length === 0)
174
+ return true;
175
+ const only = statements.length === 1 ? statements[0] : undefined;
176
+ if (only?.kind !== kinds.ReturnStatement)
177
+ return false;
178
+ const returned = field(only, 'expression');
179
+ return returned === undefined || nothing(returned);
89
180
  };
90
181
  /** The method name of `a.b(...)`, if the callee is a property access. */
91
182
  const methodName = (call) => {
@@ -95,12 +186,6 @@ export async function inspectFile(file, kinds, root, imports) {
95
186
  const name = field(callee, 'name');
96
187
  return name?.kind === kinds.Identifier ? String(property(name, 'text')) : undefined;
97
188
  };
98
- const unwrap = (node) => {
99
- let current = node;
100
- while (current?.kind === kinds.ParenthesizedExpression)
101
- current = field(current, 'expression');
102
- return current;
103
- };
104
189
  /** Namespaces with only types (or `declare`) are erased; anything else makes a value. */
105
190
  const instantiated = (body) => {
106
191
  if (!body)
@@ -146,26 +231,95 @@ export async function inspectFile(file, kinds, root, imports) {
146
231
  current = field(current, 'expression');
147
232
  return current?.kind === kinds.Identifier ? String(property(current, 'text')) : undefined;
148
233
  };
234
+ const functionKinds = new Set([
235
+ kinds.ArrowFunction,
236
+ kinds.FunctionExpression,
237
+ kinds.FunctionDeclaration,
238
+ kinds.MethodDeclaration,
239
+ kinds.Constructor,
240
+ kinds.GetAccessor,
241
+ kinds.SetAccessor,
242
+ kinds.ClassDeclaration,
243
+ kinds.ClassExpression,
244
+ ]);
245
+ /**
246
+ * D-0118: a catch block that throws a new error (`throw new X(…)`, or `throw fail(…)` — a call
247
+ * makes a new error too, AI-M46; outside nested functions) and never reads the error it caught —
248
+ * no binding, or a plain name that is never used. A destructured binding counts as read.
249
+ */
250
+ const losesCause = (clause, block) => {
251
+ const declaration = field(clause, 'variableDeclaration');
252
+ const binding = declaration && field(declaration, 'name');
253
+ if (binding && binding.kind !== kinds.Identifier)
254
+ return false;
255
+ const name = binding ? String(property(binding, 'text')) : undefined;
256
+ let throwsNew = false;
257
+ let reads = false;
258
+ const walk = (current, inFunction) => {
259
+ if (current.kind === kinds.ThrowStatement && !inFunction) {
260
+ const thrown = unwrap(field(current, 'expression'));
261
+ if (thrown?.kind === kinds.NewExpression || thrown?.kind === kinds.CallExpression)
262
+ throwsNew = true;
263
+ }
264
+ if (name !== undefined &&
265
+ current.kind === kinds.Identifier &&
266
+ property(current, 'text') === name)
267
+ reads = true;
268
+ const nested = inFunction || functionKinds.has(current.kind);
269
+ current.forEachChild((child) => walk(child, nested));
270
+ return undefined;
271
+ };
272
+ block.forEachChild((child) => walk(child, false));
273
+ return throwsNew && !reads;
274
+ };
275
+ /**
276
+ * The value names an import binds (not `import type`, not a .vue component: a component is
277
+ * never called, and a project without a shim types none). Judged after the walk.
278
+ */
279
+ const untypedImport = (node, value) => {
280
+ const clause = field(node, 'importClause');
281
+ if (!clause || property(clause, 'isTypeOnly') === true)
282
+ return;
283
+ if (/\.(?:vue|css|scss|sass|less|svg|png|jpe?g|gif|webp|json)(?:\?.*)?$/.test(value))
284
+ return;
285
+ const bindings = field(clause, 'namedBindings');
286
+ const names = [
287
+ field(clause, 'name'),
288
+ bindings?.kind === kinds.NamespaceImport ? field(bindings, 'name') : undefined,
289
+ ...(bindings && bindings.kind !== kinds.NamespaceImport
290
+ ? nodes(bindings, 'elements')
291
+ .filter((element) => property(element, 'isTypeOnly') !== true)
292
+ .map((element) => field(element, 'name'))
293
+ : []),
294
+ ].filter((name) => name?.kind === kinds.Identifier);
295
+ if (!names.length)
296
+ return;
297
+ const problem = report(node, 'untyped-browser-import', `'${value}' has no types in the browser program: floating-promise cannot judge the calls of what this file imports from it`, `make its types resolve as the bundler does (the package's types, a declaration file such as src/infra/inertia/page.d.ts, or a relative path that exists); when the module really has no types, write // dokebi-check: allow untyped-browser-import <reason>`);
298
+ if (problem)
299
+ untypedImports.push({ names, problem });
300
+ };
149
301
  const visit = (node) => {
150
302
  const kind = node.kind;
151
303
  if (kind === kinds.CatchClause) {
152
304
  const block = field(node, 'block');
153
305
  if (block && nodes(block, 'statements').length === 0)
154
306
  push(report(node, 'empty-catch', 'An empty catch block hides the error (D-0047: no silent catch)', 'rethrow it, turn it into a coded error for the caller, or log it at error level (fixed message, code, error); if ignoring is really right, write // dokebi-check: allow empty-catch <reason> in the block'));
307
+ else if (block && losesCause(node, block))
308
+ push(report(node, 'missing-cause', 'This catch throws a new error and drops the one it caught: the original message and stack are lost (D-0047)', "name the caught error and pass it on: catch (error) { throw new X(…, { cause: error }) } (or the error class's cause argument); if the original must not travel, log it at debug first, or write // dokebi-check: allow missing-cause <reason>"));
155
309
  }
156
310
  else if (kind === kinds.CallExpression) {
157
311
  const args = nodes(node, 'arguments');
158
312
  const method = methodName(node);
159
313
  const callee = field(node, 'expression');
160
314
  if (method === 'catch' && emptyFunction(args[0]))
161
- push(report(node, 'empty-catch', `${snippet(node)} drops the rejection with an empty handler (D-0047)`, 'handle or log the error in the handler (error level, code, error); to keep only an unhandled-rejection guard on a promise you also return, write // dokebi-check: allow empty-catch <reason>'));
315
+ push(report(node, 'empty-catch', `${snippet(node)} drops the rejection with a handler that does nothing (D-0047)`, 'handle or log the error in the handler (error level, code, error); to keep only an unhandled-rejection guard on a promise you also return, write // dokebi-check: allow empty-catch <reason>'));
162
316
  else if (method === 'then' && emptyFunction(args[1]))
163
- push(report(node, 'empty-catch', `${snippet(node)} drops the rejection with an empty onRejected handler (D-0047)`, 'handle or log the error in the second handler, or remove it and let the caller see the rejection'));
317
+ push(report(node, 'empty-catch', `${snippet(node)} drops the rejection with an onRejected handler that does nothing (D-0047)`, 'handle or log the error in the second handler, or remove it and let the caller see the rejection'));
164
318
  else if (method &&
165
319
  LISTEN.has(method) &&
166
320
  stringValue(args[0]) === 'error' &&
167
321
  emptyFunction(args[1]))
168
- push(report(node, 'empty-catch', `An empty '${method}('error', …)' listener hides every error of that emitter (D-0047)`, 'log the error (error level, code, error) and close or replace the resource; write // dokebi-check: allow empty-catch <reason> only for a deliberate drop'));
322
+ push(report(node, 'empty-catch', `A '${method}('error', …)' listener that does nothing hides every error of that emitter (D-0047)`, 'log the error (error level, code, error) and close or replace the resource; write // dokebi-check: allow empty-catch <reason> only for a deliberate drop'));
169
323
  if (callee?.kind === kinds.ImportKeyword) {
170
324
  const value = stringValue(args[0]);
171
325
  if (value !== undefined)
@@ -183,7 +337,11 @@ export async function inspectFile(file, kinds, root, imports) {
183
337
  (method === 'then' && args.length >= 2) ||
184
338
  (expression.kind === kinds.CallExpression && testNames.has(calleeRoot(expression) ?? ''));
185
339
  if (!handled) {
186
- const problem = report(node, 'floating-promise', `${snippet(expression)} returns a Promise that nothing awaits: its rejection becomes an unhandled rejection and stops the process`, 'await it (or return it to the caller); for deliberate background work end it with .catch(error => <log at error level>)');
340
+ const problem = report(node, 'floating-promise', browser
341
+ ? `${snippet(expression)} returns a Promise that nothing awaits: its rejection becomes an unhandled rejection in the browser that the page never handles or shows`
342
+ : `${snippet(expression)} returns a Promise that nothing awaits: its rejection becomes an unhandled rejection and stops the process`, browser
343
+ ? 'await it (or return it to the caller); for deliberate background work end it with .catch(error => <show or report the error>)'
344
+ : 'await it (or return it to the caller); for deliberate background work end it with .catch(error => <log at error level>)');
187
345
  if (problem)
188
346
  candidates.push({ expression, problem });
189
347
  }
@@ -231,6 +389,8 @@ export async function inspectFile(file, kinds, root, imports) {
231
389
  const value = stringValue(specifier);
232
390
  if (value !== undefined && specifier)
233
391
  specifiers.push({ node: specifier, value });
392
+ if (browser && kind === kinds.ImportDeclaration && value !== undefined)
393
+ untypedImport(node, value);
234
394
  }
235
395
  else if (kind === kinds.ImportType) {
236
396
  const argument = field(node, 'argument');
@@ -243,31 +403,92 @@ export async function inspectFile(file, kinds, root, imports) {
243
403
  return undefined;
244
404
  };
245
405
  file.forEachChild(visit);
406
+ if (browser)
407
+ return { problems, candidates, imports: untypedImports };
408
+ if (installed.has('dokebi-mail'))
409
+ mailRules(file, kinds, (node, rule, message, fix) => push(report(node, rule, message, fix)));
410
+ // D-0105: periodic work on the app's own timers, and files that carry several responsibilities.
411
+ if (isServerCode(path)) {
412
+ timerRules(file, kinds, (node, message, fix, rule = 'background-timer') => push(report(node, rule, message, fix)));
413
+ consoleRules(file, kinds, (node, message, fix) => push(report(node, 'console-output', message, fix)));
414
+ waitRules(file, kinds, (node, message, fix) => push(report(node, 'wait-count-refusal', message, fix)));
415
+ queueRules(file, kinds, (node, message, fix) => push(report(node, 'in-process-queue', message, fix)));
416
+ ownerCapRules(file, kinds, (node, message, fix) => push(report(node, 'fixed-owner-cap', message, fix)));
417
+ poolRules(file, path, kinds, (node, rule, message, fix) => push(report(node, rule, message, fix)));
418
+ assetRouteRules(file, kinds, (node, message, fix) => push(report(node, 'asset-route', message, fix)));
419
+ }
420
+ const layer = layerOf(path);
421
+ if (layer)
422
+ structureRules(file, layer, kinds, (node, rule, message, fix) => push(report(node, rule, message, fix)));
423
+ const auth = installed.has('dokebi-auth');
424
+ const csrf = installed.has('dokebi-csrf');
425
+ if (auth || csrf)
426
+ securityRules(file, kinds, { auth, csrf }, (node, rule, message, fix) => push(report(node, rule, message, fix)));
427
+ if (errorList && isAppCode(path))
428
+ errorCodeRules(file, path, kinds, errorList, (node, message, fix) => push(report(node, 'unknown-error-code', message, fix)));
246
429
  for (const { node, value } of specifiers) {
247
430
  const verdict = await imports.verdict(file.fileName, value);
248
431
  if (verdict)
249
432
  push(report(node, verdict.rule, verdict.message, verdict.fix));
250
433
  }
434
+ // D-0130: a module point whose plugin package is present but not installed in the project.
435
+ for (const point of modulePoints(file, kinds)) {
436
+ const plugin = await imports.pluginOf(file.fileName, point.specifier);
437
+ if (!plugin || installed.has(plugin.id))
438
+ continue;
439
+ push(report(point.node, 'module-point-not-installed', `Module ${point.module || '(unnamed)'} contributes to ${point.point} of ${plugin.package}, but this project has not installed ${plugin.id}: nothing registers the plugin that collects it, so the app drops the values (warn POINT_NOT_COLLECTED)`, `npx dokebi add ${plugin.package} --dry-run --json, then without --dry-run (read its AI.md first); or remove ${point.point} from contributes. When you register the plugin yourself, write // dokebi-check: allow module-point-not-installed <reason>`));
440
+ }
251
441
  return { problems, candidates };
252
442
  }
253
443
  /** Every program file: syntactic problems, then one batched checker call for the candidates. */
254
- export async function sourceProblems(typescript, root, imports, skip) {
444
+ export async function sourceProblems(typescript, root, imports, skip, installed = new Set()) {
255
445
  const problems = [];
256
446
  const candidates = [];
257
447
  let files = 0;
448
+ const pathOf = (file) => relative(root, file.fileName.split('/').join(sep)).split(sep).join('/');
449
+ const errorsFile = typescript.files.find((file) => pathOf(file) === ERRORS_FILE);
450
+ const errorList = errorsFile ? readErrorList(errorsFile, typescript.kinds) : undefined;
258
451
  for (const file of typescript.files) {
259
- const path = relative(root, file.fileName.split('/').join(sep)).split(sep).join('/');
452
+ const path = pathOf(file);
260
453
  if (skip(path))
261
454
  continue;
262
455
  files++;
263
- const found = await inspectFile(file, typescript.kinds, root, imports);
456
+ const found = await inspectFile(file, typescript.kinds, root, imports, installed, errorList);
264
457
  problems.push(...found.problems);
265
458
  candidates.push(...found.candidates);
266
459
  }
267
- const verdicts = typescript.promises(candidates.map((candidate) => candidate.expression));
268
- candidates.forEach((candidate, index) => {
269
- if (verdicts[index])
270
- problems.push(candidate.problem);
271
- });
460
+ const browserCandidates = [];
461
+ const browserImports = [];
462
+ for (const { file, path } of typescript.browser) {
463
+ if (skip(path))
464
+ continue;
465
+ files++;
466
+ const found = await inspectFile(file, typescript.kinds, root, imports, installed, undefined, {
467
+ path,
468
+ rules: BROWSER_RULES,
469
+ });
470
+ problems.push(...found.problems);
471
+ browserCandidates.push(...found.candidates);
472
+ browserImports.push(...(found.imports ?? []));
473
+ }
474
+ // D-0120 §1.4: an import none of whose names has a type is reported, never passed silently.
475
+ const untyped = typescript.untyped(browserImports.flatMap((item) => item.names), true);
476
+ let at = 0;
477
+ for (const item of browserImports) {
478
+ const verdicts = untyped.slice(at, at + item.names.length);
479
+ at += item.names.length;
480
+ if (verdicts.every(Boolean))
481
+ problems.push(item.problem);
482
+ }
483
+ for (const [list, browser] of [
484
+ [candidates, false],
485
+ [browserCandidates, true],
486
+ ]) {
487
+ const verdicts = typescript.promises(list.map((candidate) => candidate.expression), browser);
488
+ list.forEach((candidate, index) => {
489
+ if (verdicts[index])
490
+ problems.push(candidate.problem);
491
+ });
492
+ }
272
493
  return { problems, files };
273
494
  }
@@ -0,0 +1,23 @@
1
+ import { type Kinds, type TsNode, type TsSourceFile } from './typescript.js';
2
+ export type StructureRule = 'responsibility-size' | 'memory-filtered-page';
3
+ export type Report = (node: TsNode, rule: StructureRule, message: string, fix: string) => void;
4
+ /** D-0105 §3: the smallest measured model that carried several features had 486 code lines. */
5
+ export declare const MODEL_LINES = 400;
6
+ export declare const MODEL_TABLES = 8;
7
+ /**
8
+ * D-0105 §3: the controllers that took every route of their app had 22–34 handlers (176–467
9
+ * code lines); every other controller measured had at most 16 handlers and 214 code lines.
10
+ */
11
+ export declare const CONTROLLER_HANDLERS = 20;
12
+ export declare const CONTROLLER_LINES = 300;
13
+ /** D-0105 §3: services that only forwarded had 10–15 such methods out of 15–21. */
14
+ export declare const SERVICE_FORWARDS = 5;
15
+ export declare const FORWARD_SHARE = 0.6;
16
+ export type Layer = 'model' | 'controller' | 'service';
17
+ /** The layer of an app file by its folder (D-0031, D-0032), or undefined outside them. */
18
+ export declare function layerOf(path: string): Layer | undefined;
19
+ /** Lines with code: comments and blank lines do not count (string contents do, SQL is code). */
20
+ export declare function codeLines(text: string): number;
21
+ /** Table names in one piece of SQL text (EXTRACT(… FROM x) and CTE names excluded). */
22
+ export declare function sqlTables(sql: string, into: Set<string>): void;
23
+ export declare function structureRules(file: TsSourceFile, layer: Layer, kinds: Kinds, report: Report): void;