Skip to content

Refactor ztd-cli to reduce dependencies and support named parameters#433

Merged
mk3008 merged 10 commits intomainfrom
feat/ztd-cli-zero-deps
Feb 6, 2026
Merged

Refactor ztd-cli to reduce dependencies and support named parameters#433
mk3008 merged 10 commits intomainfrom
feat/ztd-cli-zero-deps

Conversation

@mk3008
Copy link
Owner

@mk3008 mk3008 commented Feb 6, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for named parameters in SQL queries (e.g., :user_id) with automatic compilation to driver-specific placeholders.
    • Introduced timestamp normalization utility for database-returned timestamps.
    • Enhanced ZTD scaffold generation with improved initialization workflow, including validator backend selection and DDL seeding options.
    • Added validation recipes for Zod and ArkType integrations.
  • Documentation

    • New guides for SQL contract mapping, runtime validation with Zod and ArkType.
    • Added comprehensive AGENTS.md guidance files across scaffold directories for runtime boundaries and best practices.
  • Chores

    • Restructured template scaffolds with clearer directory organization and minimal baseline setup.
    • Consolidated shared SQL parameter compilation utilities.

@coderabbitai
Copy link

coderabbitai bot commented Feb 6, 2026

Warning

Rate limit exceeded

@mk3008 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 53 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Introduces SQL-first named-parameter binding via a new shared binder package (@rawsql-ts/_shared/binder), integrates it into the PostgreSQL adapter, adds timestamp normalization utilities to sql-contract, and comprehensively refactors ztd-cli's init command with new workflow options, template generation, validator backend selection, and expanded scaffold structure with per-folder guidance.

Changes

