Skip to content

Commit ae1d969

Browse files
talionwarclaude
andcommitted
feat: v0.4.0 — manual sections, incremental mode, writeIfChanged
Port key features from StudyIA internal implementation: - Manual sections preservation (marker-based + split-point patterns) - Incremental mode via --incremental flag (git diff filtering) - writeIfChanged: only write files when content differs (zero-noise diffs) - Fix dead-code agent: add middleware.ts to framework exclusions - Fix convention-enforcer: skip generated files (Prisma client, next-env.d.ts) - 124 tests passing, README + CHANGELOG updated Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2fbad54 commit ae1d969

12 files changed

Lines changed: 482 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22

33
All notable changes to Fondamenta ArchCode are documented here.
44

5+
## [0.4.0] - 2026-03-12
6+
7+
### Added
8+
- **Manual sections preservation** — marker-based (`<!-- MANUAL-START:id -->`) and split-point (`## Manual Notes`) patterns survive `fondamenta analyze` regeneration
9+
- **Incremental mode**`--incremental` flag uses `git diff` to only analyze changed files
10+
- **writeIfChanged** — only writes files to disk when content actually differs (zero-noise git diffs)
11+
- **`--no-preserve-manual` flag** — disable manual section preservation
12+
13+
### Fixed
14+
- **Dead code agent**: added `middleware.ts` to Next.js framework file exclusions
15+
- **Convention enforcer**: skips generated files (Prisma client, `next-env.d.ts`)
16+
17+
### Changed
18+
- `fondamenta analyze` now uses `writeIfChanged` by default — unchanged files are not rewritten
19+
- Test suite expanded from 111 to 123 tests
20+
521
## [0.3.0] - 2026-02-24
622

723
### Added

