From c9d15912d37a374eb56304605eae7e5b4d5968fb Mon Sep 17 00:00:00 2001 From: Ganesh Viswanath Sambhu <68482658+gvsambhu@users.noreply.github.com> Date: Wed, 5 Aug 2026 18:55:41 +0530 Subject: [PATCH 01/10] docs(adr): add reservation process, status lifecycle, and numbering rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ADR process was only fully described in an uncommitted planning document, so a reader of the repo could not follow it. The status legend also advertised a private staging folder as the official drafting location, and the index pointed at an uncommitted tracker. Numbering is now claimed by adding a row to the index table before any ADR file exists. Because that table is a single file with one row per number, two people claiming the same number produce a merge conflict — uniqueness is serialised by merge rather than by convention. Also records that layer-based numbering closed at 021 and is allocation-order from 022 onward, which has been the de facto behaviour since; and names the areas that do not accept outside proposals, so nobody spends effort on a decision that was never open. AI-assistant: Claude Code (Claude Opus 5 via Anthropic) Model: claude-opus-5 --- docs/adr/README.md | 92 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 10 deletions(-) diff --git a/docs/adr/README.md b/docs/adr/README.md index ffea2d9..e7f3d7f 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -8,12 +8,15 @@ ADR numbers reflect **architectural layer** (input → graph → rendering → L ## Status legend -| Symbol | Meaning | -|---|---| -| ✅ | Locked — decision committed; changes require a new ADR or formal amendment | -| 📝 | Drafted — locked-decision form lives in `work/adr-drafts/`, awaiting promotion | -| ⬜ | Designed but not yet drafted | -| ⏸️ | Deferred — v1.1 or post-v1 scope; design not started | +| Symbol | `status:` | Meaning | +|---|---|---| +| ✅ | `accepted` | Decision committed; changes require a new ADR or a formal amendment | +| 📝 | `proposed` | File is in this directory, decision not yet accepted — open for review | +| ⬜ | — | Number **reserved**; no file yet | +| ⏸️ | — | Number reserved, deferred to v1.1 or later; design not started | + +The symbol in the index below and the `status:` field in each ADR's frontmatter describe the same +state. ⬜ and ⏸️ have no file, so they have no frontmatter — the reservation row is the whole record. --- @@ -100,8 +103,73 @@ For someone new to the project who wants to understand the design end-to-end, re --- +## Proposing a new ADR + +Three steps. Step 1 is a precondition — a PR that adds an ADR file without a reserved number will be +declined. + +### 1. Reserve the number + +Open a PR that adds a single row to the **Index by ADR number** table above: the next free number, +the title, `⬜`, and your GitHub handle as owner. Nothing else — no ADR file yet. + +The index is one file with one row per number, so two people claiming the same number produce a +merge conflict. That is the mechanism: reservation is serialised by merge, not by convention. + +A reservation with no PR activity for six months may be released and the number reassigned. + +### 2. Write the ADR + +Once the reservation is merged, add `ADR--.md` to this directory with +`status: proposed`, on a branch, via PR. Use the number you reserved. Follow the template and +conventions below; an ADR that doesn't meet them will be sent back rather than declined. + +Discussion happens in the PR. If the problem needs exploring before a decision is defensible, open +an issue first — that's the cheaper place for option debates than a PR thread. + +### 3. Acceptance + +Merge to `main` with `status: accepted`. A reviewer checks that the decision is genuinely +architectural (see "When to write a new ADR"), that at least two options were considered with real +tradeoffs, that the Confirmation section states how the decision can be verified in code, and that +no forward reference contradicts a locked ADR. + +### Areas that do not accept outside proposals + +These are maintainer-owned by design. Issues and discussion are welcome; ADR PRs will be declined. + +| Area | Why | +|---|---| +| ADR-003 — parsing strategy | Tool-selection call reserved to the maintainer | +| ADR-004 — complexity model | Every threshold must cite a published source | +| ADR-011 — Spring → Go idiom mapping | The mapping design is the project's core exercise | +| README "Point of view" section | Author's personal voice | + +--- + ## Conventions +### Numbering + +- Three digits, sequential from `001`. **Never reuse a number**, including for a withdrawn proposal. +- Numbers `001`–`021` were allocated by architectural layer (input → graph → rendering → LLM → + quality → cross-cutting). That scheme is closed: from `022` onward numbers are allocated in + reservation order. The recommended reading order above carries the layer grouping instead. +- One decision per file. If a file covers two decisions, split it into two numbers. + +### Status lifecycle + +``` +reserved (index row only) → proposed (file, status: proposed) → accepted + ↘ withdrawn (number retired, not reused) +accepted → deprecated + → superseded by ADR-NNN +``` + +An accepted ADR is immutable, with exactly two exceptions: its `status:` field, and append-only +`## Amendments` sections. Never edit the original decision text to reflect a later change — that +destroys the record of what was decided when, which is the entire point of an ADR. + ### When to write a new ADR Write a new ADR when: @@ -145,11 +213,15 @@ Each ADR follows MADR with these sections: --- -## How ADR design rounds work +## Why the numbers don't match the design order -ADRs are designed in **Rounds** (R1–R10), grouped by what gets locked in the same design session and sequenced by which Dev Chunk consumes them. The round sequence is **not** the same as the ADR number sequence — see `work/PROJECT-TRACKER.md` for the round-to-Dev-Chunk timeline. +ADRs 001–021 were designed in batches, grouped by which decisions had to lock together, and +numbered by architectural layer rather than by the order they were written. So the number sequence +is neither the design sequence nor the runtime sequence — which is why the reading order above is +grouped by topic instead. -The round that drafted each ADR is recorded in PROJECT-TRACKER; it isn't part of the ADR itself because the design-time ordering is operational metadata, not architectural metadata. The ADR itself only carries what's needed to understand and confirm the decision. +The design-time ordering is operational metadata, not architectural metadata, so it isn't recorded +in the ADRs themselves. Each ADR carries only what's needed to understand and confirm its decision. --- @@ -160,7 +232,7 @@ Some ADRs introduce concepts referenced across many others: - **Canonical-form sha256** — defined in ADR-006 amendment; consumed by ADR-007 (goldens), ADR-015 (cache key), ADR-022 (manifest integrity). - **`--ast-only` mode** — declared in ADR-007 §"Pipeline orchestration constraint"; honored by ADR-013 §"Decision Outcome". - **`CallContext` (purpose, prompt_id, prompt_version, prompt_content_hash, corpus_id)** — defined in ADR-013 Fork 3; consumed by ADR-015 cache key and telemetry record. -- **Banned terms list** — operational discipline documented in `CONTRIBUTING.md`; no ADR enforces it (manual review baseline, not feature scope). +- **Banned terms** — a maintainer-side review discipline, not a repo-enforced rule and not owned by any ADR. --- From 1b653674e274bdb1426bed6fcf8ab10d41c90c97 Mon Sep 17 00:00:00 2001 From: Ganesh Viswanath Sambhu <68482658+gvsambhu@users.noreply.github.com> Date: Wed, 5 Aug 2026 18:55:54 +0530 Subject: [PATCH 02/10] docs(adr): correct hook count in the D-023-1 amendment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The amendment stated the pre-commit config held four hooks. That was wrong on the date it was written: two of them had been moved out of the committed config ten days earlier, leaving two. The design pass was working from a stale picture. Corrected in place with a dated errata note rather than a further amendment, because this is an error of fact rather than a change of decision — and a silent edit to a dated amendment would read as history rewriting. The decision, its cascade, and Confirmation #2 are unaffected: append the gitleaks entry, do not overwrite, do not bootstrap the framework. AI-assistant: Claude Code (Claude Opus 5 via Anthropic) Model: claude-opus-5 --- docs/adr/ADR-023-secret-scanning-with-gitleaks.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/adr/ADR-023-secret-scanning-with-gitleaks.md b/docs/adr/ADR-023-secret-scanning-with-gitleaks.md index c14b9c5..d8c12e2 100644 --- a/docs/adr/ADR-023-secret-scanning-with-gitleaks.md +++ b/docs/adr/ADR-023-secret-scanning-with-gitleaks.md @@ -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. From 39131e5e69702a7ba39942177cdfdc7d0d9602e7 Mon Sep 17 00:00:00 2001 From: Ganesh Viswanath Sambhu <68482658+gvsambhu@users.noreply.github.com> Date: Wed, 5 Aug 2026 18:56:05 +0530 Subject: [PATCH 03/10] docs: simplify AI trailer format and point ADR guidance at docs/adr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two corrections to conventions this file records. The attribution trailer no longer carries the harness version. It is an implementation detail of the tool rather than useful provenance — the Model line already answers which intelligence produced the commit — and in practice the version was often unobtainable from inside a session, which invited exactly the guessing the convention forbids. Commits already carrying a version stay as they are; history is not rewritten. The ADR section now points at docs/adr/README.md instead of restating the template. That file already defined the project template more accurately, and two copies of a spec is the drift failure this project keeps hitting. AI-assistant: Claude Code (Claude Opus 5 via Anthropic) Model: claude-opus-5 --- CONTRIBUTING.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 817b48d..c4832bb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ Scopes follow the codebase layout: `parser`, `graph`, `input`, `analyzer`, `cli` Every AI-assisted commit ends with two attribution trailers (no email required): ``` -AI-assistant: v () +AI-assistant: () Model: ``` @@ -18,8 +18,8 @@ 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) @@ -147,6 +147,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. From c42e79120aa09a8b62ee83c699cb04c903e69070 Mon Sep 17 00:00:00 2001 From: Ganesh Viswanath Sambhu <68482658+gvsambhu@users.noreply.github.com> Date: Wed, 5 Aug 2026 18:56:19 +0530 Subject: [PATCH 04/10] test(manifest): drop work-folder paths from package docstring The docstring cited two files in an uncommitted scratch folder to explain an authoring convention. A reader of the repo cannot resolve either path, and the convention itself is local to the maintainer rather than something a contributor needs. The docstring now describes what the tests do. AI-assistant: Claude Code (Claude Opus 5 via Anthropic) Model: claude-opus-5 --- tests/unit/manifest/__init__.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/unit/manifest/__init__.py b/tests/unit/manifest/__init__.py index db4c705..9733f8b 100644 --- a/tests/unit/manifest/__init__.py +++ b/tests/unit/manifest/__init__.py @@ -1,9 +1,5 @@ """Tests for the manifest package (schema, IO, run_id, assembler, CLI gate). -Per the project convention in ``work/guidelines/04-dev-chunk-implementation.md`` -and ``AGENTS.md`` (boilerplate-OK zones for AI generation), the **test -scaffolding** in this package is AI-generated; the **assertion bodies** are -learner-write (the DC5 M12 spec at ``work/dev/DEV-CHUNK-5-KICKOFF.md`` -§"M12" says so explicitly). Each test method has a clear ``# TODO(learner):`` -marker in its body pointing at the ADR-025 Confirmation item it implements. +Each test method carries a ``# TODO`` marker naming the ADR-025 Confirmation +item it implements, so every assertion traces back to the decision it verifies. """ From 8c93684f6b6e22a4135bac9979e3ab765ccaa446 Mon Sep 17 00:00:00 2001 From: Ganesh Viswanath Sambhu <68482658+gvsambhu@users.noreply.github.com> Date: Thu, 6 Aug 2026 10:21:21 +0530 Subject: [PATCH 05/10] ci: correct the lint job comment to match its actual steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comment advertised a banned-terms step. The job has six steps and none of them is that; it was never added, or was removed without the comment following. It also omitted three of the four freshness gates that are there. A comment describing checks that do not run is worse than no comment — it reads as coverage the pipeline does not have. AI-assistant: Claude Code (Claude Opus 5 via Anthropic) Model: claude-opus-5 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1012f14..2f767de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ concurrency: jobs: # --------------------------------------------------------------------------- - # Lint — ruff + mypy + banned-terms + prompt freshness + # Lint — ruff + mypy + 4 freshness gates (prompt, gitleaks pin, scorecard schema, manifest schema) # Fast-fail target: ~30s signal on trivial errors # --------------------------------------------------------------------------- lint: From 576cbe38e23f45e4c11e5f1690b8ea0ff53fddff Mon Sep 17 00:00:00 2001 From: Ganesh Viswanath Sambhu <68482658+gvsambhu@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:03:09 +0530 Subject: [PATCH 06/10] docs: inline ADR-001 code sketches and gate unresolvable references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-001 linked its code sketches as [adr-001-examples.md](adr-001-examples.md). That file was never committed at any path, so the link had rendered as a clickable 404 since ADR-001's first commit — in the first ADR the reading order sends people to. Line 128 even annotated the caveat while keeping the link. The sketches are inlined verbatim under More Information. No decision changes; this content was always meant to be part of the ADR's supporting detail, and a note records that the provider set shown predates D-013-1 and D-013-7 so the comparison still reads as it did when the choice was made. The gate is the point. A path written in this repo must resolve for someone who has only cloned this repo, and two classes break that: links whose target does not exist, and prose references to the private sibling directories work/, plan/ and archive/. Both have happened repeatedly, so it is now enforced rather than remembered — a pre-commit hook for fast feedback and a CI lint step as the gate that cannot be skipped. The check found a second live defect on its first run: docs/manifest-versions.md linked ADR-017-eval-framework.md, which is named ADR-017-evaluation-framework.md. Also fixed here. AI-assistant: Claude Code (Claude Opus 5 via Anthropic) Model: claude-opus-5 --- .github/workflows/ci.yml | 6 +- .pre-commit-config.yaml | 5 + docs/adr/ADR-001-project-skeleton.md | 201 ++++++++++++++++++++++++++- docs/manifest-versions.md | 2 +- scripts/check_no_workspace_refs.py | 140 +++++++++++++++++++ 5 files changed, 350 insertions(+), 4 deletions(-) create mode 100644 scripts/check_no_workspace_refs.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f767de..3758d52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ concurrency: jobs: # --------------------------------------------------------------------------- - # Lint — ruff + mypy + 4 freshness gates (prompt, gitleaks pin, scorecard schema, manifest schema) + # Lint — ruff + mypy + 5 gates (prompt, gitleaks pin, scorecard schema, manifest schema, refs) # Fast-fail target: ~30s signal on trivial errors # --------------------------------------------------------------------------- lint: @@ -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) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1350ddb..5d56cdb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/docs/adr/ADR-001-project-skeleton.md b/docs/adr/ADR-001-project-skeleton.md index f0b7b64..9101e9d 100644 --- a/docs/adr/ADR-001-project-skeleton.md +++ b/docs/adr/ADR-001-project-skeleton.md @@ -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 @@ -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: @@ -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.) diff --git a/docs/manifest-versions.md b/docs/manifest-versions.md index 09ff006..9881dbf 100644 --- a/docs/manifest-versions.md +++ b/docs/manifest-versions.md @@ -64,5 +64,5 @@ bump date. Per ADR-022 Fork 7, every bump requires: (the load-bearing design ADR; Forks 1 + 2 + 7 govern this log) - [ADR-006 — Knowledge Graph Schema](adr/ADR-006-knowledge-graph-schema.md) (the base manifest shape; canonical-form sha256 substrate) -- [ADR-017 — Eval Framework](adr/ADR-017-eval-framework.md) +- [ADR-017 — Eval Framework](adr/ADR-017-evaluation-framework.md) (consuming ADR for the `scorecards` + `compile_checks` pointer collections and the `source_path` / `corpus_id` / `run_id` scalars) diff --git a/scripts/check_no_workspace_refs.py b/scripts/check_no_workspace_refs.py new file mode 100644 index 0000000..32f3721 --- /dev/null +++ b/scripts/check_no_workspace_refs.py @@ -0,0 +1,140 @@ +"""Fail if a committed file points at something a reader cannot open. + +Two checks, both guarding the same promise: a path written in this repository +must resolve for someone who has only cloned this repository. + +1. **Broken relative links.** A markdown link whose target does not exist in the + repo. This is the general case — the target may be a bare filename, a + relative path, or a path into a private sibling directory; all of them render + as clickable and all of them 404. + +2. **Private workspace paths.** Prose or code references to ``work/``, ``plan/`` + or ``archive/`` — sibling directories of the repo root that are not committed. + These are usually not written as links, so check 1 does not see them. + +Both have happened, repeatedly, so this is a gate rather than a convention. It +runs over staged files in pre-commit and over the whole tree in CI. + +Usage:: + + python scripts/check_no_workspace_refs.py # whole tree (CI) + python scripts/check_no_workspace_refs.py FILE... # named files (pre-commit) +""" + +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent + +# Markdown inline links and images: [text](target) / ![alt](target). +LINK = re.compile(r"!?\[[^\]]*\]\(([^)\s]+)(?:\s+\"[^\"]*\")?\)") + +# A private-directory reference: the name followed by a separator. Guarded on the +# left so "framework/", "workflows/", "network/" do not match, and on the right so +# bare prose ("the work we did") does not either. +WORKSPACE_PATH = re.compile( + r"(? list[Path]: + out = subprocess.run( + ["git", "ls-files"], cwd=REPO_ROOT, capture_output=True, text=True, check=True + ) + return [REPO_ROOT / line for line in out.stdout.splitlines() if line] + + +def should_scan(path: Path) -> bool: + try: + rel = path.relative_to(REPO_ROOT).as_posix() + except ValueError: + return False + if rel in EXEMPT_PATHS: + return False + return path.suffix.lower() in TEXT_SUFFIXES + + +def check_links(path: Path, text: str) -> list[tuple[int, str, str]]: + """Markdown links whose target does not exist in the repo.""" + if path.suffix.lower() != ".md": + return [] + hits = [] + for lineno, line in enumerate(text.splitlines(), start=1): + for match in LINK.finditer(line): + target = match.group(1) + if EXTERNAL_SCHEME.match(target): + continue + target = target.split("#", 1)[0] # drop any anchor + if not target: + continue + resolved = (path.parent / target).resolve() + if not resolved.exists(): + hits.append((lineno, f"link target does not exist: {target}", line.strip())) + return hits + + +def check_workspace_paths(text: str) -> list[tuple[int, str, str]]: + """References to private sibling directories.""" + hits = [] + for lineno, line in enumerate(text.splitlines(), start=1): + if any(s in line for s in EXEMPT_SUBSTRINGS): + continue + match = WORKSPACE_PATH.search(line) + if match: + hits.append((lineno, f"private workspace path: {match.group(0)}", line.strip())) + return hits + + +def main(argv: list[str]) -> int: + candidates = [Path(a).resolve() for a in argv] if argv else tracked_files() + + failures: list[tuple[str, int, str, str]] = [] + for path in candidates: + if not path.is_file() or not should_scan(path): + continue + try: + text = path.read_text(encoding="utf-8", errors="replace") + except OSError: + continue + rel = path.relative_to(REPO_ROOT).as_posix() + for lineno, reason, line in check_links(path, text) + check_workspace_paths(text): + failures.append((rel, lineno, reason, line)) + + if not failures: + return 0 + + print("Committed files must not point at things a reader cannot open.\n") + for rel, lineno, reason, line in sorted(failures): + print(f" {rel}:{lineno} — {reason}") + print(f" {line[:120]}") + print( + "\nA path written here must resolve for someone who has only cloned this\n" + "repository. `work/`, `plan/` and `archive/` are not part of it. Inline the\n" + "content, describe it without a path, or drop the reference." + ) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) From 9db2b5b6080704b093d24385dd032e02c62360ec Mon Sep 17 00:00:00 2001 From: Ganesh Viswanath Sambhu <68482658+gvsambhu@users.noreply.github.com> Date: Thu, 6 Aug 2026 23:36:44 +0530 Subject: [PATCH 07/10] docs: publish the requirements baseline The 36 functional and non-functional requirements the ADRs are written against existed only in an uncommitted planning document, so no reader of this repo could resolve a single FR reference. The ADRs cite 21 of them. Traceability now runs one way. Requirements state what the system must do; ADRs state how, and cite the requirement they satisfy. The requirement text names no ADR. Those citations had accumulated because design decisions were cascaded back into the requirements and left their provenance inline, which inverted the dependency and left references resolving nowhere public. Three requirements were rewritten rather than copied, because they were wrong: * FR-2 said the knowledge graph is extracted by LLM analysis, which FR-10 explicitly denies and the README point-of-view contradicts. It predates the parsing-strategy decision. * FR-3 said scaffold, the word deliberately replaced by source everywhere else, including the repository description. * NFR-4 claimed CI runs both TypeScript and Go evaluation paths. Go is v1.1. Every requirement now carries a v1 or v1.1 marker, so paths and flags named by unbuilt requirements read as declared scope rather than as broken references. A revision history records the changes made during design review and closes at this commit; from here the git history of the file is the record. It notes that FR-22, NFR-2 and NFR-5 moved from v1 to v1.1, each with a narrower capability shipped in their place. AI-assistant: Claude Code (Claude Opus 5 via Anthropic) Model: claude-opus-5 --- README.md | 1 + docs/requirements.md | 153 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 docs/requirements.md diff --git a/README.md b/README.md index 7d3165b..1d577fa 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/requirements.md b/docs/requirements.md new file mode 100644 index 0000000..741d70c --- /dev/null +++ b/docs/requirements.md @@ -0,0 +1,153 @@ +# Requirements + +The functional and non-functional requirements codeograph is built against. Architecture decisions +that implement them are recorded as ADRs under [`docs/adr/`](adr/README.md), and each ADR cites the +requirements it satisfies — so traceability runs from a decision back to its requirement, not the +other way round. Requirements here state *what* the system must do; they do not name the designs +that chose *how*. + +**Release markers.** Every requirement carries `[v1]` or `[v1.1]`. + +| Marker | Meaning | +|---|---| +| `[v1]` | Shipped in v1 | +| `[v1.1]` | Planned for v1.1 — paths and flags named in these requirements do not exist yet | + +Identifiers are stable and are never reused. A retired requirement keeps its number so existing +references continue to resolve. + +--- + +## Functional + +### Core pipeline + +- **FR-1** `[v1]` — Accept any Java/Spring Boot project directory as input (Gradle or Maven). +- **FR-2** `[v1]` — Extract a structured knowledge graph (JSON) from the source, independent of any + render target. Structure is extracted deterministically (FR-10); semantic enrichment is layered on + top of it (FR-11). +- **FR-3** `[v1]` — Render the knowledge graph into idiomatic TypeScript/NestJS source. +- **FR-4** `[v1.1]` — Render the knowledge graph into idiomatic Go source — same graph, different + target. +- **FR-5** `[v1]` — CLI accepts a `--target` flag to select the renderer. +- **FR-6** `[v1]` — All renderers implement a common `Renderer` interface; no target-specific code in + the core pipeline. +- **FR-7** `[v1]` — Run code-quality evaluations per target (compile, coverage, LLM judge) and + produce target-specific scorecards. +- **FR-7a** `[v1]` — Run graph-quality evaluations — structural completeness, relationship + correctness, schema validity, internal consistency, semantic accuracy, reproducibility, and + golden-graph agreement — and produce a graph scorecard. +- **FR-7b** `[v1]` — Maintain documented JSON Schema files for the knowledge graph under + `codeograph/schema/*.schema.json` as the language-neutral source of truth. Every emitted graph + validates against them: enforced at write time through the generated Pydantic models, and + re-checked by the FR-7a schema-validity evaluation. +- **FR-7c** `[v1]` — The deterministic graph (`graph.json`) is regression-tested against committed + golden files under `tests/golden//` for the project's test corpora — a hand-built + edge-case fixture and a pinned real Spring project. CI diffs every run byte-equal in canonical + form. Golden refreshes are deliberate (`--update-goldens`), and each refresh records its diff + category: update-golden, fix-bug, or intentional-change. The committed example run (FR-9) ships + its `graph.json` as illustrative output, not as a byte-equal regression golden. +- **FR-8** `[v1]` — Support provider switching by environment variable. The v1 provider set is + `anthropic | openrouter | openai_compatible`; `ollama` and `bedrock` are `[v1.1]`. The + `openai_compatible` provider reaches any API-key, OpenAI-compatible endpoint through a + configurable base URL, with no coded vendor allowlist; `openrouter` is its OpenRouter-URL preset. + All providers accept any model id as free-form pass-through — no project-blessed allowlist. +- **FR-9** `[v1]` — Include at least one complete example run per target, with committed output and + evaluation scorecards. +- **FR-10** `[v1]` — Structural and syntactic facts — classes, methods, annotations, imports, + dependencies — are extracted deterministically from the source, not by an LLM. +- **FR-11** `[v1]` — Semantic facts — service intent, domain boundaries, migration hints — are + extracted by an LLM operating on the deterministic output. +- **FR-12** `[v1]` — The complexity scorecard uses named industry-standard metrics with numeric + thresholds. No subjective high/medium/low buckets. +- **FR-13** `[v1]` — CLI accepts `--max-classes-per-domain N` (default 3) to cap rendering output + per domain, bounding token spend and keeping runs reproducible. A value of `0` disables the cap. +- **FR-14** `[v1]` — When the cap is reached, rendering selects classes deterministically, ordered by + a documented rule, so runs are reproducible. The knowledge graph always covers every class — only + rendering is capped. + +### Quality infrastructure + +- **FR-15** `[v1]` — All prompts live as versioned files under `codeograph/prompts/`; scorecards + record the prompt filenames and versions used. +- **FR-16** `[v1]` — Every LLM call emits telemetry: `run_id`, provider, model, prompt file, prompt + version, input and output tokens, cost estimate, latency, and timestamp. Per-call records are + written to a sidecar JSONL; aggregate statistics roll into the run manifest. `run_id` is the + correlation key joining manifest, logs, and telemetry. +- **FR-17** `[v1]` — LLM responses are cached content-addressed on provider, model, prompt version, + rendered prompt, and input hash. Cache hits are recorded in telemetry. +- **FR-18** `[v1]` — Every scorecard records exact model version strings, not family names. +- **FR-19** `[v1]` — A `pytest` suite at `tests/` covers unit and integration tests. CI blocks merge + on failure; unit-test coverage target is 80% or above. +- **FR-20** `[v1]` — Every tool invocation creates a run manifest and structured logs under + `runs//`. +- **FR-21** `[v1]` — Secret scanning runs in CI on every push and pull request; merges are blocked on + detection. +- **FR-27** `[v1]` — Output-path safety: no writes outside the output directory, no silent overwrite + of a non-empty output directory, and `--force` is required to overwrite. + +### Quality hardening + +- **FR-22** `[v1.1]` — CLI supports cost estimation before a run (`--dry-run`) and a hard spend + ceiling (`--max-cost-usd`). *v1 ships an interim mechanism instead: call and token ceilings with a + confirmation gate (`--max-llm-calls`, `--max-tokens-total`, `--llm-call-confirm-threshold`).* +- **FR-23** `[v1.1]` — Snapshot tests lock byte-stable rendered output for a fixed input and cached + responses; unexplained drift fails CI. +- **FR-24** `[v1.1]` — Negative tests assert graceful failure with specific error messages for at + least six malformed-input classes. The baseline classes: + 1. **Zip bomb** — extracted size exceeds the configured cap; abort with a size-limit error before + extraction completes. + 2. **Zip path traversal** — an archive entry resolves outside the extraction root; reject the + archive. + 3. **Corrupt or malformed archive** — input is not a readable archive; return a clear error. + 4. **Acquisition failure** — the source cannot be fetched, or the URL is unreachable or + credential-gated; return an actionable error naming the cause. + 5. **Empty corpus** — no `.java` files remain in scope after discovery; fail loudly with a + user-readable message and a non-zero exit, never an empty-graph pass-through. + 6. **No recognised build file** — neither a Maven nor a Gradle build file is present; reject the + corpus with a clear error rather than recording an unknown build system. +- **FR-25** `[v1.1]` — An LLM-judge calibration suite discriminates known-good from known-bad output + at documented thresholds, committed as a calibration record. +- **FR-26** `[v1.1]` — Prompt-injection mitigation: source is wrapped in data tags with explicit + instruction guards, escaped for prompt-breaking sequences, and validated by a negative test. +- **FR-28** `[v1.1]` — A determinism contract documents per-field determinism classification for the + graph schema. + +--- + +## Non-functional + +- **NFR-1** — **Retired.** No longer tracked as a requirement. The number is retained so existing + references resolve. +- **NFR-2** `[v1]` — A defined token-utilization strategy governs LLM input construction; v1 ships + prefix caching. `[v1.1]` A token and cost scorecard surfaces spend per run. +- **NFR-3** `[v1]` — Evaluation results are reproducible: seed, model version, and prompt version are + tracked per target. +- **NFR-4** `[v1]` — CI runs lint, unit tests, and the TypeScript evaluation path on every push. + `[v1.1]` The Go evaluation path lands with the Go renderer. +- **NFR-5** `[v1]` — Adding a new target language requires no changes to the analyzer or the core + pipeline — only a new renderer, a compile-check entry, and renderer-authoring documentation. v1 + ships a short "adding a renderer" section in `CONTRIBUTING.md`. `[v1.1]` The full renderer guide + lands with the second renderer, which is what makes the pattern real. + +--- + +## Revision history + +These requirements were first drafted before implementation began, and were revised during design +review as decisions sharpened or rescoped them. **This section closes at this document's first +commit** — from that point, the git history of this file is the record of change. + +| Date | Requirement | Change | Driver | +|---|---|---|---| +| 2026-06-14 | FR-24 | "6+ malformed-input classes" enumerated as six named classes with required failure behaviours | design review; ADR-002 | +| 2026-06-19 | FR-8 | Provider set replaced — `anthropic \| ollama \| bedrock` became `anthropic \| openrouter \| openai_compatible`; `ollama` and `bedrock` moved to v1.1 | design review; ADR-013 | +| 2026-06-19 | FR-16 | `run_id` added to the telemetry payload as the manifest-to-logs correlation key; per-call records routed to a sidecar JSONL with aggregates in the run manifest | design review; ADR-015, ADR-022 | +| 2026-06-19 | FR-22 | Deferred to v1.1. v1 ships call and token ceilings with a confirmation gate in place of cost estimation and a spend ceiling | design review; ADR-027 | +| 2026-06-19 | NFR-2 | Token and cost scorecard deferred to v1.1. v1 commits graph scorecards and ships prefix caching | design review | +| 2026-06-19 | NFR-5 | Full renderer guide deferred to v1.1; v1 ships a short `CONTRIBUTING.md` section. The guide waits on a second renderer to make the pattern real | design review; ADR-008 | +| 2026-06-29 | FR-8 | Generalised — the OpenAI-compatible provider accepts any base URL, with no vendor allowlist | design review; ADR-013 | +| 2026-08-05 | NFR-1 | Retired. Number retained so existing references resolve | — | + +Three requirements moved from v1 to v1.1 during design review — FR-22, NFR-2 and NFR-5. In each case +v1 ships a narrower capability that meets the immediate need, and the fuller form is scheduled. From 5d2dea9b3db329c13339bfe2957c74bc6c7af465 Mon Sep 17 00:00:00 2001 From: Ganesh Viswanath Sambhu <68482658+gvsambhu@users.noreply.github.com> Date: Fri, 7 Aug 2026 00:50:31 +0530 Subject: [PATCH 08/10] docs: correct FR-20 and FR-27 against the shipped behaviour FR-20 named a directory that does not exist. It claimed the run manifest and structured logs are written under runs//. They are written into the output directory: /manifest.json and /logs.jsonl, per logging_config and the README. There is no runs/ directory in the repo or at runtime. This was wrong in the original requirement and survived the extraction because that pass checked requirements against each other and against the point-of-view section, but did not verify the paths they name against the code. Every other path claimed by a v1 requirement was checked and does exist. FR-27 is confirmed v1 and restated precisely. All three clauses hold, but the first one holds by a different mechanism than the wording implied: the guard rejects an output directory that is the working directory or an ancestor of it, rather than constraining writes to a fixed path. With FR-20 corrected, the claim that all artefacts land inside the output directory is now accurate. No revision-history entry: that section closes at this document's first commit, and the git history is the record from there. AI-assistant: Claude Code (Claude Opus 5 via Anthropic) Model: claude-opus-5 --- docs/requirements.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/requirements.md b/docs/requirements.md index 741d70c..a463aa9 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -79,12 +79,13 @@ references continue to resolve. - **FR-18** `[v1]` — Every scorecard records exact model version strings, not family names. - **FR-19** `[v1]` — A `pytest` suite at `tests/` covers unit and integration tests. CI blocks merge on failure; unit-test coverage target is 80% or above. -- **FR-20** `[v1]` — Every tool invocation creates a run manifest and structured logs under - `runs//`. +- **FR-20** `[v1]` — Every tool invocation writes a run manifest (`manifest.json`) and structured + logs (`logs.jsonl`) into the output directory, alongside the run's other artefacts. - **FR-21** `[v1]` — Secret scanning runs in CI on every push and pull request; merges are blocked on detection. -- **FR-27** `[v1]` — Output-path safety: no writes outside the output directory, no silent overwrite - of a non-empty output directory, and `--force` is required to overwrite. +- **FR-27** `[v1]` — Output-path safety: all run artefacts are written inside the output directory; + an output directory that is the working directory, or an ancestor of it, is rejected; a non-empty + output directory is never silently overwritten — `--force` is required. ### Quality hardening From 4127fedbf67bbef9cdb375e839893ce8ebf0e910 Mon Sep 17 00:00:00 2001 From: Ganesh Viswanath Sambhu <68482658+gvsambhu@users.noreply.github.com> Date: Fri, 7 Aug 2026 01:10:20 +0530 Subject: [PATCH 09/10] style(scripts): apply ruff format to the reference gate The new script was committed unformatted and would have red-failed the CI lint job on ruff format --check. AI-assistant: Claude Code (Claude Opus 5 via Anthropic) Model: claude-opus-5 --- scripts/check_no_workspace_refs.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/scripts/check_no_workspace_refs.py b/scripts/check_no_workspace_refs.py index 32f3721..1da6a97 100644 --- a/scripts/check_no_workspace_refs.py +++ b/scripts/check_no_workspace_refs.py @@ -36,9 +36,7 @@ # A private-directory reference: the name followed by a separator. Guarded on the # left so "framework/", "workflows/", "network/" do not match, and on the right so # bare prose ("the work we did") does not either. -WORKSPACE_PATH = re.compile( - r"(? list[Path]: - out = subprocess.run( - ["git", "ls-files"], cwd=REPO_ROOT, capture_output=True, text=True, check=True - ) + out = subprocess.run(["git", "ls-files"], cwd=REPO_ROOT, capture_output=True, text=True, check=True) return [REPO_ROOT / line for line in out.stdout.splitlines() if line] From 502dabc52b46b76b22ee27424750e2e7e74e1cc5 Mon Sep 17 00:00:00 2001 From: Ganesh Viswanath Sambhu <68482658+gvsambhu@users.noreply.github.com> Date: Fri, 7 Aug 2026 01:16:34 +0530 Subject: [PATCH 10/10] docs: state maintainer status and how to engage CONTRIBUTING opened straight into commit conventions. A reader arriving with an idea had no way to learn that the project is solo-maintained, that some areas are closed to outside proposals, or that architectural changes go through a reservation step before any file is written. They would find that out by having work declined. The section asks for an issue before effort is invested, and says why: scope agreement, not process. It layers with the ADR flow rather than competing with it, since docs/adr/README.md already states that a PR adding an ADR without a reserved number will be declined. CONTRIBUTING is the outer gate; that file is what happens once scope is agreed. Framing is deliberate. Solo-maintained with a stated way in reads as a maintainer with standards; an absence of contributors reads as neglect. The facts are identical, and this project holds that line everywhere else. AI-assistant: Claude Code (Claude Opus 5 via Anthropic) Model: claude-opus-5 --- CONTRIBUTING.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c4832bb..9b34d1d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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: `(): `.