@ontrails/warden 1.0.0-beta.4 → 1.0.0-beta.42

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 (279) hide show
  1. package/CHANGELOG.md +837 -13
  2. package/README.md +133 -26
  3. package/bin/warden.ts +51 -0
  4. package/package.json +27 -6
  5. package/src/adapter-check.ts +136 -0
  6. package/src/cli.ts +1570 -105
  7. package/src/command.ts +986 -0
  8. package/src/config.ts +193 -0
  9. package/src/draft.ts +22 -0
  10. package/src/drift.ts +233 -23
  11. package/src/fix.ts +126 -0
  12. package/src/formatters.ts +78 -13
  13. package/src/guide.ts +245 -0
  14. package/src/index.ts +247 -15
  15. package/src/project-context.ts +446 -0
  16. package/src/project-rules.ts +290 -0
  17. package/src/resolve.ts +531 -0
  18. package/src/rules/activation-orphan.ts +97 -0
  19. package/src/rules/captured-kernel.ts +375 -0
  20. package/src/rules/circular-refs.ts +150 -0
  21. package/src/rules/cli-command-route-coherence.ts +177 -0
  22. package/src/rules/composes-declarations.ts +839 -0
  23. package/src/rules/context-no-surface-types.ts +79 -15
  24. package/src/rules/dead-internal-trail.ts +161 -0
  25. package/src/rules/dead-public-trail.ts +258 -0
  26. package/src/rules/draft-file-marking.ts +155 -0
  27. package/src/rules/draft-visible-debt.ts +82 -0
  28. package/src/rules/duplicate-exported-symbol.ts +211 -0
  29. package/src/rules/duplicate-public-contract.ts +137 -0
  30. package/src/rules/entity-exists.ts +254 -0
  31. package/src/rules/entity-ids.ts +15 -0
  32. package/src/rules/error-mapping-completeness.ts +290 -0
  33. package/src/rules/example-valid.ts +395 -0
  34. package/src/rules/fires-declarations.ts +740 -0
  35. package/src/rules/governed-symbol-residue.ts +438 -0
  36. package/src/rules/implementation-returns-result.ts +1409 -166
  37. package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
  38. package/src/rules/incomplete-crud.ts +583 -0
  39. package/src/rules/index.ts +277 -10
  40. package/src/rules/intent-propagation.ts +125 -0
  41. package/src/rules/layer-field-name-drift.ts +102 -0
  42. package/src/rules/library-projection-coherence.ts +100 -0
  43. package/src/rules/metadata.ts +871 -0
  44. package/src/rules/missing-reconcile.ts +97 -0
  45. package/src/rules/missing-visibility.ts +111 -0
  46. package/src/rules/no-destructured-compose.ts +196 -0
  47. package/src/rules/no-dev-permit-in-source.ts +99 -0
  48. package/src/rules/no-direct-implementation-call.ts +12 -7
  49. package/src/rules/no-legacy-cli-alias-export.ts +247 -0
  50. package/src/rules/no-legacy-layer-imports.ts +211 -0
  51. package/src/rules/no-native-error-result.ts +118 -0
  52. package/src/rules/no-redundant-result-error-wrap.ts +384 -0
  53. package/src/rules/no-retired-cross-vocabulary.ts +204 -0
  54. package/src/rules/no-sync-result-assumption.ts +1141 -98
  55. package/src/rules/no-throw-in-detour-recover.ts +225 -0
  56. package/src/rules/no-throw-in-implementation.ts +15 -8
  57. package/src/rules/no-top-level-surface.ts +371 -0
  58. package/src/rules/on-references-exist.ts +194 -0
  59. package/src/rules/orphaned-signal.ts +149 -0
  60. package/src/rules/owner-projection-parity.ts +143 -0
  61. package/src/rules/permit-governance.ts +25 -0
  62. package/src/rules/public-export-example-coverage.ts +573 -0
  63. package/src/rules/public-internal-deep-imports.ts +456 -0
  64. package/src/rules/public-output-schema.ts +29 -0
  65. package/src/rules/public-union-output-discriminants.ts +150 -0
  66. package/src/rules/read-intent-fires.ts +188 -0
  67. package/src/rules/reference-exists.ts +97 -0
  68. package/src/rules/registry-names.ts +167 -0
  69. package/src/rules/resolved-import-boundary.ts +146 -0
  70. package/src/rules/resource-declarations.ts +697 -0
  71. package/src/rules/resource-exists.ts +181 -0
  72. package/src/rules/resource-id-grammar.ts +65 -0
  73. package/src/rules/resource-mock-coverage.ts +115 -0
  74. package/src/rules/retired-vocabulary.ts +633 -0
  75. package/src/rules/scan.ts +38 -25
  76. package/src/rules/scheduled-destroy-intent.ts +44 -0
  77. package/src/rules/signal-graph-coaching.ts +220 -0
  78. package/src/rules/source/composition.ts +165 -0
  79. package/src/rules/source/drafts.ts +164 -0
  80. package/src/rules/source/entities.ts +618 -0
  81. package/src/rules/source/pragmas.ts +45 -0
  82. package/src/rules/source/resources.ts +64 -0
  83. package/src/rules/source/signals.ts +397 -0
  84. package/src/rules/source/stores.ts +310 -0
  85. package/src/rules/specs.ts +9 -5
  86. package/src/rules/static-resource-accessor-preference.ts +653 -0
  87. package/src/rules/surface-overlay-coherence.ts +262 -0
  88. package/src/rules/surface-trailhead-coherence.ts +366 -0
  89. package/src/rules/trail-fork-coaching.ts +625 -0
  90. package/src/rules/trail-versioning-source.ts +1076 -0
  91. package/src/rules/trail-versioning-topo.ts +172 -0
  92. package/src/rules/trailhead-override-divergence.ts +356 -0
  93. package/src/rules/types.ts +354 -8
  94. package/src/rules/unmaterialized-activation-source.ts +85 -0
  95. package/src/rules/unreachable-detour-shadowing.ts +339 -0
  96. package/src/rules/valid-describe-refs.ts +162 -32
  97. package/src/rules/valid-detour-contract.ts +78 -0
  98. package/src/rules/warden-export-symmetry.ts +540 -0
  99. package/src/rules/warden-rules-use-ast.ts +1109 -0
  100. package/src/rules/webhook-route-collision.ts +306 -0
  101. package/src/trails/activation-orphan.trail.ts +84 -0
  102. package/src/trails/captured-kernel.trail.ts +108 -0
  103. package/src/trails/circular-refs.trail.ts +29 -0
  104. package/src/trails/cli-command-route-coherence.trail.ts +47 -0
  105. package/src/trails/composes-declarations.trail.ts +22 -0
  106. package/src/trails/context-no-surface-types.trail.ts +21 -0
  107. package/src/trails/dead-internal-trail.trail.ts +26 -0
  108. package/src/trails/dead-public-trail.trail.ts +31 -0
  109. package/src/trails/deprecation-without-guidance.trail.ts +21 -0
  110. package/src/trails/draft-file-marking.trail.ts +16 -0
  111. package/src/trails/draft-visible-debt.trail.ts +16 -0
  112. package/src/trails/duplicate-exported-symbol.trail.ts +48 -0
  113. package/src/trails/duplicate-public-contract.trail.ts +47 -0
  114. package/src/trails/entity-exists.trail.ts +21 -0
  115. package/src/trails/error-mapping-completeness.trail.ts +30 -0
  116. package/src/trails/example-valid.trail.ts +25 -0
  117. package/src/trails/fires-declarations.trail.ts +23 -0
  118. package/src/trails/fork-without-preserved-implementation.trail.ts +31 -0
  119. package/src/trails/governed-symbol-residue.trail.ts +24 -0
  120. package/src/trails/implementation-returns-result.trail.ts +20 -0
  121. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
  122. package/src/trails/incomplete-crud.trail.ts +39 -0
  123. package/src/trails/index.ts +89 -0
  124. package/src/trails/intent-propagation.trail.ts +30 -0
  125. package/src/trails/layer-field-name-drift.trail.ts +39 -0
  126. package/src/trails/library-projection-coherence.trail.ts +43 -0
  127. package/src/trails/marker-schema-unsupported.trail.ts +23 -0
  128. package/src/trails/missing-reconcile.trail.ts +33 -0
  129. package/src/trails/missing-visibility.trail.ts +22 -0
  130. package/src/trails/no-destructured-compose.trail.ts +44 -0
  131. package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
  132. package/src/trails/no-direct-implementation-call.trail.ts +16 -0
  133. package/src/trails/no-legacy-cli-alias-export.trail.ts +41 -0
  134. package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
  135. package/src/trails/no-native-error-result.trail.ts +18 -0
  136. package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
  137. package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
  138. package/src/trails/no-sync-result-assumption.trail.ts +19 -0
  139. package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
  140. package/src/trails/no-throw-in-implementation.trail.ts +20 -0
  141. package/src/trails/no-top-level-surface.trail.ts +43 -0
  142. package/src/trails/on-references-exist.trail.ts +21 -0
  143. package/src/trails/orphaned-signal.trail.ts +36 -0
  144. package/src/trails/owner-projection-parity.trail.ts +26 -0
  145. package/src/trails/pending-force.trail.ts +21 -0
  146. package/src/trails/permit-governance.trail.ts +51 -0
  147. package/src/trails/prefer-schema-inference.trail.ts +21 -0
  148. package/src/trails/public-export-example-coverage.trail.ts +16 -0
  149. package/src/trails/public-internal-deep-imports.trail.ts +94 -0
  150. package/src/trails/public-output-schema.trail.ts +55 -0
  151. package/src/trails/public-union-output-discriminants.trail.ts +33 -0
  152. package/src/trails/read-intent-fires.trail.ts +20 -0
  153. package/src/trails/reference-exists.trail.ts +25 -0
  154. package/src/trails/resolved-import-boundary.trail.ts +109 -0
  155. package/src/trails/resource-declarations.trail.ts +25 -0
  156. package/src/trails/resource-exists.trail.ts +27 -0
  157. package/src/trails/resource-id-grammar.trail.ts +39 -0
  158. package/src/trails/resource-mock-coverage.trail.ts +40 -0
  159. package/src/trails/run.ts +160 -0
  160. package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
  161. package/src/trails/schema.ts +237 -0
  162. package/src/trails/signal-graph-coaching.trail.ts +77 -0
  163. package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
  164. package/src/trails/surface-overlay-coherence.trail.ts +24 -0
  165. package/src/trails/surface-trailhead-coherence.trail.ts +25 -0
  166. package/src/trails/topo.ts +6 -0
  167. package/src/trails/trail-fork-coaching.trail.ts +42 -0
  168. package/src/trails/trailhead-override-divergence.trail.ts +47 -0
  169. package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
  170. package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
  171. package/src/trails/valid-describe-refs.trail.ts +18 -0
  172. package/src/trails/valid-detour-contract.trail.ts +71 -0
  173. package/src/trails/version-gap.trail.ts +35 -0
  174. package/src/trails/version-pinned-compose.trail.ts +23 -0
  175. package/src/trails/version-without-examples.trail.ts +38 -0
  176. package/src/trails/warden-export-symmetry.trail.ts +16 -0
  177. package/src/trails/warden-rules-use-ast.trail.ts +64 -0
  178. package/src/trails/webhook-route-collision.trail.ts +50 -0
  179. package/src/trails/wrap-rule.ts +224 -0
  180. package/src/workspaces.ts +199 -0
  181. package/.turbo/turbo-build.log +0 -1
  182. package/.turbo/turbo-lint.log +0 -3
  183. package/.turbo/turbo-typecheck.log +0 -1
  184. package/dist/cli.d.ts +0 -46
  185. package/dist/cli.d.ts.map +0 -1
  186. package/dist/cli.js +0 -218
  187. package/dist/cli.js.map +0 -1
  188. package/dist/drift.d.ts +0 -26
  189. package/dist/drift.d.ts.map +0 -1
  190. package/dist/drift.js +0 -27
  191. package/dist/drift.js.map +0 -1
  192. package/dist/formatters.d.ts +0 -29
  193. package/dist/formatters.d.ts.map +0 -1
  194. package/dist/formatters.js +0 -87
  195. package/dist/formatters.js.map +0 -1
  196. package/dist/index.d.ts +0 -26
  197. package/dist/index.d.ts.map +0 -1
  198. package/dist/index.js +0 -26
  199. package/dist/index.js.map +0 -1
  200. package/dist/rules/ast.d.ts +0 -41
  201. package/dist/rules/ast.d.ts.map +0 -1
  202. package/dist/rules/ast.js +0 -161
  203. package/dist/rules/ast.js.map +0 -1
  204. package/dist/rules/context-no-surface-types.d.ts +0 -12
  205. package/dist/rules/context-no-surface-types.d.ts.map +0 -1
  206. package/dist/rules/context-no-surface-types.js +0 -96
  207. package/dist/rules/context-no-surface-types.js.map +0 -1
  208. package/dist/rules/implementation-returns-result.d.ts +0 -13
  209. package/dist/rules/implementation-returns-result.d.ts.map +0 -1
  210. package/dist/rules/implementation-returns-result.js +0 -277
  211. package/dist/rules/implementation-returns-result.js.map +0 -1
  212. package/dist/rules/index.d.ts +0 -15
  213. package/dist/rules/index.d.ts.map +0 -1
  214. package/dist/rules/index.js +0 -34
  215. package/dist/rules/index.js.map +0 -1
  216. package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
  217. package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
  218. package/dist/rules/no-direct-impl-in-route.js +0 -47
  219. package/dist/rules/no-direct-impl-in-route.js.map +0 -1
  220. package/dist/rules/no-direct-implementation-call.d.ts +0 -12
  221. package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
  222. package/dist/rules/no-direct-implementation-call.js +0 -39
  223. package/dist/rules/no-direct-implementation-call.js.map +0 -1
  224. package/dist/rules/no-sync-result-assumption.d.ts +0 -6
  225. package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
  226. package/dist/rules/no-sync-result-assumption.js +0 -98
  227. package/dist/rules/no-sync-result-assumption.js.map +0 -1
  228. package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
  229. package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
  230. package/dist/rules/no-throw-in-detour-target.js +0 -87
  231. package/dist/rules/no-throw-in-detour-target.js.map +0 -1
  232. package/dist/rules/no-throw-in-implementation.d.ts +0 -9
  233. package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
  234. package/dist/rules/no-throw-in-implementation.js +0 -34
  235. package/dist/rules/no-throw-in-implementation.js.map +0 -1
  236. package/dist/rules/prefer-schema-inference.d.ts +0 -7
  237. package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
  238. package/dist/rules/prefer-schema-inference.js +0 -86
  239. package/dist/rules/prefer-schema-inference.js.map +0 -1
  240. package/dist/rules/scan.d.ts +0 -8
  241. package/dist/rules/scan.d.ts.map +0 -1
  242. package/dist/rules/scan.js +0 -32
  243. package/dist/rules/scan.js.map +0 -1
  244. package/dist/rules/specs.d.ts +0 -29
  245. package/dist/rules/specs.d.ts.map +0 -1
  246. package/dist/rules/specs.js +0 -192
  247. package/dist/rules/specs.js.map +0 -1
  248. package/dist/rules/structure.d.ts +0 -13
  249. package/dist/rules/structure.d.ts.map +0 -1
  250. package/dist/rules/structure.js +0 -142
  251. package/dist/rules/structure.js.map +0 -1
  252. package/dist/rules/types.d.ts +0 -52
  253. package/dist/rules/types.d.ts.map +0 -1
  254. package/dist/rules/types.js +0 -2
  255. package/dist/rules/types.js.map +0 -1
  256. package/dist/rules/valid-describe-refs.d.ts +0 -7
  257. package/dist/rules/valid-describe-refs.d.ts.map +0 -1
  258. package/dist/rules/valid-describe-refs.js +0 -51
  259. package/dist/rules/valid-describe-refs.js.map +0 -1
  260. package/dist/rules/valid-detour-refs.d.ts +0 -6
  261. package/dist/rules/valid-detour-refs.d.ts.map +0 -1
  262. package/dist/rules/valid-detour-refs.js +0 -116
  263. package/dist/rules/valid-detour-refs.js.map +0 -1
  264. package/src/__tests__/cli.test.ts +0 -198
  265. package/src/__tests__/drift.test.ts +0 -74
  266. package/src/__tests__/formatters.test.ts +0 -157
  267. package/src/__tests__/implementation-returns-result.test.ts +0 -129
  268. package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
  269. package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
  270. package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
  271. package/src/__tests__/prefer-schema-inference.test.ts +0 -84
  272. package/src/__tests__/rules.test.ts +0 -227
  273. package/src/__tests__/valid-describe-refs.test.ts +0 -60
  274. package/src/rules/ast.ts +0 -213
  275. package/src/rules/no-direct-impl-in-route.ts +0 -81
  276. package/src/rules/no-throw-in-detour-target.ts +0 -150
  277. package/src/rules/valid-detour-refs.ts +0 -187
  278. package/tsconfig.json +0 -9
  279. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,573 @@
