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>
This commit is contained in:
parent
318ba915de
commit
fe28f89574
19 changed files with 3545 additions and 15 deletions
243
examples/context-engineering/assembler.ts
Normal file
243
examples/context-engineering/assembler.ts
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
#!/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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue