Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ concurrency:

jobs:
# ---------------------------------------------------------------------------
# Lint — ruff + mypy + banned-terms + prompt freshness
# Lint — ruff + mypy + 5 gates (prompt, gitleaks pin, scorecard schema, manifest schema, refs)
# Fast-fail target: ~30s signal on trivial errors
# ---------------------------------------------------------------------------
lint:
Expand Down Expand Up @@ -71,6 +71,10 @@ jobs:
shell: bash
run: python -m codeograph.manifest.schema_cli --check

- name: No unresolvable or private-workspace references
shell: bash
run: python scripts/check_no_workspace_refs.py

# ---------------------------------------------------------------------------
# Unit — pytest fast suite (excludes slow/external/eval markers)
# strategy.matrix.os: [ubuntu-latest] — extension hook for multi-OS (v1.1)
Expand Down
5 changes: 5 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ repos:
language: system
pass_filenames: false
always_run: true
- id: check-no-workspace-refs
name: No unresolvable or private-workspace references
entry: python scripts/check_no_workspace_refs.py
language: system
pass_filenames: true
33 changes: 30 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# Contributing

## Before you start

Codeograph is solo-maintained. v1 has shipped; v1.1 is in planning, and the roadmap is visible in
the [ADR index](docs/adr/README.md) — anything marked deferred there is scope that has been thought
about but not built.

**Please open an issue before investing effort in a change.** Not a formality: it is so we can agree
scope before you write anything. Some areas are settled and closed to outside proposals, some are
mid-design, and some have constraints that are not obvious from the code — the deterministic-versus-LLM
boundary in particular is load-bearing, and a change that blurs it will be declined however well it
is written.

What that means in practice:

| You want to | Start with |
|---|---|
| Report a bug, or ask whether an idea fits | An issue |
| Propose an architectural decision | An issue first, then the [ADR process](docs/adr/README.md) — which requires reserving a number before any ADR file exists |
| Fix something small and self-evident | A PR directly is fine |

The [ADR index](docs/adr/README.md) also lists the areas that do not accept outside proposals, with
the reason for each. Reading that first will save you time.

Response times are best-effort — this is not anyone's day job.

## Commits

**Conventional Commits.** Subject line: `<type>(<scope>): <summary>`.
Expand All @@ -10,16 +35,16 @@ Scopes follow the codebase layout: `parser`, `graph`, `input`, `analyzer`, `cli`
Every AI-assisted commit ends with two attribution trailers (no email required):

```
AI-assistant: <tool> v<version> (<provider-note>)
AI-assistant: <tool> (<provider-note>)
Model: <api-model-identifier>
```

Concrete forms by tool:

```
# Claude Code (Opus / Sonnet)
AI-assistant: Claude Code v0.2.6 (Claude Opus 4.1 via Anthropic)
Model: claude-opus-4-1-20250514
AI-assistant: Claude Code (Claude Opus 5 via Anthropic)
Model: claude-opus-5

# Google Antigravity (Gemini)
AI-assistant: Google Antigravity (Gemini 3.5 Flash via Google)
Expand Down Expand Up @@ -147,6 +172,8 @@ Modifying an existing prompt version *changes its hash*. The loader strictly ver

Architecture decisions land as ADRs under `docs/adr/`, numbered sequentially. Amendments to an existing ADR go in the same file under an "Amendment" heading with the date and rationale. Don't backfill ADRs to justify code — write the ADR first, then implement.

**Proposing a new ADR requires reserving its number first** — see [`docs/adr/README.md`](docs/adr/README.md), which is the single home for the ADR template, numbering rules, status lifecycle, and the reservation process. A PR that adds an ADR without a reserved number will be declined.

## Running tests

We use `pytest` for Python tests and Maven for Java tests.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,5 +191,6 @@ The run manifest (`manifest.json`) is written once at the terminal checkpoint, a
- [Running Codeograph](docs/running-codeograph.md) — pipeline stages, command order, and exact flag combinations for common scenarios
- [Model selection & cost](docs/model-selection-and-cost.md) — which model/provider to pick and what it costs
- [Architecture snapshot](docs/architecture.md) — what's wired today
- [Requirements](docs/requirements.md) — what the tool is built to do, marked v1 or v1.1
- [ADRs](docs/adr/) — design decisions and their rationale
- [Contributing](CONTRIBUTING.md) — commit conventions, branching, CI
201 changes: 199 additions & 2 deletions docs/adr/ADR-001-project-skeleton.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Scope: the `codeograph` Python package skeleton, its entry point, and the single
* Bundle A — `argparse` + manual 3-level YAML merge + `python-dotenv`
* Bundle B — `typer` + `pydantic-settings` (unified)
* Bundle C — `click` + manual 3-level YAML merge + `python-dotenv`
* Bundle D — `click` + `pydantic-settings` (unified) ← code sketches in [adr-001-examples.md](adr-001-examples.md)
* Bundle D — `click` + `pydantic-settings` (unified) ← code sketches in "Code sketches" below

## Decision Outcome

Expand Down Expand Up @@ -125,7 +125,7 @@ One typed `Settings` object, one click entry point.

## More Information

Code sketches for all three CLI frameworks and all three config bundles live in [adr-001-examples.md](adr-001-examples.md) (scratch workspace, not committed to the repo).
Code sketches for all three CLI frameworks and all three config bundles are in "Code sketches" at the end of this section.

Structural outcome — the two files this ADR commits the skeleton to:

Expand Down Expand Up @@ -188,6 +188,203 @@ References:
* `pydantic-settings` docs — https://docs.pydantic.dev/latest/concepts/pydantic_settings/
* `click` docs — https://click.palletsprojects.com/

### Code sketches

The sketches the options above were compared against. Same CLI surface in every example: source
path, `--target`, `--max-classes-per-domain`, `--config`, `--dry-run`.

> **Read these as of the decision date.** They are the material this ADR weighed, not current API.
> The provider set shown (`anthropic | ollama | bedrock`) was later revised by D-013-1 and D-013-7 —
> see ADR-013. Left unchanged so the comparison reads as it did when the choice was made.

#### CLI frameworks

`argparse` (stdlib):

```python
# codeograph/main.py
import argparse