1
+ /**
2
+ * Repo-local rule (TRL-943): public API exports re-exported from the v1
3
+ * surface package index barrels must carry a leading `@example` TSDoc block
4
+ * on their exported declaration. Graduated from
5
+ * `scripts/check-public-api-examples.ts` so the contract is governed by
6
+ * Warden instead of a standalone script.
7
+ *
8
+ * The inventory mirrors the script's semantics:
9
+ *
10
+ * 1. Only non-type-only named re-exports with relative module specifiers
11
+ * are inventoried. Type-only export declarations and type-only
12
+ * specifiers are skipped.
13
+ * 2. Star re-exports, non-relative specifiers, and local export lists on a
14
+ * target barrel are reported as errors — the inventory cannot resolve
15
+ * them to a declaration.
16
+ * 3. Each re-export resolves to its source module (`.js` → `.ts`, relative
17
+ * to the barrel), and the exported declaration is located by the
18
+ * IMPORTED name (the `propertyName` when aliased). The declaration is
19
+ * covered when a leading comment in the trivia gap before it matches
20
+ * `@example`.
21
+ * 4. A `minimumExports` entry that never appears in the barrel inventory is
22
+ * an error — the minimum policy list must stay inventoried.
23
+ *
24
+ * Severity model: missing `@example` on a `minimumExports` entry is an
25
+ * `error`; missing `@example` on any other inventoried export is a `warn`
26
+ * so the rest of the inventory stays visible without failing
27
+ * `failOn: 'error'` runs.
28
+ */
29
+ import { readFileSync } from 'node:fs';
30
+ import { dirname, join, normalize, relative, resolve } from 'node:path';
31
+ import { fileURLToPath } from 'node:url';
32
+
33
+ import {
34
+ getNodeBodyStatements,
35
+ getNodeDeclaration,
36
+ getNodeDeclarations,
37
+ getNodeExported,
38
+ getNodeExportKind,
39
+ getNodeId,
40
+ getNodeLocal,
41
+ getNodeName,
42
+ getNodeSource,
43
+ getNodeSpecifiers,
44
+ getNodeValue,
45
+ offsetToLine,
46
+ parse,
47
+ } from '@ontrails/source';
48
+ import type { AstNode } from '@ontrails/source';
49
+ import type { WardenDiagnostic, WardenRule } from './types.js';
50
+
51
+ const RULE_NAME = 'public-export-example-coverage';
52
+
53
+ export interface PublicApiPackageTarget {
54
+ /** Repo-root-relative path to the package's public index barrel. */
55
+ readonly indexPath: string;
56
+ /** Exports that MUST be inventoried and carry `@example` coverage. */
57
+ readonly minimumExports: readonly string[];
58
+ /** Published package name, used in diagnostics. */
59
+ readonly packageName: string;
60
+ }
61
+
62
+ /**
63
+ * Repo-local public API `@example` coverage policy.
64
+ *
65
+ * Ported verbatim from `scripts/check-public-api-examples.ts`
66
+ * (`PUBLIC_API_EXAMPLE_TARGETS`). This table lives in the rule module as
67
+ * repo-local policy: `wardenConfigSchema` is a strict runner-options schema
68
+ * and source-static rules receive only `(sourceCode, filePath)`, so there is
69
+ * no per-rule config channel today. Move the table into Warden config if a
70
+ * per-rule config channel lands.
71
+ */
72
+ export const PUBLIC_API_EXAMPLE_TARGETS: readonly PublicApiPackageTarget[] = [
73
+ {
74
+ indexPath: 'packages/cli/src/index.ts',
75
+ minimumExports: [
76
+ 'deriveCliCommands',
77
+ 'deriveFlags',
78
+ 'output',
79
+ 'deriveOutputMode',
80
+ 'findAppModuleCandidates',
81
+ 'findAppModule',
82
+ ],
83
+ packageName: '@ontrails/cli',
84
+ },
85
+ {
86
+ indexPath: 'packages/http/src/index.ts',
87
+ minimumExports: [
88
+ 'deriveHttpRoutes',
89
+ 'deriveHttpInputSource',
90
+ 'deriveHttpMethod',
91
+ 'deriveHttpOperationMethod',
92
+ 'deriveOpenApiSpec',
93
+ ],
94
+ packageName: '@ontrails/http',
95
+ },
96
+ {
97
+ indexPath: 'packages/mcp/src/index.ts',
98
+ minimumExports: [
99
+ 'deriveMcpTools',
100
+ 'createServer',
101
+ 'surface',
102
+ 'connectStdio',
103
+ ],
104
+ packageName: '@ontrails/mcp',
105
+ },
106
+ {
107
+ indexPath: 'adapters/commander/src/index.ts',
108
+ minimumExports: ['createProgram', 'surface', 'toCommander'],
109
+ packageName: '@ontrails/commander',
110
+ },
111
+ {
112
+ indexPath: 'adapters/hono/src/index.ts',
113
+ minimumExports: ['createApp', 'surface'],
114
+ packageName: '@ontrails/hono',
115
+ },
116
+ {
117
+ indexPath: 'adapters/cloudflare/src/index.ts',
118
+ minimumExports: [
119
+ 'createWorkersHandler',
120
+ 'cloudflareKv',
121
+ 'cloudflareD1',
122
+ 'cloudflareQueue',
123
+ 'cloudflareR2',
124
+ ],
125
+ packageName: '@ontrails/cloudflare',
126
+ },
127
+ ] as const;
128
+
129
+ export interface ResolvedPublicApiTarget extends PublicApiPackageTarget {
130
+ /** Absolute path of the target barrel — the rule's path anchor. */
131
+ readonly absoluteIndexPath: string;
132
+ /** Absolute repo (or fixture) root used to relativize diagnostic paths. */
133
+ readonly rootDir: string;
134
+ }
135
+
136
+ /**
137
+ * Resolve repo-relative policy targets against a root directory. Exported
138
+ * for unit testing — tests build fixture trees under a temp root instead of
139
+ * depending on the real repo barrels. Not part of the public rule API.
140
+ */
141
+ export const resolvePublicApiExampleTargets = (
142
+ rootDir: string,
143
+ targets: readonly PublicApiPackageTarget[]
144
+ ): readonly ResolvedPublicApiTarget[] => {
145
+ const resolvedRoot = resolve(rootDir);
146
+ return targets.map((target) => ({
147
+ ...target,
148
+ absoluteIndexPath: resolve(resolvedRoot, target.indexPath),
149
+ rootDir: resolvedRoot,
150
+ }));
151
+ };
152
+
153
+ /**
154
+ * Repo root resolved from this rule's own module URL
155
+ * (`packages/warden/src/rules/` → four levels up). Anchoring to the real
156
+ * on-disk location gives the same consumer-repo safety property as
157
+ * `warden-export-symmetry`'s SELF_BARREL_PATH: in a consumer repository the
158
+ * warden package resolves inside `node_modules`, so the computed absolute
159
+ * target paths never match consumer files and the rule stays silent.
160
+ */
161
+ const REPO_ROOT = resolve(
162
+ fileURLToPath(new URL('../../../..', import.meta.url))
163
+ );
164
+
165
+ const RESOLVED_TARGETS = resolvePublicApiExampleTargets(
166
+ REPO_ROOT,
167
+ PUBLIC_API_EXAMPLE_TARGETS
168
+ );
169
+
170
+ interface PublicExportSpecifier {
171
+ /** Public export name as seen on the barrel. */
172
+ readonly exportName: string;
173
+ /** Local source binding name (`propertyName` when aliased). */
174
+ readonly importedName: string;
175
+ readonly moduleSpecifier: string;
176
+ /** Start offset of the export specifier on the barrel. */
177
+ readonly start: number;
178
+ }
179
+
180
+ interface BarrelInventory {
181
+ readonly diagnostics: readonly WardenDiagnostic[];
182
+ readonly specifiers: readonly PublicExportSpecifier[];
183
+ }
184
+
185
+ const isTypeKind = (node: AstNode): boolean =>
186
+ getNodeExportKind(node) === 'type';
187
+
188
+ const readNameNode = (node: AstNode | undefined): string | null => {
189
+ if (!node) {
190
+ return null;
191
+ }
192
+ if (node.type === 'Identifier') {
193
+ return getNodeName(node) ?? null;
194
+ }
195
+ if (node.type === 'Literal' || node.type === 'StringLiteral') {
196
+ const value = getNodeValue(node);
197
+ return typeof value === 'string' ? value : null;
198
+ }
199
+ return null;
200
+ };
201
+
202
+ const moduleSpecifierValue = (node: AstNode): string | null => {
203
+ const source = getNodeSource(node);
204
+ if (!source) {
205
+ return null;
206
+ }
207
+ const value = getNodeValue(source);
208
+ return typeof value === 'string' ? value : null;
209
+ };
210
+
211
+ const programBody = (ast: AstNode): readonly AstNode[] =>
212
+ getNodeBodyStatements(ast);
213
+
214
+ const diagnostic = (
215
+ sourceCode: string,
216
+ filePath: string,
217
+ start: number,
218
+ severity: WardenDiagnostic['severity'],
219
+ message: string
220
+ ): WardenDiagnostic => ({
221
+ filePath,
222
+ line: offsetToLine(sourceCode, start),
223
+ message: `${RULE_NAME}: ${message}`,
224
+ rule: RULE_NAME,
225
+ severity,
226
+ });
227
+
228
+ const specifiersFromExportDeclaration = (
229
+ node: AstNode,
230
+ moduleSpecifier: string
231
+ ): readonly PublicExportSpecifier[] => {
232
+ const specifiers = getNodeSpecifiers(node) ?? [];
233
+ return specifiers.flatMap((specifier) => {
234
+ if (specifier.type !== 'ExportSpecifier' || isTypeKind(specifier)) {
235
+ return [];
236
+ }
237
+ const exported = getNodeExported(specifier);
238
+ const local = getNodeLocal(specifier);
239
+ const exportName = readNameNode(exported);
240
+ if (!exportName) {
241
+ return [];
242
+ }
243
+ return [
244
+ {
245
+ exportName,
246
+ importedName: readNameNode(local) ?? exportName,
247
+ moduleSpecifier,
248
+ start: specifier.start,
249
+ },
250
+ ];
251
+ });
252
+ };
253
+
254
+ interface InventoryContext {
255
+ readonly diagnostics: WardenDiagnostic[];
256
+ readonly filePath: string;
257
+ readonly sourceCode: string;
258
+ readonly specifiers: PublicExportSpecifier[];
259
+ readonly target: ResolvedPublicApiTarget;
260
+ }
261
+
262
+ const inventoryNamedExport = (node: AstNode, ctx: InventoryContext): void => {
263
+ if (isTypeKind(node)) {
264
+ return;
265
+ }
266
+ const declaration = getNodeDeclaration(node);
267
+ if (declaration) {
268
+ // Declaration-form exports (`export const foo = ...`) are not module
269
+ // re-exports; the script's inventory skipped them the same way.
270
+ return;
271
+ }
272
+ const moduleSpecifier = moduleSpecifierValue(node);
273
+ if (moduleSpecifier === null) {
274
+ ctx.diagnostics.push(
275
+ diagnostic(
276
+ ctx.sourceCode,
277
+ ctx.filePath,
278
+ node.start,
279
+ 'error',
280
+ `${ctx.target.packageName} barrel has a local export list without a module specifier. The public API inventory only supports module re-exports — re-export each name from its source module.`
281
+ )
282
+ );
283
+ return;
284
+ }
285
+ if (!moduleSpecifier.startsWith('.')) {
286
+ ctx.diagnostics.push(
287
+ diagnostic(
288
+ ctx.sourceCode,
289
+ ctx.filePath,
290
+ node.start,
291
+ 'error',
292
+ `${ctx.target.packageName} barrel re-exports from non-relative module specifier '${moduleSpecifier}'. The public API inventory can only resolve relative re-exports to their declarations.`
293
+ )
294
+ );
295
+ return;
296
+ }
297
+ ctx.specifiers.push(
298
+ ...specifiersFromExportDeclaration(node, moduleSpecifier)
299
+ );
300
+ };
301
+
302
+ const inventoryStarExport = (node: AstNode, ctx: InventoryContext): void => {
303
+ if (isTypeKind(node)) {
304
+ return;
305
+ }
306
+ const moduleSpecifier = moduleSpecifierValue(node) ?? '<unknown>';
307
+ ctx.diagnostics.push(
308
+ diagnostic(
309
+ ctx.sourceCode,
310
+ ctx.filePath,
311
+ node.start,
312
+ 'error',
313
+ `${ctx.target.packageName} barrel uses a star re-export from '${moduleSpecifier}'. The public API inventory does not support star re-exports — list each export by name so @example coverage stays checkable.`
314
+ )
315
+ );
316
+ };
317
+
318
+ /**
319
+ * Inventory the non-type-only named re-exports on a target barrel, emitting
320
+ * error diagnostics for shapes the inventory cannot resolve (star
321
+ * re-exports, non-relative specifiers, local export lists).
322
+ */
323
+ const collectBarrelInventory = (
324
+ sourceCode: string,
325
+ filePath: string,
326
+ ast: AstNode,
327
+ target: ResolvedPublicApiTarget
328
+ ): BarrelInventory => {
329
+ const ctx: InventoryContext = {
330
+ diagnostics: [],
331
+ filePath,
332
+ sourceCode,
333
+ specifiers: [],
334
+ target,
335
+ };
336
+ for (const statement of programBody(ast)) {
337
+ if (statement.type === 'ExportNamedDeclaration') {
338
+ inventoryNamedExport(statement, ctx);
339
+ } else if (statement.type === 'ExportAllDeclaration') {
340
+ inventoryStarExport(statement, ctx);
341
+ }
342
+ }
343
+ return { diagnostics: ctx.diagnostics, specifiers: ctx.specifiers };
344
+ };
345
+
346
+ const TS_RE_EXPORT_EXTENSION = /\.js$/;
347
+
348
+ const resolveReexportSourcePath = (
349
+ absoluteIndexPath: string,
350
+ moduleSpecifier: string
351
+ ): string => {
352
+ const withTsExtension = moduleSpecifier.replace(
353
+ TS_RE_EXPORT_EXTENSION,
354
+ '.ts'
355
+ );
356
+ return normalize(join(dirname(absoluteIndexPath), withTsExtension));
357
+ };
358
+
359
+ const declarationNameMatches = (
360
+ declaration: AstNode,
361
+ exportName: string
362
+ ): boolean => {
363
+ if (
364
+ declaration.type === 'FunctionDeclaration' ||
365
+ declaration.type === 'ClassDeclaration' ||
366
+ declaration.type === 'TSInterfaceDeclaration' ||
367
+ declaration.type === 'TSTypeAliasDeclaration'
368
+ ) {
369
+ const id = getNodeId(declaration);
370
+ return readNameNode(id) === exportName;
371
+ }
372
+ if (declaration.type === 'VariableDeclaration') {
373
+ const declarations = getNodeDeclarations(declaration);
374
+ return declarations.some((declarator) => {
375
+ const id = getNodeId(declarator);
376
+ return readNameNode(id) === exportName;
377
+ });
378
+ }
379
+ return false;
380
+ };
381
+
382
+ /**
383
+ * Comment texts found in an inter-statement trivia gap. The gap between two
384
+ * top-level statements contains only whitespace and comments, so a small
385
+ * line/block comment scan recovers the same comment set TypeScript's
386
+ * `getLeadingCommentRanges` returns for the statement's full start.
387
+ */
388
+ const collectCommentTexts = (gapText: string): readonly string[] => {
389
+ const comments: string[] = [];
390
+ let index = 0;
391
+ while (index < gapText.length - 1) {
392
+ if (gapText[index] === '/' && gapText[index + 1] === '/') {
393
+ const lineEnd = gapText.indexOf('\n', index);
394
+ const stop = lineEnd === -1 ? gapText.length : lineEnd;
395
+ comments.push(gapText.slice(index, stop));
396
+ index = stop + 1;
397
+ } else if (gapText[index] === '/' && gapText[index + 1] === '*') {
398
+ const blockEnd = gapText.indexOf('*/', index + 2);
399
+ const stop = blockEnd === -1 ? gapText.length : blockEnd + 2;
400
+ comments.push(gapText.slice(index, stop));
401
+ index = stop;
402
+ } else {
403
+ index += 1;
404
+ }
405
+ }
406
+ return comments;
407
+ };
408
+
409
+ const EXAMPLE_TAG_PATTERN = /@example\b/;
410
+
411
+ /**
412
+ * True when the exported declaration named `importedName` in `sourceText`
413
+ * carries a leading comment containing `@example`. Leading comments are
414
+ * recovered from the trivia gap between the preceding top-level statement's
415
+ * end (or file start) and the matching export statement's start.
416
+ */
417
+ const hasLeadingExampleForExport = (
418
+ sourceText: string,
419
+ ast: AstNode,
420
+ importedName: string
421
+ ): boolean => {
422
+ const body = programBody(ast);
423
+ for (const [statementIndex, statement] of body.entries()) {
424
+ if (statement.type !== 'ExportNamedDeclaration') {
425
+ continue;
426
+ }
427
+ const declaration = getNodeDeclaration(statement);
428
+ if (!declaration || !declarationNameMatches(declaration, importedName)) {
429
+ continue;
430
+ }
431
+ const previous = body[statementIndex - 1];
432
+ const gapText = sourceText.slice(previous?.end ?? 0, statement.start);
433
+ return collectCommentTexts(gapText).some((comment) =>
434
+ EXAMPLE_TAG_PATTERN.test(comment)
435
+ );
436
+ }
437
+ return false;
438
+ };
439
+
440
+ const readSourceFile = (sourcePath: string): string | null => {
441
+ try {
442
+ return readFileSync(sourcePath, 'utf8');
443
+ } catch {
444
+ return null;
445
+ }
446
+ };
447
+
448
+ const coverageDiagnosticsForSpecifier = (
449
+ sourceCode: string,
450
+ filePath: string,
451
+ specifier: PublicExportSpecifier,
452
+ target: ResolvedPublicApiTarget
453
+ ): readonly WardenDiagnostic[] => {
454
+ const sourcePath = resolveReexportSourcePath(
455
+ target.absoluteIndexPath,
456
+ specifier.moduleSpecifier
457
+ );
458
+ const relativeSourcePath = relative(target.rootDir, sourcePath);
459
+ const sourceText = readSourceFile(sourcePath);
460
+ if (sourceText === null) {
461
+ return [
462
+ diagnostic(
463
+ sourceCode,
464
+ filePath,
465
+ specifier.start,
466
+ 'error',
467
+ `${target.packageName} export "${specifier.exportName}" re-exports from unreadable source ${relativeSourcePath}. The public API inventory could not read the resolved module.`
468
+ ),
469
+ ];
470
+ }
471
+ const sourceAst = parse(sourcePath, sourceText);
472
+ if (!sourceAst) {
473
+ return [
474
+ diagnostic(
475
+ sourceCode,
476
+ filePath,
477
+ specifier.start,
478
+ 'error',
479
+ `${target.packageName} export "${specifier.exportName}" re-exports from unparseable source ${relativeSourcePath}. The public API inventory could not parse the resolved module.`
480
+ ),
481
+ ];
482
+ }
483
+ if (
484
+ hasLeadingExampleForExport(sourceText, sourceAst, specifier.importedName)
485
+ ) {
486
+ return [];
487
+ }
488
+ const isMinimum = target.minimumExports.includes(specifier.exportName);
489
+ const tier = isMinimum ? 'minimum' : 'inventory';
490
+ return [
491
+ diagnostic(
492
+ sourceCode,
493
+ filePath,
494
+ specifier.start,
495
+ isMinimum ? 'error' : 'warn',
496
+ `${target.packageName} export "${specifier.exportName}" (${tier}) is missing a leading @example TSDoc block on its exported declaration "${specifier.importedName}" in ${relativeSourcePath}. Add an @example to the declaration's TSDoc.`
497
+ ),
498
+ ];
499
+ };
500
+
501
+ const missingMinimumDiagnostics = (
502
+ sourceCode: string,
503
+ filePath: string,
504
+ specifiers: readonly PublicExportSpecifier[],
505
+ target: ResolvedPublicApiTarget
506
+ ): readonly WardenDiagnostic[] => {
507
+ const present = new Set(specifiers.map((specifier) => specifier.exportName));
508
+ return target.minimumExports
509
+ .filter((exportName) => !present.has(exportName))
510
+ .map((exportName) =>
511
+ diagnostic(
512
+ sourceCode,
513
+ filePath,
514
+ 0,
515
+ 'error',
516
+ `${target.packageName} minimum export "${exportName}" is missing from the barrel inventory at ${target.indexPath}. Every minimumExports policy entry must stay re-exported by name on the package barrel.`
517
+ )
518
+ );
519
+ };
520
+
521
+ /**
522
+ * Run the coverage analysis against an explicit resolved-target table.
523
+ * Exported for unit testing so fixtures can anchor to a temp root instead of
524
+ * the real repo barrels. Not part of the public rule API.
525
+ */
526
+ export const checkPublicExportExampleCoverage = (
527
+ sourceCode: string,
528
+ filePath: string,
529
+ targets: readonly ResolvedPublicApiTarget[]
530
+ ): readonly WardenDiagnostic[] => {
531
+ const resolvedPath = resolve(filePath);
532
+ const target = targets.find(
533
+ (candidate) => candidate.absoluteIndexPath === resolvedPath
534
+ );
535
+ if (!target) {
536
+ return [];
537
+ }
538
+ const ast = parse(filePath, sourceCode);
539
+ if (!ast) {
540
+ return [];
541
+ }
542
+ const inventory = collectBarrelInventory(sourceCode, filePath, ast, target);
543
+ return [
544
+ ...inventory.diagnostics,
545
+ ...missingMinimumDiagnostics(
546
+ sourceCode,
547
+ filePath,
548
+ inventory.specifiers,
549
+ target
550
+ ),
551
+ ...inventory.specifiers.flatMap((specifier) =>
552
+ coverageDiagnosticsForSpecifier(sourceCode, filePath, specifier, target)
553
+ ),
554
+ ];
555
+ };
556
+
557
+ /**
558
+ * Warden rule enforcing leading `@example` TSDoc coverage on the public API
559
+ * exports of the v1 surface package barrels (TRL-943).
560
+ */
561
+ export const publicExportExampleCoverage: WardenRule = {
562
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
563
+ return checkPublicExportExampleCoverage(
564
+ sourceCode,
565
+ filePath,
566
+ RESOLVED_TARGETS
567
+ );
568
+ },
569
+ description:
570
+ 'Enforces that public API exports re-exported from the v1 surface package index barrels carry a leading @example TSDoc block, with a mandatory per-package minimumExports coverage list.',
571
+ name: RULE_NAME,
572
+ severity: 'error',
573
+ };