@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
@@ -0,0 +1,104 @@
1
+ // D-0130: a module that contributes to a point (`defineModule({ contributes: [mcpTools] })`)
2
+ // needs the plugin that owns the point in the project. At start core drops values that no
3
+ // registered plugin collects (warn POINT_NOT_COLLECTED, so module tests without that plugin
4
+ // keep working); check makes the missing plugin an error before verify reports done.
5
+ // module-point-not-installed a point of `contributes` imported from an @dokebi/* plugin
6
+ // package that is in node_modules but not installed in this
7
+ // project (`dokebi add`), so the generated code never registers it
8
+ // A package missing from node_modules is already missing-package on its import.
9
+ import { field, nodes, property, } from './typescript.js';
10
+ const text = (node) => node ? String(property(node, 'text') ?? '') : '';
11
+ /**
12
+ * The points of every `defineModule({ contributes: [...] })` of the file whose `defineModule`
13
+ * comes from @dokebi/module, when the point is a name (or `ns.name`) imported from a package.
14
+ */
15
+ export function modulePoints(file, kinds) {
16
+ if (!file.text.includes('defineModule') || !file.text.includes('contributes'))
17
+ return [];
18
+ const bindings = new Map();
19
+ const arrays = new Map();
20
+ for (const statement of nodes(file, 'statements')) {
21
+ if (statement.kind === kinds.ImportDeclaration) {
22
+ const specifier = field(statement, 'moduleSpecifier');
23
+ const clause = field(statement, 'importClause');
24
+ if (!specifier || !clause || property(clause, 'isTypeOnly') === true)
25
+ continue;
26
+ const from = text(specifier);
27
+ const named = field(clause, 'namedBindings');
28
+ const names = [
29
+ [field(clause, 'name'), undefined],
30
+ [named?.kind === kinds.NamespaceImport ? field(named, 'name') : undefined, undefined],
31
+ ...(named && named.kind !== kinds.NamespaceImport
32
+ ? nodes(named, 'elements')
33
+ .filter((element) => property(element, 'isTypeOnly') !== true)
34
+ .map((element) => [
35
+ field(element, 'name'),
36
+ text(field(element, 'propertyName') ?? field(element, 'name')),
37
+ ])
38
+ : []),
39
+ ];
40
+ for (const [name, imported] of names)
41
+ if (name?.kind === kinds.Identifier)
42
+ bindings.set(text(name), { specifier: from, ...(imported ? { imported } : {}) });
43
+ }
44
+ else if (statement.kind === kinds.VariableStatement) {
45
+ // `const points = [mcpTools]` … `contributes: points` (top-level arrays of the same file).
46
+ const list = field(statement, 'declarationList');
47
+ for (const declaration of list ? nodes(list, 'declarations') : []) {
48
+ const name = field(declaration, 'name');
49
+ const value = field(declaration, 'initializer');
50
+ if (name?.kind === kinds.Identifier && value?.kind === kinds.ArrayLiteralExpression)
51
+ arrays.set(text(name), value);
52
+ }
53
+ }
54
+ }
55
+ // The local name of @dokebi/module's defineModule (an alias counts: `as dm`).
56
+ const define = new Set([...bindings]
57
+ .filter(([, binding]) => binding.imported === 'defineModule' && binding.specifier === '@dokebi/module')
58
+ .map(([name]) => name));
59
+ if (!define.size)
60
+ return [];
61
+ const found = [];
62
+ const origin = (element) => {
63
+ let root = element;
64
+ while (root?.kind === kinds.PropertyAccessExpression)
65
+ root = field(root, 'expression');
66
+ return root?.kind === kinds.Identifier ? bindings.get(text(root)) : undefined;
67
+ };
68
+ const visit = (node) => {
69
+ const callee = node.kind === kinds.CallExpression ? field(node, 'expression') : undefined;
70
+ const argument = callee ? nodes(node, 'arguments')[0] : undefined;
71
+ if (callee?.kind === kinds.Identifier &&
72
+ define.has(text(callee)) &&
73
+ argument?.kind === kinds.ObjectLiteralExpression) {
74
+ const properties = nodes(argument, 'properties');
75
+ const module = text(field(properties.find((item) => text(field(item, 'name')) === 'name') ?? argument, 'initializer'));
76
+ for (const item of properties) {
77
+ if (text(field(item, 'name')) !== 'contributes')
78
+ continue;
79
+ let value = item.kind === kinds.ShorthandPropertyAssignment
80
+ ? field(item, 'name')
81
+ : field(item, 'initializer');
82
+ if (value?.kind === kinds.Identifier)
83
+ value = arrays.get(text(value));
84
+ if (value?.kind !== kinds.ArrayLiteralExpression)
85
+ continue;
86
+ for (const element of nodes(value, 'elements')) {
87
+ const binding = origin(element);
88
+ if (!binding)
89
+ continue;
90
+ found.push({
91
+ node: element,
92
+ module,
93
+ point: file.text.slice(element.getStart(), element.end),
94
+ specifier: binding.specifier,
95
+ });
96
+ }
97
+ }
98
+ }
99
+ node.forEachChild(visit);
100
+ return undefined;
101
+ };
102
+ file.forEachChild(visit);
103
+ return found;
104
+ }
@@ -12,6 +12,14 @@ export declare function publicPaths(name: string, exports: unknown): string[];
12
12
  export declare class DokebiImports {
13
13
  #private;
14
14
  constructor(catalog: Catalog);
15
+ /**
16
+ * D-0130: the plugin of an `@dokebi/*` specifier that resolves from `file` (the catalog's id),
17
+ * or undefined (not a Dokebi plugin package, or not in node_modules: missing-package says so).
18
+ */
19
+ pluginOf(file: string, specifier: string): Promise<{
20
+ readonly id: string;
21
+ readonly package: string;
22
+ } | undefined>;
15
23
  /** The problem with one module specifier of `file`, if any. */
16
24
  verdict(file: string, specifier: string): Promise<ImportVerdict | undefined>;
17
25
  }