def main():
parser = argparse.ArgumentParser(description="Analyse a Spring Boot codebase")
parser.add_argument("source", help="Path to source project")
parser.add_argument("--target", choices=["ts", "go"], default="ts")
parser.add_argument("--max-classes-per-domain", type=int, default=3)
parser.add_argument("--config", default="config.yaml")
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
run(args.source, args.target, args.max_classes_per_domain, args.dry_run)
```

`click`:

```python
# codeograph/main.py
import click

@click.command()
@click.argument("source")
@click.option("--target", type=click.Choice(["ts", "go"]), default="ts", show_default=True)
@click.option("--max-classes-per-domain", type=int, default=3, show_default=True)
@click.option("--config", default="config.yaml", show_default=True)
@click.option("--dry-run", is_flag=True, default=False)
def main(source, target, max_classes_per_domain, config, dry_run):
run(source, target, max_classes_per_domain, dry_run)
```

`typer`:

```python
# codeograph/main.py
import typer
from enum import Enum
from typing import Annotated

class Target(str, Enum):
ts = "ts"
go = "go"

app = typer.Typer()

@app.command()
def main(
source: Annotated[str, typer.Argument(help="Path to source project")],
target: Annotated[Target, typer.Option(help="Target language")] = Target.ts,
max_classes_per_domain: Annotated[int, typer.Option(help="Cap per domain, 0=unlimited")] = 3,
config: Annotated[str, typer.Option()] = "config.yaml",
dry_run: Annotated[bool, typer.Option("--dry-run")] = False,
):
run(source, target.value, max_classes_per_domain, dry_run)
```

#### Bundle A — `argparse` + 3-level YAML merge + `python-dotenv`

```
# .env (gitignored)
ANTHROPIC_API_KEY=sk-ant-...
LLM_PROVIDER=anthropic
```

```yaml
# config.yaml (committed — no secrets)
llm:
model: claude-sonnet-4-6
max_classes_per_domain: 3
```

```python
# codeograph/config.py
import os
import yaml
from dotenv import load_dotenv

load_dotenv() # reads .env into os.environ before anything else

DEFAULTS = {
"target": "ts",
"max_classes_per_domain": 3,
"llm": {
"provider": "anthropic",
"model": "claude-sonnet-4-6",
}
}

def load(yaml_path: str, cli_overrides: dict) -> dict:
cfg = dict(DEFAULTS)
if os.path.exists(yaml_path):
with open(yaml_path) as f:
cfg = deep_merge(cfg, yaml.safe_load(f) or {})
cfg.update({k: v for k, v in cli_overrides.items() if v is not None})
return cfg

# Secrets accessed at call-site:
# api_key = os.environ["ANTHROPIC_API_KEY"]
```

Three explicit layers, nothing implicit. To see where a value came from, you read the merge logic.

#### Bundle B — `typer` + `pydantic-settings` (unified)

```
# .env (gitignored)
ANTHROPIC_API_KEY=sk-ant-...
LLM_PROVIDER=anthropic
```

```yaml
# config.yaml (committed — no secrets)
llm_model: claude-sonnet-4-6
max_classes_per_domain: 3
```

```python
# codeograph/settings.py
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field

class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
yaml_file="config.yaml", # pydantic-settings 2.3+
# priority: init kwargs > env vars > .env > yaml > field defaults
)

# Secrets — from .env or real env only, never from config.yaml
anthropic_api_key: str = Field(default="")
ollama_base_url: str = Field(default="http://localhost:11434")

