claude-code-ultimate-guide/examples/context-engineering/assembler.ts
Florian BRUNIAUX fe28f89574 feat(context): Context Engineering Configurator + consolidated guide (v3.34.0)
New: interactive configurator at cc.bruniaux.com/context/ that generates a
personalized CLAUDE.md starter kit based on team size, stack, and current setup.
Multi-step flow (profile, current state, stack, results) with maturity scoring
(Level 1-5), copy-to-clipboard artifacts, localStorage persistence.

Guide content:
- guide/core/context-engineering.md (1,188 lines, 8 sections): context budget,
  150-instruction ceiling, modular architecture, team assembly, ACE pipeline,
  quality measurement, context reduction techniques
- examples/context-engineering/ (10 templates): assembler.ts, profile-template.yaml,
  skeleton-template.md, canary-check.sh, ci-drift-check.yml, eval-questions.yaml,
  context-budget-calculator.sh, rules/knowledge-feeding.md, rules/update-loop-retro.md
- tools/context-audit-prompt.md (543 lines): 8-dimension scoring /100

Navigation: guide/README.md, machine-readable/reference.yaml (24 new entries)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 11:18:04 +01:00

243 lines
7.4 KiB
TypeScript

#!/usr/bin/env ts-node
/**
* Context Assembler
*
* Assembles CLAUDE.md from a developer profile YAML + shared module markdown files.
* Supports @import resolution, module exclusions, and per-developer overrides.
*
* Usage:
* ts-node assembler.ts --profile .claude/profiles/alice.yaml --output CLAUDE.md
* ts-node assembler.ts --profile .claude/profiles/alice.yaml --modules .claude/modules --output CLAUDE.md --dry-run
*
* Dependencies:
* npm install js-yaml @types/js-yaml ts-node typescript
*/
import fs from 'fs'
import path from 'path'
import yaml from 'js-yaml'
// ── Types ────────────────────────────────────────────────────────────────────
interface ProfileTools {
primary_lang: string
frontend?: string
backend?: string
database?: string
cloud?: string
test_framework?: string
}
interface ProfileStyle {
verbosity: 'verbose' | 'concise' | 'minimal'
comment_style: 'none' | 'inline' | 'jsdoc'
test_coverage: 'none' | 'optional' | 'required'
}
interface Profile {
profile: {
name: string
role: string
seniority: string
tools: ProfileTools
style: ProfileStyle
}
modules: {
include: string[]
exclude: string[]
}
overrides: {
custom_rules: string[]
}
}
interface AssemblyResult {
content: string
totalChars: number
estimatedTokens: number
modulesLoaded: number
modulesMissing: string[]
importsResolved: number
}
// ── Helpers ──────────────────────────────────────────────────────────────────
function loadProfile(profilePath: string): Profile {
if (!fs.existsSync(profilePath)) {
throw new Error(`Profile not found: ${profilePath}`)
}
const content = fs.readFileSync(profilePath, 'utf8')
return yaml.load(content) as Profile
}
function resolveImports(content: string, baseDir: string, depth = 0): string {
if (depth > 5) {
console.warn(' Warning: @import depth limit reached (5) — circular imports?')
return content
}
return content.replace(/^@(.+)$/gm, (_, importPath) => {
const fullPath = path.resolve(baseDir, importPath.trim())
if (!fs.existsSync(fullPath)) {
return `<!-- @import ${importPath} — FILE NOT FOUND -->`
}
const imported = fs.readFileSync(fullPath, 'utf8')
return resolveImports(imported, path.dirname(fullPath), depth + 1)
})
}
function loadModule(
modulePath: string,
modulesDir: string,
result: AssemblyResult
): string {
const fullPath = path.resolve(modulesDir, modulePath)
if (!fs.existsSync(fullPath)) {
console.warn(` Warning: module not found — ${fullPath}`)
result.modulesMissing.push(modulePath)
return ''
}
const raw = fs.readFileSync(fullPath, 'utf8')
const resolved = resolveImports(raw, path.dirname(fullPath))
const importsInFile = (raw.match(/^@.+$/gm) || []).length
result.importsResolved += importsInFile
result.modulesLoaded++
return resolved
}
function buildHeader(profile: Profile): string {
const { name, role, seniority, tools, style } = profile.profile
return [
`# CLAUDE.md`,
`# Assembled for: ${name} | Role: ${role} | Seniority: ${seniority}`,
`# Generated: ${new Date().toISOString().split('T')[0]}`,
`# Stack: ${tools.primary_lang}${tools.frontend !== 'none' ? ` + ${tools.frontend}` : ''}${tools.backend !== 'none' ? ` + ${tools.backend}` : ''}`,
`# Style: verbosity=${style.verbosity}, comments=${style.comment_style}, tests=${style.test_coverage}`,
'',
].join('\n')
}
function buildOverrides(rules: string[]): string {
if (rules.length === 0) return ''
return [
'\n## Personal Rules',
'',
...rules.map((r) => `- ${r}`),
'',
].join('\n')
}
// ── Core ─────────────────────────────────────────────────────────────────────
function assemble(
profilePath: string,
outputPath: string,
modulesDir: string,
dryRun: boolean
): AssemblyResult {
const profile = loadProfile(profilePath)
const result: AssemblyResult = {
content: '',
totalChars: 0,
estimatedTokens: 0,
modulesLoaded: 0,
modulesMissing: [],
importsResolved: 0,
}
const sections: string[] = [buildHeader(profile)]
for (const modulePath of profile.modules.include) {
if (profile.modules.exclude.includes(modulePath)) {
console.log(` Skipped (excluded): ${modulePath}`)
continue
}
const content = loadModule(modulePath, modulesDir, result)
if (content) {
sections.push(`\n<!-- ── Module: ${modulePath} ── -->\n`)
sections.push(content.trim())
}
}
const overrides = buildOverrides(profile.overrides.custom_rules)
if (overrides) sections.push(overrides)
result.content = sections.join('\n')
result.totalChars = result.content.length
result.estimatedTokens = Math.round(result.totalChars / 4)
if (!dryRun) {
fs.mkdirSync(path.dirname(path.resolve(outputPath)), { recursive: true })
fs.writeFileSync(outputPath, result.content)
}
return result
}
// ── CLI ───────────────────────────────────────────────────────────────────────
function parseArgs(): {
profilePath: string
outputPath: string
modulesDir: string
dryRun: boolean
} {
const args = process.argv.slice(2)
const get = (flag: string, fallback: string): string => {
const idx = args.indexOf(flag)
return idx !== -1 && args[idx + 1] ? args[idx + 1] : fallback
}
return {
profilePath: get('--profile', '.claude/profiles/default.yaml'),
outputPath: get('--output', 'CLAUDE.md'),
modulesDir: get('--modules', '.claude/modules'),
dryRun: args.includes('--dry-run'),
}
}
function main() {
const { profilePath, outputPath, modulesDir, dryRun } = parseArgs()
console.log('Context Assembler')
console.log('=================')
console.log(`Profile: ${profilePath}`)
console.log(`Modules: ${modulesDir}`)
console.log(`Output: ${outputPath}${dryRun ? ' (dry-run — not written)' : ''}`)
console.log('')
try {
const result = assemble(profilePath, outputPath, modulesDir, dryRun)
console.log('')
console.log('Summary:')
console.log(` Modules loaded: ${result.modulesLoaded}`)
console.log(` Modules missing: ${result.modulesMissing.length}`)
console.log(` @imports resolved: ${result.importsResolved}`)
console.log(` Output size: ${result.totalChars} chars (~${result.estimatedTokens} tokens)`)
if (result.modulesMissing.length > 0) {
console.log('')
console.log('Missing modules (create these files or remove from profile):')
for (const m of result.modulesMissing) {
console.log(` - ${m}`)
}
}
if (result.estimatedTokens > 10000) {
console.log('')
console.log(
`Warning: ~${result.estimatedTokens} tokens is on the heavy side (target <10K).`
)
console.log(' Consider splitting into per-task @imports instead of always-on.')
}
if (!dryRun) {
console.log('')
console.log(`Done: ${outputPath}`)
}
} catch (err) {
console.error(`Error: ${(err as Error).message}`)
process.exit(1)
}
}
main()