@@ -104,6 +104,22 @@ export class DokebiImports {
104
104
  this.#found.set(key, result);
105
105
  return result;
106
106
  }
107
+ /**
108
+ * D-0130: the plugin of an `@dokebi/*` specifier that resolves from `file` (the catalog's id),
109
+ * or undefined (not a Dokebi plugin package, or not in node_modules: missing-package says so).
110
+ */
111
+ async pluginOf(file, specifier) {
112
+ const match = PACKAGE.exec(specifier);
113
+ if (!match)
114
+ return undefined;
115
+ const name = `${SCOPE}${match[1]}`;
116
+ const known = this.#catalog.packages.find((item) => item.package === name);
117
+ if (!known?.id || known.kind === 'core' || known.kind === 'cli')
118
+ return undefined;
119
+ if (!(await this.#installed(file, name)))
120
+ return undefined;
121
+ return { id: known.id, package: name };
122
+ }
107
123
  /** The problem with one module specifier of `file`, if any. */
108
124
  async verdict(file, specifier) {
109
125
  const viaPath = /(^|\/)node_modules\/(@dokebi\/[^/]+)/.exec(specifier);
@@ -0,0 +1,8 @@
1
+ import { type Kinds, type TsNode, type TsSourceFile } from './typescript.js';
2
+ export type PoolReport = (node: TsNode, rule: 'private-pool' | 'overload-off' | 'readiness-off', message: string, fix: string) => void;
3
+ export declare const PRIVATE_POOL_FIX = "take database connections from @dokebi/db: the configured one (context.getDependency(db).connection(main)), another pool to the same database for one kind of work (await context.getDependency(db).derive('main', { purpose: 'export', overrides: { max: 4 } }) in setup, closed by dokebi-db), or another database as one more connection in dokebi.yaml (dokebi-db.connections.<name>); its size and waitTimeoutMs then come from dokebi.yaml and a caller past the wait gets 503 DB_UNAVAILABLE with Retry-After. A connection that must stay outside (a LISTEN client) takes // dokebi-check: allow private-pool <reason>";
4
+ export declare const OVERLOAD_OFF_FIX = "remove overload: false: under a burst the route then answers 503 OVERLOADED with Retry-After before its handler runs, and the client retries, instead of queueing behind a stalled worker (@dokebi/web AI.md 2.5; raise overload.maxEventLoopDelayMs in dokebi.yaml if it refuses too early). Only a metrics scrape or an operator status page stays served: // dokebi-check: allow overload-off <reason>";
5
+ export declare const READINESS_OFF_FIX = "remove readiness: false: while a required dependency (a database, Redis) is down the route then answers 503 NOT_READY with Retry-After instead of running its handler against it (@dokebi/web AI.md). Only a metrics scrape or an operator status page, which must answer during an outage, keeps it: // dokebi-check: allow readiness-off <reason>";
6
+ /** The adapters of @dokebi/db live here: the one place that makes pools. */
7
+ export declare function isDatabaseInfra(path: string): boolean;
8
+ export declare function poolRules(file: TsSourceFile, path: string, kinds: Kinds, report: PoolReport): void;
@@ -0,0 +1,164 @@
1
+ // The pool and overload rules of `dokebi check` (AI-M37), over the TypeScript AST:
2
+ // private-pool (error) server code under src/ outside src/infra/db/ that makes a database
3
+ // pool or client of its own with the driver: `new Pool`/`new Client` of pg,
4
+ // `createPool`/`createConnection`/`createPoolCluster` of mysql2 and mariadb,
5
+ // `new MongoClient` of mongodb. Such a pool escapes dokebi.yaml (its size, its
6
+ // wait deadline waitTimeoutMs and the 503 with Retry-After past it), the
7
+ // connection sum of @dokebi/db AI.md 1.2 and the cleanup at stop (M37: five pools
8
+ // per worker with a 30 s wait held callers for 22-51 s instead of refusing).
9
+ // Another pool to the same database is `derive(name, { purpose, overrides })`
10
+ // of @dokebi/db; another database is one more connection in dokebi.yaml.
11
+ // overload-off (error) a route config with `overload: false`: the route is served while the
12
+ // server sheds load, so under a burst its callers queue behind a stalled event
13
+ // loop instead of getting 503 OVERLOADED with Retry-After (@dokebi/web AI.md
14
+ // 2.5; M37 turned it off on public ticket intake). It is meant for a metrics
15
+ // scrape or an operator's status page, which keep it with a marker.
16
+ // readiness-off (error) a route config with `readiness: false` (M51): the route is served
17
+ // while a required dependency is down and while the app drains, so an API route
18
+ // with it runs its handler against a missing database or Redis. Meant for the
19
+ // same metrics or status routes, with a marker.
20
+ // A `// dokebi-check: allow private-pool <reason>` (or `overload-off`, `readiness-off`) comment keeps a deliberate
21
+ // one (a LISTEN/NOTIFY client that holds one connection, a metrics route).
22
+ import { field } from './typescript.js';
23
+ export const PRIVATE_POOL_FIX = "take database connections from @dokebi/db: the configured one (context.getDependency(db).connection(main)), another pool to the same database for one kind of work (await context.getDependency(db).derive('main', { purpose: 'export', overrides: { max: 4 } }) in setup, closed by dokebi-db), or another database as one more connection in dokebi.yaml (dokebi-db.connections.<name>); its size and waitTimeoutMs then come from dokebi.yaml and a caller past the wait gets 503 DB_UNAVAILABLE with Retry-After. A connection that must stay outside (a LISTEN client) takes // dokebi-check: allow private-pool <reason>";
24
+ export const OVERLOAD_OFF_FIX = 'remove overload: false: under a burst the route then answers 503 OVERLOADED with Retry-After before its handler runs, and the client retries, instead of queueing behind a stalled worker (@dokebi/web AI.md 2.5; raise overload.maxEventLoopDelayMs in dokebi.yaml if it refuses too early). Only a metrics scrape or an operator status page stays served: // dokebi-check: allow overload-off <reason>';
25
+ export const READINESS_OFF_FIX = 'remove readiness: false: while a required dependency (a database, Redis) is down the route then answers 503 NOT_READY with Retry-After instead of running its handler against it (@dokebi/web AI.md). Only a metrics scrape or an operator status page, which must answer during an outage, keeps it: // dokebi-check: allow readiness-off <reason>';
26
+ /** Driver modules and the members that make a pool or a client. */
27
+ const DRIVERS = new Map([
28
+ ['pg', { constructors: new Set(['Pool', 'Client']), factories: new Set() }],
29
+ ['pg-pool', { constructors: new Set(['default', 'Pool']), factories: new Set() }],
30
+ [
31
+ 'mysql2',
32
+ {
33
+ constructors: new Set(),
34
+ factories: new Set(['createPool', 'createConnection', 'createPoolCluster']),
35
+ },
36
+ ],
37
+ [
38
+ 'mysql2/promise',
39
+ {
40
+ constructors: new Set(),
41
+ factories: new Set(['createPool', 'createConnection', 'createPoolCluster']),
42
+ },
43
+ ],
44
+ [
45
+ 'mariadb',
46
+ {
47
+ constructors: new Set(),
48
+ factories: new Set(['createPool', 'createConnection', 'createPoolCluster']),
49
+ },
50
+ ],
51
+ ['mongodb', { constructors: new Set(['MongoClient']), factories: new Set() }],
52
+ ]);
53
+ /** The adapters of @dokebi/db live here: the one place that makes pools. */
54
+ export function isDatabaseInfra(path) {
55
+ return path.startsWith('src/infra/db/');
56
+ }
57
+ export function poolRules(file, path, kinds, report) {
58
+ const text = (node) => file.text.slice(node.getStart(), node.end);
59
+ if (!isDatabaseInfra(path))
60
+ privatePools(file, kinds, text, report);
61
+ if (/\b(?:overload|readiness)\s*:\s*false\b/.test(file.text))
62
+ routeOffSwitches(file, kinds, text, report);
63
+ }
64
+ function privatePools(file, kinds, text, report) {
65
+ // Most files import no driver: skip the walk.
66
+ if (!/['"](?:pg|pg-pool|mysql2|mysql2\/promise|mariadb|mongodb)['"]/.test(file.text))
67
+ return;
68
+ // Local names bound to a driver: a namespace or default import (`pg.Pool`), or a named member.
69
+ const modules = new Map();
70
+ const members = new Map();
71
+ for (const statement of file.statements) {
72
+ if (statement.kind !== kinds.ImportDeclaration)
73
+ continue;
74
+ const specifier = field(statement, 'moduleSpecifier');
75
+ if (specifier?.kind !== kinds.StringLiteral)
76
+ continue;
77
+ const module = specifier.text;
78
+ if (!DRIVERS.has(module))
79
+ continue;
80
+ const clause = field(statement, 'importClause');
81
+ if (!clause)
82
+ continue;
83
+ const own = field(clause, 'name');
84
+ if (own) {
85
+ // `import Pool from 'pg-pool'` is the class; `import pg from 'pg'` is the namespace.
86
+ if (module === 'pg-pool')
87
+ members.set(text(own), { module, member: 'default' });
88
+ else
89
+ modules.set(text(own), module);
90
+ }
91
+ const bindings = field(clause, 'namedBindings');
92
+ if (bindings?.kind === kinds.NamespaceImport) {
93
+ const name = field(bindings, 'name');
94
+ if (name)
95
+ modules.set(text(name), module);
96
+ }
97
+ else if (bindings?.kind === kinds.NamedImports) {
98
+ for (const element of bindings.elements) {
99
+ const local = field(element, 'name');
100
+ const imported = field(element, 'propertyName') ?? local;
101
+ if (local && imported)
102
+ members.set(text(local), { module, member: text(imported) });
103
+ }
104
+ }
105
+ }
106
+ if (modules.size === 0 && members.size === 0)
107
+ return;
108
+ /** The driver member this expression names, through an import binding. */
109
+ const resolveMember = (node) => {
110
+ if (!node)
111
+ return undefined;
112
+ if (node.kind === kinds.Identifier)
113
+ return members.get(text(node));
114
+ if (node.kind === kinds.PropertyAccessExpression) {
115
+ const object = field(node, 'expression');
116
+ const name = field(node, 'name');
117
+ const module = object?.kind === kinds.Identifier ? modules.get(text(object)) : undefined;
118
+ if (module && name)
119
+ return { module, member: text(name) };
120
+ }
121
+ return undefined;
122
+ };
123
+ const visit = (node) => {
124
+ const isNew = node.kind === kinds.NewExpression;
125
+ if (isNew || node.kind === kinds.CallExpression) {
126
+ const found = resolveMember(field(node, 'expression'));
127
+ const driver = found ? DRIVERS.get(found.module) : undefined;
128
+ if (found && driver && (isNew ? driver.constructors : driver.factories).has(found.member)) {
129
+ const made = found.member === 'default' ? 'Pool' : found.member;
130
+ report(node, 'private-pool', `${isNew ? 'new ' : ''}${made}${isNew ? '' : '()'} of ${found.module} outside src/infra/db: a pool of its own escapes dokebi.yaml (its size and the wait deadline waitTimeoutMs that ends in 503 with Retry-After), the connection sum and the cleanup at stop (@dokebi/db AI.md 1.2; M37)`, PRIVATE_POOL_FIX);
131
+ }
132
+ }
133
+ node.forEachChild(visit);
134
+ return undefined;
135
+ };
136
+ file.forEachChild(visit);
137
+ }
138
+ /** `config: { overload: false }` and `config: { readiness: false }` of a route. */
139
+ function routeOffSwitches(file, kinds, text, report) {
140
+ const named = (node, name) => {
141
+ const key = node ? field(node, 'name') : undefined;
142
+ return (key !== undefined &&
143
+ (key.kind === kinds.Identifier || key.kind === kinds.StringLiteral) &&
144
+ text(key).replace(/^['"]|['"]$/g, '') === name);
145
+ };
146
+ const visit = (node) => {
147
+ // `config: { overload: false }` of a route: an object literal under a `config` key.
148
+ const value = node.kind === kinds.PropertyAssignment ? field(node, 'initializer') : undefined;
149
+ if (named(node, 'config') && value?.kind === kinds.ObjectLiteralExpression) {
150
+ for (const property of value.properties) {
151
+ if (property.kind !== kinds.PropertyAssignment ||
152
+ field(property, 'initializer')?.kind !== kinds.FalseKeyword)
153
+ continue;
154
+ if (named(property, 'overload'))
155
+ report(property, 'overload-off', 'overload: false serves this route while the server sheds load: under a burst its callers queue behind a stalled worker instead of getting 503 OVERLOADED with Retry-After (@dokebi/web AI.md 2.5; M37)', OVERLOAD_OFF_FIX);
156
+ else if (named(property, 'readiness'))
157
+ report(property, 'readiness-off', 'readiness: false serves this route while a required dependency is down and while the app drains: its handler then runs against a missing database or Redis instead of the client getting 503 NOT_READY with Retry-After (M51)', READINESS_OFF_FIX);
158
+ }
159
+ }
160
+ node.forEachChild(visit);
161
+ return undefined;
162
+ };
163
+ file.forEachChild(visit);
164
+ }
@@ -1,9 +1,14 @@
1
- export declare const RULES: readonly ['empty-catch', 'floating-promise', 'non-erasable-syntax', 'private-import', 'generated-file', 'secret-in-config', 'missing-package', 'inertia-form-only'];
1
+ export declare const RULES: readonly ['empty-catch', 'missing-cause', 'floating-promise', 'non-erasable-syntax', 'private-import', 'generated-file', 'secret-in-config', 'missing-package', 'inertia-form-only', 'form-without-csrf', 'csrf-exempt-unverified', 'plain-password-compare', 'unknown-error-code', 'mail-send-in-request', 'mail-secret-in-data', 'background-timer', 'polling-loop', 'responsibility-size', 'memory-filtered-page', 'untyped-browser-import', 'vite-dev-files', 'module-point-not-installed', 'formatter-migrations', 'formatter-generated', 'shared-state-memory', 'asset-in-views', 'asset-route', 'seal-head-copied', 'i18n-missing-key', 'i18n-layer-override', 'i18n-layer-missing', 'console-output', 'undeclared-section', 'template-fix-missing', 'wait-count-refusal', 'in-process-queue', 'private-pool', 'overload-off', 'readiness-off', 'fixed-owner-cap'];
2
2
  export type RuleName = (typeof RULES)[number];
3
3
  /** Rules a `// dokebi-check: allow <rule> <reason>` comment may silence (D-0072 §4, markers). */
4
4
  export declare const MARKABLE: ReadonlySet<RuleName>;
5
5
  /** Rules that warn: reported with `severity: 'warning'`, they do not fail check or verify. */
6
6
  export declare const WARNING_RULES: ReadonlySet<RuleName>;
7
+ /**
8
+ * The rules that read browser code outside the program — .vue scripts and the .js under views/
9
+ * (browser-sources.ts, D-0120): the catch rules and floating promises.
10
+ */
11
+ export declare const BROWSER_RULES: ReadonlySet<RuleName>;
7
12
  export interface Problem {
8
13
  readonly rule: RuleName;
9
14
  /** `warning` for a rule of WARNING_RULES (absent: an error that fails check). */
@@ -1,6 +1,15 @@
1
- // `dokebi check` problem shape and rule names (D-0072; inertia-form-only D-0074).
1
+ // `dokebi check` problem shape and rule names (D-0072; inertia-form-only D-0074; the security
2
+ // rules form-without-csrf, csrf-exempt-unverified and plain-password-compare D-0076 §6;
3
+ // unknown-error-code D-0077 D; background-timer, responsibility-size and memory-filtered-page
4
+ // D-0105; missing-cause D-0118; untyped-browser-import D-0120 §1.4; vite-dev-files D-0129;
5
+ // module-point-not-installed D-0130; formatter-migrations D-0145 (formatter-generated R1); console-output D-0151;
6
+ // undeclared-section D-0155; wait-count-refusal AI-M34, @dokebi/db AI.md 1.2 and 6.4; polling-loop
7
+ // AI-M35, not markable; in-process-queue AI-M35; private-pool, overload-off and fixed-owner-cap AI-M37; readiness-off M51;
8
+ // shared-state-memory D-0161; asset-in-views R1; asset-route R6; seal-head-copied R7;
9
+ // i18n-missing-key community request A2; i18n-layer-override and i18n-layer-missing D-0181).
2
10
  export const RULES = [
3
11
  'empty-catch',
12
+ 'missing-cause',
4
13
  'floating-promise',
5
14
  'non-erasable-syntax',
6
15
  'private-import',
@@ -8,15 +17,104 @@ export const RULES = [
8
17
  'secret-in-config',
9
18
  'missing-package',
10
19
  'inertia-form-only',
20
+ 'form-without-csrf',
21
+ 'csrf-exempt-unverified',
22
+ 'plain-password-compare',
23
+ 'unknown-error-code',
24
+ 'mail-send-in-request',
25
+ 'mail-secret-in-data',
26
+ 'background-timer',
27
+ 'polling-loop',
28
+ 'responsibility-size',
29
+ 'memory-filtered-page',
30
+ 'untyped-browser-import',
31
+ 'vite-dev-files',
32
+ 'module-point-not-installed',
33
+ 'formatter-migrations',
34
+ 'formatter-generated',
35
+ 'shared-state-memory',
36
+ 'asset-in-views',
37
+ 'asset-route',
38
+ 'seal-head-copied',
39
+ 'i18n-missing-key',
40
+ 'i18n-layer-override',
41
+ 'i18n-layer-missing',
42
+ 'console-output',
43
+ 'undeclared-section',
44
+ 'template-fix-missing',
45
+ 'wait-count-refusal',
46
+ 'in-process-queue',
47
+ 'private-pool',
48
+ 'overload-off',
49
+ 'readiness-off',
50
+ 'fixed-owner-cap',
11
51
  ];
12
52
  /** Rules a `// dokebi-check: allow <rule> <reason>` comment may silence (D-0072 §4, markers). */
13
53
  export const MARKABLE = new Set([
14
54
  'empty-catch',
55
+ 'missing-cause',
15
56
  'floating-promise',
16
57
  'inertia-form-only',
58
+ 'form-without-csrf',
59
+ 'csrf-exempt-unverified',
60
+ 'plain-password-compare',
61
+ 'unknown-error-code',
62
+ 'mail-send-in-request',
63
+ 'mail-secret-in-data',
64
+ 'background-timer',
65
+ 'responsibility-size',
66
+ 'memory-filtered-page',
67
+ 'untyped-browser-import',
68
+ 'vite-dev-files',
69
+ 'module-point-not-installed',
70
+ 'asset-route',
71
+ 'seal-head-copied',
72
+ 'i18n-missing-key',
73
+ 'console-output',
74
+ 'wait-count-refusal',
75
+ 'in-process-queue',
76
+ 'private-pool',
77
+ 'overload-off',
78
+ 'readiness-off',
79
+ 'fixed-owner-cap',
17
80
  ]);
18
81
  /** Rules that warn: reported with `severity: 'warning'`, they do not fail check or verify. */
19
- export const WARNING_RULES = new Set(['inertia-form-only']);
82
+ export const WARNING_RULES = new Set([
83
+ 'inertia-form-only',
84
+ 'form-without-csrf',
85
+ 'csrf-exempt-unverified',
86
+ 'unknown-error-code',
87
+ 'mail-send-in-request',
88
+ 'mail-secret-in-data',
89
+ 'responsibility-size',
90
+ 'memory-filtered-page',
91
+ 'untyped-browser-import',
92
+ 'vite-dev-files',
93
+ 'formatter-migrations',
94
+ 'formatter-generated',
95
+ 'asset-in-views',
96
+ 'asset-route',
97
+ 'seal-head-copied',
98
+ 'i18n-missing-key',
99
+ 'i18n-layer-override',
100
+ 'i18n-layer-missing',
101
+ 'console-output',
102
+ 'undeclared-section',
103
+ 'template-fix-missing',
104
+ 'wait-count-refusal',
105
+ 'in-process-queue',
106
+ 'fixed-owner-cap',
107
+ ]);
108
+ /**
109
+ * The rules that read browser code outside the program — .vue scripts and the .js under views/
110
+ * (browser-sources.ts, D-0120): the catch rules and floating promises.
111
+ */
112
+ export const BROWSER_RULES = new Set([
113
+ 'empty-catch',
114
+ 'missing-cause',
115
+ 'floating-promise',
116
+ 'untyped-browser-import',
117
+ ]);
20
118
  export function byPlace(a, b) {
21
119
  return a.file.localeCompare(b.file) || a.line - b.line || a.rule.localeCompare(b.rule);
22
120
  }
@@ -1,10 +1,30 @@
1
+ import { type ConfigTarget } from '../configuration.js';
1
2
  import type { Inspection } from '../project-inspector.js';
2
3
  import type { Problem } from './problems.js';
3
4
  /**
4
- * Whether dokebi.yaml can end up in version control: git tracks it, or git does not ignore it.
5
- * Without git (or outside a repository) the project's .gitignore must list it.
5
+ * Whether the settings file (dokebi.yaml, or the one DOKEBI_CONFIG names) can end up in version
6
+ * control: git tracks it, or git does not ignore it. Without git (or outside a repository) the
7
+ * project's .gitignore must list it. A file outside the project is not in its repository.
6
8
  */
7
- export declare function configReachesGit(root: string): Promise<boolean>;
9
+ export declare function configReachesGit(root: string, settings?: ConfigTarget): Promise<boolean>;
8
10
  /** 1-based line of `options.<plugin>.<path…>` in dokebi.yaml: its deepest existing key (0: none). */
9
11
  export declare function optionLine(source: string, path: readonly string[]): number;
12
+ /**
13
+ * Values of dokebi.yaml that are URLs with a password (AI-M04 F9): the option path and its line.
14
+ * Plugins read connection URLs from dokebi.yaml (it has no environment references), so the URL is
15
+ * fine there while the file stays out of git; only the path is returned, never the value.
16
+ */
17
+ export declare function passwordUrls(source: string): {
18
+ path: string[];
19
+ line: number;
20
+ }[];
21
+ /**
22
+ * Option paths (`[pluginId, …]`) whose schema marks the value secret (`writeOnly`, from
23
+ * `o.string({ secret: true })`) and that hold a value. A list of secrets is one path. Unions are
24
+ * followed branch by branch; only the paths are returned, never the values.
25
+ */
26
+ export declare function schemaSecrets(options: unknown, plugins: readonly {
27
+ id: string;
28
+ schema: unknown;
29
+ }[]): string[][];
10
30
  export declare function projectProblems(root: string, inspection: Inspection): Promise<Problem[]>;