safeword 0.15.9 → 0.15.10

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.
Files changed (60) hide show
  1. package/dist/{check-LOVNOEUC.js → check-KVWOWINW.js} +4 -4
  2. package/dist/{chunk-JLOQFDEL.js → chunk-4QVNEEYP.js} +2 -2
  3. package/dist/{chunk-JLOQFDEL.js.map → chunk-4QVNEEYP.js.map} +1 -1
  4. package/dist/{chunk-EGI2VVJ4.js → chunk-CBJLUXZ4.js} +135 -70
  5. package/dist/chunk-CBJLUXZ4.js.map +1 -0
  6. package/dist/{chunk-NUKQPPCM.js → chunk-R6ZGEH4J.js} +11 -5
  7. package/dist/chunk-R6ZGEH4J.js.map +1 -0
  8. package/dist/{chunk-ZBTABXIC.js → chunk-UIURPBCD.js} +2 -2
  9. package/dist/{chunk-OYBKTOVF.js → chunk-VWOO5PJN.js} +49 -43
  10. package/dist/chunk-VWOO5PJN.js.map +1 -0
  11. package/dist/cli.js +7 -7
  12. package/dist/cli.js.map +1 -1
  13. package/dist/{diff-P66MAGJQ.js → diff-WEDHTHFK.js} +3 -3
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.js +2 -2
  16. package/dist/presets/typescript/index.d.ts +29 -7
  17. package/dist/presets/typescript/index.js +4 -2
  18. package/dist/{reset-FD5WGVGB.js → reset-GG5AST3L.js} +3 -3
  19. package/dist/{setup-I63R6JD2.js → setup-CM7WY4GS.js} +5 -5
  20. package/dist/{sync-config-SJ2HBZM6.js → sync-config-GPYJLOW5.js} +2 -2
  21. package/dist/{upgrade-HG7TZBLZ.js → upgrade-JKAXT46Q.js} +4 -4
  22. package/package.json +6 -1
  23. package/templates/SAFEWORD.md +4 -30
  24. package/templates/cursor/rules/bdd-core.mdc +52 -0
  25. package/templates/cursor/rules/bdd-decomposition.mdc +43 -0
  26. package/templates/cursor/rules/bdd-discovery.mdc +46 -0
  27. package/templates/cursor/rules/bdd-done.mdc +44 -0
  28. package/templates/cursor/rules/bdd-scenarios.mdc +50 -0
  29. package/templates/cursor/rules/bdd-splitting.mdc +51 -0
  30. package/templates/cursor/rules/bdd-tdd.mdc +55 -0
  31. package/templates/cursor/rules/safeword-debugging.mdc +7 -0
  32. package/templates/cursor/rules/safeword-quality-reviewing.mdc +0 -1
  33. package/templates/guides/architecture-guide.md +0 -38
  34. package/templates/guides/context-files-guide.md +1 -14
  35. package/templates/guides/design-doc-guide.md +0 -12
  36. package/templates/guides/planning-guide.md +1 -18
  37. package/templates/guides/testing-guide.md +13 -44
  38. package/templates/hooks/stop-quality.ts +97 -34
  39. package/templates/skills/safeword-bdd-orchestrating/DECOMPOSITION.md +44 -0
  40. package/templates/skills/safeword-bdd-orchestrating/DISCOVERY.md +59 -0
  41. package/templates/skills/safeword-bdd-orchestrating/DONE.md +44 -0
  42. package/templates/skills/safeword-bdd-orchestrating/SCENARIOS.md +59 -0
  43. package/templates/skills/safeword-bdd-orchestrating/SKILL.md +26 -548
  44. package/templates/skills/safeword-bdd-orchestrating/SPLITTING.md +56 -0
  45. package/templates/skills/safeword-bdd-orchestrating/TDD.md +69 -0
  46. package/templates/skills/safeword-debugging/SKILL.md +19 -0
  47. package/templates/skills/safeword-quality-reviewing/SKILL.md +34 -116
  48. package/dist/chunk-EGI2VVJ4.js.map +0 -1
  49. package/dist/chunk-NUKQPPCM.js.map +0 -1
  50. package/dist/chunk-OYBKTOVF.js.map +0 -1
  51. package/templates/cursor/rules/safeword-bdd-orchestrating.mdc +0 -628
  52. package/templates/guides/code-philosophy.md +0 -207
  53. package/templates/prompts/quality-review.md +0 -10
  54. /package/dist/{check-LOVNOEUC.js.map → check-KVWOWINW.js.map} +0 -0
  55. /package/dist/{chunk-ZBTABXIC.js.map → chunk-UIURPBCD.js.map} +0 -0
  56. /package/dist/{diff-P66MAGJQ.js.map → diff-WEDHTHFK.js.map} +0 -0
  57. /package/dist/{reset-FD5WGVGB.js.map → reset-GG5AST3L.js.map} +0 -0
  58. /package/dist/{setup-I63R6JD2.js.map → setup-CM7WY4GS.js.map} +0 -0
  59. /package/dist/{sync-config-SJ2HBZM6.js.map → sync-config-GPYJLOW5.js.map} +0 -0
  60. /package/dist/{upgrade-HG7TZBLZ.js.map → upgrade-JKAXT46Q.js.map} +0 -0
@@ -3,13 +3,13 @@ import {
3
3
  } from "./chunk-FJYRWU2V.js";
4
4
  import {
5
5
  getMissingPacks
6
- } from "./chunk-ZBTABXIC.js";
6
+ } from "./chunk-UIURPBCD.js";
7
7
  import {
8
8
  SAFEWORD_SCHEMA,
9
9
  createProjectContext,
10
10
  reconcile
11
- } from "./chunk-EGI2VVJ4.js";
12
- import "./chunk-OYBKTOVF.js";
11
+ } from "./chunk-CBJLUXZ4.js";
12
+ import "./chunk-VWOO5PJN.js";
13
13
  import {
14
14
  VERSION
15
15
  } from "./chunk-ORQHKDT2.js";