Cohort / File(s) Summary
Named Parameter Binder
packages/_shared/binder/src/compileNamedParameters.ts, packages/_shared/binder/src/index.ts, packages/_shared/binder/package.json, packages/_shared/binder/tsconfig*.json, packages/_shared/binder/README.md
New shared binder package implementing SQL tokenizer state machine to compile named parameters (:name syntax) into driver-specific placeholders (pg-indexed or question-mark style) with comprehensive validation and error handling for strings, identifiers, and comments.
PostgreSQL Adapter Integration
packages/adapters/adapter-node-pg/src/driver/PgTestkitClient.ts, packages/adapters/adapter-node-pg/src/driver/createPgTestkitPool.ts, packages/adapters/adapter-node-pg/src/types.ts, packages/adapters/adapter-node-pg/package.json, packages/adapters/adapter-node-pg/tests/binder-constraints.test.ts, packages/adapters/adapter-node-pg/tsconfig.build.json, packages/adapters/adapter-node-pg/README.md
Updates adapter to support named parameters by normalizing input via compileNamedParameters when NamedParams objects are provided; adds NamedParams type and extends PgQueryInput union to accept Record<string, unknown> alongside arrays; includes binder constraints tests covering string/comment ignoring, duplicates, and missing parameter handling.
SQL Contract Timestamp Normalization
packages/sql-contract/src/utils/coercions.ts, packages/sql-contract/tests/utils/coercions.test.ts, .changeset/catalog-runtime-timestamp-normalization.md
Adds public timestampFromDriver utility for fail-fast normalization of driver-returned timestamps (Date or string) with optional field-name context in error messages; comprehensive test coverage for valid/invalid Date, ISO strings, and type mismatches.
ZTD CLI Init Command Refactoring
packages/ztd-cli/src/commands/init.ts, packages/ztd-cli/src/commands/lint.ts, packages/ztd-cli/src/commands/ztdConfig.ts, packages/ztd-cli/src/utils/optionalDependencies.ts
Major refactoring introducing InitWorkflow selection (pg_dump/empty/demo), optional validator backend feature gathering (zod/arktype), OverwritePolicy for force/non-interactive overwrites, promptInputWithDefault support, dynamic dependency loading, and extensive template generation (agents, readmes, smoke tests, validation specs); lint/ztdConfig updated for dynamic module loading.
ZTD CLI Templates & Documentation
packages/ztd-cli/templates/src/**/AGENTS.md, packages/ztd-cli/templates/src/catalog/runtime/*.ts, packages/ztd-cli/templates/src/repositories/*/AGENTS.md, packages/ztd-cli/templates/tests/smoke*.test.ts, packages/ztd-cli/templates/ztd/ddl/AGENTS.md, packages/ztd-cli/templates/ztd/ddl/demo.sql, packages/ztd-cli/templates/AGENTS.md, packages/ztd-cli/templates/README.md
Restructured scaffold with per-folder AGENTS.md guidance, new SQL organization (views/jobs under src/sql), split repositories (views/tables), added smoke tests for timestamp coercion/validation, introduced validation spec templates (zod/arktype variants), and simplified root AGENTS/README to focus on minimal structure and core rules.
Validation Recipe Documentation
docs/recipes/validation-zod.md, docs/recipes/validation-arktype.md, docs/recipes/sql-contract.md, AGENTS.md
New recipe documentation for Zod and ArkType validation workflows, SQL contract mapping, and updated root AGENTS with validation tooling guidance section; covers installation, usage patterns, and integration with sql-contract mappers.
Test Infrastructure Updates
packages/ztd-cli/tests/init.command.test.ts, packages/ztd-cli/templates/tests/support/testkit-client.ts, packages/ztd-cli/templates/tests/support/global-setup.ts, packages/ztd-cli/templates/tests/smoke.test.ts
Extended init tests with schema-aware validation, required directories/agents/files verification, multiple workflow scenarios (empty/demo/pg_dump), and optional feature validation; replaced comprehensive testkit client with minimal stub; simplified global-setup to warn on missing DATABASE_URL.
Package Configuration & Workspace
packages/ztd-cli/package.json, pnpm-workspace.yaml, tsconfig.json, .gitignore
Moved Postgres-specific dependencies from ztd-cli runtime to devDependencies, added optional peer dependency for adapter-node-pg, introduced @rawsql-ts/_shared/binder path aliases, and added new workspace pattern and playground ignore rule.
Playground Removal
playgrounds/ztd-playground/*
Entire ztd-playground directory and its configuration, tests, SQL definitions, domain specs, and enums removed (files deleted: ecommerce.sql, customer/product/sales query files, integration tests, testkit/vitest config, AGENTS/README guidance).
Template SQL Client
packages/ztd-cli/templates/src/db/sql-client.ts
Updated SqlClient.query values parameter type to accept Record<string, unknown> alongside readonly unknown[] to support named-parameter binding.
Changeset Files
.changeset/catalog-runtime-timestamp-normalization.md, .changeset/sql-first-binder.md, .changeset/ztd-cli-init-minimal-scaffold.md, .changeset/ztd-cli-scaffold-structure.md
Four changesets documenting timestamp normalization helper, SQL-first binder adoption, minimal deterministic scaffold redesign, and new ZTD scaffold layout with --yes flag support.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI as ztd init Command
    participant Prompter
    participant Templates as Template Generator
    participant FS as File System
    participant PackageJSON as package.json Manager

    User->>CLI: Run ztd init with options
    CLI->>Prompter: Gather project details (path, validator backend)
    Prompter->>User: Prompt for input, schema workflow (empty/demo/pg_dump)
    User-->>Prompter: Provide answers (validator: zod/arktype, workflow choice)
    Prompter-->>CLI: Return gathered config + optional features
    
    CLI->>Templates: Resolve template paths (agents, readmes, specs, runtime)
    Templates->>FS: Write multi-folder structure
    FS-->>Templates: Confirm write with overwrite policy (force/interactive)
    
    alt Validator Backend Selected
        CLI->>Templates: Inject validator dependencies (ZOD_DEPENDENCY or ARKTYPE_DEPENDENCY)
        Templates->>FS: Write validator-specific spec (_smoke.spec.zod.ts or .arktype.ts)
    end
    
    alt Schema Workflow = pg_dump
        CLI->>CLI: Execute pg_dump to pull schema
        CLI->>FS: Write pulled schema to ztd/ddl/<schema>.sql
    else Workflow = demo
        CLI->>FS: Write demo.sql with sample tables
    else Workflow = empty
        CLI->>FS: Write empty template placeholder
    end
    
    CLI->>PackageJSON: Inject scripts, devDependencies, formatting hooks
    PackageJSON->>FS: Update/create package.json
    
    CLI->>FS: Write per-folder AGENTS.md (runtime, specs, repos, sql, etc.)
    CLI->>User: Return summary with next steps and validator info
Loading
sequenceDiagram
    participant Runtime as Generated Runtime
    participant Driver as SQL Driver
    participant Binder as `@rawsql-ts/_shared/binder`
    participant Adapter as adapter-node-pg
    participant Contract as sql-contract

    Runtime->>Runtime: Prepare named SQL query with :paramName syntax
    Runtime->>Adapter: Call query(sql, { paramName: value, ... })
    
    Adapter->>Adapter: Detect NamedParams object input
    Adapter->>Binder: compileNamedParameters(sql, params, 'pg-indexed')
    
    Binder->>Binder: Tokenize SQL (strings, identifiers, comments)
    Binder->>Binder: Extract :name placeholders
    Binder->>Binder: Replace with $1, $2, ... placeholders
    Binder-->>Adapter: Return { sql: compiled, values: ordered[], orderedNames: [] }
    
    Adapter->>Driver: query(compiledSQL, orderedValues)
    Driver-->>Adapter: rows: { id, createdAt: Date | string, ... }[]
    
    Adapter->>Runtime: Return raw rows
    Runtime->>Contract: normalizeTimestamp(row.createdAt, 'createdAt')
    
    alt Input is Date
        Contract->>Contract: Validate non-Invalid Date
        Contract-->>Runtime: Return same Date instance
    else Input is string
        Contract->>Contract: Parse via Date.parse
        Contract-->>Runtime: Return Date instance
    else Invalid input
        Contract->>Contract: Throw with field context
    end
    
    Runtime->>Runtime: Apply validator (zod/arktype) to normalized DTO
    Runtime-->>Runtime: Return typed result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Named parameters hop through SQL strings with glee,
A binder compiles them to $1, $2, and three,
With ArkType and Zod by our developer's side,
Timestamps dance safe—normalized with pride,
The scaffold springs forth, per-folder so neat,
A ZTD journey made deterministic and sweet! 🌱

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Feat/ztd-cli-zero-deps' is vague and does not clearly convey the primary changes in this substantial pull request. Use a more descriptive title that captures the main objective, such as 'Refactor ztd-cli to reduce dependencies and support named parameters' or 'Extract binder utilities and make ztd-cli adapter-agnostic'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/ztd-cli-zero-deps

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/ztd-cli/src/commands/init.ts (1)

273-283: ⚠️ Potential issue | 🟡 Minor

APP_INTERFACE_SECTION hardcodes "zod v4" — misleading when ArkType is chosen.

Line 279 states "Input validation relies on zod v4 or later", but this guidance is appended regardless of which validator backend the user selected. When ArkType is chosen, this advice is incorrect.

Consider parameterizing this section based on optionalFeatures.validator, or making the statement validator-agnostic (e.g., "Input validation relies on the configured validator backend").

Proposed fix
-3. Input validation relies on zod v4 or later and happens at the repository boundary before any SQL runs.
+3. Input validation happens at the repository boundary before any SQL runs (using the project's configured validator).
packages/ztd-cli/src/commands/lint.ts (1)

174-219: ⚠️ Potential issue | 🟡 Minor

Container cleanup error is unhandled on Line 218.

container?.stop() is awaited but not guarded with .catch(), unlike client?.end() on Line 217. If the Testcontainers stop call throws (network timeout, Docker daemon issues), the error will propagate out of the finally block and mask any original error from the try body.

🛡️ Proposed fix
     await client?.end().catch(() => undefined);
-    await container?.stop();
+    await container?.stop().catch(() => undefined);
packages/ztd-cli/src/commands/ztdConfig.ts (1)

82-82: ⚠️ Potential issue | 🟡 Minor

Add JSDoc to exported snapshotTableMetadata function.

This exported function in src/commands/ztdConfig.ts is missing JSDoc documentation required by the coding guidelines for all exported functions in src/. Document what the function does, what it expects from sources and the optional resolver parameter, and what TableMetadata[] contains.

Note: The TableNameResolver type dependency is not a concern—this function is used only internally within ztd-cli, and the parameter is optional, allowing callers like genEntities.ts to omit it entirely.

🤖 Fix all issues with AI agents
In `@docs/recipes/sql-contract.md`:
- Around line 56-58: Update the "What’s next" section: replace the ambiguous
phrase "the Zod or ArkType recipe below" with explicit links to the separate
recipe files for Zod and ArkType (e.g., link labels "Zod recipe" and "ArkType
recipe") so readers know those are separate documents; keep the rest of the
sentence about wiring/runtime validation and ZTD projects intact.

In `@docs/recipes/validation-arktype.md`:
- Around line 11-13: The install command currently uses `-D` which makes
`arktype` a devDependency but the example shows application code
(`listCustomers`) implying production use; either change the install line to
`pnpm add `@rawsql-ts/sql-contract` arktype` to install `arktype` as a runtime
dependency, or explicitly state in the recipe that validation is test-only and
keep `pnpm add -D ...`; update the docs around the `pnpm add -D
`@rawsql-ts/sql-contract` arktype` snippet and reference the `listCustomers`
example so readers know which approach applies.
- Around line 34-41: The code incorrectly infers Customer with ReturnType and
redundantly casts after assertion; update the Customer type to use typeof
CustomerSchema.infer and in listCustomers' reader.validator callback return the
result of CustomerSchema.assert(value) directly (remove the "as Customer" cast)
so the validated value carries the proper inferred type from CustomerSchema;
target symbols: CustomerSchema, Customer type alias, listCustomers, and
reader.validator.

In `@docs/recipes/validation-zod.md`:
- Around line 19-42: The example calls validator() directly on the object
returned by createReader(), but createReader() returns a Mapper (no validator);
fix by creating a mapping via rowMapping(...) (e.g., customerMapping) and then
call bind(customerMapping) on the mapper returned from createReader() to obtain
a MapperReader before calling validator(); update references so you use const
mapper = createReader(executor) and then
mapper.bind(customerMapping).validator(CustomerSchema).list(...).

In `@packages/_shared/binder/src/compileNamedParameters.ts`:
- Around line 120-131: When entering the single-quote state in
compileNamedParameters, remember if the quote was opened with an E prefix (e.g.,
E'...') by setting a flag (e.g., singleQuoteIsEScape) when you see an 'E'
immediately before the opening single quote; then, inside the single_quote
branch (where compiledSql, state, char, next, index are handled), treat a single
quote preceded by a backslash as escaped when singleQuoteIsEScape is true (do
not close the string or exit the single_quote state), and clear that flag when
you transition out of single_quote; this prevents misinterpreting
backslash-escaped quotes like E'\'' as string terminators and avoids rewriting
named params inside such strings.

In `@packages/adapters/adapter-node-pg/package.json`:
- Around line 34-39: The public package `@rawsql-ts/adapter-node-pg` lists a
dependency on the private package `@rawsql-ts/_shared/binder` using "workspace:^",
which will fail for npm consumers because the binder package is
private/unpublished; fix by removing the private binder dependency from
adapter-node-pg and either (A) make `@rawsql-ts/_shared/binder` publishable
(remove private: true and assign a version) and update package.json to reference
its published version instead of workspace:^, or (B) inline/bundle the binder
code into the adapter at build time so `@rawsql-ts/_shared/binder` is not a
runtime dependency, or (C) move the binder utilities into an already-published
package (e.g., rawsql-ts or `@rawsql-ts/testkit-core`) and update the dependency
to that package; ensure package.json no longer contains
"@rawsql-ts/_shared/binder": "workspace:^" when publishing.

In `@packages/adapters/adapter-node-pg/src/types.ts`:
- Around line 34-36: The local duplicate NamedParams type in types.ts must be
removed and replaced with an import from the canonical package; update the file
so PgQueryInput continues to use NamedParams but imports NamedParams from
"@rawsql-ts/_shared/binder" (matching the pattern used in PgTestkitClient.ts) to
eliminate the local definition and ensure a single source of truth for
NamedParams.

In `@packages/ztd-cli/src/utils/optionalDependencies.ts`:
- Around line 25-28: Add JSDoc blocks for the exported functions
clearOptionalDependencyCache, ensureTestkitCoreModule,
ensureAdapterNodePgModule, ensurePgModule, and ensurePostgresContainerModule:
for each function include a one-line summary, descriptions of parameters (if
any) and their types, the exact return type (or Promise<...>), and note thrown
errors (e.g., when a dependency cannot be resolved) and side effects (like
clearing cache); ensure the JSDoc is in plain English, placed immediately above
each exported function, and follows the existing project style (`@param`,
`@returns`, and `@throws` tags where applicable) so documentation and tooling pick
up the signatures.

In `@packages/ztd-cli/templates/src/catalog/specs/_smoke.spec.arktype.ts`:
- Around line 8-13: The type alias SmokeOutput should use ArkType's .infer
rather than ReturnType; replace the current ReturnType usage for the exported
SmokeOutput with the proper ArkType extraction using typeof
smokeOutputSchema.infer so the inferred validated shape from smokeOutputSchema
is used (refer to smokeOutputSchema and SmokeOutput).

In `@packages/ztd-cli/templates/src/jobs/AGENTS.md`:
- Line 10: Update the compound adjective in the AGENTS.md content: change the
phrase "Temporary-table driven workflows" to "Temporary-table-driven workflows"
so the compound modifier is correctly hyphenated; locate the string
"Temporary-table driven" in the AGENTS.md template and replace it with
"Temporary-table-driven".

In `@packages/ztd-cli/tests/init.command.test.ts`:
- Around line 20-35: Export the normalization and sanitization logic from the
production module and import them in the test instead of duplicating them: in
init.ts export the existing normalizeSchemaName function and rename/re-export
the schemaFileName logic as sanitizeSchemaFileName (keep its behavior of
trimming, removing surrounding quotes, lowercasing, replacing non [a-z0-9_-]
chars with '_' and trimming leading/trailing underscores, returning 'schema'
when empty), then update the test to import normalizeSchemaName and
sanitizeSchemaFileName and remove the duplicated implementations so the test
reuses the single source of truth.
🧹 Nitpick comments (17)
packages/ztd-cli/templates/ztd/ddl/AGENTS.md (1)

12-12: Nit: examples use PostgreSQL reserved words.

"user" and "order" are reserved keywords in PostgreSQL and require quoting when used as identifiers. Consider using non-reserved examples (e.g., account, purchase_order) to avoid misleading newcomers, or add a note about quoting.

packages/ztd-cli/templates/ztd/ddl/demo.sql (1)

45-50: Consider adding foreign key constraints to task_assignment.

task_id and user_id reference task and "user" respectively (per column comments), but no REFERENCES constraints are declared. For a demo scaffold that guides users on best practices, explicit FK constraints would reinforce referential integrity patterns.

♻️ Suggested addition
 create table task_assignment (
   task_assignment_id serial8 primary key,
-  task_id            bigint not null,
-  user_id            bigint not null,
+  task_id            bigint not null references task(task_id),
+  user_id            bigint not null references "user"(user_id),
   assigned_at        timestamptz not null default current_timestamp
 );
packages/_shared/binder/tsconfig.json (1)

1-14: tsconfig.json and tsconfig.build.json are identical.

Both files in packages/_shared/binder/ have the exact same content. Typically tsconfig.json serves the IDE (and may include tests), while tsconfig.build.json is referenced by the build script. If this package ever adds its own tests, tsconfig.json should include them. Consider having tsconfig.json extend tsconfig.build.json and add test paths, or remove the duplicate if one config suffices.

packages/_shared/binder/README.md (1)

1-3: Consider adding a brief description of the package's purpose.

The import guidance is clear, but a one-liner explaining what the binder does (e.g., named-parameter → positional-placeholder compilation) would help newcomers orient quickly without reading source.

packages/ztd-cli/templates/tests/support/global-setup.ts (1)

8-14: Consider failing early when DATABASE_URL is absent.

The setup now warns but still lets the test suite run. Every SQL-backed test will then fail individually with opaque connection errors, making it harder to diagnose the root cause. Throwing here (or returning a non-zero exit) would surface the problem immediately.

If warn-only is intentional (e.g. to allow non-SQL unit tests in the same suite), a brief inline comment documenting that rationale would help future maintainers of scaffolded projects.

💡 Optional: fail-fast alternative
   if (!configuredUrl) {
-    console.warn(
-      'DATABASE_URL is not configured. Install a database adapter or set DATABASE_URL before running SQL-backed tests.',
-    );
+    throw new Error(
+      'DATABASE_URL is not configured. Install a database adapter or set DATABASE_URL before running SQL-backed tests.',
+    );
   }
packages/_shared/binder/src/compileNamedParameters.ts (1)

223-225: Throwing when no named parameters are found may be too strict for some callers.

If a caller passes SQL that legitimately has no parameters (e.g. SELECT 1), this function throws. The caller must know to avoid calling compileNamedParameters for parameterless queries, which pushes validation upstream. Consider whether returning { sql, values: [], orderedNames: [] } would be more ergonomic, or document this precondition prominently.

packages/ztd-cli/src/commands/init.ts (5)

192-222: gatherOptionalFeatures accepts an unused _dependencies parameter.

The _dependencies parameter is prefixed to suppress lint, but passing it through adds noise at the call site (line 528). If there's no planned use, consider removing it now and adding it later via a signature change when needed.


531-919: Consider extracting the repetitive template-writing block into a data-driven loop.

Lines 531–919 follow an identical pattern for ~25 template files: call writeTemplateFile, check the result, store in summaries. This could be reduced to a configuration array mapping FileKey → templateName iterated in a loop.

This isn't blocking, but it would significantly reduce the ~400 lines of boilerplate and make adding new templates a one-line change.

Sketch of a data-driven approach
const templateEntries: Array<{ key: FileKey; template: string; allowFallback?: boolean }> = [
  { key: 'readme', template: README_TEMPLATE, allowFallback: true },
  { key: 'ztdDocsAgent', template: ZTD_AGENTS_TEMPLATE },
  { key: 'ztdDocsReadme', template: ZTD_README_TEMPLATE },
  // ... all other entries
];

for (const { key, template, allowFallback } of templateEntries) {
  const summary = await writeTemplateFile(
    rootDir, absolutePaths[key], relativePath(key),
    template, dependencies, prompter, overwritePolicy, allowFallback
  );
  if (summary) {
    summaries[key] = summary;
  }
}

979-984: Unsafe type assertion summaries as Record<FileKey, FileSummary>.

summaries is Partial<Record<FileKey, FileSummary>>, but buildSummaryLines declares its first parameter as the non-partial Record<FileKey, FileSummary>. The function body (line 1725) safely handles missing entries with summaries[key] and a truthiness check, so the runtime is fine — but the signature is misleading. Either keep the assertion and update buildSummaryLines to accept Partial<...>, or leave the assertion as-is with a comment.

Align the signature with actual usage
 function buildSummaryLines(
-  summaries: Record<FileKey, FileSummary>,
+  summaries: Partial<Record<FileKey, FileSummary>>,
   optionalFeatures: OptionalFeatures,
   nextSteps: string[]
 ): string[] {

1750-1769: Missing --non-interactive CLI flag — currently inferred from TTY only.

Line 1764 sets nonInteractive: !process.stdin.isTTY, which is a reasonable default. However, there's no explicit --non-interactive flag for cases where stdin is a TTY but the user wants non-interactive behavior (e.g., scripted CI runners that allocate a pseudo-TTY). Consider adding --non-interactive alongside --yes for full control.

This is low-priority since --yes already covers the common CI use case.


1750-1751: Exported registerInitCommand is missing JSDoc.

As per coding guidelines, all exported functions in src/ must include JSDoc. A one-liner would suffice.

Add JSDoc
+/** Register the `ztd init` sub-command on the given Commander program. */
 export function registerInitCommand(program: Command): void {
packages/ztd-cli/tests/init.command.test.ts (1)

220-241: Consider verifying demo-specific scaffolding artifacts.

The empty-scaffold test explicitly asserts that demo files (e.g. src/sql/example-user/, src/repositories/views/user-profiles.ts) are absent. This test should mirror that by asserting the demo-specific files are present, confirming the two workflows diverge as intended.

docs/recipes/sql-contract.md (1)

24-26: Combine the two imports from the same module in the example.

Lines 25–26 import createReader and rowMapping separately from the same path. Combining them makes the example cleaner for readers.

📝 Suggested fix
-import { createReader } from '@rawsql-ts/sql-contract/mapper';
-import { rowMapping } from '@rawsql-ts/sql-contract/mapper';
+import { createReader, rowMapping } from '@rawsql-ts/sql-contract/mapper';
packages/sql-contract/tests/utils/coercions.test.ts (1)

4-37: Solid core coverage; consider adding null and empty-string edge cases.

The five tests cover the main success and error branches well. The implementation has distinct error messages for null values ("received null") and empty strings ("non-empty timestamp string") that aren't currently exercised. Adding those two cases would complete the branch coverage.

🧪 Optional: additional edge-case tests
+  it('throws for null values', () => {
+    expect(() => timestampFromDriver(null, 'ts')).toThrow(
+      'Expected Date or timestamp string for "ts", received null.'
+    )
+  })
+
+  it('throws for empty strings', () => {
+    expect(() => timestampFromDriver('', 'ts')).toThrow(
+      'Expected a non-empty timestamp string'
+    )
+  })
packages/adapters/adapter-node-pg/src/types.ts (1)

31-32: Re-export NamedParams from @rawsql-ts/_shared/binder instead of redeclaring it locally.

The shared binder package already exports this type publicly. Since adapter-node-pg already imports from the binder elsewhere, importing NamedParams from there maintains a single source of truth and eliminates unnecessary duplication.

packages/adapters/adapter-node-pg/src/driver/PgTestkitClient.ts (1)

56-76: Internal cast on Line 63 drops NamedParams from the union.

The first overload (Line 50) accepts values?: unknown[] | NamedParams, but the implementation narrows to unknown[] | undefined on Line 63. At runtime this is fine because normalizeQuery distinguishes via Array.isArray, but the cast silently discards the Record branch from the type system.

Consider widening the assertion so the types stay honest:

♻️ Proposed type alignment
-    const values = typeof valuesOrCallback === 'function' ? undefined : (valuesOrCallback as unknown[] | undefined);
+    const values = typeof valuesOrCallback === 'function' ? undefined : (valuesOrCallback as unknown[] | NamedParams | undefined);
packages/ztd-cli/src/utils/optionalDependencies.ts (1)

30-64: Missing JSDoc on all exported functions.

Per coding guidelines, all exported functions in src/ must include English JSDoc explaining purpose, inputs, and return value. The five public loaders and clearOptionalDependencyCache are exported without documentation. The function names are descriptive, but the guideline is explicit.

As per coding guidelines: packages/*/src/**/*.ts: "All exported classes, functions, and types in src/ must include clear English JSDoc explaining what the API does, what it consumes, and what it returns"

📝 Example JSDoc for one loader
+/**
+ * Lazily load `@rawsql-ts/testkit-core`, throwing a descriptive error with an
+ * install hint when the package is not available.
+ * `@returns` The testkit-core module namespace.
+ */
 export async function ensureTestkitCoreModule(): Promise<TestkitCoreModule> {

Comment on lines +11 to +13
```bash
pnpm add -D @rawsql-ts/sql-contract arktype
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

-D installs as a dev dependency — is that intended for production code?

The example snippet defines a listCustomers function that looks like application (not test) code. If this recipe targets production usage, the install command should use pnpm add (without -D) for arktype. If it's test-only validation, the current -D is correct but the example should clarify that context.

🤖 Prompt for AI Agents
In `@docs/recipes/validation-arktype.md` around lines 11 - 13, The install command
currently uses `-D` which makes `arktype` a devDependency but the example shows
application code (`listCustomers`) implying production use; either change the
install line to `pnpm add `@rawsql-ts/sql-contract` arktype` to install `arktype`
as a runtime dependency, or explicitly state in the recipe that validation is
test-only and keep `pnpm add -D ...`; update the docs around the `pnpm add -D
`@rawsql-ts/sql-contract` arktype` snippet and reference the `listCustomers`
example so readers know which approach applies.

Comment on lines +34 to +41
type Customer = ReturnType<typeof CustomerSchema>;

export async function listCustomers(): Promise<Customer[]> {
return reader
.validator((value) => {
CustomerSchema.assert(value);
return value as Customer;
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

ArkType type.infer property type inference

💡 Result:

In ArkType, .infer is an inference-only property (it’s undefined at runtime) that gives you the output TypeScript type of a type(...) definition. (arktype.io)

Infer the whole object type

import { type } from "arktype"

const User = type({
  name: "string",
  "age?": "number"
})

type UserT = typeof User.infer

(arktype.io)

Infer a property type from the inferred object

Use normal TypeScript indexed access on the inferred type:

type UserNameT = (typeof User.infer)["name"]      // string
type UserAgeT  = (typeof User.infer)["age"]       // number | undefined

If you need the input type (e.g., when morphs/pipes transform)

Use .inferIn for the input side:

const ParsedNumber = type("string").pipe(s => Number.parseInt(s))

type Out = typeof ParsedNumber.infer    // number
type In  = typeof ParsedNumber.inferIn  // string

(arktype.io)

Citations:


🌐 Web query:

ArkType assert method return type validation

💡 Result:

In ArkType, .assert(...) is the “throwing” validation API: it validates the value and returns the (possibly transformed) output, otherwise it throws (a TraversalError). (arktype.io)

That means the TypeScript return type of T.assert(x) is the Type’s output type (i.e., what you’d see as typeof T.infer, including any morph/pipe transformations). (arktype.io)

Validating a function’s return value

You typically just assert at the return site:

import { type } from "arktype"

const Out = type({ id: "number" })

export function makeOut(): unknown {
  const v = JSON.parse('{"id":1}') // unknown
  return Out.assert(v)             // returns { id: number } or throws
}

Validating transformed returns (pipelines)

If your function produces a value you want validated as an output step, use a pipeline and/or .to(...) (which pipes into an output validator Type): (arktype.io)

const ParseOut = type("string").pipe(JSON.parse).to({ id: "number" })

const out = ParseOut.assert('{"id":1}') // { id: number } or throws

Citations:


Use ArkType's .infer property for type inference and leverage the return value from .assert().

The correct way to infer a type from an ArkType Type object is typeof CustomerSchema.infer, not ReturnType. Additionally, CustomerSchema.assert(value) already returns the validated & typed value, so the redundant cast can be removed.

📝 Suggested fix
-type Customer = ReturnType<typeof CustomerSchema>;
+type Customer = typeof CustomerSchema.infer;
     .validator((value) => {
-      CustomerSchema.assert(value);
-      return value as Customer;
+      return CustomerSchema.assert(value);
     })
🤖 Prompt for AI Agents
In `@docs/recipes/validation-arktype.md` around lines 34 - 41, The code
incorrectly infers Customer with ReturnType and redundantly casts after
assertion; update the Customer type to use typeof CustomerSchema.infer and in
listCustomers' reader.validator callback return the result of
CustomerSchema.assert(value) directly (remove the "as Customer" cast) so the
validated value carries the proper inferred type from CustomerSchema; target
symbols: CustomerSchema, Customer type alias, listCustomers, and
reader.validator.

Comment on lines +19 to +42
```ts
import { z } from 'zod';
import { createReader } from '@rawsql-ts/sql-contract/mapper';
import { getSqlClient } from '../support/sql-client-factory';

const executor = async (sql: string, params: readonly unknown[]) => {
const client = await getSqlClient();
return client.query(sql, params);
};

const reader = createReader(executor);

const CustomerSchema = z.object({
customerId: z.number(),
customerName: z.string(),
});

export async function listCustomers() {
return reader.validator(CustomerSchema).list(
'SELECT customer_id, customer_name FROM public.user_account',
[]
);
}
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and display the sql-contract package.json
find . -type f -name "package.json" | grep -E "packages/sql-contract/package\.json$" | head -1 | xargs cat

Repository: mk3008/rawsql-ts

Length of output: 1850


🏁 Script executed:

#!/bin/bash
# Check the mapper/index.ts file to verify createReader and its API
find . -path "*/packages/sql-contract/src/mapper/index.ts" -exec cat {} \;

Repository: mk3008/rawsql-ts

Length of output: 40150


🏁 Script executed:

#!/bin/bash
# Check if Mapper class has a validator method
grep -A 5 "class Mapper" packages/sql-contract/src/mapper/index.ts | grep -E "validator|async query"
# Also check the full Mapper interface/class structure
sed -n '/^export class Mapper/,/^}/p' packages/sql-contract/src/mapper/index.ts | grep -E "^\s+(async |[a-z]+\()" | head -10

Repository: mk3008/rawsql-ts

Length of output: 191


🏁 Script executed:

#!/bin/bash
# Verify the Mapper class does NOT have validator method
sed -n '/^export class Mapper/,/^  }/p' packages/sql-contract/src/mapper/index.ts | grep -c "validator"
# Check if validator is only on MapperReader
grep -n "interface MapperReader" packages/sql-contract/src/mapper/index.ts -A 7

Repository: mk3008/rawsql-ts

Length of output: 377


🏁 Script executed:

#!/bin/bash
# Confirm that Mapper.bind() returns MapperReader with validator support
grep -n "bind<T>" packages/sql-contract/src/mapper/index.ts -A 2

Repository: mk3008/rawsql-ts

Length of output: 201


The example code will fail at runtime — createReader() returns a Mapper, which does not expose the validator() method.

The validator() method only exists on MapperReader, which must be obtained by calling bind(mapping) first:

Corrected example
import { z } from 'zod';
import { createReader, rowMapping } from '@rawsql-ts/sql-contract/mapper';
import { getSqlClient } from '../support/sql-client-factory';

const executor = async (sql: string, params: readonly unknown[]) => {
  const client = await getSqlClient();
  return client.query(sql, params);
};

const mapper = createReader(executor);

const CustomerSchema = z.object({
  customerId: z.number(),
  customerName: z.string(),
});

const customerMapping = rowMapping({
  name: 'Customer',
  key: 'customerId',
  prefix: 'customer_',
});

export async function listCustomers() {
  return mapper.bind(customerMapping).validator(CustomerSchema).list(
    'SELECT customer_id, customer_name FROM public.user_account',
    []
  );
}
🤖 Prompt for AI Agents
In `@docs/recipes/validation-zod.md` around lines 19 - 42, The example calls
validator() directly on the object returned by createReader(), but
createReader() returns a Mapper (no validator); fix by creating a mapping via
rowMapping(...) (e.g., customerMapping) and then call bind(customerMapping) on
the mapper returned from createReader() to obtain a MapperReader before calling
validator(); update references so you use const mapper = createReader(executor)
and then mapper.bind(customerMapping).validator(CustomerSchema).list(...).

Comment on lines +120 to +131
if (state === 'single_quote') {
compiledSql += char;
if (char === "'" && next === "'") {
compiledSql += next;
index += 1;
continue;
}
if (char === "'") {
state = 'normal';
}
continue;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

PostgreSQL E'...' escape strings with backslash escapes are not handled.

Inside single_quote state, only '' (doubled quote) is handled as an escape. PostgreSQL's E'\'' (backslash-escaped single quote inside escape strings) would prematurely close the string state, potentially causing a named param inside the string body to be rewritten.

This is a minor edge case — E'...' syntax is uncommon in modern PostgreSQL, and the AGENTS.md discourages non-standard patterns. However, if this binder is used broadly, it could bite someone.

🤖 Prompt for AI Agents
In `@packages/_shared/binder/src/compileNamedParameters.ts` around lines 120 -
131, When entering the single-quote state in compileNamedParameters, remember if
the quote was opened with an E prefix (e.g., E'...') by setting a flag (e.g.,
singleQuoteIsEScape) when you see an 'E' immediately before the opening single
quote; then, inside the single_quote branch (where compiledSql, state, char,
next, index are handled), treat a single quote preceded by a backslash as
escaped when singleQuoteIsEScape is true (do not close the string or exit the
single_quote state), and clear that flag when you transition out of
single_quote; this prevents misinterpreting backslash-escaped quotes like E'\''
as string terminators and avoids rewriting named params inside such strings.

Comment on lines +34 to +36
export type PgQueryInput =
| string
| (Omit<QueryConfig<unknown[]>, 'values'> & { values?: unknown[] | NamedParams; params?: NamedParams });
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Search for PgTestkitClient implementation and how it processes query input
find packages/adapters/adapter-node-pg/src -name "*.ts" | head -20

Repository: mk3008/rawsql-ts

Length of output: 345


🏁 Script executed:

# Check the PgTestkitClient class for query handling logic
rg -n -A 20 "class PgTestkitClient" packages/adapters/adapter-node-pg/src/

Repository: mk3008/rawsql-ts

Length of output: 2216


🏁 Script executed:

# Look for how values and params are processed
rg -n -B 5 -A 10 "values.*params|params.*values" packages/adapters/adapter-node-pg/src/ -t ts

Repository: mk3008/rawsql-ts

Length of output: 10097


🏁 Script executed:

# Check where NamedParams is defined and exported
rg -n "NamedParams" packages/adapters/adapter-node-pg/src/ -t ts

Repository: mk3008/rawsql-ts

Length of output: 679


🏁 Script executed:

# Look at the full types.ts file to understand the complete context
cat -n packages/adapters/adapter-node-pg/src/types.ts

Repository: mk3008/rawsql-ts

Length of output: 3335


🏁 Script executed:

# Check if NamedParams is exported from shared binder and should be imported instead
rg -n "NamedParams" packages/_shared/binder/src/ -t ts

# Verify the shared binder module location and what it exports
cat -n packages/_shared/binder/src/index.ts | head -50

Repository: mk3008/rawsql-ts

Length of output: 769


🏁 Script executed:

# Check all imports of NamedParams across the codebase to understand the pattern
rg -n "import.*NamedParams|from.*NamedParams" packages/ -t ts

Repository: mk3008/rawsql-ts

Length of output: 212


Remove ambiguity claim; fix NamedParams duplication.

The resolution order between values and params is explicit via the ?? operator (line 70, line 99) and unambiguous—no issue here.

However, NamedParams is incorrectly defined locally in types.ts (line 32) when it should be imported from @rawsql-ts/_shared/binder, which is the canonical source. Replace the local definition with an import to maintain a single source of truth and match the pattern used in PgTestkitClient.ts.

🤖 Prompt for AI Agents
In `@packages/adapters/adapter-node-pg/src/types.ts` around lines 34 - 36, The
local duplicate NamedParams type in types.ts must be removed and replaced with
an import from the canonical package; update the file so PgQueryInput continues
to use NamedParams but imports NamedParams from "@rawsql-ts/_shared/binder"
(matching the pattern used in PgTestkitClient.ts) to eliminate the local
definition and ensure a single source of truth for NamedParams.

Comment on lines 25 to 28
export type TestkitCoreModule = typeof import('@rawsql-ts/testkit-core');
export type AdapterNodePgModule = typeof import('@rawsql-ts/adapter-node-pg');
export type PgModule = typeof import('pg');
export type PostgresContainerModule = typeof import('@testcontainers/postgresql');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check ztd-cli package.json for optional module declarations
cat packages/ztd-cli/package.json | jq '{
  deps: (.dependencies // {} | keys),
  devDeps: (.devDependencies // {} | keys),
  peerDeps: (.peerDependencies // {} | keys)
}'

Repository: mk3008/rawsql-ts

Length of output: 466


🏁 Script executed:

# Check tsconfig files in ztd-cli
echo "=== tsconfig.json ===" && \
cat packages/ztd-cli/tsconfig.json | jq '.compilerOptions.paths // empty' 2>/dev/null || echo "No paths config or file not found"

echo "=== tsconfig.build.json ===" && \
cat packages/ztd-cli/tsconfig.build.json | jq '.compilerOptions.paths // empty' 2>/dev/null || echo "No paths config or file not found"

Repository: mk3008/rawsql-ts

Length of output: 198


🏁 Script executed:

# Check the actual optionalDependencies.ts file
cat -n packages/ztd-cli/src/utils/optionalDependencies.ts | head -70

Repository: mk3008/rawsql-ts

Length of output: 2654


Missing JSDoc on exported utility functions.

Five exported functions in optionalDependencies.ts lack JSDoc documentation: clearOptionalDependencyCache, ensureTestkitCoreModule, ensureAdapterNodePgModule, ensurePgModule, and ensurePostgresContainerModule. Per coding guidelines, all exported functions in src/ must include clear English JSDoc explaining what they do, what they consume, and what they return. Add JSDoc blocks to each function.

🧰 Tools
🪛 GitHub Actions: PR Check

[error] 26-26: Cannot find module '@rawsql-ts/adapter-node-pg' or its corresponding type declarations.

🤖 Prompt for AI Agents
In `@packages/ztd-cli/src/utils/optionalDependencies.ts` around lines 25 - 28, Add
JSDoc blocks for the exported functions clearOptionalDependencyCache,
ensureTestkitCoreModule, ensureAdapterNodePgModule, ensurePgModule, and
ensurePostgresContainerModule: for each function include a one-line summary,
descriptions of parameters (if any) and their types, the exact return type (or
Promise<...>), and note thrown errors (e.g., when a dependency cannot be
resolved) and side effects (like clearing cache); ensure the JSDoc is in plain
English, placed immediately above each exported function, and follows the
existing project style (`@param`, `@returns`, and `@throws` tags where applicable) so
documentation and tooling pick up the signatures.

Comment on lines +8 to +13
export const smokeOutputSchema = type({
id: 'number.integer',
createdAt: 'Date'
});

export type SmokeOutput = ReturnType<typeof smokeOutputSchema>;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

ArkType type() ReturnType inference number.integer

💡 Result:

In ArkType, you generally don’t use ReturnType<> to get the “validated type”. Instead, you extract it from the Type’s inference-only property .infer.

import { type } from "arktype"

const Integer = type("number.integer")

type IntegerOut = typeof Integer.infer   // number
type IntegerIn  = typeof Integer.inferIn // unknown (what the validator accepts)

number.integer is a constraint on number, so the inferred TypeScript type is still number (TypeScript can’t represent “integer” as a distinct primitive type). [1]

If you do this instead:

type R = ReturnType<typeof Integer>

R corresponds to what calling the validator returns at runtime: either the validated value or an ArkErrors instance (the docs show checking out instanceof type.errors). [2]

Sources: ArkType Type API (infer, inferIn) [1], ArkType “Your First Type” (validator returns value or type.errors) [2].


Use .infer property instead of ReturnType<typeof> for ArkType type extraction.

ReturnType<typeof smokeOutputSchema> returns the validator's runtime return type (value | ArkErrors), not the inferred TypeScript type. Use typeof smokeOutputSchema.infer to extract the validated type shape:

export type SmokeOutput = typeof smokeOutputSchema.infer;

While 'number.integer' does correctly infer as number in TypeScript, the .infer pattern is the proper ArkType API for type extraction.

🤖 Prompt for AI Agents
In `@packages/ztd-cli/templates/src/catalog/specs/_smoke.spec.arktype.ts` around
lines 8 - 13, The type alias SmokeOutput should use ArkType's .infer rather than
ReturnType; replace the current ReturnType usage for the exported SmokeOutput
with the proper ArkType extraction using typeof smokeOutputSchema.infer so the
inferred validated shape from smokeOutputSchema is used (refer to
smokeOutputSchema and SmokeOutput).

- Maintenance jobs
- Data backfills
- Batch operations
- Temporary-table driven workflows
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor: hyphenate compound adjective.

"Temporary-table driven" should be "Temporary-table-driven" when used as a compound modifier before a noun.

Proposed fix
-- Temporary-table driven workflows
+- Temporary-table-driven workflows
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- Temporary-table driven workflows
- Temporary-table-driven workflows
🧰 Tools
🪛 LanguageTool

[grammar] ~10-~10: Use a hyphen to join words.
Context: ...lls - Batch operations - Temporary-table driven workflows ## Safety rules - Be ...

(QB_NEW_EN_HYPHEN)

🤖 Prompt for AI Agents
In `@packages/ztd-cli/templates/src/jobs/AGENTS.md` at line 10, Update the
compound adjective in the AGENTS.md content: change the phrase "Temporary-table
driven workflows" to "Temporary-table-driven workflows" so the compound modifier
is correctly hyphenated; locate the string "Temporary-table driven" in the
AGENTS.md template and replace it with "Temporary-table-driven".

@mk3008 mk3008 changed the title Feat/ztd-cli-zero-deps Refactor ztd-cli to reduce dependencies and support named parameters Feb 6, 2026
@mk3008 mk3008 merged commit 9789934 into main Feb 6, 2026
8 checks passed
@mk3008 mk3008 deleted the feat/ztd-cli-zero-deps branch February 6, 2026 13:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant