@codefast/cli 0.3.13 → 0.3.14-canary.1

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
@@ -1,10 +1,37 @@
1
1
  # @codefast/cli
2
2
 
3
- A focused CLI for two recurring maintenance tasks in monorepos:
3
+ A small developer CLI for maintenance tasks in a TypeScript monorepo — Tailwind class arranging, `package.json` `exports` mirroring, and `@since` JSDoc tagging.
4
4
 
5
- - **`arrange`** — analyze and regroup Tailwind class strings inside `cn()` / `tv()` calls according to a consistent render-pipeline order.
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ - [Why @codefast/cli](#why-codefastcli)
10
+ - [Requirements](#requirements)
11
+ - [Installation](#installation)
12
+ - [Quick start](#quick-start)
13
+ - [Global options](#global-options)
14
+ - [Exit codes](#exit-codes)
15
+ - [`arrange`](#arrange)
16
+ - [`mirror sync`](#mirror-sync)
17
+ - [`tag` / `annotate`](#tag--annotate)
18
+ - [Configuration (`codefast.config.*`)](#configuration-codefastconfig)
19
+ - [Lifecycle hooks](#lifecycle-hooks)
20
+ - [Grouping philosophy — Render Pipeline Order](#grouping-philosophy--render-pipeline-order)
21
+ - [Troubleshooting](#troubleshooting)
22
+ - [Contributing (monorepo setup)](#contributing-monorepo-setup)
23
+ - [License](#license)
24
+ - [Changelog](#changelog)
25
+
26
+ ---
27
+
28
+ ## Why @codefast/cli
29
+
30
+ Three recurring maintenance chores you don't want to script by hand:
31
+
32
+ - **`arrange`** — regroup Tailwind class strings inside `cn()` / `tv()` calls in render-pipeline order.
6
33
  - **`mirror`** — regenerate `package.json` `exports` fields from built `dist/` trees across a pnpm workspace.
7
- - **`tag`** (alias: **`annotate`**) — auto-add `@since <version>` to exported TypeScript declarations that are still missing version metadata.
34
+ - **`tag`** (alias **`annotate`**) — add `@since <version>` JSDoc tags to exported declarations that are missing version metadata.
8
35
 
9
36
  ```mermaid
10
37
  flowchart LR
@@ -25,20 +52,8 @@ flowchart LR
25
52
 
26
53
  ## Requirements
27
54
 
28
- - Node.js `>=22.0.0`
29
- - pnpm (recommended)
30
-
31
- ---
32
-
33
- ## Exit codes
34
-
35
- | Code | Meaning |
36
- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
37
- | `0` | Success. |
38
- | `1` | General failure (missing paths, infrastructure errors, partial failures such as `mirror sync` with package errors, or hook / run errors for `tag` / `arrange`). |
39
- | `2` | Invalid invocation or input (Zod / schema validation for CLI requests). |
40
-
41
- Human-readable errors and diagnostics go to **stderr**; primary command output goes to **stdout**.
55
+ - Node.js `>= 22.0.0`
56
+ - pnpm (recommended — the CLI discovers workspaces via `pnpm-workspace.yaml`)
42
57
 
43
58
  ---
44
59
 
@@ -47,9 +62,15 @@ Human-readable errors and diagnostics go to **stderr**; primary command output g
47
62
  ```bash
48
63
  # Install globally
49
64
  pnpm add -g @codefast/cli
65
+ # or
66
+ npm install -g @codefast/cli
67
+ # or
68
+ yarn global add @codefast/cli
50
69
 
51
70
  # Or run without installing
52
71
  pnpm dlx @codefast/cli --help
72
+ # or
73
+ npx @codefast/cli --help
53
74
  ```
54
75
 
55
76
  ---
@@ -57,28 +78,55 @@ pnpm dlx @codefast/cli --help
57
78
  ## Quick start
58
79
 
59
80
  ```bash
60
- # 1. Preview proposed changes — no files written
81
+ # Analyze Tailwind classes in the nearest package
82
+ codefast arrange analyze
83
+
84
+ # Preview proposed rewrites — no files written
61
85
  codefast arrange preview packages/ui/src/components
62
86
 
63
- # 2. Apply after reviewing the diff
87
+ # Apply after reviewing
64
88
  codefast arrange apply packages/ui/src/components
65
89
 
66
- # 3. Regenerate package exports from built dist/
90
+ # Regenerate every package's `exports` from built dist/
67
91
  codefast mirror sync
68
92
 
69
- # 4. Add @since tags to exported APIs under src/
93
+ # Add @since <version> to exported APIs under ./src
70
94
  codefast tag
71
95
  ```
72
96
 
73
97
  ---
74
98
 
99
+ ## Global options
100
+
101
+ | Flag | Effect |
102
+ | ----------------- | ---------------------------------------------------------------------- |
103
+ | `--no-color` | Disable ANSI color output (also respected by JSON output suppression). |
104
+ | `-V`, `--version` | Print the CLI version and exit. |
105
+ | `-h`, `--help` | Show contextual help for the invoked command. |
106
+
107
+ ---
108
+
109
+ ## Exit codes
110
+
111
+ | Code | Meaning |
112
+ | ---- | -------------------------------------------------------------------------------------------------------- |
113
+ | `0` | Success. |
114
+ | `1` | General failure (missing paths, infrastructure errors, partial failures in `mirror sync`, failed hooks). |
115
+ | `2` | Invalid invocation or input (Zod schema validation on CLI requests — `CLI_EXIT_USAGE`). |
116
+
117
+ Diagnostics go to **stderr**; primary command output goes to **stdout**. When a subcommand accepts `--json`, only the JSON object is written to stdout and all human progress is suppressed, so the stream stays pipeline-safe.
118
+
119
+ ---
120
+
75
121
  ## `arrange`
76
122
 
77
- Reads `cn()` and `tv()` call sites, classifies each Tailwind utility, and rewrites the class strings in render-pipeline order (see [Grouping philosophy](#grouping-philosophy--render-pipeline-order) below).
123
+ Reads `cn()` and `tv()` call sites, classifies each Tailwind utility, and rewrites the class string in render-pipeline order (see [Grouping philosophy](#grouping-philosophy--render-pipeline-order)).
78
124
 
79
- ### Workflow
125
+ ### Target resolution
126
+
127
+ When `[target]` is omitted, `arrange` auto-detects the **nearest package directory** by walking up from the current working directory until it finds a `package.json`. Pass an explicit path (file or directory) to override.
80
128
 
81
- Run the three subcommands in order:
129
+ ### Workflow
82
130
 
83
131
  | Step | Command | Effect |
84
132
  | ---- | ----------------------------------- | ------------------------------------- |
@@ -86,63 +134,106 @@ Run the three subcommands in order:
86
134
  | 2 | `codefast arrange preview [target]` | Show exactly what `apply` would write |
87
135
  | 3 | `codefast arrange apply [target]` | Write the changes |
88
136
 
89
- The default `target` when omitted is `packages/ui/src/components`, resolved from `process.cwd()`.
137
+ ### Flags (preview / apply)
90
138
 
91
- ### Flags
139
+ | Flag | Description |
140
+ | -------------------- | --------------------------------------------------------------------------- |
141
+ | `--with-class-name` | Append `className` as the last argument when rewriting a `cn(...)` call. |
142
+ | `--cn-import <spec>` | Override the module specifier used when a missing `cn` import is added. |
143
+ | `--json` | Print a single JSON object on stdout; suppresses human progress and colors. |
92
144
 
93
- | Flag | Description |
94
- | -------------------- | ----------------------------------------------------------------------- |
95
- | `--with-class-name` | Append `className` as the last argument when rewriting a `cn(...)` call |
96
- | `--cn-import <spec>` | Override the module specifier used when adding a missing `cn` import |
145
+ `analyze` also supports `--json`.
97
146
 
98
- ### `arrange group` — one-shot string grouping
147
+ ### `arrange group` — one-shot classification
99
148
 
100
- Groups a single class string without touching the filesystem. Useful for checking how a string would be classified before running `apply`:
149
+ Groups a class string without touching the filesystem. Useful for checking how classes would be grouped before running `apply`:
101
150
 
102
151
  ```bash
152
+ # Quoted string
103
153
  codefast arrange group "relative flex items-center h-10 w-full rounded-md bg-primary text-white hover:bg-primary/90"
154
+
155
+ # Or space-separated tokens (no quotes needed)
156
+ codefast arrange group relative flex items-center h-10 w-full rounded-md
157
+
158
+ # Emit a tv()-style array instead of a cn() call
159
+ codefast arrange group --tv "flex items-center gap-2"
104
160
  ```
105
161
 
106
- ### `--json` (machine-readable output)
162
+ | Flag | Description |
163
+ | ------------------- | -------------------------------------------------------------------- |
164
+ | `--tv` | Emit a `tv()`-style array literal instead of a `cn(...)` call. |
165
+ | `--with-class-name` | Append `className` to the emitted `cn(...)` call. |
166
+ | `--json` | Emit `{ schemaVersion, primaryLine, bucketsCommentLine }` on stdout. |
107
167
 
108
- Use **`--json`** on any `arrange` subcommand to print **one JSON object** on stdout (human tables / colors are suppressed):
168
+ ### `--json` payloads
109
169
 
110
- | Subcommand | Payload highlights |
111
- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
112
- | `analyze` | `schemaVersion`, `analyzeRootPath`, full `report` (same data as the human report). |
113
- | `preview` / `apply` | `schemaVersion`, `write`, `ok` (false if `onAfterWrite` hook failed), full `result` (`filePaths`, `modifiedFiles`, `totalFound`, …). |
114
- | `group` | `schemaVersion`, `primaryLine`, `bucketsCommentLine`. |
170
+ | Subcommand | Payload highlights |
171
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
172
+ | `analyze` | `schemaVersion`, `analyzeRootPath`, full `report` (same data as the human report). |
173
+ | `preview` / `apply` | `schemaVersion`, `write`, `ok` (`false` if the `onAfterWrite` hook failed), full `result` (`filePaths`, `modifiedFiles`, `totalFound`, …). |
174
+ | `group` | `schemaVersion`, `primaryLine`, `bucketsCommentLine`. |
115
175
 
116
176
  ---
117
177
 
118
178
  ## `mirror sync`
119
179
 
120
- Scans built `dist/` trees and regenerates the `exports` field in each `package.json`. Run from anywhere inside the monorepo — the workspace root is discovered automatically via `pnpm-workspace.yaml`.
180
+ Scans built `dist/` trees and regenerates the `exports` field of every workspace package. Run it from anywhere inside the monorepo — the workspace root is discovered via `pnpm-workspace.yaml`.
121
181
 
122
182
  ```bash
123
- codefast mirror sync # all packages in the workspace
124
- codefast mirror sync packages/ui # a single package path
125
- codefast mirror sync -v # verbose output
126
- codefast mirror sync --json # one JSON object on stdout (for scripts / CI)
183
+ codefast mirror sync # all workspace packages
184
+ codefast mirror sync packages/ui # one package (path relative to repo root)
185
+ codefast mirror sync -v # verbose diagnostics
186
+ codefast mirror sync --json # JSON summary for scripts / CI
127
187
  ```
128
188
 
129
- `--json` prints a single line of JSON with `schemaVersion`, `ok`, `elapsedSeconds`, and `stats` (same counters as the human summary). Human progress and styling are suppressed; use normal mode for interactive runs.
189
+ | Flag | Description |
190
+ | ----------------- | --------------------------------------------------------------------- |
191
+ | `-v`, `--verbose` | Print extra diagnostics. |
192
+ | `--json` | Print a single `{ schemaVersion, ok, elapsedSeconds, stats }` object. |
130
193
 
131
- > **Note:** Packages must be built first so `dist/` exists. Run your build step before `mirror sync`.
194
+ > **Build first.** `mirror sync` reads from `dist/`. Run your build before syncing or exports will reflect stale output.
132
195
 
133
- ### Configuration
196
+ Exit code is `1` when any package fails (`stats.packagesErrored > 0`), `0` otherwise.
134
197
 
135
- Create a `codefast.config.js` (or `.mjs`, `.cjs`, `.json`) at the repo root with a `mirror` key:
198
+ ---
199
+
200
+ ## `tag` / `annotate`
136
201
 
137
- ```js
202
+ Scans `.ts` / `.tsx` sources and adds `@since <current-package-version>` to exported declarations that don't already carry one. The version is read from the nearest `package.json` walking up from the target path.
203
+
204
+ ```bash
205
+ codefast tag # auto-discover workspace packages from cwd
206
+ codefast tag packages/ui/src # annotate a custom target
207
+ codefast annotate --dry-run # preview only, do not write
208
+ codefast tag --json # JSON summary for scripts / CI
209
+ ```
210
+
211
+ | Flag | Description |
212
+ | ----------- | ----------------------------------------------------------------- |
213
+ | `--dry-run` | Show summary without writing files. |
214
+ | `--json` | Print a single JSON summary on stdout; suppresses human progress. |
215
+
216
+ What it updates:
217
+
218
+ - Adds `/** @since <version> */` when an exported declaration has no JSDoc.
219
+ - Injects `@since <version>` into an existing JSDoc block that lacks one.
220
+ - Leaves declarations alone when `@since` is already present.
221
+
222
+ ---
223
+
224
+ ## Configuration (`codefast.config.*`)
225
+
226
+ Create `codefast.config.js`, `.mjs`, `.cjs`, or `.json` at the repo root. Each command reads its own slice.
227
+
228
+ ```javascript
138
229
  // codefast.config.mjs
230
+ import { execSync } from "node:child_process";
231
+
139
232
  export default {
140
233
  mirror: {
141
234
  skipPackages: ["@acme/internal"],
142
235
  pathTransformations: {
143
- "@acme/ui": {
144
- removePrefix: "./components/",
145
- },
236
+ "@acme/ui": { removePrefix: "./components/" },
146
237
  },
147
238
  customExports: {
148
239
  "@acme/ui": {
@@ -150,80 +241,76 @@ export default {
150
241
  },
151
242
  },
152
243
  cssExports: {
244
+ // Shorthand: `true` enables default CSS export detection
245
+ "@acme/theme": true,
246
+ // Or configure explicitly:
153
247
  "@acme/ui": {
154
248
  enabled: true,
249
+ forceExportFiles: false,
155
250
  customExports: {
156
251
  "./tokens.css": "./dist/tokens.css",
157
252
  },
158
253
  },
159
254
  },
160
255
  },
256
+
257
+ tag: {
258
+ skipPackages: ["@acme/internal"],
259
+ onAfterWrite: ({ files }) => {
260
+ execSync(`oxfmt ${files.join(" ")}`, { stdio: "inherit" });
261
+ },
262
+ },
263
+
264
+ arrange: {
265
+ onAfterWrite: ({ files }) => {
266
+ execSync(`oxfmt ${files.join(" ")}`, { stdio: "inherit" });
267
+ },
268
+ },
161
269
  };
162
270
  ```
163
271
 
164
- Use your real package names from `package.json#name` (for example `@acme/ui`) and adjust entries to match your workspace.
272
+ Notes:
165
273
 
166
- > **Migration:** Path-based keys (for example `packages/ui`) are deprecated for `pathTransformations`, `customExports`, `cssExports`, and `skipPackages`. Migrate to package-name keys.
274
+ - Keys in `mirror.skipPackages` / `pathTransformations` / `customExports` / `cssExports` are **package names** from `package.json#name` (e.g. `@acme/ui`). Path-based keys such as `packages/ui` are deprecated and will be removed.
275
+ - `cssExports[pkg]` accepts a boolean shorthand or the full `{ enabled, customExports, forceExportFiles }` object.
276
+ - `tag.skipPackages` lists package names to skip entirely when `codefast tag` is run without an explicit target.
167
277
 
168
- > **Security note:** `.js`, `.mjs`, and `.cjs` config files are loaded via `import()`. Only run `mirror sync` in repositories you trust.
278
+ > **Security.** `.js`, `.mjs`, and `.cjs` config files are loaded via `import()` — only run `codefast` inside repositories you trust.
169
279
 
170
280
  ---
171
281
 
172
- ## Lifecycle hooks (`codefast.config.mjs`)
282
+ ## Lifecycle hooks
173
283
 
174
- `codefast` supports lifecycle hooks so teams can plug in their own post-write workflow (formatter, lint-fix, codemods) without hardcoding any formatter inside CLI core.
284
+ Both `tag` and `arrange apply` expose an `onAfterWrite` hook so teams can plug in their own formatter, lint-fix step, or codemod without embedding one in the CLI core.
175
285
 
176
286
  ```javascript
177
- import { execSync } from "node:child_process";
178
-
179
287
  export default {
180
288
  tag: {
181
- onAfterWrite: ({ files }) => {
182
- console.log(`Formatting ${files.length} files with Oxc...`);
183
- execSync(`oxfmt ${files.join(" ")}`, { stdio: "inherit" });
289
+ onAfterWrite: async ({ files }) => {
290
+ console.log(`Formatting ${files.length} files…`);
291
+ // sync or async work
184
292
  },
185
293
  },
186
294
  arrange: {
187
295
  onAfterWrite: ({ files }) => {
188
- execSync(`oxfmt ${files.join(" ")}`, { stdio: "inherit" });
296
+ /* … */
189
297
  },
190
298
  },
191
299
  };
192
300
  ```
193
301
 
194
- Hook contract:
302
+ Contract:
195
303
 
196
304
  - `tag.onAfterWrite?.({ files })` runs after `codefast tag` writes files.
197
305
  - `arrange.onAfterWrite?.({ files })` runs after `codefast arrange apply` writes files.
198
- - Hooks support both sync and async functions (`void | Promise<void>`).
199
- - Hook failures are reported on stderr; `tag` and `arrange apply` exit with code `1` when a hook fails (see [Exit codes](#exit-codes)).
200
-
201
- ---
202
-
203
- ## `tag` / `annotate`
204
-
205
- Scans `.ts` / `.tsx` source files and annotates exported declarations with `@since <current-package-version>`. This keeps API evolution visible and reduces documentation drift in long-lived codebases.
206
-
207
- ```bash
208
- codefast tag # annotate exports in ./src
209
- codefast tag packages/ui/src # annotate a custom target
210
- codefast annotate --dry-run # preview only, do not write files
211
- codefast tag --json # one JSON object on stdout (for scripts / CI)
212
- ```
213
-
214
- What it updates:
215
-
216
- - Adds `/** @since <version> */` when an exported declaration has no JSDoc.
217
- - Injects `@since <version>` into an existing JSDoc block when missing.
218
- - Leaves declarations unchanged when `@since` is already present.
219
-
220
- The `<version>` value is read from the nearest `package.json` found by walking up from the target path.
306
+ - Hooks may be synchronous or asynchronous (`void | Promise<void>`).
307
+ - Hook failures are reported on stderr; both commands exit with code `1` when the hook rejects.
221
308
 
222
309
  ---
223
310
 
224
311
  ## Grouping philosophy — Render Pipeline Order
225
312
 
226
- `arrange` does **not** sort classes alphabetically. Instead, it groups utilities in roughly the order the browser applies them — from the box's existence, through its shape and surface, to interactive behavior. This makes class strings easier to scan and reason about at a glance.
313
+ `arrange` does **not** sort classes alphabetically. Instead, it groups utilities in roughly the order the browser applies them — from the box's existence, through its shape and surface, to interactive behavior. This makes class strings easier to scan and diff.
227
314
 
228
315
  **Existence → Position → Layout → Sizing → Spacing → Shape → Background → Shadow → Typography → Composite → Motion → Starting → Behavior → State → Selector**
229
316
 
@@ -245,9 +332,9 @@ The `<version>` value is read from the nearest `package.json` found by walking u
245
332
  | **State** | Interactive and conditional variants (non-selector) | `hover:`, `md:`, `@md/sidebar:`, `data-[…]:` |
246
333
  | **Selector** | Selector-driven variants | `[&…]:`, `*:`, `**:`, `has-*`, `group-[…]:` |
247
334
 
248
- Adjacent buckets may be merged into one string literal when declared _compatible_ (e.g. `layout` + `sizing`). This keeps `cn()` calls readable without flattening unrelated concerns into a single undifferentiated blob.
335
+ Adjacent buckets may be merged into a single string literal when declared _compatible_ (e.g. Layout + Sizing), which keeps `cn()` calls readable without flattening unrelated concerns.
249
336
 
250
- To change a placement, edit `classifyBareUtility` in `src/lib/arrange/domain/tokenizer.util.ts` and add a corresponding `classifyToken` test in `src/lib/arrange/domain/tokenizer.util.test.ts`.
337
+ To change placement, edit `classifyBareUtility` in `src/lib/arrange/domain/tokenizer.util.ts` and add a matching case in `tokenizer.util.test.ts`.
251
338
 
252
339
  ---
253
340
 
@@ -257,25 +344,44 @@ To change a placement, edit `classifyBareUtility` in `src/lib/arrange/domain/tok
257
344
  Install globally with `pnpm add -g @codefast/cli`, or run via `pnpm dlx @codefast/cli <command>`.
258
345
 
259
346
  **`mirror sync` writes little or no output**
260
- Packages must be built before syncing. Ensure `dist/` exists by running your build step first, then re-run `codefast mirror sync`.
347
+ Packages must be built first. Ensure `dist/` exists by running your build step, then re-run `codefast mirror sync`.
261
348
 
262
349
  **Unexpected class reorder after `arrange apply`**
263
- Run `arrange preview` before applying and smoke-test the UI. Some components rely on cascade-sensitive ordering that `arrange` cannot detect automatically.
350
+ Run `arrange preview` first and smoke-test the UI. Some components rely on cascade-sensitive ordering that `arrange` cannot detect automatically.
351
+
352
+ **`--json` output mixed with progress lines**
353
+ Some shells buffer progress writes on stderr into stdout when piping — redirect stderr explicitly: `codefast mirror sync --json 2>/dev/null | jq`.
264
354
 
265
355
  ---
266
356
 
267
357
  ## Contributing (monorepo setup)
268
358
 
269
359
  ```bash
270
- # Build the local CLI (produces dist/bin.js)
360
+ # Build the local CLI (produces dist/bin.mjs)
271
361
  pnpm --filter @codefast/cli build
272
362
 
273
363
  # Run the local entrypoint
274
364
  pnpm exec codefast --help
365
+
366
+ # Test + type-check
367
+ pnpm --filter @codefast/cli test
368
+ pnpm --filter @codefast/cli check-types
275
369
  ```
276
370
 
277
- A few naming conventions to keep in mind:
371
+ A few naming conventions:
278
372
 
279
- - **`codefast <command>`** refers to CLI commands exposed via the `@codefast/cli` `bin` entry.
373
+ - **`codefast <command>`** refers to CLI commands exposed via the `bin` entry in `@codefast/cli`.
280
374
  - **Scripts in `packages/cli/package.json`** (`build`, `test`, …) are package-local dev scripts, not CLI commands.
281
- - The root `package.json` includes optional convenience wrappers such as `cli:mirror-sync` and `cli:arrange-analyze` for common dev workflows.
375
+ - The root `package.json` includes optional convenience wrappers (e.g. `cli:mirror-sync`, `cli:arrange-analyze`) for common dev workflows.
376
+
377
+ ---
378
+
379
+ ## License
380
+
381
+ [MIT](https://opensource.org/licenses/MIT) — see [`package.json`](./package.json).
382
+
383
+ ---
384
+
385
+ ## Changelog
386
+
387
+ See [CHANGELOG.md](./CHANGELOG.md) for the full version history. Releases are also published on [npm](https://www.npmjs.com/package/@codefast/cli?activeTab=versions).
@@ -2,7 +2,9 @@ import { EMPTY_CN_TV_BINDINGS } from "../constants.domain.mjs";
2
2
  import { applyEditsDescending, indentOfLineContaining } from "../../../shared/source-code/domain/text-edit.model.mjs";
3
3
  import { isDomainIdentifier, isDomainImportDeclaration, isDomainNamedImports, isDomainNamespaceImport, isDomainPropertyAccessExpression, isDomainStringLiteral, lineOfSourcePosition } from "./ast-node.model.mjs";
4
4
  //#region src/lib/arrange/domain/ast/ast-helpers.helper.ts
5
- /** Known module specifiers that export `cn` / `tv`. */ const KNOWN_CN_TV_MODULES = new Set([
5
+ /**
6
+ * Known module specifiers that export `cn` / `tv`.
7
+ */ const KNOWN_CN_TV_MODULES = new Set([
6
8
  "@codefast/tailwind-variants",
7
9
  "clsx",
8
10
  "class-variance-authority",
@@ -1,5 +1,7 @@
1
1
  //#region src/lib/arrange/domain/constants.domain.ts
2
- /** Analyze report: long literal threshold (token count). */ const LONG_STRING_TOKEN_THRESHOLD = 18;
2
+ /**
3
+ * Analyze report: long literal threshold (token count).
4
+ */ const LONG_STRING_TOKEN_THRESHOLD = 18;
3
5
  /**
4
6
  * Minimum token count for a string to be considered a candidate for grouping
5
7
  * in the apply/preview pipeline. Intentionally much lower than
@@ -9,15 +11,25 @@
9
11
  * Minimum tokens a group must have to stand alone before singleton-merging.
10
12
  * Set to 2 so single-token groups are not collapsed into unrelated buckets by the merger.
11
13
  */ const MIN_GROUP_TOKENS = 2;
12
- /** Dynamic max groups clamp: base bound. */ const MAX_GROUPS_BASE = 4;
13
- /** Dynamic max groups clamp: upper cap. */ const MAX_GROUPS_CAP = 24;
14
+ /**
15
+ * Dynamic max groups clamp: base bound.
16
+ */ const MAX_GROUPS_BASE = 4;
17
+ /**
18
+ * Dynamic max groups clamp: upper cap.
19
+ */ const MAX_GROUPS_CAP = 24;
14
20
  /**
15
21
  * Extra slots so a few state / aria groups do not force `capGroups` to merge
16
22
  * unrelated buckets (e.g. `bg-border` + `outline-hidden`).
17
23
  */ const MAX_GROUPS_HEADROOM = 2;
18
- /** Maximum findings printed per category in the analyze report. */ const MAX_REPORT_LINES = 40;
19
- /** Maximum recursion depth when traversing `tv()` object literals. */ const MAX_OBJECT_DEPTH = 12;
20
- /** Maximum depth when peeling conditional / parens / arrays inside `cn(...)` args. */ const MAX_CLASS_EXPR_DEPTH = 12;
24
+ /**
25
+ * Maximum findings printed per category in the analyze report.
26
+ */ const MAX_REPORT_LINES = 40;
27
+ /**
28
+ * Maximum recursion depth when traversing `tv()` object literals.
29
+ */ const MAX_OBJECT_DEPTH = 12;
30
+ /**
31
+ * Maximum depth when peeling conditional / parens / arrays inside `cn(...)` args.
32
+ */ const MAX_CLASS_EXPR_DEPTH = 12;
21
33
  /**
22
34
  * Maximum variant-stripping passes in {@link stripVariants}.
23
35
  * Real-world Tailwind stacks rarely exceed 4–5 segments (e.g. `@md/sidebar:group-hover:dark:focus-visible:`);
@@ -82,7 +94,9 @@
82
94
  * v3: sm: md: … — v4: @sm:, @min-[600px]:, @[480px]:, named @md/sidebar:,
83
95
  * viewport md/sidebar:, min-[100px]: / max-[100px]:, …
84
96
  */ const RESPONSIVE_PREFIX = /^(?:@(?:min|max)-\[[^\]]+\]:|@\[[^\]]+\]:|@(?:[a-z0-9]+(?:-[a-z0-9]+)*)(?:\/[a-z][a-z0-9-]*)?:|(?:max-|min-)?(?:sm|md|lg|xl|2xl|3xl)(?:\/[a-z][a-z0-9-]*)?:|(?:max-|min-)\[[^\]]+\]:)/;
85
- /** State variant stems — hoisted to module scope (not recreated per call). */ const STATE_PREFIXES = new Set([
97
+ /**
98
+ * State variant stems — hoisted to module scope (not recreated per call).
99
+ */ const STATE_PREFIXES = new Set([
86
100
  "hover",
87
101
  "focus",
88
102
  "focus-within",
@@ -5,7 +5,9 @@ import { bucketsCompatible, bucketsMergeCompatible, classifyToken, compositeSeco
5
5
  * `cn()` grouping: bucket sequence is {@link BUCKET_ORDER} only; tokens are classified with
6
6
  * {@link classifyToken}. Comparators here (`compareClassifiedTailwindTokensForCnGrouping`, …)
7
7
  * are the single place for variant-aware sort — do not reintroduce parallel bucket ordering.
8
- */ /** Separates bucket id from variant key in {@link buildFirstVariantKeySourceIndex} map keys. */ const VARIANT_BUCKET_KEY_SEP = "\0";
8
+ */ /**
9
+ * Separates bucket id from variant key in {@link buildFirstVariantKeySourceIndex} map keys.
10
+ */ const VARIANT_BUCKET_KEY_SEP = "\0";
9
11
  function isVariantKeyedBucket(bucket) {
10
12
  return bucket === "selector" || bucket === "state" || bucket === "starting";
11
13
  }
@@ -70,7 +72,9 @@ function compareClassifiedTailwindTokensForCnGrouping(left, right, firstVariantK
70
72
  for (let i = 0; i < staticPartitionSigs.length; i++) if (staticPartitionSigs[i] !== suggestedPartitionSigs[i]) return false;
71
73
  return true;
72
74
  }
73
- /** Dominant bucket of a whitespace-delimited class group (for merge heuristics). */ function dominantBucketOfGroup(groupStr) {
75
+ /**
76
+ * Dominant bucket of a whitespace-delimited class group (for merge heuristics).
77
+ */ function dominantBucketOfGroup(groupStr) {
74
78
  const counts = /* @__PURE__ */ new Map();
75
79
  for (const classToken of tokenizeClassString(groupStr)) {
76
80
  const tokenBucket = classifyToken(classToken);
@@ -88,7 +92,9 @@ function compareClassifiedTailwindTokensForCnGrouping(left, right, firstVariantK
88
92
  }
89
93
  return best;
90
94
  }
91
- /** Dynamic cap: more tokens → allow more groups, within [BASE, CAP]. */ function dynamicMaxGroups(tokenCount) {
95
+ /**
96
+ * Dynamic cap: more tokens → allow more groups, within [BASE, CAP].
97
+ */ function dynamicMaxGroups(tokenCount) {
92
98
  const byTokens = Math.ceil(tokenCount / 2) + 2;
93
99
  return Math.max(4, Math.min(24, byTokens));
94
100
  }
@@ -136,7 +142,9 @@ function compareClassifiedTailwindTokensForCnGrouping(left, right, firstVariantK
136
142
  }
137
143
  return result;
138
144
  }
139
- /** Tie-break when two merge candidates are both {@link bucketsMergeCompatible} (same bucket or COMPATIBLE_BUCKET_SETS). */ function capMergePenalty(leftBucket, rightBucket) {
145
+ /**
146
+ * Tie-break when two merge candidates are both {@link bucketsMergeCompatible} (same bucket or COMPATIBLE_BUCKET_SETS).
147
+ */ function capMergePenalty(leftBucket, rightBucket) {
140
148
  if (leftBucket === rightBucket) return 0;
141
149
  if (bucketsCompatible(leftBucket, rightBucket)) return 0;
142
150
  return 500;
@@ -246,7 +254,9 @@ function suggestCnGroups(classString) {
246
254
  const firstVariantKeySourceIndex = buildFirstVariantKeySourceIndex(classified);
247
255
  classified.sort((left, right) => compareClassifiedTailwindTokensForCnGrouping(left, right, firstVariantKeySourceIndex));
248
256
  const rawGroups = [];
249
- /** Bucket of the last token already placed in the current run (pairwise compat with `COMPATIBLE_BUCKET_SETS`). */ let lastBucketInRun = null;
257
+ /**
258
+ * Bucket of the last token already placed in the current run (pairwise compat with `COMPATIBLE_BUCKET_SETS`).
259
+ */ let lastBucketInRun = null;
250
260
  let currentStateKey = null;
251
261
  let currentTokens = [];
252
262
  const flush = () => {
@@ -46,7 +46,9 @@ function stripVariants(token) {
46
46
  }
47
47
  return withoutVariants;
48
48
  }
49
- /** Outermost variant segment: first `:` at bracket depth 0 → text before it. */ function firstLeadingVariantPrefix(token) {
49
+ /**
50
+ * Outermost variant segment: first `:` at bracket depth 0 → text before it.
51
+ */ function firstLeadingVariantPrefix(token) {
50
52
  const colonIdx = indexOfFirstVariantColon(token);
51
53
  if (colonIdx === -1) return;
52
54
  return token.slice(0, colonIdx);
@@ -93,7 +95,9 @@ function isStateToken(token) {
93
95
  if (/^will-change/.test(b)) return 50;
94
96
  return 99;
95
97
  }
96
- /** Classify a **bare** utility (no `hover:` / `md:` / … prefixes). */ function classifyBareUtility(bareUtility) {
98
+ /**
99
+ * Classify a **bare** utility (no `hover:` / `md:` / … prefixes).
100
+ */ function classifyBareUtility(bareUtility) {
97
101
  const b = bareUtility;
98
102
  if (/^@container(?:\/[a-z][a-z0-9-]*)?$/i.test(b)) return "existence";
99
103
  if (/^(?:hidden|contents|sr-only|not-sr-only|list-item|flow-root)$/.test(b) || /^(?:block|inline-block|inline)$/.test(b)) return "existence";
@@ -148,11 +152,15 @@ function classifyToken(token) {
148
152
  const match = token.match(/^data-\[([^\]]*)\]/);
149
153
  return match ? `data-[${match[1]}]` : "data";
150
154
  }
151
- /** Same idea as {@link dataAttributeStem} for `aria-[…]:` variants. */ function ariaAttributeStem(token) {
155
+ /**
156
+ * Same idea as {@link dataAttributeStem} for `aria-[…]:` variants.
157
+ */ function ariaAttributeStem(token) {
152
158
  const match = token.match(/^aria-\[([^\]]*)\]/);
153
159
  return match ? `aria-[${match[1]}]` : "aria";
154
160
  }
155
- /** `in-data-[…]:` — normalize on the full bracket expression like {@link dataAttributeStem}. */ function inDataAttributeStem(token) {
161
+ /**
162
+ * `in-data-[…]:` — normalize on the full bracket expression like {@link dataAttributeStem}.
163
+ */ function inDataAttributeStem(token) {
156
164
  const match = token.match(/^in-data-\[([^\]]*)\]/);
157
165
  return match ? `in-data-[${match[1]}]` : "in-data";
158
166
  }
@@ -198,7 +206,9 @@ function bucketsCompatible(a, b) {
198
206
  if (a === b) return true;
199
207
  return COMPATIBLE_BUCKET_SETS.some((bucketSet) => bucketSet.has(a) && bucketSet.has(b));
200
208
  }
201
- /** Like {@link bucketsCompatible}, but never merge two distinct state / starting / selector variant blobs. */ function bucketsMergeCompatible(a, b) {
209
+ /**
210
+ * Like {@link bucketsCompatible}, but never merge two distinct state / starting / selector variant blobs.
211
+ */ function bucketsMergeCompatible(a, b) {
202
212
  if (a === "state" && b === "state") return false;
203
213
  if (a === "starting" && b === "starting") return false;
204
214
  if (a === "selector" && b === "selector") return false;
@@ -235,6 +235,8 @@ let _DomainSourceParserAdapter;
235
235
  _initClass();
236
236
  }
237
237
  });
238
- /** Default instance without diagnostics logging (tests and legacy imports). */ const domainSourceParserAdapter = new _DomainSourceParserAdapter();
238
+ /**
239
+ * Default instance without diagnostics logging (tests and legacy imports).
240
+ */ const domainSourceParserAdapter = new _DomainSourceParserAdapter();
239
241
  //#endregion
240
242
  export { _DomainSourceParserAdapter as DomainSourceParserAdapter, domainSourceParserAdapter };
@@ -1,7 +1,9 @@
1
1
  import { ensureCnImport as ensureCnImport$1 } from "../domain/imports.domain.mjs";
2
2
  import { parseDomainSourceFile } from "./ts-ast-translator.adapter.mjs";
3
3
  //#region src/lib/arrange/infra/ensure-cn-import.adapter.ts
4
- /** Parses source then applies domain cn import injection rules (public CLI-style entry). */ function ensureCnImport(sourceText, filePath, cnImportOverride) {
4
+ /**
5
+ * Parses source then applies domain cn import injection rules (public CLI-style entry).
6
+ */ function ensureCnImport(sourceText, filePath, cnImportOverride) {
5
7
  return ensureCnImport$1(parseDomainSourceFile(filePath, sourceText), cnImportOverride);
6
8
  }
7
9
  //#endregion
@@ -1,7 +1,9 @@
1
1
  //#region src/lib/infra/config-reporter.adapter.ts
2
2
  const YELLOW = "\x1B[33m";
3
3
  const RESET = "\x1B[0m";
4
- /** User-visible lines for non-fatal issues returned with a successfully parsed config. */ function printConfigSchemaWarnings(logger, warnings) {
4
+ /**
5
+ * User-visible lines for non-fatal issues returned with a successfully parsed config.
6
+ */ function printConfigSchemaWarnings(logger, warnings) {
5
7
  const { out } = logger;
6
8
  for (const warningMessage of warnings) out(`${YELLOW}\u26A0\uFE0F ${warningMessage}${RESET}`);
7
9
  }
@@ -27,7 +27,9 @@ function compareExportSpecifiers(leftSpecifier, rightSpecifier, originalPathBySp
27
27
  if (originalPathComparison !== 0) return originalPathComparison;
28
28
  return leftSpecifier.localeCompare(rightSpecifier);
29
29
  }
30
- /** Shared rule: show `package.json#name` when it is a non-empty string; else folder basename. */ function resolvePackageDisplayName(packageJson, folderBasename) {
30
+ /**
31
+ * Shared rule: show `package.json#name` when it is a non-empty string; else folder basename.
32
+ */ function resolvePackageDisplayName(packageJson, folderBasename) {
31
33
  const declaredName = packageJson.name;
32
34
  return typeof declaredName === "string" && declaredName.length > 0 ? declaredName : folderBasename;
33
35
  }
@@ -6,7 +6,9 @@ import picomatch from "picomatch";
6
6
  import { parse } from "yaml";
7
7
  //#region src/lib/mirror/infra/workspace-packages.adapter.ts
8
8
  const PNPM_WORKSPACE = "pnpm-workspace.yaml";
9
- /** Default when the workspace file is missing or has no `packages` key. */ const DEFAULT_INCLUDE = ["packages/*"];
9
+ /**
10
+ * Default when the workspace file is missing or has no `packages` key.
11
+ */ const DEFAULT_INCLUDE = ["packages/*"];
10
12
  function toPosix(filePath) {
11
13
  return filePath.split(path.sep).join("/");
12
14
  }
@@ -15,7 +17,9 @@ function isGlobPermissionError(caughtError) {
15
17
  const code = caughtError.code;
16
18
  return code === "EACCES" || code === "EPERM";
17
19
  }
18
- /** Turn a pnpm `packages` glob into a `globSync` pattern that finds `package.json` files. */ function workspacePatternToPackageJsonGlob(pattern) {
20
+ /**
21
+ * Turn a pnpm `packages` glob into a `globSync` pattern that finds `package.json` files.
22
+ */ function workspacePatternToPackageJsonGlob(pattern) {
19
23
  const normalizedPattern = toPosix(pattern.trim()).replace(/\/+$/, "");
20
24
  if (!normalizedPattern) return "**/package.json";
21
25
  return `${normalizedPattern}/package.json`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codefast/cli",
3
- "version": "0.3.13",
3
+ "version": "0.3.14-canary.1",
4
4
  "description": "Developer CLI for the Codefast monorepo (arrange, mirror, tag)",
5
5
  "keywords": [
6
6
  "cli",
@@ -50,7 +50,7 @@
50
50
  "typescript": "^6.0.2",
51
51
  "yaml": "^2.8.3",
52
52
  "zod": "^4.3.6",
53
- "@codefast/di": "0.3.13"
53
+ "@codefast/di": "0.3.14-canary.1"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/node": "^25.6.0",
@@ -61,7 +61,7 @@
61
61
  "unplugin-swc": "^1.5.9",
62
62
  "vite": "^8.0.8",
63
63
  "vitest": "^4.1.4",
64
- "@codefast/typescript-config": "0.3.13"
64
+ "@codefast/typescript-config": "0.3.14-canary.1"
65
65
  },
66
66
  "engines": {
67
67
  "node": ">=22.0.0"
@@ -1,311 +0,0 @@
1
- //#region src/lib/core/application/architecture-boundaries.policy.ts
2
- /** Product bounded contexts: no direct cross-imports between these (domain + application rules). */ const PRODUCT_BOUNDED_CONTEXTS = new Set([
3
- "arrange",
4
- "mirror",
5
- "tag"
6
- ]);
7
- const SHARED_SOURCE_CODE_CONTEXT = "shared-source-code";
8
- const PATH_SEPARATOR = "/";
9
- const LAYERS = new Set([
10
- "domain",
11
- "application",
12
- "infra",
13
- "presentation"
14
- ]);
15
- /** Import roots that any product slice may depend on without going "through" another slice. */ const NEUTRAL_LIB_IMPORT_ROOTS = new Set([
16
- "core",
17
- "config",
18
- "infra",
19
- "shared"
20
- ]);
21
- /** Internal imports: tsconfig `#/*` → `src/*`. */ const INTERNAL_LIB_PREFIX = "#/lib/";
22
- function pathAfterInternalLibPrefix(specifier) {
23
- if (!specifier.startsWith(INTERNAL_LIB_PREFIX)) return null;
24
- return specifier.slice(6);
25
- }
26
- function internalLibSpecifierSegmentList(specifier) {
27
- const afterPrefix = pathAfterInternalLibPrefix(specifier);
28
- if (afterPrefix === null) return null;
29
- const segments = afterPrefix.split(PATH_SEPARATOR).filter(Boolean);
30
- return segments.length === 0 ? null : segments;
31
- }
32
- function normalizePathSeparators(pathValue) {
33
- return pathValue.split("\\").join(PATH_SEPARATOR);
34
- }
35
- function splitPath(pathValue) {
36
- return normalizePathSeparators(pathValue).split(PATH_SEPARATOR).filter(Boolean);
37
- }
38
- function basenamePath(pathValue) {
39
- const parts = splitPath(pathValue);
40
- return parts.length === 0 ? "" : parts[parts.length - 1] ?? "";
41
- }
42
- function dirnamePath(pathValue) {
43
- const parts = normalizePathSeparators(pathValue).split(PATH_SEPARATOR);
44
- parts.pop();
45
- if (parts.length === 0) return PATH_SEPARATOR;
46
- const joined = parts.join(PATH_SEPARATOR);
47
- return joined === "" ? PATH_SEPARATOR : joined;
48
- }
49
- function joinPath(...segments) {
50
- return segments.map((segment) => normalizePathSeparators(segment)).join(PATH_SEPARATOR).replace(/\/+/g, PATH_SEPARATOR);
51
- }
52
- function normalizeDotSegments(pathValue) {
53
- const normalized = normalizePathSeparators(pathValue);
54
- const isAbsolute = normalized.startsWith(PATH_SEPARATOR);
55
- const segments = normalized.split(PATH_SEPARATOR);
56
- const stack = [];
57
- for (const segment of segments) {
58
- if (!segment || segment === ".") continue;
59
- if (segment === "..") {
60
- if (stack.length > 0) stack.pop();
61
- continue;
62
- }
63
- stack.push(segment);
64
- }
65
- const suffix = stack.join(PATH_SEPARATOR);
66
- return isAbsolute ? `${PATH_SEPARATOR}${suffix}` : suffix;
67
- }
68
- function sharedSourceCodeLayerFromSpecifier(segments) {
69
- if (segments.length < 3 || segments[0] !== "shared" || segments[1] !== "source-code") return null;
70
- return segments[2] ?? null;
71
- }
72
- /**
73
- * Extract `#/lib/...` and relative import specifiers from TypeScript source
74
- * (static `from` / `import("...")` only).
75
- */ function extractImportSpecifiers(sourceText) {
76
- const specifiers = [];
77
- const fromPattern = /\bfrom\s+["']([^"']+)["']/g;
78
- let fromMatch;
79
- while ((fromMatch = fromPattern.exec(sourceText)) !== null) {
80
- const cap = fromMatch[1];
81
- if (cap !== void 0) specifiers.push(cap);
82
- }
83
- const importOnlyPattern = /^\s*import\s+["']([^"']+)["']\s*;/gm;
84
- let importOnlyMatch;
85
- while ((importOnlyMatch = importOnlyPattern.exec(sourceText)) !== null) {
86
- const cap = importOnlyMatch[1];
87
- if (cap !== void 0) specifiers.push(cap);
88
- }
89
- const importCallPattern = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
90
- let importCallMatch;
91
- while ((importCallMatch = importCallPattern.exec(sourceText)) !== null) {
92
- const cap = importCallMatch[1];
93
- if (cap !== void 0) specifiers.push(cap);
94
- }
95
- return specifiers;
96
- }
97
- function pathUnderCliSrcLib(absolutePath) {
98
- const normalized = normalizePathSeparators(absolutePath);
99
- const marker = `${PATH_SEPARATOR}src${PATH_SEPARATOR}lib${PATH_SEPARATOR}`;
100
- const index = normalized.lastIndexOf(marker);
101
- if (index === -1) return null;
102
- return normalized.slice(index + marker.length);
103
- }
104
- function parseLibSourceLocation(absoluteFilePath) {
105
- const tail = pathUnderCliSrcLib(absoluteFilePath);
106
- if (tail === null) return null;
107
- const parts = tail.split(PATH_SEPARATOR).filter(Boolean);
108
- if (parts.length < 2) return null;
109
- if (parts[0] === "shared" && parts[1] === "source-code" && parts.length >= 3) {
110
- const layer = parts[2];
111
- if (layer === void 0 || !LAYERS.has(layer)) return null;
112
- const rest = parts.slice(3);
113
- if (layer === "domain" && rest[0] === "__tests__") return {
114
- context: SHARED_SOURCE_CODE_CONTEXT,
115
- layer: "domain"
116
- };
117
- return {
118
- context: SHARED_SOURCE_CODE_CONTEXT,
119
- layer
120
- };
121
- }
122
- const context = parts[0];
123
- const layer = parts[1];
124
- const rest = parts.slice(2);
125
- if (context === void 0 || layer === void 0 || !LAYERS.has(layer)) return null;
126
- if (layer === "domain" && rest[0] === "__tests__") return {
127
- context,
128
- layer
129
- };
130
- return {
131
- context,
132
- layer
133
- };
134
- }
135
- function isArchitectureExcludedSourceFile(absoluteFilePath) {
136
- const base = basenamePath(absoluteFilePath);
137
- if (base.endsWith(".test.ts") || base.endsWith(".integration.test.ts")) return true;
138
- return normalizePathSeparators(absoluteFilePath).split(PATH_SEPARATOR).includes("__tests__");
139
- }
140
- function resolveRelativeSpecifier(fromAbsoluteFile, specifier) {
141
- if (!specifier.startsWith(".")) return null;
142
- return normalizeDotSegments(joinPath(dirnamePath(fromAbsoluteFile), specifier));
143
- }
144
- function libTailAfterResolution(absoluteResolved) {
145
- const tail = pathUnderCliSrcLib(absoluteResolved);
146
- if (tail === null) return null;
147
- return tail.split(PATH_SEPARATOR).filter(Boolean);
148
- }
149
- function lastSegmentOfLibSpecifier(specifier) {
150
- const segments = internalLibSpecifierSegmentList(specifier);
151
- if (segments === null) return null;
152
- return segments[segments.length - 1] ?? null;
153
- }
154
- function importedModuleStem(specifier, fromAbsoluteFile) {
155
- if (pathAfterInternalLibPrefix(specifier) !== null) return lastSegmentOfLibSpecifier(specifier);
156
- const resolved = resolveRelativeSpecifier(fromAbsoluteFile, specifier);
157
- if (resolved === null) return null;
158
- const baseName = basenamePath(resolved.endsWith(".ts") ? resolved : `${resolved}.ts`);
159
- return baseName.endsWith(".ts") ? baseName.slice(0, -3) : baseName;
160
- }
161
- function isPureDomainOrModelFileName(fileBaseName) {
162
- return /\.(domain|model)\.ts$/.test(fileBaseName);
163
- }
164
- function isRuleAForbiddenImportedStem(moduleStem) {
165
- return moduleStem.includes(".use-case") || moduleStem.includes(".adapter") || moduleStem.includes(".presenter");
166
- }
167
- function isRuleBForbiddenImportedStem(moduleStem) {
168
- return moduleStem.includes(".adapter") || moduleStem.includes(".presenter");
169
- }
170
- function violationRuleAPureDomainModel(absoluteFilePath, fileBaseName, specifier) {
171
- if (!isPureDomainOrModelFileName(fileBaseName)) return null;
172
- const stem = importedModuleStem(specifier, absoluteFilePath);
173
- if (stem === null || !isRuleAForbiddenImportedStem(stem)) return null;
174
- return `${absoluteFilePath}: Rule A (pure domain/model) must not import orchestration or IO modules — blocked ${specifier} (stem "${stem}")`;
175
- }
176
- function violationRuleBApplicationIsolation(loc, absoluteFilePath, specifier) {
177
- if (loc.layer !== "application") return null;
178
- const stem = importedModuleStem(specifier, absoluteFilePath);
179
- if (stem === null || !isRuleBForbiddenImportedStem(stem)) return null;
180
- return `${absoluteFilePath}: Rule B (application isolation) must not import adapters or presenters — blocked ${specifier} (stem "${stem}")`;
181
- }
182
- function violationRuleCAntiCrossSlice(loc, specifier, fromAbsoluteFile) {
183
- if (!PRODUCT_BOUNDED_CONTEXTS.has(loc.context)) return null;
184
- const ruleCSegments = internalLibSpecifierSegmentList(specifier);
185
- if (ruleCSegments !== null) {
186
- const importedRoot = ruleCSegments[0];
187
- if (importedRoot === void 0 || NEUTRAL_LIB_IMPORT_ROOTS.has(importedRoot)) return null;
188
- if (importedRoot === loc.context) return null;
189
- if (PRODUCT_BOUNDED_CONTEXTS.has(importedRoot)) return `${fromAbsoluteFile}: Rule C (slice isolation) context "${loc.context}" must not import "${specifier}" — use #/lib/shared/... or neutral roots (core, config, infra) instead`;
190
- return null;
191
- }
192
- const resolved = resolveRelativeSpecifier(fromAbsoluteFile, specifier);
193
- if (resolved === null) return null;
194
- const parts = libTailAfterResolution(resolved.endsWith(".ts") ? resolved : `${resolved}.ts`);
195
- if (parts === null || parts.length === 0) return null;
196
- const importedContext = parts[0];
197
- if (importedContext === void 0 || NEUTRAL_LIB_IMPORT_ROOTS.has(importedContext)) return null;
198
- if (importedContext === loc.context) return null;
199
- if (PRODUCT_BOUNDED_CONTEXTS.has(importedContext)) return `${fromAbsoluteFile}: Rule C (slice isolation) context "${loc.context}" must not resolve into sibling slice via ${specifier}`;
200
- return null;
201
- }
202
- function violationDomainLayer(loc, specifier, fromAbsoluteFile) {
203
- const domainSegments = internalLibSpecifierSegmentList(specifier);
204
- if (domainSegments !== null) {
205
- const segments = domainSegments;
206
- if (segments[0] === "infra") return `${fromAbsoluteFile}: domain must not import ${specifier}`;
207
- const sharedLayerImported = sharedSourceCodeLayerFromSpecifier(segments);
208
- if (sharedLayerImported !== null && sharedLayerImported !== "domain") return `${fromAbsoluteFile}: domain must not import ${specifier}`;
209
- if (segments[0] === loc.context && segments.length >= 2) {
210
- const importLayer = segments[1];
211
- if (importLayer === "application" || importLayer === "infra" || importLayer === "presentation") return `${fromAbsoluteFile}: domain must not import ${specifier}`;
212
- }
213
- const segmentZero = segments[0];
214
- if (segments.length >= 2 && segments[1] === "domain" && segmentZero !== void 0 && PRODUCT_BOUNDED_CONTEXTS.has(segmentZero) && segmentZero !== loc.context) return `${fromAbsoluteFile}: domain (${loc.context}) must not import other bounded context domain ${specifier}`;
215
- if (loc.context === "core" && segments.length >= 2 && segments[1] === "domain" && segmentZero !== void 0 && PRODUCT_BOUNDED_CONTEXTS.has(segmentZero)) return `${fromAbsoluteFile}: core/domain must not import bounded context domain ${specifier}`;
216
- if (loc.context === "config" && segments.length >= 2 && segments[1] === "domain" && segmentZero !== void 0 && PRODUCT_BOUNDED_CONTEXTS.has(segmentZero)) return `${fromAbsoluteFile}: config/domain must not import bounded context domain ${specifier}`;
217
- if (loc.context === SHARED_SOURCE_CODE_CONTEXT && segmentZero !== void 0 && PRODUCT_BOUNDED_CONTEXTS.has(segmentZero)) return `${fromAbsoluteFile}: shared source-code domain must not import product context ${specifier}`;
218
- return null;
219
- }
220
- const resolved = resolveRelativeSpecifier(fromAbsoluteFile, specifier);
221
- if (resolved === null) return null;
222
- const parts = libTailAfterResolution(resolved);
223
- if (parts === null || parts.length < 2) return null;
224
- const relCtx = parts[0];
225
- const relLayer = parts[1];
226
- if (relCtx === void 0 || relLayer === void 0) return null;
227
- if (relCtx === "infra") return `${fromAbsoluteFile}: domain must not resolve into infra via ${specifier}`;
228
- if (relCtx === loc.context) {
229
- if (relLayer === "application" || relLayer === "infra" || relLayer === "presentation") return `${fromAbsoluteFile}: domain must not resolve into ${relLayer} via ${specifier}`;
230
- }
231
- if (relLayer === "domain" && PRODUCT_BOUNDED_CONTEXTS.has(relCtx) && relCtx !== loc.context) return `${fromAbsoluteFile}: domain must not resolve into other bounded context domain via ${specifier}`;
232
- if (loc.context === "core" && relLayer === "domain" && PRODUCT_BOUNDED_CONTEXTS.has(relCtx)) return `${fromAbsoluteFile}: core/domain must not resolve into bounded context domain via ${specifier}`;
233
- return null;
234
- }
235
- function violationApplicationLayer(loc, specifier, fromAbsoluteFile) {
236
- const applicationSegments = internalLibSpecifierSegmentList(specifier);
237
- if (applicationSegments !== null) {
238
- const segments = applicationSegments;
239
- if (segments[0] === "infra") return `${fromAbsoluteFile}: application must not import ${specifier}`;
240
- const sharedLayerImported = sharedSourceCodeLayerFromSpecifier(segments);
241
- if (sharedLayerImported === "infra" || sharedLayerImported === "presentation" || sharedLayerImported === "application") return `${fromAbsoluteFile}: application must not import ${specifier}`;
242
- if (segments.length >= 2 && (segments[1] === "infra" || segments[1] === "presentation")) return `${fromAbsoluteFile}: application must not import ${specifier}`;
243
- const appSegmentZero = segments[0];
244
- if (PRODUCT_BOUNDED_CONTEXTS.has(loc.context) && appSegmentZero !== void 0 && PRODUCT_BOUNDED_CONTEXTS.has(appSegmentZero) && appSegmentZero !== loc.context) return `${fromAbsoluteFile}: application (${loc.context}) must not import ${specifier}`;
245
- return null;
246
- }
247
- const resolved = resolveRelativeSpecifier(fromAbsoluteFile, specifier);
248
- if (resolved === null) return null;
249
- const parts = libTailAfterResolution(resolved);
250
- if (parts === null || parts.length < 2) return null;
251
- const relCtx = parts[0];
252
- const relLayer = parts[1];
253
- if (relCtx === void 0 || relLayer === void 0) return null;
254
- if (relCtx === "infra") return `${fromAbsoluteFile}: application must not resolve into infra via ${specifier}`;
255
- if (relLayer === "infra" || relLayer === "presentation") return `${fromAbsoluteFile}: application must not resolve into ${relLayer} via ${specifier}`;
256
- return null;
257
- }
258
- /**
259
- * Returns human-readable violations for a single file's imports (for unit tests and tooling).
260
- */ function violationsForFileContent(absoluteFilePath, loc, sourceText) {
261
- const violations = [];
262
- const fileBaseName = basenamePath(absoluteFilePath);
263
- for (const specifier of extractImportSpecifiers(sourceText)) {
264
- const ruleA = violationRuleAPureDomainModel(absoluteFilePath, fileBaseName, specifier);
265
- if (ruleA !== null) violations.push(ruleA);
266
- const ruleB = violationRuleBApplicationIsolation(loc, absoluteFilePath, specifier);
267
- if (ruleB !== null) violations.push(ruleB);
268
- const ruleC = violationRuleCAntiCrossSlice(loc, specifier, absoluteFilePath);
269
- if (ruleC !== null) violations.push(ruleC);
270
- if (loc.layer === "domain") {
271
- const domainViolation = violationDomainLayer(loc, specifier, absoluteFilePath);
272
- if (domainViolation !== null) violations.push(domainViolation);
273
- } else if (loc.layer === "application") {
274
- const appViolation = violationApplicationLayer(loc, specifier, absoluteFilePath);
275
- if (appViolation !== null) violations.push(appViolation);
276
- }
277
- }
278
- return [...new Set(violations)];
279
- }
280
- function walkNonTestTsFiles(rootDir, fs) {
281
- const results = [];
282
- const scan = (dir) => {
283
- for (const entryName of fs.readdirSync(dir)) {
284
- const full = joinPath(dir, entryName);
285
- const stat = fs.statSync(full);
286
- if (stat.isDirectory()) {
287
- if (entryName === "node_modules" || entryName === "dist") continue;
288
- scan(full);
289
- } else if (stat.isFile() && entryName.endsWith(".ts")) {
290
- if (!isArchitectureExcludedSourceFile(full)) results.push(full);
291
- }
292
- }
293
- };
294
- scan(rootDir);
295
- return results;
296
- }
297
- /**
298
- * Scans `packages/cli/src/lib` for explicit-architecture boundary violations (non-test sources only).
299
- */ function scanCliPackageArchitectureViolations(cliPackageRoot, fs, pathService) {
300
- const libRoot = pathService.join(cliPackageRoot, "src", "lib");
301
- const violations = [];
302
- for (const absoluteFilePath of walkNonTestTsFiles(libRoot, fs)) {
303
- const loc = parseLibSourceLocation(absoluteFilePath);
304
- if (loc === null) continue;
305
- const sourceText = fs.readFileSync(absoluteFilePath, "utf8");
306
- violations.push(...violationsForFileContent(absoluteFilePath, loc, sourceText));
307
- }
308
- return violations;
309
- }
310
- //#endregion
311
- export { extractImportSpecifiers, isArchitectureExcludedSourceFile, parseLibSourceLocation, scanCliPackageArchitectureViolations, violationsForFileContent };