@@ -189,4 +189,4 @@ async function check(options) {
189
189
  export {
190
190
  check
191
191
  };
192
- //# sourceMappingURL=check-LOVNOEUC.js.map
192
+ //# sourceMappingURL=check-KVWOWINW.js.map
@@ -209,7 +209,7 @@ function generateDepCruiseMainConfig() {
209
209
  return `/**
210
210
  * Dependency Cruiser Configuration
211
211
  *
212
- * Imports auto-generated rules from .safeword/depcruise-config.js
212
+ * Imports auto-generated rules from .safeword/depcruise-config.cjs
213
213
  * ADD YOUR CUSTOM RULES BELOW the spread operator.
214
214
  */
215
215
 
@@ -280,4 +280,4 @@ export {
280
280
  hasArchitectureDetected,
281
281
  syncConfig
282
282
  };
283
- //# sourceMappingURL=chunk-JLOQFDEL.js.map
283
+ //# sourceMappingURL=chunk-4QVNEEYP.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/commands/sync-config.ts","../src/utils/boundaries.ts","../src/utils/depcruise-config.ts"],"sourcesContent":["/**\n * Sync Config command - Regenerate depcruise config from current project structure\n *\n * Used by `/audit` slash command to refresh config before running checks.\n */\n\nimport { writeFileSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { detectArchitecture } from '../utils/boundaries.js';\nimport {\n type DepCruiseArchitecture,\n detectWorkspaces,\n generateDepCruiseConfigFile,\n generateDepCruiseMainConfig,\n} from '../utils/depcruise-config.js';\nimport { exists } from '../utils/fs.js';\nimport { error, info, success } from '../utils/output.js';\n\ninterface SyncConfigResult {\n generatedConfig: boolean;\n createdMainConfig: boolean;\n}\n\n/**\n * Core sync logic - writes depcruise configs to disk\n * Can be called from setup or as standalone command\n */\nexport function syncConfigCore(cwd: string, arch: DepCruiseArchitecture): SyncConfigResult {\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n const result: SyncConfigResult = {\n generatedConfig: false,\n createdMainConfig: false,\n };\n\n // Generate and write .safeword/depcruise-config.cjs (CJS for compatibility)\n const generatedConfigPath = nodePath.join(safewordDirectory, 'depcruise-config.cjs');\n const generatedConfig = generateDepCruiseConfigFile(arch);\n writeFileSync(generatedConfigPath, generatedConfig);\n result.generatedConfig = true;\n\n // Create main config if not exists (self-healing)\n // Use .cjs extension to work in ESM projects (type: \"module\")\n const mainConfigPath = nodePath.join(cwd, '.dependency-cruiser.cjs');\n if (!exists(mainConfigPath)) {\n const mainConfig = generateDepCruiseMainConfig();\n writeFileSync(mainConfigPath, mainConfig);\n result.createdMainConfig = true;\n }\n\n return result;\n}\n\n/**\n * Build full architecture info by combining detected layers with workspaces\n */\nexport function buildArchitecture(cwd: string): DepCruiseArchitecture {\n const arch = detectArchitecture(cwd);\n const workspaces = detectWorkspaces(cwd);\n return { ...arch, workspaces };\n}\n\n/**\n * Check if architecture was detected (layers, monorepo structure, or workspaces)\n */\nexport function hasArchitectureDetected(arch: DepCruiseArchitecture): boolean {\n return arch.elements.length > 0 || arch.isMonorepo || (arch.workspaces?.length ?? 0) > 0;\n}\n\n/**\n * CLI command: Sync depcruise config with current project structure\n */\nexport async function syncConfig(): Promise<void> {\n const cwd = process.cwd();\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n\n // Check if .safeword exists\n if (!exists(safewordDirectory)) {\n error('Not configured. Run `safeword setup` first.');\n process.exit(1);\n }\n\n // Detect current architecture and workspaces\n const arch = buildArchitecture(cwd);\n const result = syncConfigCore(cwd, arch);\n\n if (result.generatedConfig) {\n info('Generated .safeword/depcruise-config.cjs');\n }\n if (result.createdMainConfig) {\n info('Created .dependency-cruiser.cjs');\n }\n\n success('Config synced');\n}\n","/**\n * Architecture boundaries detection and config generation\n *\n * Auto-detects common architecture directories and generates\n * eslint-plugin-boundaries config with sensible hierarchy rules.\n *\n * Supports:\n * - Standard projects (src/utils, utils/)\n * - Monorepos (packages/*, apps/*)\n * - Various naming conventions (helpers, shared, core, etc.)\n */\n\nimport { readdirSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { exists } from './fs.js';\n\n/**\n * Architecture layer definitions with alternative names.\n * Each layer maps to equivalent directory names.\n * Order defines hierarchy: earlier = lower layer.\n */\nconst ARCHITECTURE_LAYERS = [\n // Layer 0: Pure types (no imports)\n { layer: 'types', dirs: ['types', 'interfaces', 'schemas'] },\n // Layer 1: Utilities (only types)\n { layer: 'utils', dirs: ['utils', 'helpers', 'shared', 'common', 'core'] },\n // Layer 2: Libraries (types, utils)\n { layer: 'lib', dirs: ['lib', 'libraries'] },\n // Layer 3: State & logic (types, utils, lib)\n { layer: 'hooks', dirs: ['hooks', 'composables'] },\n { layer: 'services', dirs: ['services', 'api', 'stores', 'state'] },\n // Layer 4: UI components (all above)\n { layer: 'components', dirs: ['components', 'ui'] },\n // Layer 5: Features (all above)\n { layer: 'features', dirs: ['features', 'modules', 'domains'] },\n // Layer 6: Entry points (can import everything)\n { layer: 'app', dirs: ['app', 'pages', 'views', 'routes', 'commands'] },\n] as const;\n\ntype Layer = (typeof ARCHITECTURE_LAYERS)[number]['layer'];\n\n/**\n * Hierarchy rules: what each layer can import\n * Lower layers have fewer import permissions\n */\nconst HIERARCHY: Record<Layer, Layer[]> = {\n types: [],\n utils: ['types'],\n lib: ['utils', 'types'],\n hooks: ['lib', 'utils', 'types'],\n services: ['lib', 'utils', 'types'],\n components: ['hooks', 'services', 'lib', 'utils', 'types'],\n features: ['components', 'hooks', 'services', 'lib', 'utils', 'types'],\n app: ['features', 'components', 'hooks', 'services', 'lib', 'utils', 'types'],\n};\n\ninterface DetectedElement {\n layer: Layer;\n pattern: string; // glob pattern for boundaries config\n location: string; // human-readable location\n}\n\nexport interface DetectedArchitecture {\n elements: DetectedElement[];\n isMonorepo: boolean;\n}\n\n/**\n * Find monorepo package directories\n * @param projectDirectory\n */\nfunction findMonorepoPackages(projectDirectory: string): string[] {\n const packages: string[] = [];\n\n // Check common monorepo patterns\n const monorepoRoots = ['packages', 'apps', 'libs', 'modules'];\n\n for (const root of monorepoRoots) {\n const rootPath = nodePath.join(projectDirectory, root);\n if (!exists(rootPath)) continue;\n\n try {\n const entries = readdirSync(rootPath, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isDirectory() && !entry.name.startsWith('.')) {\n packages.push(nodePath.join(root, entry.name));\n }\n }\n } catch {\n // Directory not readable, skip\n }\n }\n\n return packages;\n}\n\n/**\n * Check if a layer already exists for this path prefix\n * @param elements\n * @param layer\n * @param pathPrefix\n */\nfunction hasLayerForPrefix(elements: DetectedElement[], layer: Layer, pathPrefix: string): boolean {\n return elements.some(\n element => element.layer === layer && element.pattern.startsWith(pathPrefix),\n );\n}\n\n/**\n * Scan a single search path for architecture layers\n * @param projectDirectory\n * @param searchPath\n * @param pathPrefix\n * @param elements\n */\nfunction scanSearchPath(\n projectDirectory: string,\n searchPath: string,\n pathPrefix: string,\n elements: DetectedElement[],\n): void {\n for (const layerDefinition of ARCHITECTURE_LAYERS) {\n for (const dirName of layerDefinition.dirs) {\n const fullPath = nodePath.join(projectDirectory, searchPath, dirName);\n if (exists(fullPath) && !hasLayerForPrefix(elements, layerDefinition.layer, pathPrefix)) {\n elements.push({\n layer: layerDefinition.layer,\n pattern: `${pathPrefix}${dirName}/**`,\n location: `${pathPrefix}${dirName}`,\n });\n }\n }\n }\n}\n\n/**\n * Scan a directory for architecture layers\n * @param projectDirectory\n * @param basePath\n */\nfunction scanForLayers(projectDirectory: string, basePath: string): DetectedElement[] {\n const elements: DetectedElement[] = [];\n const prefix = basePath ? `${basePath}/` : '';\n\n // Check src/ and root level\n scanSearchPath(projectDirectory, nodePath.join(basePath, 'src'), `${prefix}src/`, elements);\n scanSearchPath(projectDirectory, basePath, prefix, elements);\n\n return elements;\n}\n\n/**\n * Detects architecture directories in the project\n * Handles both standard projects and monorepos\n * @param projectDirectory\n */\nexport function detectArchitecture(projectDirectory: string): DetectedArchitecture {\n const elements: DetectedElement[] = [];\n\n // First, check for monorepo packages\n const packages = findMonorepoPackages(projectDirectory);\n const isMonorepo = packages.length > 0;\n\n if (isMonorepo) {\n // Scan each package\n for (const pkg of packages) {\n elements.push(...scanForLayers(projectDirectory, pkg));\n }\n }\n\n // Also scan root level (works for both monorepo root and standard projects)\n elements.push(...scanForLayers(projectDirectory, ''));\n\n // Deduplicate by pattern\n const seen = new Set<string>();\n const uniqueElements = elements.filter(element => {\n if (seen.has(element.pattern)) return false;\n seen.add(element.pattern);\n return true;\n });\n\n return { elements: uniqueElements, isMonorepo };\n}\n\n/**\n * Format a single element for the config\n * @param el\n */\nfunction formatElement(element: DetectedElement): string {\n return ` { type: '${element.layer}', pattern: '${element.pattern}', mode: 'full' }`;\n}\n\n/**\n * Format allowed imports for a rule\n * @param allowed\n */\nfunction formatAllowedImports(allowed: Layer[]): string {\n return allowed.map(d => `'${d}'`).join(', ');\n}\n\n/**\n * Generate a single rule for what a layer can import\n * @param layer\n * @param detectedLayers\n */\nfunction generateRule(layer: Layer, detectedLayers: Set<Layer>): string | undefined {\n const allowedLayers = HIERARCHY[layer];\n if (allowedLayers.length === 0) return undefined;\n\n const allowed = allowedLayers.filter(dep => detectedLayers.has(dep));\n if (allowed.length === 0) return undefined;\n\n return ` { from: ['${layer}'], allow: [${formatAllowedImports(allowed)}] }`;\n}\n\n/**\n * Build description of what was detected\n * @param arch\n */\nfunction buildDetectedInfo(arch: DetectedArchitecture): string {\n if (arch.elements.length === 0) {\n return 'No architecture directories detected yet - add types/, utils/, components/, etc.';\n }\n const locations = arch.elements.map(element => element.location).join(', ');\n const monorepoNote = arch.isMonorepo ? ' (monorepo)' : '';\n return `Detected: ${locations}${monorepoNote}`;\n}\n\n/**\n *\n * @param arch\n */\nexport function generateBoundariesConfig(arch: DetectedArchitecture): string {\n const hasElements = arch.elements.length > 0;\n\n // Generate element definitions\n const elementsContent = arch.elements.map(element => formatElement(element)).join(',\\n');\n\n // Generate rules (what each layer can import)\n const detectedLayers = new Set(arch.elements.map(element => element.layer));\n const rules = [...detectedLayers]\n .map(layer => generateRule(layer, detectedLayers))\n .filter((rule): rule is string => rule !== undefined);\n const rulesContent = rules.join(',\\n');\n\n const detectedInfo = buildDetectedInfo(arch);\n\n return `/**\n * Architecture Boundaries Configuration (AUTO-GENERATED)\n *\n * ${detectedInfo}\n *\n * This enforces import boundaries between architectural layers:\n * - Lower layers (types, utils) cannot import from higher layers (components, features)\n * - Uses 'error' severity - LLMs ignore warnings, errors force compliance\n *\n * Recognized directories (in hierarchy order):\n * types → utils → lib → hooks/services → components → features/modules → app\n *\n * To customize, override in your eslint.config.mjs:\n * rules: { 'boundaries/element-types': ['error', { ... }] }\n */\n\nimport boundaries from 'eslint-plugin-boundaries';\n\nexport default {\n plugins: { boundaries },\n settings: {\n 'boundaries/elements': [\n${elementsContent}\n ],\n },\n rules: {${\n hasElements\n ? `\n 'boundaries/element-types': ['error', {\n default: 'disallow',\n rules: [\n${rulesContent}\n ],\n }],`\n : ''\n }\n 'boundaries/no-unknown': 'off', // Allow files outside defined elements\n 'boundaries/no-unknown-files': 'off', // Allow non-matching files\n },\n};\n`;\n}\n","/**\n * Dependency-cruiser config generator\n *\n * Generates dependency-cruiser configuration from detected architecture.\n * Used by `safeword sync-config` command and `/audit` slash command.\n */\n\nimport nodePath from 'node:path';\n\nimport type { DetectedArchitecture } from './boundaries.js';\nimport { readJson } from './fs.js';\n\nexport interface DepCruiseArchitecture extends DetectedArchitecture {\n workspaces?: string[];\n}\n\ninterface PackageJson {\n workspaces?: string[] | { packages?: string[] };\n}\n\n/**\n * Detect workspaces from package.json\n * Supports both array format and object format (yarn workspaces)\n */\nexport function detectWorkspaces(cwd: string): string[] | undefined {\n const packageJsonPath = nodePath.join(cwd, 'package.json');\n const packageJson = readJson(packageJsonPath) as PackageJson | undefined;\n\n if (!packageJson?.workspaces) return undefined;\n\n // Handle both formats: string[] or { packages: string[] }\n const workspaces = Array.isArray(packageJson.workspaces)\n ? packageJson.workspaces\n : packageJson.workspaces.packages;\n\n return workspaces && workspaces.length > 0 ? workspaces : undefined;\n}\n\n/**\n * Generate monorepo hierarchy rules based on workspace patterns\n */\nfunction generateMonorepoRules(workspaces: string[]): string {\n const rules: string[] = [];\n\n const hasLibs = workspaces.some(w => w.startsWith('libs'));\n const hasPackages = workspaces.some(w => w.startsWith('packages'));\n const hasApps = workspaces.some(w => w.startsWith('apps'));\n\n // libs cannot import packages or apps\n if (hasLibs && (hasPackages || hasApps)) {\n rules.push(` {\n name: 'libs-cannot-import-packages-or-apps',\n severity: 'error',\n from: { path: '^libs/' },\n to: { path: '^(packages|apps)/' },\n }`);\n }\n\n // packages cannot import apps\n if (hasPackages && hasApps) {\n rules.push(` {\n name: 'packages-cannot-import-apps',\n severity: 'error',\n from: { path: '^packages/' },\n to: { path: '^apps/' },\n }`);\n }\n\n return rules.join(',\\n');\n}\n\n/**\n * Generate .safeword/depcruise-config.js content (forbidden rules + options)\n */\nexport function generateDepCruiseConfigFile(arch: DepCruiseArchitecture): string {\n const monorepoRules = arch.workspaces ? generateMonorepoRules(arch.workspaces) : '';\n const hasMonorepoRules = monorepoRules.length > 0;\n\n return String.raw`module.exports = {\n forbidden: [\n // =========================================================================\n // ERROR RULES (block on violations)\n // =========================================================================\n {\n name: 'no-circular',\n comment: 'Circular dependencies cause runtime issues and make code hard to reason about',\n severity: 'error',\n from: {},\n to: { circular: true },\n },\n {\n name: 'no-deprecated-deps',\n comment: 'Deprecated npm packages should be replaced - they may have security issues or be unmaintained',\n severity: 'error',\n from: {},\n to: { dependencyTypes: ['deprecated'] },\n },${hasMonorepoRules ? `\\n${monorepoRules},` : ''}\n\n // =========================================================================\n // WARNING RULES (flag issues but don't block)\n // =========================================================================\n {\n name: 'no-dev-deps-in-src',\n comment: 'Production code should not import devDependencies - may cause runtime failures',\n severity: 'warn',\n from: {\n path: ['^src', '^packages/[^/]+/src'],\n pathNot: '\\\\.test\\\\.[tj]sx?$',\n },\n to: { dependencyTypes: ['npm-dev'] },\n },\n {\n name: 'no-orphans',\n comment: 'Orphan modules are not imported anywhere - may be dead code',\n severity: 'warn',\n from: {\n orphan: true,\n pathNot: [\n // Entry points\n '(^|/)index\\\\.[tj]sx?$',\n '(^|/)main\\\\.[tj]sx?$',\n '(^|/)cli\\\\.[tj]s$',\n '\\\\.config\\\\.[tj]s$',\n '\\\\.config\\\\.mjs$',\n // Test files\n '\\\\.test\\\\.[tj]sx?$',\n '\\\\.spec\\\\.[tj]sx?$',\n '/tests/',\n '/__tests__/',\n // Astro/Next.js pages and content\n '/src/content/',\n '/src/pages/',\n '/app/',\n ],\n },\n to: {},\n },\n ],\n options: {\n doNotFollow: { path: ['node_modules', '.safeword'] },\n exclude: {\n path: ['node_modules', 'dist', 'build', 'coverage', '\\\\.d\\\\.ts$'],\n },\n tsPreCompilationDeps: true,\n tsConfig: { fileName: 'tsconfig.json' },\n enhancedResolveOptions: {\n extensions: ['.ts', '.tsx', '.js', '.jsx'],\n exportsFields: ['exports'],\n conditionNames: ['import', 'require', 'node', 'default'],\n },\n },\n};\n`;\n}\n\n/**\n * Generate .dependency-cruiser.js (main config that imports generated)\n */\nexport function generateDepCruiseMainConfig(): string {\n return `/**\n * Dependency Cruiser Configuration\n *\n * Imports auto-generated rules from .safeword/depcruise-config.js\n * ADD YOUR CUSTOM RULES BELOW the spread operator.\n */\n\nconst generated = require('./.safeword/depcruise-config.cjs');\n\nmodule.exports = {\n forbidden: [\n ...generated.forbidden,\n // ADD YOUR CUSTOM RULES BELOW:\n // { name: 'no-legacy', from: { path: 'legacy/' }, to: { path: 'new/' } },\n ],\n options: {\n ...generated.options,\n // Your overrides here\n },\n};\n`;\n}\n"],"mappings":"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","names":["nodePath","nodePath","nodePath","nodePath"]}
1
+ {"version":3,"sources":["../src/commands/sync-config.ts","../src/utils/boundaries.ts","../src/utils/depcruise-config.ts"],"sourcesContent":["/**\n * Sync Config command - Regenerate depcruise config from current project structure\n *\n * Used by `/audit` slash command to refresh config before running checks.\n */\n\nimport { writeFileSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { detectArchitecture } from '../utils/boundaries.js';\nimport {\n type DepCruiseArchitecture,\n detectWorkspaces,\n generateDepCruiseConfigFile,\n generateDepCruiseMainConfig,\n} from '../utils/depcruise-config.js';\nimport { exists } from '../utils/fs.js';\nimport { error, info, success } from '../utils/output.js';\n\ninterface SyncConfigResult {\n generatedConfig: boolean;\n createdMainConfig: boolean;\n}\n\n/**\n * Core sync logic - writes depcruise configs to disk\n * Can be called from setup or as standalone command\n */\nexport function syncConfigCore(cwd: string, arch: DepCruiseArchitecture): SyncConfigResult {\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n const result: SyncConfigResult = {\n generatedConfig: false,\n createdMainConfig: false,\n };\n\n // Generate and write .safeword/depcruise-config.cjs (CJS for compatibility)\n const generatedConfigPath = nodePath.join(safewordDirectory, 'depcruise-config.cjs');\n const generatedConfig = generateDepCruiseConfigFile(arch);\n writeFileSync(generatedConfigPath, generatedConfig);\n result.generatedConfig = true;\n\n // Create main config if not exists (self-healing)\n // Use .cjs extension to work in ESM projects (type: \"module\")\n const mainConfigPath = nodePath.join(cwd, '.dependency-cruiser.cjs');\n if (!exists(mainConfigPath)) {\n const mainConfig = generateDepCruiseMainConfig();\n writeFileSync(mainConfigPath, mainConfig);\n result.createdMainConfig = true;\n }\n\n return result;\n}\n\n/**\n * Build full architecture info by combining detected layers with workspaces\n */\nexport function buildArchitecture(cwd: string): DepCruiseArchitecture {\n const arch = detectArchitecture(cwd);\n const workspaces = detectWorkspaces(cwd);\n return { ...arch, workspaces };\n}\n\n/**\n * Check if architecture was detected (layers, monorepo structure, or workspaces)\n */\nexport function hasArchitectureDetected(arch: DepCruiseArchitecture): boolean {\n return arch.elements.length > 0 || arch.isMonorepo || (arch.workspaces?.length ?? 0) > 0;\n}\n\n/**\n * CLI command: Sync depcruise config with current project structure\n */\nexport async function syncConfig(): Promise<void> {\n const cwd = process.cwd();\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n\n // Check if .safeword exists\n if (!exists(safewordDirectory)) {\n error('Not configured. Run `safeword setup` first.');\n process.exit(1);\n }\n\n // Detect current architecture and workspaces\n const arch = buildArchitecture(cwd);\n const result = syncConfigCore(cwd, arch);\n\n if (result.generatedConfig) {\n info('Generated .safeword/depcruise-config.cjs');\n }\n if (result.createdMainConfig) {\n info('Created .dependency-cruiser.cjs');\n }\n\n success('Config synced');\n}\n","/**\n * Architecture boundaries detection and config generation\n *\n * Auto-detects common architecture directories and generates\n * eslint-plugin-boundaries config with sensible hierarchy rules.\n *\n * Supports:\n * - Standard projects (src/utils, utils/)\n * - Monorepos (packages/*, apps/*)\n * - Various naming conventions (helpers, shared, core, etc.)\n */\n\nimport { readdirSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { exists } from './fs.js';\n\n/**\n * Architecture layer definitions with alternative names.\n * Each layer maps to equivalent directory names.\n * Order defines hierarchy: earlier = lower layer.\n */\nconst ARCHITECTURE_LAYERS = [\n // Layer 0: Pure types (no imports)\n { layer: 'types', dirs: ['types', 'interfaces', 'schemas'] },\n // Layer 1: Utilities (only types)\n { layer: 'utils', dirs: ['utils', 'helpers', 'shared', 'common', 'core'] },\n // Layer 2: Libraries (types, utils)\n { layer: 'lib', dirs: ['lib', 'libraries'] },\n // Layer 3: State & logic (types, utils, lib)\n { layer: 'hooks', dirs: ['hooks', 'composables'] },\n { layer: 'services', dirs: ['services', 'api', 'stores', 'state'] },\n // Layer 4: UI components (all above)\n { layer: 'components', dirs: ['components', 'ui'] },\n // Layer 5: Features (all above)\n { layer: 'features', dirs: ['features', 'modules', 'domains'] },\n // Layer 6: Entry points (can import everything)\n { layer: 'app', dirs: ['app', 'pages', 'views', 'routes', 'commands'] },\n] as const;\n\ntype Layer = (typeof ARCHITECTURE_LAYERS)[number]['layer'];\n\n/**\n * Hierarchy rules: what each layer can import\n * Lower layers have fewer import permissions\n */\nconst HIERARCHY: Record<Layer, Layer[]> = {\n types: [],\n utils: ['types'],\n lib: ['utils', 'types'],\n hooks: ['lib', 'utils', 'types'],\n services: ['lib', 'utils', 'types'],\n components: ['hooks', 'services', 'lib', 'utils', 'types'],\n features: ['components', 'hooks', 'services', 'lib', 'utils', 'types'],\n app: ['features', 'components', 'hooks', 'services', 'lib', 'utils', 'types'],\n};\n\ninterface DetectedElement {\n layer: Layer;\n pattern: string; // glob pattern for boundaries config\n location: string; // human-readable location\n}\n\nexport interface DetectedArchitecture {\n elements: DetectedElement[];\n isMonorepo: boolean;\n}\n\n/**\n * Find monorepo package directories\n * @param projectDirectory\n */\nfunction findMonorepoPackages(projectDirectory: string): string[] {\n const packages: string[] = [];\n\n // Check common monorepo patterns\n const monorepoRoots = ['packages', 'apps', 'libs', 'modules'];\n\n for (const root of monorepoRoots) {\n const rootPath = nodePath.join(projectDirectory, root);\n if (!exists(rootPath)) continue;\n\n try {\n const entries = readdirSync(rootPath, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isDirectory() && !entry.name.startsWith('.')) {\n packages.push(nodePath.join(root, entry.name));\n }\n }\n } catch {\n // Directory not readable, skip\n }\n }\n\n return packages;\n}\n\n/**\n * Check if a layer already exists for this path prefix\n * @param elements\n * @param layer\n * @param pathPrefix\n */\nfunction hasLayerForPrefix(elements: DetectedElement[], layer: Layer, pathPrefix: string): boolean {\n return elements.some(\n element => element.layer === layer && element.pattern.startsWith(pathPrefix),\n );\n}\n\n/**\n * Scan a single search path for architecture layers\n * @param projectDirectory\n * @param searchPath\n * @param pathPrefix\n * @param elements\n */\nfunction scanSearchPath(\n projectDirectory: string,\n searchPath: string,\n pathPrefix: string,\n elements: DetectedElement[],\n): void {\n for (const layerDefinition of ARCHITECTURE_LAYERS) {\n for (const dirName of layerDefinition.dirs) {\n const fullPath = nodePath.join(projectDirectory, searchPath, dirName);\n if (exists(fullPath) && !hasLayerForPrefix(elements, layerDefinition.layer, pathPrefix)) {\n elements.push({\n layer: layerDefinition.layer,\n pattern: `${pathPrefix}${dirName}/**`,\n location: `${pathPrefix}${dirName}`,\n });\n }\n }\n }\n}\n\n/**\n * Scan a directory for architecture layers\n * @param projectDirectory\n * @param basePath\n */\nfunction scanForLayers(projectDirectory: string, basePath: string): DetectedElement[] {\n const elements: DetectedElement[] = [];\n const prefix = basePath ? `${basePath}/` : '';\n\n // Check src/ and root level\n scanSearchPath(projectDirectory, nodePath.join(basePath, 'src'), `${prefix}src/`, elements);\n scanSearchPath(projectDirectory, basePath, prefix, elements);\n\n return elements;\n}\n\n/**\n * Detects architecture directories in the project\n * Handles both standard projects and monorepos\n * @param projectDirectory\n */\nexport function detectArchitecture(projectDirectory: string): DetectedArchitecture {\n const elements: DetectedElement[] = [];\n\n // First, check for monorepo packages\n const packages = findMonorepoPackages(projectDirectory);\n const isMonorepo = packages.length > 0;\n\n if (isMonorepo) {\n // Scan each package\n for (const pkg of packages) {\n elements.push(...scanForLayers(projectDirectory, pkg));\n }\n }\n\n // Also scan root level (works for both monorepo root and standard projects)\n elements.push(...scanForLayers(projectDirectory, ''));\n\n // Deduplicate by pattern\n const seen = new Set<string>();\n const uniqueElements = elements.filter(element => {\n if (seen.has(element.pattern)) return false;\n seen.add(element.pattern);\n return true;\n });\n\n return { elements: uniqueElements, isMonorepo };\n}\n\n/**\n * Format a single element for the config\n * @param el\n */\nfunction formatElement(element: DetectedElement): string {\n return ` { type: '${element.layer}', pattern: '${element.pattern}', mode: 'full' }`;\n}\n\n/**\n * Format allowed imports for a rule\n * @param allowed\n */\nfunction formatAllowedImports(allowed: Layer[]): string {\n return allowed.map(d => `'${d}'`).join(', ');\n}\n\n/**\n * Generate a single rule for what a layer can import\n * @param layer\n * @param detectedLayers\n */\nfunction generateRule(layer: Layer, detectedLayers: Set<Layer>): string | undefined {\n const allowedLayers = HIERARCHY[layer];\n if (allowedLayers.length === 0) return undefined;\n\n const allowed = allowedLayers.filter(dep => detectedLayers.has(dep));\n if (allowed.length === 0) return undefined;\n\n return ` { from: ['${layer}'], allow: [${formatAllowedImports(allowed)}] }`;\n}\n\n/**\n * Build description of what was detected\n * @param arch\n */\nfunction buildDetectedInfo(arch: DetectedArchitecture): string {\n if (arch.elements.length === 0) {\n return 'No architecture directories detected yet - add types/, utils/, components/, etc.';\n }\n const locations = arch.elements.map(element => element.location).join(', ');\n const monorepoNote = arch.isMonorepo ? ' (monorepo)' : '';\n return `Detected: ${locations}${monorepoNote}`;\n}\n\n/**\n *\n * @param arch\n */\nexport function generateBoundariesConfig(arch: DetectedArchitecture): string {\n const hasElements = arch.elements.length > 0;\n\n // Generate element definitions\n const elementsContent = arch.elements.map(element => formatElement(element)).join(',\\n');\n\n // Generate rules (what each layer can import)\n const detectedLayers = new Set(arch.elements.map(element => element.layer));\n const rules = [...detectedLayers]\n .map(layer => generateRule(layer, detectedLayers))\n .filter((rule): rule is string => rule !== undefined);\n const rulesContent = rules.join(',\\n');\n\n const detectedInfo = buildDetectedInfo(arch);\n\n return `/**\n * Architecture Boundaries Configuration (AUTO-GENERATED)\n *\n * ${detectedInfo}\n *\n * This enforces import boundaries between architectural layers:\n * - Lower layers (types, utils) cannot import from higher layers (components, features)\n * - Uses 'error' severity - LLMs ignore warnings, errors force compliance\n *\n * Recognized directories (in hierarchy order):\n * types → utils → lib → hooks/services → components → features/modules → app\n *\n * To customize, override in your eslint.config.mjs:\n * rules: { 'boundaries/element-types': ['error', { ... }] }\n */\n\nimport boundaries from 'eslint-plugin-boundaries';\n\nexport default {\n plugins: { boundaries },\n settings: {\n 'boundaries/elements': [\n${elementsContent}\n ],\n },\n rules: {${\n hasElements\n ? `\n 'boundaries/element-types': ['error', {\n default: 'disallow',\n rules: [\n${rulesContent}\n ],\n }],`\n : ''\n }\n 'boundaries/no-unknown': 'off', // Allow files outside defined elements\n 'boundaries/no-unknown-files': 'off', // Allow non-matching files\n },\n};\n`;\n}\n","/**\n * Dependency-cruiser config generator\n *\n * Generates dependency-cruiser configuration from detected architecture.\n * Used by `safeword sync-config` command and `/audit` slash command.\n */\n\nimport nodePath from 'node:path';\n\nimport type { DetectedArchitecture } from './boundaries.js';\nimport { readJson } from './fs.js';\n\nexport interface DepCruiseArchitecture extends DetectedArchitecture {\n workspaces?: string[];\n}\n\ninterface PackageJson {\n workspaces?: string[] | { packages?: string[] };\n}\n\n/**\n * Detect workspaces from package.json\n * Supports both array format and object format (yarn workspaces)\n */\nexport function detectWorkspaces(cwd: string): string[] | undefined {\n const packageJsonPath = nodePath.join(cwd, 'package.json');\n const packageJson = readJson(packageJsonPath) as PackageJson | undefined;\n\n if (!packageJson?.workspaces) return undefined;\n\n // Handle both formats: string[] or { packages: string[] }\n const workspaces = Array.isArray(packageJson.workspaces)\n ? packageJson.workspaces\n : packageJson.workspaces.packages;\n\n return workspaces && workspaces.length > 0 ? workspaces : undefined;\n}\n\n/**\n * Generate monorepo hierarchy rules based on workspace patterns\n */\nfunction generateMonorepoRules(workspaces: string[]): string {\n const rules: string[] = [];\n\n const hasLibs = workspaces.some(w => w.startsWith('libs'));\n const hasPackages = workspaces.some(w => w.startsWith('packages'));\n const hasApps = workspaces.some(w => w.startsWith('apps'));\n\n // libs cannot import packages or apps\n if (hasLibs && (hasPackages || hasApps)) {\n rules.push(` {\n name: 'libs-cannot-import-packages-or-apps',\n severity: 'error',\n from: { path: '^libs/' },\n to: { path: '^(packages|apps)/' },\n }`);\n }\n\n // packages cannot import apps\n if (hasPackages && hasApps) {\n rules.push(` {\n name: 'packages-cannot-import-apps',\n severity: 'error',\n from: { path: '^packages/' },\n to: { path: '^apps/' },\n }`);\n }\n\n return rules.join(',\\n');\n}\n\n/**\n * Generate .safeword/depcruise-config.cjs content (forbidden rules + options)\n */\nexport function generateDepCruiseConfigFile(arch: DepCruiseArchitecture): string {\n const monorepoRules = arch.workspaces ? generateMonorepoRules(arch.workspaces) : '';\n const hasMonorepoRules = monorepoRules.length > 0;\n\n return String.raw`module.exports = {\n forbidden: [\n // =========================================================================\n // ERROR RULES (block on violations)\n // =========================================================================\n {\n name: 'no-circular',\n comment: 'Circular dependencies cause runtime issues and make code hard to reason about',\n severity: 'error',\n from: {},\n to: { circular: true },\n },\n {\n name: 'no-deprecated-deps',\n comment: 'Deprecated npm packages should be replaced - they may have security issues or be unmaintained',\n severity: 'error',\n from: {},\n to: { dependencyTypes: ['deprecated'] },\n },${hasMonorepoRules ? `\\n${monorepoRules},` : ''}\n\n // =========================================================================\n // WARNING RULES (flag issues but don't block)\n // =========================================================================\n {\n name: 'no-dev-deps-in-src',\n comment: 'Production code should not import devDependencies - may cause runtime failures',\n severity: 'warn',\n from: {\n path: ['^src', '^packages/[^/]+/src'],\n pathNot: '\\\\.test\\\\.[tj]sx?$',\n },\n to: { dependencyTypes: ['npm-dev'] },\n },\n {\n name: 'no-orphans',\n comment: 'Orphan modules are not imported anywhere - may be dead code',\n severity: 'warn',\n from: {\n orphan: true,\n pathNot: [\n // Entry points\n '(^|/)index\\\\.[tj]sx?$',\n '(^|/)main\\\\.[tj]sx?$',\n '(^|/)cli\\\\.[tj]s$',\n '\\\\.config\\\\.[tj]s$',\n '\\\\.config\\\\.mjs$',\n // Test files\n '\\\\.test\\\\.[tj]sx?$',\n '\\\\.spec\\\\.[tj]sx?$',\n '/tests/',\n '/__tests__/',\n // Astro/Next.js pages and content\n '/src/content/',\n '/src/pages/',\n '/app/',\n ],\n },\n to: {},\n },\n ],\n options: {\n doNotFollow: { path: ['node_modules', '.safeword'] },\n exclude: {\n path: ['node_modules', 'dist', 'build', 'coverage', '\\\\.d\\\\.ts$'],\n },\n tsPreCompilationDeps: true,\n tsConfig: { fileName: 'tsconfig.json' },\n enhancedResolveOptions: {\n extensions: ['.ts', '.tsx', '.js', '.jsx'],\n exportsFields: ['exports'],\n conditionNames: ['import', 'require', 'node', 'default'],\n },\n },\n};\n`;\n}\n\n/**\n * Generate .dependency-cruiser.js (main config that imports generated)\n */\nexport function generateDepCruiseMainConfig(): string {\n return `/**\n * Dependency Cruiser Configuration\n *\n * Imports auto-generated rules from .safeword/depcruise-config.cjs\n * ADD YOUR CUSTOM RULES BELOW the spread operator.\n */\n\nconst generated = require('./.safeword/depcruise-config.cjs');\n\nmodule.exports = {\n forbidden: [\n ...generated.forbidden,\n // ADD YOUR CUSTOM RULES BELOW:\n // { name: 'no-legacy', from: { path: 'legacy/' }, to: { path: 'new/' } },\n ],\n options: {\n ...generated.options,\n // Your overrides here\n },\n};\n`;\n}\n"],"mappings":"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","names":["nodePath","nodePath","nodePath","nodePath"]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  detect
3
- } from "./chunk-OYBKTOVF.js";
3
+ } from "./chunk-VWOO5PJN.js";
4
4
  import {
5
5
  VERSION
6
6
  } from "./chunk-ORQHKDT2.js";
@@ -482,7 +482,7 @@ function executeAction(action, ctx, result) {
482
482
  break;
483
483
  }
484
484
  case "json-unmerge": {
485
- executeJsonUnmerge(ctx.cwd, action.path, action.definition);
485
+ executeJsonUnmerge(ctx.cwd, action.path, action.definition, ctx);
486
486
  break;
487
487
  }
488
488
  case "text-patch": {
@@ -542,12 +542,12 @@ function executeJsonMerge(cwd, path2, definition, ctx) {
542
542
  if (JSON.stringify(existing) === JSON.stringify(merged)) return;
543
543
  writeJson(fullPath, merged);
544
544
  }
545
- function executeJsonUnmerge(cwd, path2, definition) {
545
+ function executeJsonUnmerge(cwd, path2, definition, ctx) {
546
546
  const fullPath = nodePath2.join(cwd, path2);
547
547
  if (!exists(fullPath)) return;
548
548
  const existing = readJson(fullPath);
549
549
  if (!existing) return;
550
- const unmerged = definition.unmerge(existing);
550
+ const unmerged = definition.unmerge(existing, ctx);
551
551
  if (definition.removeFileIfEmpty) {
552
552
  const remainingKeys = Object.keys(unmerged).filter((k) => unmerged[k] !== void 0);
553
553
  if (remainingKeys.length === 0) {
@@ -787,14 +787,14 @@ ignore = [
787
787
 
788
788
  [lint.mccabe]
789
789
  max-complexity = 10`;
790
- function generateRuffBaseConfig(existingRuffConfig = false) {
790
+ function generateRuffBaseConfig(existingRuffConfig) {
791
791
  if (existingRuffConfig) {
792
792
  return `# Safeword Ruff config - extends project config with stricter rules
793
793
  # Used by hooks for LLM enforcement. Human pre-commits use project config.
794
794
  # Re-run \`safeword upgrade\` to regenerate after project config changes.
795
795
 
796
- # Inherit from project's pyproject.toml [tool.ruff] section
797
- extend = "../pyproject.toml"
796
+ # Inherit from project's ${existingRuffConfig}
797
+ extend = "../${existingRuffConfig}"
798
798
 
799
799
  # Safeword overrides (stricter than project defaults)
800
800
  line-length = 100
@@ -889,6 +889,30 @@ function getPrettierConfig(hasExistingFormatter2) {
889
889
  configEntry: " eslintConfigPrettier,"
890
890
  };
891
891
  }
892
+ function getMonorepoSnippet(directoryVariable) {
893
+ return `// Monorepo support: detect Next.js apps to scope Next.js-only rules
894
+ // - Returns undefined for single-app Next.js projects (use full Next config)
895
+ // - Returns string[] of glob patterns for monorepos (scope Next.js rules)
896
+ const nextPaths = detect.findNextConfigPaths(${directoryVariable});
897
+
898
+ // Map framework to base config
899
+ // Note: Astro config only lints .astro files, so we combine it with TypeScript config
900
+ // to also lint .ts files in Astro projects
901
+ // Note: In monorepos, Next.js uses React config + scoped Next.js rules
902
+ const baseConfigs = {
903
+ next: nextPaths ? configs.recommendedTypeScriptReact : configs.recommendedTypeScriptNext,
904
+ react: configs.recommendedTypeScriptReact,
905
+ astro: [...configs.recommendedTypeScript, ...configs.astro],
906
+ typescript: configs.recommendedTypeScript,
907
+ javascript: configs.recommended,
908
+ };
909
+
910
+ // Build scoped Next.js rules for monorepos
911
+ // Each Next.js app gets its own scoped config with files: pattern
912
+ const scopedNextConfigs = nextPaths?.flatMap((filePath) =>
913
+ configs.nextOnlyRules.map((config) => ({ ...config, files: [filePath] }))
914
+ ) ?? [];`;
915
+ }
892
916
  var SAFEWORD_STRICT_RULES_FULL = `// Safeword strict rules - applied after project rules (win on conflict)
893
917
  const safewordStrictRules = {
894
918
  rules: {
@@ -936,24 +960,19 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
936
960
  const deps = detect.collectAllDeps(__dirname);
937
961
  const framework = detect.detectFramework(deps);
938
962
 
939
- // Map framework to base config
940
- // Note: Astro config only lints .astro files, so we combine it with TypeScript config
941
- // to also lint .ts files in Astro projects
942
- const baseConfigs = {
943
- next: configs.recommendedTypeScriptNext,
944
- react: configs.recommendedTypeScriptReact,
945
- astro: [...configs.recommendedTypeScript, ...configs.astro],
946
- typescript: configs.recommendedTypeScript,
947
- javascript: configs.recommended,
948
- };
963
+ ${getMonorepoSnippet("__dirname")}
949
964
 
950
965
  export default [
951
966
  { ignores: detect.getIgnores(deps) },
952
967
  ...baseConfigs[framework],
953
- ...(detect.hasVitest(deps) ? configs.vitest : []),
954
- ...(detect.hasPlaywright(deps) ? configs.playwright : []),
968
+ ...scopedNextConfigs,
969
+ // Testing configs - always included (file-scoped to *.test.* and *.e2e.*)
970
+ ...configs.vitest,
971
+ ...configs.playwright,
972
+ // TanStack Query - always included (rules only match useQuery/useMutation patterns)
973
+ ...configs.tanstackQuery,
974
+ // Tailwind - only if detected (plugin needs tailwind config to validate classes)
955
975
  ...(detect.hasTailwind(deps) ? configs.tailwind : []),
956
- ...(detect.hasTanstackQuery(deps) ? configs.tanstackQuery : []),
957
976
  eslintConfigPrettier,
958
977
  ];
959
978
  `;
@@ -968,24 +987,19 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
968
987
  const deps = detect.collectAllDeps(__dirname);
969
988
  const framework = detect.detectFramework(deps);
970
989
 
971
- // Map framework to base config
972
- // Note: Astro config only lints .astro files, so we combine it with TypeScript config
973
- // to also lint .ts files in Astro projects
974
- const baseConfigs = {
975
- next: configs.recommendedTypeScriptNext,
976
- react: configs.recommendedTypeScriptReact,
977
- astro: [...configs.recommendedTypeScript, ...configs.astro],
978
- typescript: configs.recommendedTypeScript,
979
- javascript: configs.recommended,
980
- };
990
+ ${getMonorepoSnippet("__dirname")}
981
991
 
982
992
  export default [
983
993
  { ignores: detect.getIgnores(deps) },
984
994
  ...baseConfigs[framework],
985
- ...(detect.hasVitest(deps) ? configs.vitest : []),
986
- ...(detect.hasPlaywright(deps) ? configs.playwright : []),
995
+ ...scopedNextConfigs,
996
+ // Testing configs - always included (file-scoped to *.test.* and *.e2e.*)
997
+ ...configs.vitest,
998
+ ...configs.playwright,
999
+ // TanStack Query - always included (rules only match useQuery/useMutation patterns)
1000
+ ...configs.tanstackQuery,
1001
+ // Tailwind - only if detected (plugin needs tailwind config to validate classes)
987
1002
  ...(detect.hasTailwind(deps) ? configs.tailwind : []),
988
- ...(detect.hasTanstackQuery(deps) ? configs.tanstackQuery : []),
989
1003
  ];
990
1004
  `;
991
1005
  }
@@ -1069,34 +1083,33 @@ ${prettier.import}
1069
1083
  const { detect, configs } = safeword;
1070
1084
  const __dirname = dirname(fileURLToPath(import.meta.url));
1071
1085
  // Look in parent directory for deps (this file is in .safeword/)
1072
- const deps = detect.collectAllDeps(dirname(__dirname));
1086
+ const projectDir = dirname(__dirname);
1087
+ const deps = detect.collectAllDeps(projectDir);
1073
1088
  const framework = detect.detectFramework(deps);
1074
1089
 
1075
- const baseConfigs = {
1076
- next: configs.recommendedTypeScriptNext,
1077
- react: configs.recommendedTypeScriptReact,
1078
- astro: [...configs.recommendedTypeScript, ...configs.astro],
1079
- typescript: configs.recommendedTypeScript,
1080
- javascript: configs.recommended,
1081
- };
1090
+ ${getMonorepoSnippet("projectDir")}
1082
1091
 
1083
1092
  ${SAFEWORD_STRICT_RULES_FULL}
1084
1093
 
1085
1094
  export default [
1086
1095
  { ignores: detect.getIgnores(deps) },
1087
1096
  ...baseConfigs[framework],
1088
- ...(detect.hasVitest(deps) ? configs.vitest : []),
1089
- ...(detect.hasPlaywright(deps) ? configs.playwright : []),
1097
+ ...scopedNextConfigs,
1098
+ // Testing configs - always included (file-scoped to *.test.* and *.e2e.*)
1099
+ ...configs.vitest,
1100
+ ...configs.playwright,
1101
+ // TanStack Query - always included (rules only match useQuery/useMutation patterns)
1102
+ ...configs.tanstackQuery,
1103
+ // Tailwind - only if detected (plugin needs tailwind config to validate classes)
1090
1104
  ...(detect.hasTailwind(deps) ? configs.tailwind : []),
1091
- ...(detect.hasTanstackQuery(deps) ? configs.tanstackQuery : []),
1092
1105
  safewordStrictRules,
1093
1106
  ${prettier.configEntry}
1094
1107
  ];
1095
1108
  `;
1096
1109
  }
1097
1110
  var CURSOR_HOOKS = {
1098
- afterFileEdit: [{ command: "bun ./.safeword/hooks/cursor/after-file-edit.ts" }],
1099
- stop: [{ command: "bun ./.safeword/hooks/cursor/stop.ts" }]
1111
+ afterFileEdit: [{ command: "bun ../.safeword/hooks/cursor/after-file-edit.ts" }],
1112
+ stop: [{ command: "bun ../.safeword/hooks/cursor/stop.ts" }]
1100
1113
  };
1101
1114
  var SETTINGS_HOOKS = {
1102
1115
  SessionStart: [
@@ -1207,7 +1220,7 @@ var BIOME_JSON_MERGE = {
1207
1220
  }
1208
1221
  };
1209
1222
  },
1210
- unmerge: (existing) => {
1223
+ unmerge: (existing, _ctx) => {
1211
1224
  const files = existing.files ?? {};
1212
1225
  const existingIncludes = Array.isArray(files.includes) ? files.includes : [];
1213
1226
  const safewordExcludes = /* @__PURE__ */ new Set(["!eslint.config.mjs", "!.safeword", "!.safeword/**"]);
@@ -1352,7 +1365,7 @@ var typescriptJsonMerges = {
1352
1365
  result.scripts = scripts;
1353
1366
  return result;
1354
1367
  },
1355
- unmerge: (existing) => {
1368
+ unmerge: (existing, _ctx) => {
1356
1369
  const result = { ...existing };
1357
1370
  const scripts = { ...existing.scripts };
1358
1371
  delete scripts["lint:eslint"];
@@ -1379,17 +1392,37 @@ var typescriptJsonMerges = {
1379
1392
  result[key] = value;
1380
1393
  }
1381
1394
  }
1382
- const plugins = getPrettierPlugins(ctx.projectType);
1383
- if (plugins.length > 0) {
1384
- result.plugins = plugins;
1395
+ const safewordPlugins = getPrettierPlugins(ctx.projectType);
1396
+ const existingPlugins = Array.isArray(result.plugins) ? result.plugins : [];
1397
+ const allPlugins = [...existingPlugins];
1398
+ for (const plugin of safewordPlugins) {
1399
+ if (!allPlugins.includes(plugin)) {
1400
+ allPlugins.push(plugin);
1401
+ }
1402
+ }
1403
+ const tailwindIndex = allPlugins.indexOf("prettier-plugin-tailwindcss");
1404
+ if (tailwindIndex !== -1 && tailwindIndex !== allPlugins.length - 1) {
1405
+ allPlugins.splice(tailwindIndex, 1);
1406
+ allPlugins.push("prettier-plugin-tailwindcss");
1407
+ }
1408
+ if (allPlugins.length > 0) {
1409
+ result.plugins = allPlugins;
1385
1410
  } else {
1386
1411
  delete result.plugins;
1387
1412
  }
1388
1413
  return result;
1389
1414
  },
1390
- unmerge: (existing) => {
1415
+ unmerge: (existing, ctx) => {
1391
1416
  const result = { ...existing };
1392
- delete result.plugins;
1417
+ const safewordPlugins = new Set(getPrettierPlugins(ctx.projectType));
1418
+ if (Array.isArray(result.plugins)) {
1419
+ const remaining = result.plugins.filter((p) => !safewordPlugins.has(p));
1420
+ if (remaining.length > 0) {
1421
+ result.plugins = remaining;
1422
+ } else {
1423
+ delete result.plugins;
1424
+ }
1425
+ }
1393
1426
  return result;
1394
1427
  }
1395
1428
  },
@@ -1531,6 +1564,8 @@ var SAFEWORD_SCHEMA = {
1531
1564
  ".cursor/rules/safeword-tdd-enforcing.mdc",
1532
1565
  ".claude/commands/tdd.md",
1533
1566
  ".cursor/commands/tdd.md",
1567
+ // BDD skill split into phase files (v0.16.0)
1568
+ ".cursor/rules/safeword-bdd-orchestrating.mdc",
1534
1569
  ".safeword/commands/tdd.md",
1535
1570
  // Brainstorming skill removed - never used, BDD discovery phase covers this (v0.16.0)
1536
1571
  ".claude/skills/safeword-brainstorming/SKILL.md",
@@ -1619,16 +1654,13 @@ var SAFEWORD_SCHEMA = {
1619
1654
  template: "hooks/post-tool-lint.ts"
1620
1655
  },
1621
1656
  ".safeword/hooks/stop-quality.ts": { template: "hooks/stop-quality.ts" },
1622
- // Guides (11 files)
1657
+ // Guides (10 files)
1623
1658
  ".safeword/guides/architecture-guide.md": {
1624
1659
  template: "guides/architecture-guide.md"
1625
1660
  },
1626
1661
  ".safeword/guides/cli-reference.md": {
1627
1662
  template: "guides/cli-reference.md"
1628
1663
  },
1629
- ".safeword/guides/code-philosophy.md": {
1630
- template: "guides/code-philosophy.md"
1631
- },
1632
1664
  ".safeword/guides/context-files-guide.md": {
1633
1665
  template: "guides/context-files-guide.md"
1634
1666
  },
@@ -1675,13 +1707,10 @@ var SAFEWORD_SCHEMA = {
1675
1707
  ".safeword/templates/work-log-template.md": {
1676
1708
  template: "doc-templates/work-log-template.md"
1677
1709
  },
1678
- // Prompts (2 files)
1710
+ // Prompts (1 file)
1679
1711
  ".safeword/prompts/architecture.md": {
1680
1712
  template: "prompts/architecture.md"
1681
1713
  },
1682
- ".safeword/prompts/quality-review.md": {
1683
- template: "prompts/quality-review.md"
1684
- },
1685
1714
  // Scripts (3 files)
1686
1715
  ".safeword/scripts/bisect-test-pollution.sh": {
1687
1716
  template: "scripts/bisect-test-pollution.sh"
@@ -1705,6 +1734,24 @@ var SAFEWORD_SCHEMA = {
1705
1734
  ".claude/skills/safeword-bdd-orchestrating/SKILL.md": {
1706
1735
  template: "skills/safeword-bdd-orchestrating/SKILL.md"
1707
1736
  },
1737
+ ".claude/skills/safeword-bdd-orchestrating/DISCOVERY.md": {
1738
+ template: "skills/safeword-bdd-orchestrating/DISCOVERY.md"
1739
+ },
1740
+ ".claude/skills/safeword-bdd-orchestrating/SCENARIOS.md": {
1741
+ template: "skills/safeword-bdd-orchestrating/SCENARIOS.md"
1742
+ },
1743
+ ".claude/skills/safeword-bdd-orchestrating/DECOMPOSITION.md": {
1744
+ template: "skills/safeword-bdd-orchestrating/DECOMPOSITION.md"
1745
+ },
1746
+ ".claude/skills/safeword-bdd-orchestrating/TDD.md": {
1747
+ template: "skills/safeword-bdd-orchestrating/TDD.md"
1748
+ },
1749
+ ".claude/skills/safeword-bdd-orchestrating/DONE.md": {
1750
+ template: "skills/safeword-bdd-orchestrating/DONE.md"
1751
+ },
1752
+ ".claude/skills/safeword-bdd-orchestrating/SPLITTING.md": {
1753
+ template: "skills/safeword-bdd-orchestrating/SPLITTING.md"
1754
+ },
1708
1755
  ".claude/commands/bdd.md": { template: "commands/bdd.md" },
1709
1756
  ".claude/commands/done.md": { template: "commands/done.md" },
1710
1757
  ".claude/commands/audit.md": { template: "commands/audit.md" },
@@ -1729,8 +1776,26 @@ var SAFEWORD_SCHEMA = {
1729
1776
  ".cursor/rules/safeword-refactoring.mdc": {
1730
1777
  template: "cursor/rules/safeword-refactoring.mdc"
1731
1778
  },
1732
- ".cursor/rules/safeword-bdd-orchestrating.mdc": {
1733
- template: "cursor/rules/safeword-bdd-orchestrating.mdc"
1779
+ ".cursor/rules/bdd-core.mdc": {
1780
+ template: "cursor/rules/bdd-core.mdc"
1781
+ },
1782
+ ".cursor/rules/bdd-discovery.mdc": {
1783
+ template: "cursor/rules/bdd-discovery.mdc"
1784
+ },
1785
+ ".cursor/rules/bdd-scenarios.mdc": {
1786
+ template: "cursor/rules/bdd-scenarios.mdc"
1787
+ },
1788
+ ".cursor/rules/bdd-decomposition.mdc": {
1789
+ template: "cursor/rules/bdd-decomposition.mdc"
1790
+ },
1791
+ ".cursor/rules/bdd-tdd.mdc": {
1792
+ template: "cursor/rules/bdd-tdd.mdc"
1793
+ },
1794
+ ".cursor/rules/bdd-done.mdc": {
1795
+ template: "cursor/rules/bdd-done.mdc"
1796
+ },
1797
+ ".cursor/rules/bdd-splitting.mdc": {
1798
+ template: "cursor/rules/bdd-splitting.mdc"
1734
1799
  },
1735
1800
  // Cursor commands (8 files - same as Claude)
1736
1801
  ".cursor/commands/bdd.md": { template: "commands/bdd.md" },
@@ -1913,15 +1978,15 @@ function findExistingEslintConfig(cwd) {
1913
1978
  }
1914
1979
  return void 0;
1915
1980
  }
1916
- function hasExistingRuffConfig(cwd) {
1917
- if (existsSync3(nodePath4.join(cwd, "ruff.toml"))) return true;
1981
+ function findExistingRuffConfig(cwd) {
1982
+ if (existsSync3(nodePath4.join(cwd, "ruff.toml"))) return "ruff.toml";
1918
1983
  const pyprojectPath = nodePath4.join(cwd, PYPROJECT_TOML);
1919
- if (!existsSync3(pyprojectPath)) return false;
1984
+ if (!existsSync3(pyprojectPath)) return void 0;
1920
1985
  try {
1921
1986
  const content = readFileSync2(pyprojectPath, "utf8");
1922
- return content.includes("[tool.ruff]");
1987
+ return content.includes("[tool.ruff]") ? "pyproject.toml" : void 0;
1923
1988
  } catch {
1924
- return false;
1989
+ return void 0;
1925
1990
  }
1926
1991
  }
1927
1992
  function hasExistingMypyConfig(cwd) {
@@ -1979,7 +2044,7 @@ function detectExistingTooling(cwd, scripts) {
1979
2044
  existingFormatter: cwd ? hasExistingFormatter(cwd, scripts) : "format" in scripts,
1980
2045
  existingEslintConfig: eslintConfig,
1981
2046
  legacyEslint: eslintConfig?.startsWith(".eslintrc") ?? false,
1982
- existingRuffConfig: cwd ? hasExistingRuffConfig(cwd) : false,
2047
+ existingRuffConfig: cwd ? findExistingRuffConfig(cwd) : void 0,
1983
2048
  existingMypyConfig: cwd ? hasExistingMypyConfig(cwd) : false,
1984
2049
  existingImportLinterConfig: cwd ? hasExistingImportLinterConfig(cwd) : false,
1985
2050
  existingGolangciConfig: cwd ? findExistingGolangciConfig(cwd) : void 0
@@ -2035,4 +2100,4 @@ export {
2035
2100
  detectLanguages,
2036
2101
  createProjectContext
2037
2102
  };
2038
- //# sourceMappingURL=chunk-EGI2VVJ4.js.map
2103
+ //# sourceMappingURL=chunk-CBJLUXZ4.js.map