# Pipeline config — can come from any layer
llm_provider: str = Field(default="anthropic")
llm_model: str = Field(default="claude-sonnet-4-6")
max_classes_per_domain: int = Field(default=3)
target: str = Field(default="ts")
```

```python
# codeograph/main.py (typer CLI overrides settings)
@app.command()
def main(source: str, target: Target = Target.ts, ...):
settings = Settings(target=target.value) # CLI wins over everything
run(source, settings)
```

One object, typed and validated. CLI kwargs passed to the constructor override all lower layers.

#### Bundle C — `click` + 3-level YAML merge + `python-dotenv`

Same `config.py` and `.env` as Bundle A; the CLI layer is `click` instead of `argparse`. No
structural difference beyond decorator syntax.

#### Secrets handling — identical across all bundles

```gitignore
# Secrets — never committed
.env
.env.local
.env.*.local

# But DO commit the example template
# .env.example <-- not ignored
```

```
# .env.example (committed, placeholder values only)
ANTHROPIC_API_KEY=your-key-here
LLM_PROVIDER=anthropic # anthropic | ollama | bedrock
OLLAMA_BASE_URL=http://localhost:11434
AWS_PROFILE= # for Bedrock
```

Contributors clone, copy `.env.example` to `.env`, and fill in their own keys. The real `.env` is
never committed regardless of which bundle is chosen.

---

*Editorial note, 2026-08-05: these sketches were referenced from this ADR's first commit as a link
to a file that was never committed, so the link had always resolved to nothing. The content is
inlined here unchanged. No decision is altered — the material was always intended to be part of
this ADR's supporting detail.*

## Amendments

**2026-06-14 — Design-review pass (2 decisions + 1 spin-off).** A design review produced two decisions recorded here and one finding that spins off into a dedicated ADR. No prior locked fork is reversed. (Other findings — single-command-vs-group, the "no custom merge code" wording, config.yaml-absent behaviour, the CLI-kwarg→field contract, the stale `--dry-run` note — are description-level corrections handled as documentation, not decisions.)
Expand Down
6 changes: 4 additions & 2 deletions docs/adr/ADR-023-secret-scanning-with-gitleaks.md
Original file line number Diff line number Diff line change
Expand Up @@ -567,11 +567,13 @@ An admin bypass mechanism is explicitly deferred. The v1.1 trigger is "a real fa

The Fork 1 Decision Outcome and the pyproject.toml sketch both treated `.pre-commit-config.yaml` and the `pre-commit` Python framework as new introductions in DC5, including a `# NEW` annotation and an explicit "add `pre-commit` to dev deps" step.

**This framing is incorrect.** The `pre-commit` framework has been present since DC2. The existing `.pre-commit-config.yaml` already contains four hooks: the ADR-014 prompt-hash and gen-constants freshness gates, plus two NFR-1 banned-terms hooks. The `pre-commit` package is already in `[project.optional-dependencies] dev`.
**This framing is incorrect.** The `pre-commit` framework has been present since DC2. The existing `.pre-commit-config.yaml` already contains two hooks: the ADR-014 prompt-hash and gen-constants freshness gates. The `pre-commit` package is already in `[project.optional-dependencies] dev`.

**Corrected Fork 1 scope:** the DC5 action is to **add the gitleaks `repo` entry to the existing `.pre-commit-config.yaml`** — not to bootstrap the pre-commit framework. The Decision Outcome's "add `pre-commit` to dev deps" step and the `# NEW` annotation in the pyproject example are removed from the design intent; they were already satisfied before DC5.

**Cascade:** The DC5 M8 implementation plan must **not** overwrite the existing `.pre-commit-config.yaml`; it must append the gitleaks hook entry. Overwriting would clobber the four existing hooks. This is the learner's dev fix (DC5-04 in `dev-dc5.md`).
**Cascade:** The DC5 M8 implementation plan must **not** overwrite the existing `.pre-commit-config.yaml`; it must append the gitleaks hook entry. Overwriting would clobber the two existing hooks. This is the learner's dev fix (DC5-04).

> **Errata — 2026-08-05.** As first written, this amendment said the config held *four* hooks, counting two additional origin-hygiene hooks. That count was wrong on the date of the amendment: those two hooks had been moved out of the committed config on 2026-06-12, ten days earlier, and the config held two. The figures above are corrected to two. **The decision and its cascade are unaffected** — append the gitleaks entry, do not overwrite, do not bootstrap the framework — and Confirmation #2 was never affected, as it tests for the gitleaks entry specifically rather than counting entries.

**No change to Confirmation items** — Confirmation #2 ("`.pre-commit-config.yaml` exists at repo root with one entry whose `repo` field is `https://github.com/gitleaks/gitleaks`") remains correct; it tests for the gitleaks entry specifically, not for other entries' presence.

Expand Down
Loading
Loading