Skip to content

Commit 45765e0

Browse files
talionwarclaude
andcommitted
feat: v0.7.0 — bidirectional backporting from StudyIA custom codebase
Schema parser: @@index, @@unique multi-field parsing, onDelete/fkFields/references extraction, cascadeCount/setNullCount stats, Orphan Risk Analysis section. Overrides system: configurable .overrides.json for human-curated annotations (purpose, risk, description) shown in all generator outputs. Route classification: configurable A/B/C grouping via routeClassification config with prefix-based mappings and classification summary. Lib subsystems: configurable libClassification for subsystem grouping with inferPurpose() for automatic purpose detection from filenames. All new features are opt-in via fondamenta.config.ts — zero breaking changes for existing consumers. Output unchanged without config. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 39f2ba9 commit 45765e0

12 files changed

Lines changed: 330 additions & 36 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "fondamenta-archcode",
3-
"version": "0.6.0",
3+
"version": "0.7.0",
44
"description": "Zero-dependency codebase intelligence for AI agents. Static analysis → structured Markdown.",
55
"type": "module",
66
"bin": {

src/cli.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from './generators/index.js';
1919
import { generateClaudeMd, generateCursorRules, generateCopilotInstructions } from './generators/ai-context-generator.js';
2020
import { saveState, computeDiff, loadState } from './utils/state.js';
21+
import { loadOverrides } from './utils/overrides.js';
2122
import { DEFAULT_CONFIG, type FondamentaConfig } from './types/index.js';
2223
import {
2324
ALL_AGENTS,
@@ -100,10 +101,12 @@ program
100101
const spinnerGen = ora(' Generating documentation...').start();
101102

102103
const projectName = await getProjectName(projectRoot);
104+
const overrides = loadOverrides(projectRoot, config.overrides);
103105
const ctx = {
104106
graph: result.graph,
105107
projectName,
106108
generatedAt: new Date().toISOString().split('T')[0],
109+
...(Object.keys(overrides).length > 0 && { overrides }),
107110
};
108111

109112
await mkdir(resolve(outputDir, 'dependencies'), { recursive: true });
@@ -406,10 +409,12 @@ program
406409
spinnerAnalyze.succeed(` Analyzed ${chalk.cyan(String(result.totalFiles))} files`);
407410

408411
const projectName = await getProjectName(projectRoot);
412+
const aiOverrides = loadOverrides(projectRoot, config.overrides);
409413
const ctx = {
410414
graph: result.graph,
411415
projectName,
412416
generatedAt: new Date().toISOString().split('T')[0],
417+
...(Object.keys(aiOverrides).length > 0 && { overrides: aiOverrides }),
413418
};
414419

415420
let generated = 0;
@@ -673,8 +678,8 @@ function getGenerators(
673678
return [
674679
{ name: 'pages', fn: () => generatePages(ctx), path: 'dependencies/pages-atomic.md', enabled: config.generators.pages },
675680
{ name: 'components', fn: () => generateComponents(ctx), path: 'dependencies/components-atomic.md', enabled: config.generators.components },
676-
{ name: 'api-routes', fn: () => generateApiRoutes(ctx), path: 'dependencies/api-routes-atomic.md', enabled: config.generators.apiRoutes },
677-
{ name: 'lib', fn: () => generateLib(ctx), path: 'dependencies/lib-atomic.md', enabled: config.generators.lib },
681+
{ name: 'api-routes', fn: () => generateApiRoutes(ctx, config), path: 'dependencies/api-routes-atomic.md', enabled: config.generators.apiRoutes },
682+
{ name: 'lib', fn: () => generateLib(ctx, config), path: 'dependencies/lib-atomic.md', enabled: config.generators.lib },
678683
{ name: 'schema', fn: () => generateSchema(ctx), path: 'dependencies/schema-crossref-atomic.md', enabled: config.generators.schemaXref },
679684
{ name: 'component-graph', fn: () => generateComponentGraph(ctx), path: 'dependencies/component-graph.md', enabled: config.generators.componentGraph },
680685
{ name: 'dependency-map', fn: () => generateDependencyMap(ctx, framework), path: 'DEPENDENCY-MAP.md', enabled: config.generators.dependencyMap },
@@ -689,10 +694,12 @@ async function runGeneration(
689694
result: import('./analyzers/project-analyzer.js').AnalysisResult,
690695
runAgentsFlag?: boolean,
691696
) {
697+
const overrides = loadOverrides(projectRoot, config.overrides);
692698
const ctx = {
693699
graph: result.graph,
694700
projectName,
695701
generatedAt: new Date().toISOString().split('T')[0],
702+
...(Object.keys(overrides).length > 0 && { overrides }),
696703
};
697704

698705
const generators = getGenerators(ctx, config, result.framework);

src/generators/api-routes-generator.ts

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
import type { ProjectGraph, ApiRouteInfo } from '../types/index.js';
2-
import { header, section, bullet, bulletList, table, anchor, tocEntry, type GeneratorContext } from './base.js';
1+
import type { ApiRouteInfo, FondamentaConfig } from '../types/index.js';
2+
import { header, section, bullet, bulletList, table, anchor, tocEntry, overrideNote, type GeneratorContext } from './base.js';
33

4-
export function generateApiRoutes(ctx: GeneratorContext): string {
4+
export function generateApiRoutes(ctx: GeneratorContext, config?: FondamentaConfig): string {
55
const { graph } = ctx;
66
const routes = graph.apiRoutes;
77

88
if (routes.length === 0) return '';
99

10-
// Group by top-level path
11-
const groups = groupByPath(routes);
10+
// Group by classification (if configured) or by top-level path
11+
const classification = config?.routeClassification;
12+
const groups = classification
13+
? groupByClassification(routes, classification.mappings, classification.defaultGroup)
14+
: groupByPath(routes);
1215
const groupNames = Object.keys(groups).sort();
1316

1417
let output = header('API Routes — Atomic Analysis', ctx, routes.length, 'routes');
@@ -23,6 +26,16 @@ export function generateApiRoutes(ctx: GeneratorContext): string {
2326
}
2427
output += '\n---\n\n';
2528

29+
// Classification summary (if configured)
30+
if (classification) {
31+
output += `${section(2, 'Classification Summary')}\n\n`;
32+
output += table(
33+
['Group', 'Routes'],
34+
groupNames.map((g) => [g, String(groups[g].length)]),
35+
);
36+
output += '\n\n';
37+
}
38+
2639
// Summary table
2740
output += `${section(2, 'Summary')}\n\n`;
2841
const summaryRows = routes.map((r) => [
@@ -40,7 +53,7 @@ export function generateApiRoutes(ctx: GeneratorContext): string {
4053
output += `${section(2, `${groupIndex}. ${name}`)}\n\n`;
4154

4255
for (const route of groups[name]) {
43-
output += generateRouteEntry(route);
56+
output += generateRouteEntry(route, ctx);
4457
}
4558

4659
groupIndex++;
@@ -65,7 +78,37 @@ function groupByPath(routes: ApiRouteInfo[]): Record<string, ApiRouteInfo[]> {
6578
return groups;
6679
}
6780

68-
function generateRouteEntry(route: ApiRouteInfo): string {
81+
function groupByClassification(
82+
routes: ApiRouteInfo[],
83+
mappings: Record<string, [string, string]>,
84+
defaultGroup?: string,
85+
): Record<string, ApiRouteInfo[]> {
86+
const groups: Record<string, ApiRouteInfo[]> = {};
87+
// Sort prefixes longest-first for correct matching
88+
const prefixes = Object.keys(mappings).sort((a, b) => b.length - a.length);
89+
90+
for (const route of routes) {
91+
const routePath = route.routePath.replace(/^\/api\//, '');
92+
let groupLabel = defaultGroup ?? 'Other';
93+
94+
for (const prefix of prefixes) {
95+
if (routePath.startsWith(prefix)) {
96+
const [group, subgroup] = mappings[prefix];
97+
groupLabel = `${group}${subgroup}`;
98+
route.group = group;
99+
route.subgroup = subgroup;
100+
break;
101+
}
102+
}
103+
104+
if (!groups[groupLabel]) groups[groupLabel] = [];
105+
groups[groupLabel].push(route);
106+
}
107+
108+
return groups;
109+
}
110+
111+
function generateRouteEntry(route: ApiRouteInfo, ctx: GeneratorContext): string {
69112
let output = `${section(3, `\`${route.routePath}\``)}\n\n`;
70113

71114
output += `${bullet('File', `\`${route.filePath}\``)}\n`;
@@ -80,6 +123,8 @@ function generateRouteEntry(route: ApiRouteInfo): string {
80123
output += `${bulletList('Side Effects', route.sideEffects)}\n`;
81124
}
82125

126+
output += overrideNote(ctx, route.filePath);
127+
83128
output += '\n';
84129
return output;
85130
}

src/generators/base.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type { ProjectGraph } from '../types/index.js';
2+
import type { Overrides } from '../utils/overrides.js';
23

34
export interface GeneratorContext {
45
graph: ProjectGraph;
56
projectName: string;
67
generatedAt: string;
8+
overrides?: Overrides;
79
}
810

911
export function header(title: string, ctx: GeneratorContext, count: number, description: string): string {
@@ -55,3 +57,15 @@ export function table(headers: string[], rows: string[][]): string {
5557
const dataRows = rows.map((r) => `| ${r.join(' | ')} |`);
5658
return [headerRow, separator, ...dataRows].join('\n');
5759
}
60+
61+
export function overrideNote(ctx: GeneratorContext, filePath: string): string {
62+
if (!ctx.overrides) return '';
63+
const entry = ctx.overrides[filePath];
64+
if (!entry) return '';
65+
const desc = entry.description || entry.purpose || entry.notes;
66+
const risk = entry.risk;
67+
let note = '';
68+
if (desc) note += `- **Override:** ${desc}\n`;
69+
if (risk) note += `- **Risk:** ${risk}\n`;
70+
return note;
71+
}

src/generators/components-generator.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { ProjectGraph, ComponentInfo } from '../types/index.js';
2-
import { header, section, bullet, bulletList, anchor, tocEntry, type GeneratorContext } from './base.js';
1+
import type { ComponentInfo } from '../types/index.js';
2+
import { header, section, bullet, bulletList, anchor, tocEntry, overrideNote, type GeneratorContext } from './base.js';
33

44
export function generateComponents(ctx: GeneratorContext): string {
55
const { graph } = ctx;
@@ -40,7 +40,7 @@ export function generateComponents(ctx: GeneratorContext): string {
4040
if (hooks.length > 0) {
4141
output += `${section(2, 'Hooks')}\n\n`;
4242
for (const hook of hooks) {
43-
output += generateComponentEntry(hook);
43+
output += generateComponentEntry(hook, ctx);
4444
}
4545
}
4646

@@ -50,7 +50,7 @@ export function generateComponents(ctx: GeneratorContext): string {
5050
output += `${section(2, `${groupIndex}. ${name}`)}\n\n`;
5151

5252
for (const comp of groups[name]) {
53-
output += generateComponentEntry(comp);
53+
output += generateComponentEntry(comp, ctx);
5454
}
5555

5656
groupIndex++;
@@ -80,7 +80,7 @@ function groupByDirectory(components: ComponentInfo[]): Record<string, Component
8080
return groups;
8181
}
8282

83-
function generateComponentEntry(comp: ComponentInfo): string {
83+
function generateComponentEntry(comp: ComponentInfo, ctx: GeneratorContext): string {
8484
let output = `${section(3, `\`${comp.name}\``)}\n\n`;
8585

8686
output += `${bullet('File', `\`${comp.filePath}\``)}\n`;
@@ -118,6 +118,8 @@ function generateComponentEntry(comp: ComponentInfo): string {
118118
output += `${bulletList('Used By', comp.usedBy.map((u) => `\`${u}\``))}\n`;
119119
}
120120

121+
output += overrideNote(ctx, comp.filePath);
122+
121123
output += '\n';
122124
return output;
123125
}

src/generators/lib-generator.ts

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
import type { ProjectGraph, LibInfo } from '../types/index.js';
2-
import { header, section, bullet, bulletList, anchor, tocEntry, type GeneratorContext } from './base.js';
1+
import type { LibInfo, FondamentaConfig } from '../types/index.js';
2+
import { header, section, bullet, bulletList, anchor, tocEntry, overrideNote, type GeneratorContext } from './base.js';
33

4-
export function generateLib(ctx: GeneratorContext): string {
4+
export function generateLib(ctx: GeneratorContext, config?: FondamentaConfig): string {
55
const { graph } = ctx;
66
const libs = graph.libs;
77

88
if (libs.length === 0) return '';
99

10-
// Group by directory
11-
const groups = groupByDirectory(libs);
10+
// Group by subsystem classification (if configured) or by directory
11+
const libClassification = config?.libClassification;
12+
const groups = libClassification
13+
? groupByClassification(libs, libClassification)
14+
: groupByDirectory(libs);
1215
const groupNames = Object.keys(groups).sort();
1316

1417
let output = header('Lib / Utils — Atomic Analysis', ctx, libs.length, 'files');
@@ -29,7 +32,7 @@ export function generateLib(ctx: GeneratorContext): string {
2932
output += `${section(2, `${groupIndex}. ${name}`)}\n\n`;
3033

3134
for (const lib of groups[name]) {
32-
output += generateLibEntry(lib);
35+
output += generateLibEntry(lib, ctx);
3336
}
3437

3538
groupIndex++;
@@ -51,12 +54,54 @@ function groupByDirectory(libs: LibInfo[]): Record<string, LibInfo[]> {
5154
return groups;
5255
}
5356

54-
function generateLibEntry(lib: LibInfo): string {
57+
function groupByClassification(
58+
libs: LibInfo[],
59+
classification: Record<string, string>,
60+
): Record<string, LibInfo[]> {
61+
const groups: Record<string, LibInfo[]> = {};
62+
// Sort prefixes longest-first for correct matching
63+
const prefixes = Object.keys(classification).sort((a, b) => b.length - a.length);
64+
65+
for (const lib of libs) {
66+
let groupName = 'Other';
67+
for (const prefix of prefixes) {
68+
if (lib.filePath.startsWith(prefix)) {
69+
groupName = classification[prefix];
70+
break;
71+
}
72+
}
73+
if (!groups[groupName]) groups[groupName] = [];
74+
groups[groupName].push(lib);
75+
}
76+
77+
return groups;
78+
}
79+
80+
function inferPurpose(lib: LibInfo): string {
81+
const fileName = lib.filePath.split('/').pop()?.replace(/\.\w+$/, '') ?? '';
82+
if (fileName.includes('service')) return `${fileName.replace(/-service$/, '')} service`;
83+
if (fileName.includes('client')) return `${fileName.replace(/-client$/, '')} client`;
84+
if (fileName.includes('parser')) return `${fileName.replace(/-parser$/, '')} parser`;
85+
if (fileName.includes('handler')) return `${fileName.replace(/-handler$/, '')} handler`;
86+
if (fileName.includes('utils')) return `${fileName.replace(/-utils$/, '')} utilities`;
87+
if (lib.exports.length > 0) {
88+
return `Exports: ${lib.exports.slice(0, 3).map((e) => e.name).join(', ')}`;
89+
}
90+
return '';
91+
}
92+
93+
function generateLibEntry(lib: LibInfo, ctx: GeneratorContext): string {
5594
const name = lib.filePath.split('/').pop()?.replace(/\.\w+$/, '') ?? lib.filePath;
5695
let output = `${section(3, `\`${name}\``)}\n\n`;
5796

5897
output += `${bullet('File', `\`${lib.filePath}\``)}\n`;
5998

99+
// Purpose (inferred from filename)
100+
const purpose = inferPurpose(lib);
101+
if (purpose) {
102+
output += `${bullet('Purpose', purpose)}\n`;
103+
}
104+
60105
// Exports
61106
if (lib.exports.length > 0) {
62107
const exportLines = lib.exports.map((e) => {
@@ -93,6 +138,8 @@ function generateLibEntry(lib: LibInfo): string {
93138
output += `${bulletList('Side Effects', lib.sideEffects)}\n`;
94139
}
95140

141+
output += overrideNote(ctx, lib.filePath);
142+
96143
output += '\n';
97144
return output;
98145
}

src/generators/pages-generator.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { ProjectGraph, PageInfo } from '../types/index.js';
2-
import { header, section, bullet, bulletList, anchor, tocEntry, type GeneratorContext } from './base.js';
1+
import type { PageInfo } from '../types/index.js';
2+
import { header, section, bullet, bulletList, anchor, tocEntry, overrideNote, type GeneratorContext } from './base.js';
33

44
export function generatePages(ctx: GeneratorContext): string {
55
const { graph } = ctx;
@@ -29,7 +29,7 @@ export function generatePages(ctx: GeneratorContext): string {
2929
output += `${section(2, `${groupIndex}. ${groupName}`)}\n\n`;
3030

3131
for (const page of groups[groupName]) {
32-
output += generatePageEntry(page);
32+
output += generatePageEntry(page, ctx);
3333
output += '\n';
3434
}
3535

@@ -52,7 +52,7 @@ function groupByRoute(pages: PageInfo[]): Record<string, PageInfo[]> {
5252
return groups;
5353
}
5454

55-
function generatePageEntry(page: PageInfo): string {
55+
function generatePageEntry(page: PageInfo, ctx: GeneratorContext): string {
5656
let output = `${section(3, `\`${page.routePath || '/'}\``)}\n\n`;
5757

5858
output += `${bullet('File', `\`${page.filePath}\``)}\n`;
@@ -94,6 +94,8 @@ function generatePageEntry(page: PageInfo): string {
9494
output += `${bullet('i18n', `\`${page.i18nNamespace}\``)}\n`;
9595
}
9696

97+
output += overrideNote(ctx, page.filePath);
98+
9799
output += '\n';
98100
return output;
99101
}

0 commit comments

Comments
 (0)