@ontrails/warden 1.0.0-beta.2 → 1.0.0-beta.22

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