Skip to content

style-profile ​

Tier 2. No titan dependencies. zod v4 is a peer; handlebars is a regular dependency.

sh
npm install @titan-design/style-profile zod

The problem it solves ​

A team's coding standards usually live in two places that drift apart: prose that people and agents read, and linter configs that machines enforce. This package holds the standard once, as a profile: a set of style rules, each with a convention, a confidence, and optional per-tool extensions. It then exports that profile to every place the standard has to show up: eslint.config.js, ruff.toml, .editorconfig, a markdown style guide, Claude rules, Claude hooks, and a Claude skill.

Confidence decides severity. A rule at or above severityThresholds.error (default 0.85) becomes an error, at or above warn (0.60) a warning, and at or above info (0.40) a hint. Anything lower is left out of every artifact.

When to reach for it ​

  • You have a profile (hand-written, or produced by a style analyzer) and need the linter configs and agent instructions that enforce it.
  • You are assembling your own ESLint config and want single rules derived from a profile: buildNamingConventionRule, buildImportOrderRule, buildFunctionLengthRule, buildFileNamingRule, buildJsdocRules.

It does not read source code. Producing a profile from a codebase is the style analyzer's job, and running the generated linters is the style checker's (both later ports from codewatch).

Public API ​

ExportWhat it does
ProfileSchema, StyleRuleSchema, ExampleSchema, StabilitySchema, FixabilitySchemazod schemas; Profile, StyleRule, Stability, Fixability, SeverityThresholds, Severity, ProfileCategory are their types
SCHEMA_VERSION, DEFAULT_SEVERITY_THRESHOLDS, PROFILE_CATEGORIESconstants
readProfile(path), writeProfile(path, profile)parse through the schema on the way in and out
validateProfile(data)zod safeParse result
migrateProfile(profile), registerMigration({ from, to, migrate })walk registered migrations up to SCHEMA_VERSION
exportProfile(profile, format), SUPPORTED_FORMATS, ExportFormatone entry point for every format; returns GeneratedFile[] ({ path, content })
generateEslintExport, generateRuffExport, generateEditorConfigExport, generateMarkdownExport, generateClaudeRules, generateHooksConfig, generateSkillFilesthe per-format generators behind exportProfile
severityForConfidence(confidence, thresholds?)the one confidence ladder: error, warn, info or off
toEslintLevel(severity), EslintSeveritythe ESLint level for a tier; info becomes warn, since ESLint has no info level
toEslintSeverity, severityRank, build*Rule, buildJsdocRulessingle ESLint rules derived from a profile; they emit error or warn, never info
extractAllRules, getTopRules, getRulesByCategory, getRulesForCategory, detectLanguagesflatten and group a profile's rules

Example ​

Verified against 0.1.0. A profile in, an ESLint config and a ruff config out:

ts
import { ProfileSchema, exportProfile } from "@titan-design/style-profile";

const rule = (convention, confidence, extensions) => ({ convention, confidence, extensions });
const profile = ProfileSchema.parse({
  schemaVersion: "1.0.0",
  author: "ada",
  generated: "2026-09-18T00:00:00.000Z",
  sources: ["src/"],
  naming: { variables: rule("camelCase", 0.93) },
  structure: {
    importOrder: rule(["builtin", "external", "relative"], 0.9),
    functionMaxLines: rule(30, 0.7),
  },
  documentation: {},
  errorHandling: { noThrowLiteral: rule(true, 0.88, { eslint: { rule: "no-throw-literal" } }) },
  formatting: { lineLength: rule(100, 0.8, { ruff: { codes: ["E501"] } }) },
  patterns: {},
  idioms: { detected: [] },
  antiPatterns: { acknowledged: [] },
  overrides: [],
});

const [eslint] = exportProfile(profile, "eslint");
const [ruff] = exportProfile(profile, "ruff");

eslint.path is eslint.config.js:

js
// Generated by codewatch for ada
// Date: 2026-09-18T00:00:00.000Z
// Do not edit manually - regenerate with: codewatch export --format eslint

import perfectionist from "eslint-plugin-perfectionist";

export default {
  "rules": {
    "no-throw-literal": "error",
    "perfectionist/sort-imports": [
      "error",
      {
        "type": "natural",
        "groups": [
          "builtin",
          "external",
          "relative"
        ]
      }
    ]
  }
};

ruff.path is ruff.toml:

toml
# Generated by codewatch for ada
# Date: 2026-09-18T00:00:00.000Z
# Do not edit manually - regenerate with: codewatch export --format ruff
line-length = 100

[lint]
select = ["C90", "E", "I", "N"]
fixable = ["ALL"]

[lint.mccabe]
max-complexity = 30

[lint.isort]
section-order = ["builtin", "external", "relative"]

What it deliberately does not do ​

It is a port of codewatch's @codewatch/profile and keeps the original's output exactly, including the quirks listed below, except for the hooks export, which now follows Claude Code's settings schema. It does not analyze code, run linters, or write files: every exporter returns { path, content } and the caller decides where it goes.

Gotchas ​

  • generateEslintExport only emits rules that carry an extensions.eslint entry, plus perfectionist/sort-imports from structure.importOrder. The naming, file-name, function-length and JSDoc builders are not wired into it; call them yourself.
  • The generated eslint.config.js default-exports a single object and imports plugins without registering them under plugins. Treat it as a rules list to merge into a real flat config, not a config ESLint can load on its own.
  • The ruff exporter maps structure.functionMaxLines (a line count) to mccabe max-complexity (a cyclomatic bound), and it strips digits from extension codes, so E501 selects the whole E family.
  • generateHooksConfig ignores the profile. It always returns one PostToolUse hook on Write|Edit that reads tool_input.file_path from the hook's stdin JSON with jq and runs codewatch check --fix on that file, so jq and codewatch must be on PATH. exportProfile writes it to .claude/codewatch-hooks.json, a fragment rather than a whole settings file. Merge it into .claude/settings.json by appending its entries: jq -s '.[0].hooks.PostToolUse += .[1].hooks.PostToolUse | .[0]' .claude/settings.json .claude/codewatch-hooks.json.
  • The migration registry is module-global. registerMigration affects every caller in the process, and readProfile does not migrate. Call migrateProfile before parsing an older profile.
  • The skill exporter reads its Handlebars templates from the package's templates/ directory at call time, so bundling this package into a single file breaks generateSkillFiles unless templates/ travels with it.

Where it came from ​

Ported in TP-131 from codewatch packages/profile, with the Handlebars templates from codewatch skills/code-style-personal/templates. Generated artifacts were diffed byte-for-byte against the original on the codewatch export fixture and a branch-coverage profile. Legacy identifiers are kept as they were: the generated headers and hook command still say codewatch (the hooks export alone was rewritten in TP-1221), and profiles may carry the https://json.schemastore.org/code-style-profile.json $schema URL.

MIT licensed