@alexgorbatchev/typescript-ai-policy 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -114,6 +114,9 @@ If you want the semantic-fix CLI, install its tsgo backend too:
114
114
  bun add -d @typescript/native-preview
115
115
  ```
116
116
 
117
+ The semantic-fix CLI requires Bun at runtime because the installed bin uses a Bun shebang. That means Bun must be
118
+ installed and available on `PATH` when you run the command.
119
+
117
120
  ## Use
118
121
 
119
122
  `oxfmt.config.ts`
@@ -153,15 +156,18 @@ This package also uses its own shared configs at the repository root:
153
156
  - `oxfmt.config.ts`
154
157
  - `oxlint.config.ts`
155
158
 
156
- ## Local semantic-fix tooling
159
+ ## Semantic-fix tooling
160
+
161
+ The package includes a semantic-fix CLI backed by `tsgo --lsp --stdio`.
162
+
163
+ Package-installed usage:
157
164
 
158
- The repository now includes a local semantic-fix command under `src/semantic-fixes/` that uses `tsgo --lsp --stdio` as its first real backend.
165
+ - `bun run typescript-ai-policy-fix-semantic -- <target-directory>` run Oxlint with this package's policy config, collect supported diagnostics, and apply semantic fixes to the target directory.
166
+ - `bun run typescript-ai-policy-fix-semantic -- <target-directory> --dry-run` — print the planned semantic-fix scope without mutating files.
159
167
 
160
- Useful commands:
168
+ Repository-local development usage:
161
169
 
162
- - `bun run fix:semantic -- <target-directory>` — run Oxlint with this repository's policy config, collect supported diagnostics, and apply semantic fixes to the target directory.
163
- - `bun run fix:semantic -- <target-directory> --dry-run` — print the planned semantic-fix scope without mutating files.
164
- - `typescript-ai-policy-fix-semantic <target-directory>` — run the same semantic fixer through the package-installed bin after installing this package and `@typescript/native-preview`.
170
+ - `bun run fix:semantic -- <target-directory>` — run the same semantic fixer from this repository checkout while developing the package itself.
165
171
 
166
172
  Today the framework applies two conservative semantic fixes:
167
173
 
@@ -191,7 +197,7 @@ For rule-by-rule rationale plus good/bad examples, see [`src/oxlint/README.md`](
191
197
  | `@alexgorbatchev/no-react-create-element` | Regular application code must use JSX instead of `createElement`. |
192
198
  | `@alexgorbatchev/require-component-root-testid` | Non-story exported React components must render a DOM root with `data-testid`/`testId` exactly equal to the component name, and child ids must use `ComponentName--thing`. |
193
199
  | `@alexgorbatchev/component-file-contract` | Component ownership files may export exactly one main runtime component plus unrestricted type-only API. |
194
- | `@alexgorbatchev/component-file-naming-convention` | Component filenames must match their exported PascalCase component name in either PascalCase or kebab-case form. |
200
+ | `@alexgorbatchev/component-file-naming-convention` | Component filenames must match their exported PascalCase component name in either PascalCase or kebab-case form; non-component `.tsx` modules should be renamed to `.ts`. |
195
201
  | `@alexgorbatchev/component-story-file-convention` | Every component ownership file must have a matching `basename.stories.tsx` file somewhere under a sibling `stories/` directory. |
196
202
  | `@alexgorbatchev/story-file-location-convention` | Storybook files must live under sibling `stories/` directories and must still match a sibling component basename. |
197
203
  | `@alexgorbatchev/story-meta-type-annotation` | The default Storybook meta must use a typed `const meta: Meta<typeof ComponentName>` binding instead of object assertions. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexgorbatchev/typescript-ai-policy",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Shared TypeScript AI policy configs and Oxlint rules",
5
5
  "homepage": "https://github.com/alexgorbatchev/typescript-ai-policy#readme",
6
6
  "repository": {
@@ -26,7 +26,7 @@ const DEFAULT_OXLINT_CONFIG = defineConfig({
26
26
  "**/.vitepress/cache",
27
27
  "**/.vitepress/dist",
28
28
  ],
29
- plugins: ["unicorn", "typescript", "oxc", "react", "jsx-a11y", "jest"],
29
+ plugins: ["unicorn", "typescript", "oxc", "react", "jest"],
30
30
  jsPlugins: [
31
31
  {
32
32
  name: "@alexgorbatchev",
@@ -187,6 +187,10 @@ function readExpectedComponentNameFromFilename(filename: string): string | null
187
187
  .join("");
188
188
  }
189
189
 
190
+ function readInvalidFilenameReportNode(program: AstProgram): TSESTree.Node {
191
+ return program.body[0] ?? program;
192
+ }
193
+
190
194
  const componentFileNamingConventionRule: RuleModule = {
191
195
  meta: {
192
196
  type: "problem" as const,
@@ -197,7 +201,7 @@ const componentFileNamingConventionRule: RuleModule = {
197
201
  schema: [],
198
202
  messages: {
199
203
  invalidComponentFileName:
200
- 'Rename this file to either "ComponentName.tsx" or "component-name.tsx" so its basename can map deterministically to the exported component name.',
204
+ 'Rename this file so its basename can map deterministically to the exported component name. Use "ComponentName.tsx" or "component-name.tsx"; if this is not a component ownership file and does not need JSX syntax, rename it to a ".ts" file instead.',
201
205
  invalidComponentExportName:
202
206
  "Rename the exported component to PascalCase. Component ownership files must export a PascalCase component name.",
203
207
  mismatchedComponentFileName:
@@ -224,7 +228,7 @@ const componentFileNamingConventionRule: RuleModule = {
224
228
  const expectedComponentName = readExpectedComponentNameFromFilename(context.filename);
225
229
  if (!expectedComponentName) {
226
230
  context.report({
227
- node,
231
+ node: readInvalidFilenameReportNode(node),
228
232
  messageId: "invalidComponentFileName",
229
233
  });
230
234
  return;
@@ -7,6 +7,7 @@ import {
7
7
  isExemptSupportBasename,
8
8
  isInStoriesDirectory,
9
9
  isInTestsDirectory,
10
+ readAbbreviatedSiblingDirectoryPath,
10
11
  } from "./helpers.ts";
11
12
 
12
13
  function readRequiredStoriesDirectoryPath(filename: string): string {
@@ -67,12 +68,12 @@ const componentStoryFileConventionRule: RuleModule = {
67
68
  type: "problem" as const,
68
69
  docs: {
69
70
  description:
70
- 'Require every component ownership file to have a matching "basename.stories.tsx" file somewhere under a sibling "stories/" directory',
71
+ 'Require every component ownership file to have a matching "basename.stories.tsx" file under a sibling "stories/" directory',
71
72
  },
72
73
  schema: [],
73
74
  messages: {
74
75
  missingComponentStoryFile:
75
- 'Add a story file named "{{ requiredStoryFileName }}" somewhere under "{{ requiredStoriesDirectoryPath }}". Component ownership files must keep their Storybook coverage under a sibling "stories/" directory.',
76
+ 'Create "{{ requiredStoryFileName }}" under "{{ requiredStoriesDirectoryPath }}". Component ownership files must keep their Storybook coverage under a sibling "stories/" directory.',
76
77
  },
77
78
  },
78
79
  create(context) {
@@ -87,6 +88,7 @@ const componentStoryFileConventionRule: RuleModule = {
87
88
  return {
88
89
  Program(node: AstProgram) {
89
90
  const requiredStoriesDirectoryPath = readRequiredStoriesDirectoryPath(context.filename);
91
+ const displayedStoriesDirectoryPath = readAbbreviatedSiblingDirectoryPath(context.filename, "stories");
90
92
  const requiredStoryFileName = readRequiredStoryFileName(context.filename);
91
93
  if (findDescendantFilePath(requiredStoriesDirectoryPath, requiredStoryFileName)) {
92
94
  return;
@@ -96,7 +98,7 @@ const componentStoryFileConventionRule: RuleModule = {
96
98
  node: readReportNode(node),
97
99
  messageId: "missingComponentStoryFile",
98
100
  data: {
99
- requiredStoriesDirectoryPath,
101
+ requiredStoriesDirectoryPath: displayedStoriesDirectoryPath,
100
102
  requiredStoryFileName,
101
103
  },
102
104
  });
@@ -74,6 +74,16 @@ export function readPathFromDirectory(filename: string, expectedDirectoryName: s
74
74
  return pathSegments.slice(directoryIndex + 1).join("/");
75
75
  }
76
76
 
77
+ export function readAbbreviatedPath(path: string, segmentCount = 3): string {
78
+ const displayedPathSegments = getPathSegments(path).slice(-segmentCount);
79
+
80
+ return `.../${displayedPathSegments.join("/")}`;
81
+ }
82
+
83
+ export function readAbbreviatedSiblingDirectoryPath(filename: string, siblingDirectoryName: string): string {
84
+ return readAbbreviatedPath(`${dirname(filename)}/${siblingDirectoryName}`);
85
+ }
86
+
77
87
  export function readPathFromFirstMatchingDirectory(
78
88
  filename: string,
79
89
  expectedDirectoryNames: DirectoryNames,
@@ -1,10 +1,12 @@
1
1
  import type { AstProgram, RuleModule } from "./types.ts";
2
+ import type { TSESTree } from "@typescript-eslint/types";
2
3
  import { dirname, join } from "node:path";
3
4
  import {
4
5
  findDescendantFilePath,
5
6
  getBaseName,
6
7
  getFilenameWithoutExtension,
7
8
  isExemptSupportBasename,
9
+ readAbbreviatedSiblingDirectoryPath,
8
10
  } from "./helpers.ts";
9
11
 
10
12
  function readRequiredTestsDirectoryPath(filename: string): string {
@@ -18,17 +20,21 @@ function readRequiredHookTestFileName(filename: string): string {
18
20
  return `${sourceBaseName}${testExtension}`;
19
21
  }
20
22
 
23
+ function readReportNode(program: AstProgram): TSESTree.Node {
24
+ return program.body[0] ?? program;
25
+ }
26
+
21
27
  const hookTestFileConventionRule: RuleModule = {
22
28
  meta: {
23
29
  type: "problem" as const,
24
30
  docs: {
25
31
  description:
26
- 'Require every hook ownership file to have a matching "basename.test.ts" or ".test.tsx" file somewhere under a sibling "__tests__/" directory',
32
+ 'Require every hook ownership file to have a matching "basename.test.ts" or ".test.tsx" file under a sibling "__tests__/" directory',
27
33
  },
28
34
  schema: [],
29
35
  messages: {
30
36
  missingHookTestFile:
31
- 'Add a test file named "{{ requiredTestFileName }}" somewhere under "{{ requiredTestsDirectoryPath }}". Hook ownership files must keep their tests under a sibling "__tests__/" directory.',
37
+ 'Create "{{ requiredTestFileName }}" under "{{ requiredTestsDirectoryPath }}". Hook ownership files must keep their tests under a sibling "__tests__/" directory.',
32
38
  },
33
39
  },
34
40
  create(context) {
@@ -39,17 +45,18 @@ const hookTestFileConventionRule: RuleModule = {
39
45
  return {
40
46
  Program(node: AstProgram) {
41
47
  const requiredTestsDirectoryPath = readRequiredTestsDirectoryPath(context.filename);
48
+ const displayedTestsDirectoryPath = readAbbreviatedSiblingDirectoryPath(context.filename, "__tests__");
42
49
  const requiredTestFileName = readRequiredHookTestFileName(context.filename);
43
50
  if (findDescendantFilePath(requiredTestsDirectoryPath, requiredTestFileName)) {
44
51
  return;
45
52
  }
46
53
 
47
54
  context.report({
48
- node,
55
+ node: readReportNode(node),
49
56
  messageId: "missingHookTestFile",
50
57
  data: {
51
58
  requiredTestFileName,
52
- requiredTestsDirectoryPath,
59
+ requiredTestsDirectoryPath: displayedTestsDirectoryPath,
53
60
  },
54
61
  });
55
62
  },
@@ -1,7 +1,13 @@
1
- import type { RuleModule } from "./types.ts";
1
+ import type { TSESTree } from "@typescript-eslint/types";
2
+ import type { AstProgram, RuleModule } from "./types.ts";
2
3
  import { existsSync } from "node:fs";
3
4
  import { join } from "node:path";
4
- import { getStorySourceBaseName, readPathFromStoriesDirectory, readRootPathBeforeDirectory } from "./helpers.ts";
5
+ import {
6
+ getStorySourceBaseName,
7
+ readAbbreviatedPath,
8
+ readPathFromStoriesDirectory,
9
+ readRootPathBeforeDirectory,
10
+ } from "./helpers.ts";
5
11
 
6
12
  function readRequiredSiblingComponentFilePath(filename: string): string | null {
7
13
  const storySourceBaseName = getStorySourceBaseName(filename);
@@ -17,28 +23,33 @@ function readRequiredSiblingComponentFilePath(filename: string): string | null {
17
23
  return join(siblingDirectoryPath, `${storySourceBaseName}.tsx`);
18
24
  }
19
25
 
26
+ function readReportNode(program: AstProgram): TSESTree.Node {
27
+ return program.body[0] ?? program;
28
+ }
29
+
20
30
  const storyFileLocationConventionRule: RuleModule = {
21
31
  meta: {
22
32
  type: "problem" as const,
23
33
  docs: {
24
34
  description:
25
- 'Require Storybook files to live somewhere under a sibling "stories/" directory and match a sibling component ownership file basename',
35
+ 'Require Storybook files to live under a sibling "stories/" directory and match a sibling component ownership file basename',
26
36
  },
27
37
  schema: [],
28
38
  messages: {
29
39
  invalidStoryFileLocation:
30
40
  'Move this story file under a "stories/" directory. Storybook files must not live outside a sibling "stories/" tree.',
31
41
  missingSiblingComponent:
32
- 'Rename or move this story so it matches an existing sibling component ownership file. Expected "{{ requiredComponentFilePath }}" to exist for this story file.',
42
+ 'Rename or move this story so it matches an existing sibling component ownership file. "{{ requiredComponentFilePath }}" must exist for this story file.',
33
43
  },
34
44
  },
35
45
  create(context) {
36
46
  return {
37
- Program(node) {
47
+ Program(node: AstProgram) {
48
+ const reportNode = readReportNode(node);
38
49
  const relativeStoryPath = readPathFromStoriesDirectory(context.filename);
39
50
  if (relativeStoryPath === null) {
40
51
  context.report({
41
- node,
52
+ node: reportNode,
42
53
  messageId: "invalidStoryFileLocation",
43
54
  });
44
55
  return;
@@ -50,10 +61,10 @@ const storyFileLocationConventionRule: RuleModule = {
50
61
  }
51
62
 
52
63
  context.report({
53
- node,
64
+ node: reportNode,
54
65
  messageId: "missingSiblingComponent",
55
66
  data: {
56
- requiredComponentFilePath,
67
+ requiredComponentFilePath: readAbbreviatedPath(requiredComponentFilePath),
57
68
  },
58
69
  });
59
70
  },