Skip to content

refactor(blast,cli,serve): Zod-Core-Out vertical slice (#109)#110

Merged
stackbilt-admin merged 1 commit intomainfrom
refactor/blast-zod-core-out
Apr 16, 2026
Merged

refactor(blast,cli,serve): Zod-Core-Out vertical slice (#109)#110
stackbilt-admin merged 1 commit intomainfrom
refactor/blast-zod-core-out

Conversation

@stackbilt-admin
Copy link
Copy Markdown
Member

Closes #109.

Proves the Zod-Core-Out pattern end-to-end on @stackbilt/blast: one Zod schema is the source of truth; the CLI and the new charter_blast MCP tool are thin adapters over the same pure analyze() core.

What ships

@stackbilt/blast

  • Adds zod@^3.24.1 as a runtime dep. README's "zero runtime dependencies" claim updated — Zod is now the authoritative contract at the package boundary.
  • New exports: BlastInputSchema, BlastOutputSchema, BlastInput, BlastOutput, DEFAULT_MAX_DEPTH, analyze().
  • analyze(input: BlastInput): BlastOutput composes buildGraph + blastRadius and is what both adapters call.
  • topHotFiles now uses a deterministic secondary sort (filename ascending) so tied importers counts no longer depend on filesystem scan order.
  • All existing exports (buildGraph, blastRadius, topHotFiles, interfaces) preserved — fully additive per the OSS update policy.

CLI (packages/cli/src/commands/blast.ts)

  • argv → build input object → BlastInputSchema.parseanalyze() → human formatter. Zod owns the --depth positive-integer rule; the manual Number.isFinite check is gone.
  • detectTsconfigAliases is now exported so serve.ts reuses it.

MCP (packages/cli/src/commands/serve.ts)

  • New charter_blast tool. Handler parses raw JSON-RPC args through BlastInputSchema (defaults applied there), auto-detects tsconfig aliases at scan root if the caller didn't supply them, calls analyze, returns structured JSON.
  • Advertised inputSchema is a simple ZodRawShape — the SDK's ZodRawShapeCompat overload trips TS2589 on anything containing .default() chains. Defaults are documented inline in .describe() strings and applied at parse time. Authoritative schema is BlastInputSchema; the advertised shape is a documentation surface.

Decisions locked per review on #109

See the comment thread — abbreviated:

  1. Zod → JSON-schema path: SDK-internal conversion. Pass the raw shape directly. No new library.
  2. maxDepth SoT: single DEFAULT_MAX_DEPTH constant referenced by both the schema's .default() and blastRadius's in-function fallback. The public blastRadius behavior is unchanged — the review's concern about silent drift is addressed by the shared constant rather than by removing the in-function default (which would have broken existing programmatic consumers).
  3. Snapshot AC → structural assertions: no byte-identical snapshots. Tests use BlastOutputSchema.parse(result) and targeted field checks; hotFiles determinism is enforced at the source, not in tests.
  4. Schema location: @stackbilt/blast. Zod becomes a runtime dep; the "zero deps" purity was aesthetic, and colocating schemas in blast is the only arrangement that preserves the "one schema, many adapters" promise for programmatic consumers.

Known follow-ups (out of scope for this PR)

  • Remove as Function casts on all three registerTool call sites once the SDK ships typings that don't hit TS2589. Reviewer's nitpick; the new registration uses the same cast as the existing two rather than introducing a mismatched style.
  • Expand the pattern to validate, drift, classify, surface — one vertical slice per follow-up PR.

Acceptance criteria

  • @stackbilt/blast exports Zod schemas; existing BlastRadiusResult / BlastOptions interfaces still importable.
  • charter blast <path> JSON output carries the same fields as before (keys in the same order).
  • charter serve registers charter_blast; tools/list surfaces the JSON schema with required: ["seeds"] and per-field descriptions.
  • Unit tests cover schema validation + analyze via synthetic fixtures (no child-process tests).
  • No removal or rename of existing public API.
  • BlastInputSchema.parse({seeds:['x']}) yields maxDepth === DEFAULT_MAX_DEPTH (3).
  • BlastOutputSchema.parse(analyze(...)) succeeds on a representative fixture.
  • Tied hotFiles ordering is deterministic (filename ascending).
  • packages/blast/package.json lists zod as a dependency, not devDependency.

Test plan

  • pnpm build clean
  • pnpm exec vitest run packages/blast — 27/27 pass (includes 11 new tests for schema + analyze)
  • Full suite: 406/407 pass. The one failure (precommit-hook.test.ts > auto-tidies bloated CLAUDE.md) is a pre-existing WSL timing flake — passes in isolation (35s), times out under parallel IO contention. Not caused by this change.
  • MCP end-to-end probe: tools/list returns charter_blast with correct JSON schema; tools/call with {"seeds":["src/b.ts"]} returns structurally-identical output to charter blast src/b.ts --format json against the same fixture.

Prove the shared-schema pattern on blast end-to-end:

@stackbilt/blast
  - Add zod@^3.24.1 as a runtime dependency
  - Add BlastInputSchema and BlastOutputSchema with .describe() on every field
  - Add high-level analyze(input) that composes buildGraph + blastRadius
  - Extract DEFAULT_MAX_DEPTH constant; schema and blastRadius both reference it
  - Make topHotFiles tie-break deterministic (secondary sort by filename)
  - analyze validates seed existence with a descriptive error

CLI blast.ts
  - Route argv through BlastInputSchema.parse; Zod owns the --depth rules
  - Call analyze() instead of buildGraph + blastRadius directly
  - Export detectTsconfigAliases for reuse in serve.ts

MCP serve.ts
  - Register charter_blast tool; handler parses raw args through BlastInputSchema,
    auto-detects tsconfig aliases, calls analyze, returns structured JSON
  - Advertised inputSchema uses a simple raw shape (SDK's ZodRawShapeCompat
    rejects .default() chains); defaults are applied inside the handler

Tests
  - Structural assertions (no byte-identical snapshots)
  - Defaults verified, invalid maxDepth/empty seeds rejected
  - analyze agrees with low-level API on affected files
  - hotFiles tie-break determinism verified

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
stackbilt-admin pushed a commit that referenced this pull request Apr 16, 2026
Addresses feedback on PR #111:

- credentials.test.ts: mock node:fs so loadCredentials returns deterministic
  results instead of reading the developer's real ~/.charter/credentials.json.
  Empty/whitespace-env-var cases are now pinned to the stored-credentials
  fallback with stubbed data rather than being conditionally skipped.
  Added explicit "env wins when both sources set" test that was claimed by
  the describe block but never exercised.

- STACKBILT_API_BASE_URL companion env var: resolveApiKey now returns a
  baseUrl from the env when source='env'. Fixes the silent base-URL drop
  for users migrating from `charter login --url https://custom ...`.

- login.ts: --logout now emits the deprecation notice before clearing
  credentials. One-line fix; users exiting the deprecated surface see the
  upgrade path.

- CLI README: "Authentication (optional)" gains a one-line security
  caveat about env-var inheritance by child processes, recommending
  per-invocation setting in CI over global export in shared shells.

- auth-wiring.test.ts (new): mocks resolveApiKey and EngineClient; pins
  the useGateway decision for `run` (scaffold vs build) and asserts
  architect forwards the resolved apiKey + baseUrl into the client
  constructor. 5 tests covering env/credentials/null.

All 16 auth-related tests pass (credentials: 9, login: 2, auth-wiring: 5).
Full suite: 413/415 pass; the two failures are the same WSL-flaky
precommit-hook integration test already flagged on PR #110 and unrelated
to this diff.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
stackbilt-admin added a commit that referenced this pull request Apr 16, 2026
)

* feat(cli): STACKBILT_API_KEY env var auth; deprecate charter login

Additive preparation for the 1.0 package split (see charter RFC TBD).
No public API removed or renamed — follows OSS update policy.

Why
  The CLI currently solicits a Stackbilt API key via `charter login`, which
  is the most eyebrow-raising OSS/commercial coupling in the package: an
  OSS governance tool writing a bearer token to ~/.charter/credentials.json.
  Supporting the standard env-var path lets users authenticate the
  commercial commands (`run`, `architect`) without the CLI persisting
  anything to disk, materially improving the audit story now while the
  full split to @stackbilt/build is scoped and sequenced separately.

What changes
  credentials.ts
    + API_KEY_ENV_VAR = 'STACKBILT_API_KEY'
    + resolveApiKey() → { apiKey, source: 'env' | 'credentials', baseUrl? } | null
      Env var wins over stored credentials; empty/whitespace env var falls
      through. loadCredentials(), saveCredentials(), clearCredentials()
      preserved unchanged.

  commands/architect.ts, commands/run.ts
    - loadCredentials() call sites → resolveApiKey(). `run`'s useGateway
      decision now honors the env var path identically to stored creds.

  commands/login.ts
    - printDeprecationNotice() on stderr for every invocation except
      --logout. Command functionality unchanged. Help text reworked: env
      var is the "Preferred" path, stored credentials the "Deprecated
      alternative." If the env var is set and no --key is given, we
      report the env var as the authoritative source.

  http-client.ts
    - Scaffold auth-error string now points at STACKBILT_API_KEY first,
      `charter login` marked deprecated.

Tests
  + credentials.test.ts — 4 tests covering env-var precedence, trimming,
    empty-string fallthrough, whitespace-only fallthrough.
  + login.test.ts — 2 tests covering the stderr deprecation notice and
    env-var reporting path.
  All 405 existing tests still pass.

Docs
  - packages/cli/README.md: new "Authentication (optional)" section
    documenting the env var and login deprecation. Root README
    unchanged — that rewrite ships with the package split.
  - CHANGELOG.md: [Unreleased] section added with Added/Deprecated/Changed
    subsections.

Follow-ups (not in this PR)
  - Root README rewrite when package split lands
  - RFC: extract commercial surface into @stackbilt/build
  - 1.0 cut removes login, credentials persistence, run/architect/scaffold

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* review(cli): fix test isolation, add baseUrl env var, wiring tests

Addresses feedback on PR #111:

- credentials.test.ts: mock node:fs so loadCredentials returns deterministic
  results instead of reading the developer's real ~/.charter/credentials.json.
  Empty/whitespace-env-var cases are now pinned to the stored-credentials
  fallback with stubbed data rather than being conditionally skipped.
  Added explicit "env wins when both sources set" test that was claimed by
  the describe block but never exercised.

- STACKBILT_API_BASE_URL companion env var: resolveApiKey now returns a
  baseUrl from the env when source='env'. Fixes the silent base-URL drop
  for users migrating from `charter login --url https://custom ...`.

- login.ts: --logout now emits the deprecation notice before clearing
  credentials. One-line fix; users exiting the deprecated surface see the
  upgrade path.

- CLI README: "Authentication (optional)" gains a one-line security
  caveat about env-var inheritance by child processes, recommending
  per-invocation setting in CI over global export in shared shells.

- auth-wiring.test.ts (new): mocks resolveApiKey and EngineClient; pins
  the useGateway decision for `run` (scaffold vs build) and asserts
  architect forwards the resolved apiKey + baseUrl into the client
  constructor. 5 tests covering env/credentials/null.

All 16 auth-related tests pass (credentials: 9, login: 2, auth-wiring: 5).
Full suite: 413/415 pass; the two failures are the same WSL-flaky
precommit-hook integration test already flagged on PR #110 and unrelated
to this diff.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Kurt Overmier <kurt@stackbilt.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@stackbilt-admin stackbilt-admin merged commit 3a3a616 into main Apr 16, 2026
3 checks passed
stackbilt-admin pushed a commit that referenced this pull request Apr 16, 2026
Synchronized version bump for all @stackbilt/* packages to 0.11.0.

Highlights:
- STACKBILT_API_KEY / STACKBILT_API_BASE_URL env-var auth (#111)
- Zod-Core-Out vertical slice for @stackbilt/blast + charter_blast MCP tool (#110)
- charter login deprecated; scheduled for 1.0 removal alongside @stackbilt/build split (#112)

See CHANGELOG.md [0.11.0] for the full entry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
stackbilt-admin pushed a commit that referenced this pull request Apr 17, 2026
…114)

Mirror the blast Core-Out refactor (#110) on @stackbilt/surface:
SurfaceInputSchema + SurfaceOutputSchema become the authoritative contract,
analyze(input) composes extractSurface, and `charter_surface` joins
`charter_blast` as an MCP tool in `charter serve`.

Motivation: prerequisite for the #113 repo-brief RFC. The brief's Surface
section consumes analyze() output; without Zod-validated schemas the brief
shape would reshape on every surface primitive refactor.

- @stackbilt/surface: add zod runtime dep, RouteSchema / SchemaTableSchema /
  SchemaColumnSchema / SurfaceInputSchema / SurfaceOutputSchema, analyze(),
  DEFAULT_SURFACE_EXTENSIONS / DEFAULT_SURFACE_IGNORE_DIRS exports
- @stackbilt/cli: route `charter surface` argv through SurfaceInputSchema,
  map ZodError to CLIError; register `charter_surface` MCP tool with
  format: "json" | "markdown" input
- Route/SchemaTable/SchemaColumn become z.infer<> aliases, structurally
  identical to the prior interfaces — OSS additive-only policy preserved
- Tests: schema validation + analyze structural assertions via
  SurfaceOutputSchema.parse on fixtures

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

refactor(blast,cli,serve): Zod-Core-Out architecture — vertical slice for blast

1 participant