README.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export default defineConfig({
128128
| Framework | Status |
129129
|-----------|--------|
130130
| Next.js App Router | Supported |
131-
| Next.js Pages Router | Planned |
131+
| Next.js Pages Router | Supported (basic) |
132132
| Nuxt 3 | Planned |
133133
| SvelteKit | Planned |
134134
| Remix | Planned |
@@ -162,7 +162,10 @@ Zero runtime dependencies after analysis — output is plain Markdown.
162162
| Command | Description |
163163
|---------|-------------|
164164
| `fondamenta analyze [path]` | Full codebase analysis → Markdown files |
165+
| `fondamenta analyze --incremental` | Only analyze git-changed files |
166+
| `fondamenta analyze --no-preserve-manual` | Skip manual section preservation |
165167
| `fondamenta agents [path]` | Run code health agents on the project graph |
168+
| `fondamenta agents --json` | Output findings as JSON (for CI/tools) |
166169
| `fondamenta diff [path]` | Show changes since last analysis |
167170
| `fondamenta watch [path]` | Watch mode — regenerate on file changes |
168171
| `fondamenta ai-context [path]` | Generate AI context files |
@@ -220,6 +223,38 @@ fondamenta ai-context --copilot # Generate .github/copilot-instructions.md
220223
fondamenta ai-context --all # All of the above
221224
```
222225

226+
## Manual Sections
227+
228+
Fondamenta preserves human-written content across regenerations. Add manual sections using markers:
229+
230+
```markdown
231+
<!-- MANUAL-START:my-notes -->
232+
Your custom notes here — they survive `fondamenta analyze`
233+
<!-- MANUAL-END:my-notes -->
234+
```
235+
236+
Or use the split-point pattern — everything after `## Manual Notes` is preserved:
237+
238+
```markdown
239+
## /dashboard
240+
(auto-generated content above)
241+
242+
## Manual Notes
243+
Your custom architecture notes here — preserved across regeneration.
244+
```
245+
246+
Disable with `--no-preserve-manual`.
247+
248+
## Incremental Mode
249+
250+
Only regenerate documentation for files changed since last commit:
251+
252+
```bash
253+
fondamenta analyze --incremental
254+
```
255+
256+
Uses `git diff` to detect changed `.ts`/`.tsx`/`.vue` files and skips unchanged files. Combined with `writeIfChanged` (always active), only files with actual content changes are written to disk — zero noise in git diffs.
257+
223258
## Automation
224259

225260
### Cron (recommended for servers)
@@ -259,7 +294,11 @@ git add .planning/
259294
- [x] AI context generation (`.cursorrules`, `CLAUDE.md`, copilot instructions)
260295
- [x] Code health agents (8 agents: dead code, circular deps, security, performance, etc.)
261296
- [x] Open Core licensing (3 free + 5 PRO)
262-
- [ ] GitHub Action
297+
- [x] Manual sections preservation (marker-based + split-point)
298+
- [x] Incremental mode (`--incremental` via git diff)
299+
- [x] writeIfChanged (zero-noise git diffs)
300+
- [x] Test suite (120+ tests, CI with GitHub Actions)
301+
- [ ] GitHub Action (marketplace)
263302
- [ ] Multi-framework support (Nuxt, SvelteKit, Remix)
264303
- [ ] Ed25519 license validation (upgrade from HMAC)
265304

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "fondamenta-archcode",
3-
"version": "0.3.0",
3+
"version": "0.4.0",
44
"description": "Zero-dependency codebase intelligence for AI agents. Static analysis → structured Markdown.",
55
"type": "module",
66
"bin": {
@@ -60,6 +60,6 @@
6060
"devDependencies": {
6161
"@types/node": "^22.0.0",
6262
"tsup": "^8.3.0",
63-
"vitest": "^2.1.0"
63+
"vitest": "^4.1.0"
6464
}
6565
}

src/agents/free/dead-code.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const deadCodeAgent: Agent = {
3535

3636
// Skip Next.js implicit files (auto-imported by App Router)
3737
const fileName = comp.filePath.split('/').pop() ?? '';
38-
if (/^(layout|loading|error|not-found|template|default|global-error)\.(tsx|ts)$/.test(fileName)) continue;
38+
if (/^(layout|loading|error|not-found|template|default|global-error|middleware)\.(tsx|ts)$/.test(fileName)) continue;
3939

4040
// Skip index/barrel files
4141
if (comp.filePath.endsWith('/index.ts') || comp.filePath.endsWith('/index.tsx')) continue;

src/agents/pro/convention-enforcer.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,20 @@ export const conventionEnforcerAgent: Agent = {
1010
run(graph: ProjectGraph, _config: FondamentaConfig): AgentFinding[] {
1111
const findings: AgentFinding[] = [];
1212

13+
// Generated files to skip (prisma client, next-env, etc.)
14+
const GENERATED_PATTERNS = [
15+
/\.prisma\/client/,
16+
/next-env\.d\.ts/,
17+
/\.generated\./,
18+
/\.g\.ts$/,
19+
/node_modules/,
20+
];
21+
1322
// 1. Component naming: files should match exported component name
1423
for (const comp of graph.components) {
24+
// Skip generated files
25+
if (GENERATED_PATTERNS.some(p => p.test(comp.filePath))) continue;
26+
1527
const fileName = comp.filePath.split('/').pop()?.replace(/\.\w+$/, '');
1628
if (!fileName) continue;
1729

src/analyzers/project-analyzer.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fg from 'fast-glob';
22
import { resolve, relative } from 'node:path';
33
import { readFile } from 'node:fs/promises';
44
import { existsSync } from 'node:fs';
5+
import { execSync } from 'node:child_process';
56
import ts from 'typescript';
67
import { parseTypeScriptFile, type ParsedFile } from './typescript-parser.js';
78
import { parseVueFile, type ParsedVueFile } from './vue-parser.js';
@@ -44,7 +45,16 @@ export async function analyzeProject(
4445
const compilerOptions = loadTsConfig(projectRoot);
4546

4647
// Discover files
47-
const files = await discoverFiles(projectRoot, config);
48+
let files = await discoverFiles(projectRoot, config);
49+
50+
// Incremental mode: filter to git-changed files only
51+
if (config.incremental) {
52+
const changedFiles = getGitChangedFiles(projectRoot);
53+
if (changedFiles.length > 0) {
54+
const changedSet = new Set(changedFiles.map(f => resolve(projectRoot, f)));
55+
files = files.filter(f => changedSet.has(f));
56+
}
57+
}
4858

4959
// Parse all files
5060
const parsedFiles = await parseAllFiles(files, projectRoot, compilerOptions);
@@ -407,3 +417,20 @@ function buildLibInfo(file: ParsedFile, usedBy: string[]): LibInfo {
407417
envVars: file.envVars,
408418
};
409419
}
420+
421+
function getGitChangedFiles(projectRoot: string): string[] {
422+
try {
423+
// Get files changed since last commit + uncommitted changes
424+
const output = execSync('git diff --name-only HEAD 2>/dev/null || git diff --name-only', {
425+
cwd: projectRoot,
426+
encoding: 'utf-8',
427+
timeout: 5000,
428+
});
429+
return output
430+
.trim()
431+
.split('\n')
432+
.filter((f: string) => f && (f.endsWith('.ts') || f.endsWith('.tsx') || f.endsWith('.vue')));
433+
} catch {
434+
return [];
435+
}
436+
}

src/cli.ts

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { resolve, basename } from 'node:path';
55
import { mkdir, writeFile, readFile } from 'node:fs/promises';
66
import { existsSync } from 'node:fs';
77
import { analyzeProject } from './analyzers/project-analyzer.js';
8+
import { writeIfChanged } from './utils/write-if-changed.js';
9+
import { extractManualSections, insertManualSections, extractManualTail } from './utils/manual-sections.js';
810
import {
911
generatePages,
1012
generateComponents,
@@ -26,7 +28,7 @@ import {
2628
generateAgentsReport,
2729
} from './agents/index.js';
2830

29-
const VERSION = '0.3.0';
31+
const VERSION = '0.4.0';
3032

3133
const program = new Command();
3234

@@ -44,6 +46,8 @@ program
4446
.option('-o, --output <dir>', 'Output directory', '.planning')
4547
.option('-f, --framework <name>', 'Force framework detection (nextjs-app, nextjs-pages, nuxt, sveltekit, remix)')
4648
.option('--no-schema', 'Skip ORM schema analysis')
49+
.option('--incremental', 'Only regenerate files affected by recent git changes')
50+
.option('--no-preserve-manual', 'Disable manual section preservation (enabled by default)')
4751
.option('-v, --verbose', 'Show detailed progress')
4852
.action(async (path: string, opts: Record<string, unknown>) => {
4953
const projectRoot = resolve(path);
@@ -77,6 +81,11 @@ program
7781
}
7882
}
7983

84+
// Incremental mode: use git diff to filter files
85+
if (opts.incremental) {
86+
config.incremental = true;
87+
}
88+
8089
// Analyze
8190
const spinnerAnalyze = ora(' Analyzing codebase...').start();
8291
const result = await analyzeProject(projectRoot, config);
@@ -101,17 +110,47 @@ program
101110

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

113+
const preserveManual = opts.preserveManual !== false;
104114
let filesWritten = 0;
115+
let filesSkipped = 0;
116+
105117
for (const gen of generators) {
106118
if (!gen.enabled) continue;
107-
const content = gen.fn();
108-
if (content) {
109-
const filePath = resolve(outputDir, gen.path);
110-
await writeFile(filePath, content, 'utf-8');
119+
let content = gen.fn();
120+
if (!content) continue;
121+
122+
const filePath = resolve(outputDir, gen.path);
123+
124+
// Preserve manual sections from existing file
125+
if (preserveManual) {
126+
try {
127+
const existing = await readFile(filePath, 'utf-8');
128+
const manualSections = extractManualSections(existing);
129+
if (manualSections.length > 0) {
130+
content = insertManualSections(content, manualSections);
131+
}
132+
// Also preserve split-point tail (content after "---\n\n## Manual Notes")
133+
const manualTail = extractManualTail(existing, '## Manual Notes');
134+
if (manualTail) {
135+
content = content.trimEnd() + '\n\n' + manualTail;
136+
}
137+
} catch {
138+
// File doesn't exist yet — nothing to preserve
139+
}
140+
}
141+
142+
// Write only if content changed
143+
const changed = await writeIfChanged(filePath, content);
144+
if (changed) {
111145
filesWritten++;
112146
if (verbose) {
113147
console.log(chalk.dim(` ✓ ${gen.path}`));
114148
}
149+
} else {
150+
filesSkipped++;
151+
if (verbose) {
152+
console.log(chalk.dim(` ○ ${gen.path} (unchanged)`));
153+
}
115154
}
116155
}
117156

@@ -125,7 +164,8 @@ program
125164
enums: graph.schema.enums.length,
126165
});
127166

128-
spinnerGen.succeed(` Generated ${chalk.cyan(String(filesWritten))} files → ${chalk.cyan(outputDir)}`);
167+
const skippedMsg = filesSkipped > 0 ? chalk.dim(` (${filesSkipped} unchanged)`) : '';
168+
spinnerGen.succeed(` Generated ${chalk.cyan(String(filesWritten))} files → ${chalk.cyan(outputDir)}${skippedMsg}`);
129169

130170
console.log('');
131171
console.log(chalk.green(' Done!'));
@@ -667,7 +707,7 @@ async function runGeneration(
667707
const content = gen.fn();
668708
if (content) {
669709
const filePath = resolve(outputDir, gen.path);
670-
await writeFile(filePath, content, 'utf-8');
710+
await writeIfChanged(filePath, content);
671711
}
672712
}
673713

src/types/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@ export interface FondamentaConfig {
214214
maxApiCallsPerPage?: number;
215215
};
216216
};
217+
preserveManual: boolean;
218+
incremental: boolean;
217219
}
218220

219221
export const DEFAULT_CONFIG: FondamentaConfig = {
@@ -247,4 +249,6 @@ export const DEFAULT_CONFIG: FondamentaConfig = {
247249
generateCursorRules: false,
248250
generateCopilotInstructions: false,
249251
},
252+
preserveManual: true,
253+
incremental: false,
250254
};

0 commit comments

Comments
 (0)