diff --git a/.claude/rules/.gitkeep b/.claude/rules/.gitkeep
new file mode 100644
index 0000000..06b90a1
--- /dev/null
+++ b/.claude/rules/.gitkeep
@@ -0,0 +1,2 @@
+# Presence of this directory is the opt-in that keeps the root CLAUDE.md an index
+# rather than a merged block. Kept so an empty rules set cannot silently change the layout.
diff --git a/.claude/rules/se-deversity-skill3-llm-AnthropicChatModel.md b/.claude/rules/se-deversity-skill3-llm-AnthropicChatModel.md
new file mode 100644
index 0000000..8763165
--- /dev/null
+++ b/.claude/rules/se-deversity-skill3-llm-AnthropicChatModel.md
@@ -0,0 +1,11 @@
+---
+paths: ["**/AnthropicChatModel.java"]
+---
+
+
+# Rules for AnthropicChatModel
+
+## Security-Critical Code
+- **Rule**: This code is security-critical. Do not weaken security properties. Every change must be explicitly reviewed for security impact.
+- **Aspect**: Anthropic API credential handling and hosted-provider network egress
+
diff --git a/.claude/rules/se-deversity-skill3-llm-ChatModel.md b/.claude/rules/se-deversity-skill3-llm-ChatModel.md
new file mode 100644
index 0000000..95e117c
--- /dev/null
+++ b/.claude/rules/se-deversity-skill3-llm-ChatModel.md
@@ -0,0 +1,11 @@
+---
+paths: ["**/ChatModel.java"]
+---
+
+
+# Rules for ChatModel
+
+## Contract-Frozen Signature
+- **Constraint**: You may change internal logic, but MUST NOT modify the method name, parameters, return type, or checked exceptions.
+- **Reason**: The single seam every model-driven stage binds to — QueryPlanner, Synthesizer, Verifier and the self-correction Reviser all take this one interface, which is what lets one --llm-provider choice apply uniformly. Test fakes implement it directly, so changing the signature breaks every unit test that avoids a live model.
+
diff --git a/.claude/rules/se-deversity-skill3-llm-LlmProviderFactory.md b/.claude/rules/se-deversity-skill3-llm-LlmProviderFactory.md
new file mode 100644
index 0000000..1d69b8a
--- /dev/null
+++ b/.claude/rules/se-deversity-skill3-llm-LlmProviderFactory.md
@@ -0,0 +1,11 @@
+---
+paths: ["**/LlmProviderFactory.java"]
+---
+
+
+# Rules for LlmProviderFactory
+
+## Security-Critical Code
+- **Rule**: This code is security-critical. Do not weaken security properties. Every change must be explicitly reviewed for security impact.
+- **Aspect**: LLM provider credential resolution and model selection
+
diff --git a/.claude/rules/se-deversity-skill3-llm-LocalLlmClient.md b/.claude/rules/se-deversity-skill3-llm-LocalLlmClient.md
new file mode 100644
index 0000000..d38c68b
--- /dev/null
+++ b/.claude/rules/se-deversity-skill3-llm-LocalLlmClient.md
@@ -0,0 +1,15 @@
+---
+paths: ["**/LocalLlmClient.java"]
+---
+
+
+# Rules for LocalLlmClient
+
+### Rules for field apiKey
+- **Rule**: Never log or expose runtime values of this element.
+- **Reason**: LLM provider API key — never log, echo, or include in errors/fixtures
+
+## Security-Critical Code
+- **Rule**: This code is security-critical. Do not weaken security properties. Every change must be explicitly reviewed for security impact.
+- **Aspect**: outbound LLM-provider credential (Bearer token) handling
+
diff --git a/.claude/rules/se-deversity-skill3-llm-NameSanitizer.md b/.claude/rules/se-deversity-skill3-llm-NameSanitizer.md
new file mode 100644
index 0000000..47983c8
--- /dev/null
+++ b/.claude/rules/se-deversity-skill3-llm-NameSanitizer.md
@@ -0,0 +1,14 @@
+---
+paths: ["**/NameSanitizer.java"]
+---
+
+
+# Rules for NameSanitizer
+
+## Security-Critical Code
+- **Rule**: This code is security-critical. Do not weaken security properties. Every change must be explicitly reviewed for security impact.
+- **Aspect**: output sanitization: reserved-word stripping must never be weakened
+
+### Rules for method sanitize
+- **Rule**: Must remain a pure function. Forbid state modifications and side effects.
+
diff --git a/.claude/rules/se-deversity-skill3-llm-SkillMdPostProcessor.md b/.claude/rules/se-deversity-skill3-llm-SkillMdPostProcessor.md
new file mode 100644
index 0000000..e45a281
--- /dev/null
+++ b/.claude/rules/se-deversity-skill3-llm-SkillMdPostProcessor.md
@@ -0,0 +1,15 @@
+---
+paths: ["**/SkillMdPostProcessor.java"]
+---
+
+
+# Rules for SkillMdPostProcessor
+
+## Core Functionality
+- **Sensitivity**: High
+- **Note**: Deterministically guarantees SKILL.md spec compliance; model output is never trusted. Changes risk emitting invalid frontmatter — keep the parsing and frontmatter synthesis covered by SkillMdPostProcessorTest.
+
+### Rules for method render
+- **Rule**: This operation is idempotent. Calling it multiple times must produce the same result as calling it once.
+- **Reason**: SelfCorrectionLoop re-runs render() on its own output, so a revised draft passes through repeatedly. Every guarantee here must converge: exactly one frontmatter block and exactly one provenance footer, no matter how many revision rounds ran.
+
diff --git a/.claude/rules/se-deversity-skill3-llm-Verifier.md b/.claude/rules/se-deversity-skill3-llm-Verifier.md
new file mode 100644
index 0000000..cc7b1f6
--- /dev/null
+++ b/.claude/rules/se-deversity-skill3-llm-Verifier.md
@@ -0,0 +1,11 @@
+---
+paths: ["**/Verifier.java"]
+---
+
+
+# Rules for Verifier
+
+## Core Functionality
+- **Sensitivity**: High
+- **Note**: Accuracy gate that re-grounds claims against the sources. Only worthwhile with a capable model — a weak model rewrites rather than grounds. Keep the prompt strict about supported-claims-only and announced-vs-shipped.
+
diff --git a/.claude/rules/se-deversity-skill3-model-ContextBundle.md b/.claude/rules/se-deversity-skill3-model-ContextBundle.md
new file mode 100644
index 0000000..bd454b7
--- /dev/null
+++ b/.claude/rules/se-deversity-skill3-model-ContextBundle.md
@@ -0,0 +1,11 @@
+---
+paths: ["**/ContextBundle.java"]
+---
+
+
+# Rules for ContextBundle
+
+## Immutable Type
+- **Rule**: This type is immutable. Never introduce non-final fields, setters, or mutating methods.
+- **Note**: Immutable record; the sources list is defensively copied in the compact constructor.
+
diff --git a/.claude/rules/se-deversity-skill3-model-RunManifest.md b/.claude/rules/se-deversity-skill3-model-RunManifest.md
new file mode 100644
index 0000000..4279753
--- /dev/null
+++ b/.claude/rules/se-deversity-skill3-model-RunManifest.md
@@ -0,0 +1,10 @@
+---
+paths: ["**/RunManifest.java"]
+---
+
+
+# Rules for RunManifest
+
+## Schema & Serialization Safety
+- **Rule**: Prohibit altering data formats, fields, database columns, or serialization structures without explicit backward-compatible migration paths.
+
diff --git a/.claude/rules/se-deversity-skill3-model-Source.md b/.claude/rules/se-deversity-skill3-model-Source.md
new file mode 100644
index 0000000..09ff7c3
--- /dev/null
+++ b/.claude/rules/se-deversity-skill3-model-Source.md
@@ -0,0 +1,14 @@
+---
+paths: ["**/Source.java"]
+---
+
+
+# Rules for Source
+
+## Architectural Boundary Constraints
+- **Layer**: model
+- **Prohibited References**: se.deversity.skill3.pipeline, se.deversity.skill3.llm, se.deversity.skill3.cli, se.deversity.skill3.skillspector, se.deversity.skill3.web, se.deversity.skill3.net
+
+## Domain Model Boundary
+- **Purity**: Framework-free DDD Entity.
+
diff --git a/.claude/rules/se-deversity-skill3-pipeline-BraveSearchClient.md b/.claude/rules/se-deversity-skill3-pipeline-BraveSearchClient.md
new file mode 100644
index 0000000..88cc581
--- /dev/null
+++ b/.claude/rules/se-deversity-skill3-pipeline-BraveSearchClient.md
@@ -0,0 +1,15 @@
+---
+paths: ["**/BraveSearchClient.java"]
+---
+
+
+# Rules for BraveSearchClient
+
+### Rules for field apiKey
+- **Rule**: Never log or expose runtime values of this element.
+- **Reason**: Brave Search subscription token — never log, echo, or include in errors/fixtures
+
+## Security-Critical Code
+- **Rule**: This code is security-critical. Do not weaken security properties. Every change must be explicitly reviewed for security impact.
+- **Aspect**: external-API credential handling and the only network egress with a secret token
+
diff --git a/.claude/rules/se-deversity-skill3-pipeline-CutoffResolver.md b/.claude/rules/se-deversity-skill3-pipeline-CutoffResolver.md
new file mode 100644
index 0000000..290ec38
--- /dev/null
+++ b/.claude/rules/se-deversity-skill3-pipeline-CutoffResolver.md
@@ -0,0 +1,11 @@
+---
+paths: ["**/CutoffResolver.java"]
+---
+
+
+# Rules for CutoffResolver
+
+## Context & Focus
+- **Focus**: Keep the cutoff TABLE small and sourced from published model documentation
+- **Avoid**: hardcoding per-skill logic; the cutoff is always overridable via --cutoff-time
+
diff --git a/.claude/rules/se-deversity-skill3-pipeline-DiscoveryProvider.md b/.claude/rules/se-deversity-skill3-pipeline-DiscoveryProvider.md
new file mode 100644
index 0000000..f10d22b
--- /dev/null
+++ b/.claude/rules/se-deversity-skill3-pipeline-DiscoveryProvider.md
@@ -0,0 +1,11 @@
+---
+paths: ["**/DiscoveryProvider.java"]
+---
+
+
+# Rules for DiscoveryProvider
+
+## Security-Critical Code
+- **Rule**: This code is security-critical. Do not weaken security properties. Every change must be explicitly reviewed for security impact.
+- **Aspect**: forwards the Brave subscription token to the search client; must not log it
+
diff --git a/.claude/rules/se-deversity-skill3-pipeline-FileCorpus.md b/.claude/rules/se-deversity-skill3-pipeline-FileCorpus.md
new file mode 100644
index 0000000..c144d0c
--- /dev/null
+++ b/.claude/rules/se-deversity-skill3-pipeline-FileCorpus.md
@@ -0,0 +1,12 @@
+---
+paths: ["**/FileCorpus.java"]
+---
+
+
+# Rules for FileCorpus
+
+## Load-Bearing Oddity
+- **Rule**: This looks removable but is deliberate. Refactor only while the invariant holds.
+- **Invariant**: FileCorpus implements BOTH discovery seams — SearchClient and PageFetcher — and LearnCommand injects the same instance into both slots. That is the design, not a layering slip: it is what makes an offline --input-file run take the identical downstream path as a live Brave run, so the two modes cannot diverge.
+- **Breaks if changed**: the class is split into two collaborators, or either interface is dropped — offline runs then follow a different path from live ones and stop proving anything about the real pipeline
+
diff --git a/.claude/rules/se-deversity-skill3-pipeline-HttpPageFetcher.md b/.claude/rules/se-deversity-skill3-pipeline-HttpPageFetcher.md
new file mode 100644
index 0000000..7b3afe1
--- /dev/null
+++ b/.claude/rules/se-deversity-skill3-pipeline-HttpPageFetcher.md
@@ -0,0 +1,11 @@
+---
+paths: ["**/HttpPageFetcher.java"]
+---
+
+
+# Rules for HttpPageFetcher
+
+## Security-Critical Code
+- **Rule**: This code is security-critical. Do not weaken security properties. Every change must be explicitly reviewed for security impact.
+- **Aspect**: outbound page fetch egress for partly-untrusted URLs; SSRF guard must not be weakened
+
diff --git a/.claude/rules/se-deversity-skill3-pipeline-PageFetcher.md b/.claude/rules/se-deversity-skill3-pipeline-PageFetcher.md
new file mode 100644
index 0000000..cafae39
--- /dev/null
+++ b/.claude/rules/se-deversity-skill3-pipeline-PageFetcher.md
@@ -0,0 +1,11 @@
+---
+paths: ["**/PageFetcher.java"]
+---
+
+
+# Rules for PageFetcher
+
+## Contract-Frozen Signature
+- **Constraint**: You may change internal logic, but MUST NOT modify the method name, parameters, return type, or checked exceptions.
+- **Reason**: Fetch seam. Keeping page retrieval behind it is what lets extraction, date parsing and scoring be tested against HTML fixtures with no network, and it is the boundary at which --input-file replaces the network entirely.
+
diff --git a/.claude/rules/se-deversity-skill3-pipeline-QueryPlanner.md b/.claude/rules/se-deversity-skill3-pipeline-QueryPlanner.md
new file mode 100644
index 0000000..1a75573
--- /dev/null
+++ b/.claude/rules/se-deversity-skill3-pipeline-QueryPlanner.md
@@ -0,0 +1,11 @@
+---
+paths: ["**/QueryPlanner.java"]
+---
+
+
+# Rules for QueryPlanner
+
+## Context & Focus
+- **Focus**: keep discovery topic-agnostic — the model plans the queries for any topic
+- **Avoid**: hardcoding per-topic search terms or a fixed query suffix like " documentation"
+
diff --git a/.claude/rules/se-deversity-skill3-pipeline-RetrievalService.md b/.claude/rules/se-deversity-skill3-pipeline-RetrievalService.md
new file mode 100644
index 0000000..fef8fea
--- /dev/null
+++ b/.claude/rules/se-deversity-skill3-pipeline-RetrievalService.md
@@ -0,0 +1,11 @@
+---
+paths: ["**/RetrievalService.java"]
+---
+
+
+# Rules for RetrievalService
+
+## Thread-Safety Guarantee
+- **Strategy**: IMMUTABLE
+- **Note**: Collaborators (PageFetcher/HttpClient, DateExtractor, AuthorityScorer) are stateless/immutable; each fetch task builds its own Source and results are merged on the caller thread. Keep it that way — do not share mutable state between fetch tasks. The opt-in `sequential` mode only removes concurrency (fetches run on the caller thread); it cannot weaken the invariant — serial execution is strictly safer than the parallel default it replaces.
+
diff --git a/.claude/rules/se-deversity-skill3-pipeline-SearchClient.md b/.claude/rules/se-deversity-skill3-pipeline-SearchClient.md
new file mode 100644
index 0000000..9409e92
--- /dev/null
+++ b/.claude/rules/se-deversity-skill3-pipeline-SearchClient.md
@@ -0,0 +1,11 @@
+---
+paths: ["**/SearchClient.java"]
+---
+
+
+# Rules for SearchClient
+
+## Contract-Frozen Signature
+- **Constraint**: You may change internal logic, but MUST NOT modify the method name, parameters, return type, or checked exceptions.
+- **Reason**: Discovery seam. BraveSearchClient (live) and FileCorpus (--input-file) both implement it, and isCuratedCorpus() is what tells the pipeline to skip LLM query planning. Removing the default method, or changing what it returns, silently re-enables planning for a corpus that is already the curated result set.
+
diff --git a/.claude/rules/se-deversity-skill3-skillspector-InputVetter.md b/.claude/rules/se-deversity-skill3-skillspector-InputVetter.md
new file mode 100644
index 0000000..5e02623
--- /dev/null
+++ b/.claude/rules/se-deversity-skill3-skillspector-InputVetter.md
@@ -0,0 +1,12 @@
+---
+paths: ["**/InputVetter.java"]
+---
+
+
+# Rules for InputVetter
+
+## Load-Bearing Oddity
+- **Rule**: This looks removable but is deliberate. Refactor only while the invariant holds.
+- **Invariant**: A quarantined source is dropped from the set handed to the synthesizer, but its finding is still recorded and still trips the run gate. Redaction runs FIRST and unconditionally, so a secret never reaches the model even when SkillSpector is unavailable — and when it is unavailable nothing is gated, because absence of findings is observed, never asserted.
+- **Breaks if changed**: quarantining is treated as resolving the finding, redaction is made conditional on the scanner being present, or a skipped scan is reported as clean
+
diff --git a/CLAUDE.md b/CLAUDE.md
index f3dc623..7a5a433 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,18 +1,67 @@
+# Skill3 — agent briefing
+
+Java 25 CLI that **relearns a technical skill** for an AI agent: it discovers
+documentation, scores it for authority and freshness against a target model's knowledge
+cutoff, synthesizes an Agent Skills `SKILL.md` with an LLM, and vets both the input corpus
+and the output skill with NVIDIA SkillSpector.
+
+## The two invariants everything else serves
+
+1. **A skill is a post-cutoff delta, not a primer.** The target model already knows the
+ topic up to its cutoff; the pipeline gathers only what changed *after* it. Re-explaining
+ known fundamentals wastes the whole mechanism.
+2. **Discovery is topic-agnostic.** The model plans the searches. There is no per-topic
+ logic anywhere, and adding some — a hardcoded query suffix, a per-skill branch — is the
+ most common way to break this project without failing a test.
+
+## Where things are
+
+| I need… | Read |
+|---|---|
+| What it does, flag by flag | [`docs/USAGE.md`](docs/USAGE.md) |
+| Install, keys, build | [`docs/INSTALL.md`](docs/INSTALL.md) |
+| How it is structured, and why | [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) |
+| The full behavioural spec | [`docs/SPEC.md`](docs/SPEC.md) |
+| Build gates, releases, guardrails | [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) |
+| Generated structure diagrams | [`docs/diagrams/`](docs/diagrams/) |
+| Roadmap | [`docs/PLAN.md`](docs/PLAN.md) |
+
+## Working on it
+
+```bash
+./gradlew build # compile + test + Error Prone, PMD, SpotBugs, ArchUnit, JaCoCo gate
+./gradlew test # tests only
+./gradlew diagrams # regenerate docs/diagrams/*.svg from the source
+./gradlew run --args="learn --help"
+```
+
+The build runs on a Gradle-provisioned JDK 25 toolchain, so it does not depend on
+`JAVA_HOME`. Coverage is gated at 75% instruction / 65% branch.
+
+## Things that will bite you
+
+- **Never hand-edit between `` and ``** in this
+ file, `llms.txt` or `llms-full.txt`. Every compile rewrites that region from the `@AI*`
+ annotations in the Java source. Text outside the markers — including everything above —
+ survives. To change a guardrail, change the annotation.
+- **The guardrails below are the safety tier only.** Per-element detail lives in
+ [`.claude/rules/`](.claude/rules/) and loads when you open a matching source file.
+- **Layering is enforced, not suggested.** An ArchUnit test asserts that `model` depends on
+ nothing internal, that only `Skill3App` touches `cli`, and that the sub-packages stay
+ acyclic. A convenient import in `model` fails the build.
+- **Untrusted data has a defined path.** Scraped pages and `--input-file` content are
+ secret-redacted and scanned *before* synthesis, fenced as DATA in the prompt, and the
+ output is scanned again. Do not add a shortcut that reaches the model earlier.
+- **Absence of findings is never asserted.** When SkillSpector is unavailable the scans are
+ skipped and nothing is gated — "not scanned" must never be reported as "clean".
+
+## Generated guardrails
+
+Everything below is regenerated from source annotations on every compile.
+
-
-
-
-
- Keep the cutoff TABLE small and sourced from published model documentation
- hardcoding per-skill logic; the cutoff is always overridable via --cutoff-time
-
-
- keep discovery topic-agnostic — the model plans the queries for any topic
- hardcoding per-topic search terms or a fixed query suffix like " documentation"
-
- LLM provider API key — never log, echo, or include in errors/fixtures
@@ -37,21 +86,6 @@
Elements listed in are well-tested core components. Make changes with extreme caution and verify comprehensive test coverage before proposing modifications.
-
-
- IMMUTABLE
- Collaborators (PageFetcher/HttpClient, DateExtractor, AuthorityScorer) are stateless/immutable; each fetch task builds its own Source and results are merged on the caller thread. Keep it that way — do not share mutable state between fetch tasks. The opt-in `sequential` mode only removes concurrency (fetches run on the caller thread); it cannot weaken the invariant — serial execution is strictly safer than the parallel default it replaces.
-
-
-
-Elements listed in are explicitly designed to be thread-safe via the named strategy. Any modification MUST preserve the synchronization invariant and document its reasoning in the change description.
-
-
- Immutable record; the sources list is defensively copied in the compact constructor.
-
-
-
-Types listed in are immutable by design. Never introduce non-final fields, setters, or methods that mutate instance state.Anthropic API credential handling and hosted-provider network egress
@@ -77,14 +111,30 @@
Elements listed in are security-critical. Never weaken their security properties. Every proposed change must be explicitly reviewed for security impact.
-
-
- Pure function: no side effects, deterministic.
-
-
+
+ Detailed per-element guardrails for the elements below live in scoped rule files that load automatically when the matching source file is opened. Consult the referenced file before modifying an element.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-Methods in must remain mathematically pure. Side effects, mutations of class/static state, or blocking operations are strictly forbidden.
+When you work on any element listed in , open its referenced rule file and apply the guardrails there. The rule files are the authoritative source for those elements.
-
-Never propose edits to files listed in .
diff --git a/README.md b/README.md
index c7d1d38..3014a0a 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
[](https://opensource.org/licenses/Apache-2.0)
[](https://github.com/PIsberg/skill3/actions/workflows/ci.yml)
[](https://adoptium.net/)
-[](#development)
+[](docs/DEVELOPMENT.md)
[](https://paypal.me/isbergpeter)
Skill3 is a lightweight Java CLI that **relearns a technical skill** for an AI
@@ -65,7 +65,7 @@ material the model could not possibly already know, rather than re-summarising w
learned in training. Everything else — authority scoring, freshness ranking, local
synthesis, vetting — exists to turn that fresh slice into something an agent can trust.
-This works for **any** topic, not just code (see the [examples](#example-output) — a
+This works for **any** topic, not just code (see the [examples](docs/EXAMPLE-OUTPUT.md) — a
software protocol *and* current events). The cutoff is the dial; the skill is the output.
---
@@ -131,7 +131,7 @@ vetting. **Brave** does discovery and **SkillSpector** does safety vetting.
| Stage | Component | Where | Notes |
|---|---|---|---|
| **Plan** | `QueryPlanner` (the synthesis model) | model | Topic-agnostic: the model expands the topic into up to 6 post-cutoff facet queries (it already knows the topic, so it knows what might have changed). No hardcoded per-topic logic. |
-| **Discover** | Brave Search API → web scraper fallback, **or** `--input-file` (`FileCorpus`) | Network / **offline** | Runs every planned query (freshness-windowed); needs an API key. Or skip the network entirely and supply a user-curated corpus file — see [Offline discovery](#offline-discovery-with---input-file). |
+| **Discover** | Brave Search API → web scraper fallback, **or** `--input-file` (`FileCorpus`) | Network / **offline** | Runs every planned query (freshness-windowed); needs an API key. Or skip the network entirely and supply a user-curated corpus file — see [Offline discovery](docs/USAGE.md#offline-discovery-with---input-file). |
| **Fetch** | `RetrievalService` over virtual threads | Network | Merges/de-dupes URLs across queries, then fetches **concurrently** (one virtual thread per URL); results merged on the caller thread. |
| **Date / authority** | `DateExtractor`, `AuthorityScorer` | Local | Published-date extraction + per-host trust; `--authoritative` hosts rank first. |
| **Rank** | `IngestionPipeline` (`ConsensusValidator`, `FreshnessFilter`) | Local | Code kept only with cross-source agreement; freshness anchored to the cutoff **and bounded above by today** (future-dated sources dropped). |
@@ -145,294 +145,20 @@ or `anthropic` moves the model calls to a hosted endpoint.
---
-## Requirements
+## Documentation
-- **JDK 25** (compiled with `--release 25`). Gradle provisions the JDK 25 toolchain
- automatically (auto-detected or downloaded via the Foojay resolver), so you don't
- need JDK 25 on `JAVA_HOME` — any JDK that runs Gradle will do.
-- A **local LLM** exposed over an OpenAI-compatible API (e.g. [Ollama](https://ollama.com)).
-- **Python 3.12–3.14** (only for `setup`; SkillSpector's supported range).
-- A **[Brave Search API](https://brave.com/search/api/) key** for discovery.
-
-## Build
-
-```bash
-./gradlew build # compile + full quality gate (analysis) + tests
-./gradlew test # tests only (JUnit + ArchUnit)
-./gradlew run --args="..." # run the CLI
-```
-
-`build` runs the complete quality gate — see [Development](#development).
-
----
-
-## Setup
-
-### 1. Install SkillSpector (one-time)
-
-```bash
-./gradlew run --args="setup"
-```
-
-This provisions a local Python venv and installs SkillSpector into it. `learn`
-runs SkillSpector with `--no-llm` so vetting stays fully local (static analysis only).
-
-### 2. Get a Brave Search key
-
-Discovery uses the [Brave Search API](https://brave.com/search/api/) — the **only
-external service `learn` needs**.
-
-1. Create an account at .
-2. Subscribe to a plan. The **Free** tier (a few thousand queries/month) is enough
- to try Skill3; a card may be required for verification even on the free plan.
-3. Create a subscription token (your API key).
-4. Provide it one of two ways:
-
-```bash
-# Option A — environment variable (picked up automatically)
-export BRAVE_SEARCH_API_KEY="your-token"
-
-# Option B — per run
-./gradlew run --args="learn mcp --llm-model qwen2.5-coder:7b --brave-key your-token"
-```
-
-The token is sent in the `X-Subscription-Token` header. If no key is found,
-`learn` stops early with a clear message; the key is treated as a secret
-(`@AIPrivacy` — never logged).
-
-> You don't strictly need a key to evaluate the pipeline: the example in
-> [`examples/`](examples/) was produced from seeded source URLs, and the tests
-> stub discovery behind the `SearchClient` interface. For a real run with **no
-> key and no network at all**, supply your own sources with `--input-file` (see
-> [Offline discovery](#offline-discovery-with---input-file)).
+| Guide | What is in it |
+|---|---|
+| [Install](docs/INSTALL.md) | Requirements, build, SkillSpector setup, Brave key |
+| [Usage](docs/USAGE.md) | Every `learn` flag, `--input-file`, model choice, the cutoff window |
+| [Architecture](docs/ARCHITECTURE.md) | How it is structured and why — with generated diagrams |
+| [Specification](docs/SPEC.md) | Full behavioural spec |
+| [Development](docs/DEVELOPMENT.md) | Build gates, releases, AI guardrails |
+| [Example output](docs/EXAMPLE-OUTPUT.md) | A complete run, start to finish |
+| [Roadmap](docs/PLAN.md) | What is planned next |
---
-## Usage
-
-### Learn a skill
-
-```bash
-./gradlew run --args="learn mcp \
- --target-model claude-opus-4-8 \
- --llm-model qwen2.5-coder:7b \
- --brave-key $BRAVE_SEARCH_API_KEY"
-```
-
-Common options for `learn`:
-
-| Option | Meaning | Default |
-|---|---|---|
-| `--target-model ` | Model the skill is *for*; used only to look up a knowledge cutoff. | `claude-opus-4-8` |
-| `--cutoff-time ` | Explicit cutoff override (wins over `--target-model`). | — |
-| `--strict-cutoff` | Hard-exclude sources at/before the cutoff. | off |
-| `--llm-model ` | Synthesis model name. | **required** |
-| `--llm-provider
` | `local` \| `openai` \| `anthropic`. | `local` |
-| `--llm-endpoint ` | OpenAI-compatible endpoint (local/openai). | `http://localhost:11434` |
-| `--llm-key ` | Key for hosted providers (`openai`: `LLM_API_KEY`; `anthropic`: `ANTHROPIC_API_KEY`). | env |
-| `--max-tokens ` | Max output tokens for synthesis. | `8192` |
-| `--temperature ` | Sampling temperature (local/openai only). | server default |
-| `--rich-context` | Feed more sources/excerpts to the model (suits big-context models). | off |
-| `--authoritative ` | Comma-separated hosts ranked first (e.g. `modelcontextprotocol.io,github.com`). | — |
-| `--verify` / `--no-verify` | Re-ground every claim against the sources (accuracy gate, one extra model call). | on for `openai`/`anthropic`, off for `local` |
-| `--brave-key ` | Brave Search key (or `BRAVE_SEARCH_API_KEY`). | env |
-| `--input-file ` | Offline discovery: a user-curated corpus file used instead of Brave (no key/network). See [Offline discovery](#offline-discovery-with---input-file). | — |
-| `--dry-run` | Stop after discovery + ranking; print the sources, dates and scores; write nothing. | off |
-| `--no-cache` | Bypass the on-disk cache of search results and fetched pages (`~/.skill3/cache`, 7-day TTL). | off |
-| `--output-dir ` | Where the skill is written. | `./skills/` |
-
-Output: `./skills//SKILL.md` (+ an `index.html` preview and a `run.json`
-provenance manifest recording the queries, the exact sources and scores that backed the
-skill, the verify/vet outcome, and per-phase timings).
-
-Discovery and model calls retry transient failures (connection errors, `429`, `5xx`) with
-exponential backoff (honoring `Retry-After`), and search results + fetched pages are cached
-under `~/.skill3/cache` (7-day TTL) so re-running a topic skips the network — pass `--no-cache`
-to force fresh fetches.
-
-### Offline discovery with `--input-file`
-
-`--input-file` replaces Brave with a **user-curated corpus file** you fill in
-yourself — the same role Brave plays (supplying source documents), but offline:
-no key, no network, fully reproducible. It slots in behind the same
-`SearchClient`/`PageFetcher` seams (`FileCorpus`), so everything downstream —
-date extraction, authority scoring, consensus, freshness, synthesis and vetting —
-runs exactly as it would for live pages.
-
-```bash
-./gradlew run --args="learn mcp \
- --llm-model qwen2.5-coder:7b \
- --input-file ./my-sources.txt"
-```
-
-**File format.** Documents are separated by a line that reads exactly
-`=== SOURCE ===`. Each starts with `key: value` headers (`url` required;
-`title` and `date` as `yyyy-MM-dd` optional), then a blank line, then the body.
-The body may be plain text, Markdown (fenced ```` ``` ```` code blocks and `#`
-headings are recognised), or raw HTML:
-
-````
-=== SOURCE ===
-url: https://modelcontextprotocol.io/specification
-title: MCP Specification (2026-03 revision)
-date: 2026-03-01
-
-# Resources
-The _meta field is now accepted on every request in the 2026-03 revision.
-
-```
-client.call("tools/list");
-```
-
-=== SOURCE ===
-url: https://github.com/org/repo/releases
-date: 2026-04-01
-
-Release notes describing the new behaviour and flags...
-````
-
-The whole file is treated as the curated result set (every document is used —
-the model's planned queries don't filter it down). Anything before the first
-`=== SOURCE ===` marker is ignored, so you can keep a comment at the top. A
-ready-to-copy template lives at
-[`examples/input-corpus-sample.txt`](examples/input-corpus-sample.txt).
-
-### Choosing a synthesis model
-
-Synthesis is the quality bottleneck (see the examples below — the *same* sources, very
-different skills). Three providers, in order of fidelity to the local-first design:
-
-1. **Bigger local model (default, keeps the no-key design).** Just pull a stronger Ollama
- model — no code, no key:
- ```bash
- ./gradlew run --args="learn mcp --llm-model qwen2.5-coder:32b --brave-key $BRAVE_SEARCH_API_KEY"
- ```
-2. **Any OpenAI-compatible gateway** (OpenRouter, Together, Groq, …) — opt-in, breaks the
- no-key property only when you use it:
- ```bash
- ./gradlew run --args="learn mcp --llm-provider openai \
- --llm-endpoint https://openrouter.ai/api --llm-model \
- --llm-key $LLM_API_KEY --rich-context --brave-key $BRAVE_SEARCH_API_KEY"
- ```
-3. **Claude (native Anthropic SDK)** — highest quality. Uses the official
- `anthropic-java` SDK and the Messages API (not an OpenAI shim):
- ```bash
- export ANTHROPIC_API_KEY=sk-ant-...
- ./gradlew run --args="learn mcp --llm-provider anthropic \
- --llm-model claude-opus-4-8 --rich-context --brave-key $BRAVE_SEARCH_API_KEY"
- ```
- `--temperature` is ignored for `anthropic` (Opus 4.8 rejects sampling parameters).
-
-### How the cutoff drives the search window
-
-The resolved cutoff (from `--target-model`, or `--cutoff-time` if given) becomes
-the **start** of the Brave discovery window; today is the end. For
-`claude-opus-4-8` (cutoff `2026-01`) a run today searches:
-
-```
-Cutoff: claude-opus-4-8 (2026-01)
-Search window: 2026-01-01to2026-06-22
-```
-
-So discovery skips what the model already knows and surfaces only what's new since
-its cutoff. Widen it for a given run with `--cutoff-time` (e.g. `--cutoff-time 2024-01`).
-
-> **Output quality scales with the synthesis model.** A small model (e.g.
-> `qwen2.5:3b`) hallucinates and conflates unrelated tools; a capable coder model
-> (e.g. `qwen2.5-coder:7b` or larger) produces accurate content from the same sources.
-
----
-
-## Development
-
-`./gradlew build` enforces a quality gate. Configuration lives in
-[`config/`](config/) and [`build.gradle`](build.gradle).
-
-| Tool | Scope | Config |
-|---|---|---|
-| **Error Prone** (`2.50.0`) | main sources, woven into `javac` | `build.gradle` |
-| **PMD** (`7.24.0`) | main sources | [`config/pmd-ruleset.xml`](config/pmd-ruleset.xml) |
-| **SpotBugs** (`6.5.8`, effort `Max`) | main classes | [`config/spotbugs-exclude.xml`](config/spotbugs-exclude.xml) |
-| **ArchUnit** (`1.4.0`) | layering / cycles | [`ArchitectureTest`](src/test/java/se/deversity/skill3/ArchitectureTest.java) |
-| **JSpecify** (`1.0.0`) | nullness | `@NullMarked` `package-info.java` per package |
-| **async-test-lib** (`1.7.0-RC1`) | concurrency stress tests (`@AsyncTest`) | [`ConcurrencySafetyTest`](src/test/java/se/deversity/skill3/ConcurrencySafetyTest.java) |
-| **JaCoCo** coverage gate | `check` fails below 75% instruction / 65% branch | `build.gradle` |
-
-Tests run on JUnit Jupiter 6; the hosted Claude provider uses the official
-`anthropic-java` SDK.
-
-ArchUnit keeps the layering honest: `model` is a dependency-free leaf, only the
-`Skill3App` composition root touches `cli`, and the sub-packages stay acyclic.
-
-### Releases
-
-Push a `v*` tag to cut a release: CI runs the full gate, builds the application
-distribution (`./gradlew build` → `build/distributions/skill3-.zip|tar`,
-launch scripts included), and publishes a GitHub Release with auto-generated notes
-and the distribution attached — see [`release.yml`](.github/workflows/release.yml).
-
-```bash
-git tag v0.1.0 && git push origin v0.1.0
-```
-
-### AI guardrails (VibeTags)
-
-The codebase is annotated with [VibeTags](https://github.com/PIsberg/vibetags) —
-compile-time, `SOURCE`-retention annotations (zero runtime cost) that mark intent
-for AI tools, e.g.:
-
-- `@AIPrivacy` on the Brave API key (never log/echo it),
-- `@AICore` on `SkillMdPostProcessor` (guarantees spec compliance — change with care),
-- `@AISecure` on `NameSanitizer` / `BraveSearchClient`,
-- `@AIImmutable` on `ContextBundle`, `@AIContext` on `CutoffResolver`.
-
-On every compile the processor regenerates the guardrail files (`CLAUDE.md`,
-`llms.txt`, `llms-full.txt`) from these annotations. There's also a
-[`vibetags-usage`](.claude/skills/vibetags-usage/SKILL.md) skill describing the
-full annotation set.
-
----
-
-## Example output
-
-A skill3 output is a **delta**, not a primer: it covers only what changed *after* the target
-model's cutoff and explicitly tells the model to rely on existing knowledge for the rest.
-
-**MCP — `claude-opus-4-8`** ([`examples/SKILL-mcp-claude.md`](examples/SKILL-mcp-claude.md)):
-the `QueryPlanner`'s protocol-focused queries (spec release, roadmap, security) surface the
-actual changelog, so the skill is a true protocol delta — the **2026-07-28 stateless release
-candidate** (SEP-2567/2575 remove the session header and `initialize` handshake; new
-`Mcp-Method`/`Mcp-Name` headers; `ttlMs`/`cacheScope` caching), the SEP-2577 deprecation of
-Roots/Sampling/Logging, the 2026 roadmap, and 2026 CVEs — with the pre-cutoff fundamentals
-treated as already known. ([`examples/SKILL-mcp.md`](examples/SKILL-mcp.md) is the older
-local-model run, hand-edited for accuracy — kept for the model-quality contrast.)
-
-**Current events — `claude-opus-4-8`** ([`examples/SKILL-trump-claude.md`](examples/SKILL-trump-claude.md)):
-proves the same machinery works for a non-technical topic. The `QueryPlanner` expanded
-`trump` into six facet queries (latest news, executive orders, tariffs, foreign policy, legal
-rulings, midterms), so the skill spans the full post-cutoff picture — the Iran war, the
-Venezuela strike, the Supreme Court striking down IEEPA tariffs and the Section 122/301/232
-pivot, ICE detention litigation, midterms — not just one story, all from sources dated after
-the cutoff. "When to use" points the model back to its existing knowledge for the baseline.
-The [local-model version](examples/SKILL-trump.md) is kept alongside it.
-
-
-
-- **Caveat:** these are *raw, unverified* model summaries of post-cutoff pages — included to
- demonstrate the pipeline, not as fact-checked references. Judge claims against the sources.
-
-- [`examples/SKILL-json-rpc.md`](examples/SKILL-json-rpc.md) — an earlier locally-synthesized
- skill, vetted clean by SkillSpector.
-
-Every generated skill ends with a provenance footer —
-`_Created with [skill3](https://github.com/PIsberg/skill3)._` — stamped deterministically
-by the generator (idempotently, even across self-correction revisions).
-
-The generated `SKILL.md` follows the
-[Agent Skills](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview)
-standard. Skill3 deterministically guarantees format compliance (name charset,
-reserved-word stripping, description limits) regardless of what the LLM emits.
-
## 💛 Support skill3
skill3 is built and maintained by a single developer in his spare time. If it saves you time, a small donation helps keep the project alive:
diff --git a/build.gradle b/build.gradle
index e01d7fd..0c976d5 100644
--- a/build.gradle
+++ b/build.gradle
@@ -12,12 +12,19 @@ plugins {
group = 'se.deversity'
version = '0.1.0'
-ext.vibetagsVersion = '1.0.0-RC1'
+ext.vibetagsVersion = '1.0.0-RC8'
+// code-karta renders the diagrams under docs/diagrams straight from this source tree.
+ext.codekartaVersion = '0.2.0'
repositories {
mavenCentral()
}
+configurations {
+ // Tool-only: the diagram renderer must never reach a compile or runtime classpath.
+ codekarta
+}
+
dependencies {
implementation 'info.picocli:picocli:4.7.7'
implementation 'org.jsoup:jsoup:1.22.2'
@@ -47,6 +54,9 @@ dependencies {
// async-test-lib: JUnit 5 extension that stress-tests concurrent code (@AsyncTest).
testImplementation 'se.deversity.async-test-lib:async-test-lib:1.7.0-RC1'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:6.1.0'
+
+ // code-karta CLI (shaded), used only by the `diagrams` task below.
+ codekarta "se.deversity.codekarta:code-karta-cli:${codekartaVersion}:all"
}
application {
@@ -148,3 +158,50 @@ tasks.named('spotbugsMain') {
xml { required = true }
}
}
+
+// ---- Generated architecture diagrams (code-karta) ---------------------------
+//
+// The SVGs under docs/diagrams are parsed out of this source tree rather than drawn,
+// so they cannot quietly disagree with the code the way a hand-maintained diagram
+// does. The mermaid graph in docs/ARCHITECTURE.md is the complementary half: it shows
+// the pipeline as *intended*, these show it as *written*.
+//
+// Regenerate with `./gradlew diagrams` and commit the result alongside the change.
+def codekartaDiagrams = [
+ [task : 'diagramModelClasses',
+ input: 'src/main/java/se/deversity/skill3/model',
+ out : 'model-class-diagram.svg',
+ extra: []],
+ [task : 'diagramPipelineClasses',
+ input: 'src/main/java/se/deversity/skill3/pipeline',
+ out : 'pipeline-class-diagram.svg',
+ extra: []],
+ [task : 'diagramLlmClasses',
+ input: 'src/main/java/se/deversity/skill3/llm',
+ out : 'llm-class-diagram.svg',
+ extra: []],
+ [task : 'diagramPipelineSequence',
+ input: 'src/main/java/se/deversity/skill3/pipeline',
+ out : 'pipeline-sequence-diagram.svg',
+ extra: ['--sequence-only']],
+]
+
+def codekartaTasks = codekartaDiagrams.collect { d ->
+ tasks.register(d.task, JavaExec) {
+ group = 'documentation'
+ description = "Render docs/diagrams/${d.out} from ${d.input}"
+ classpath = configurations.codekarta
+ mainClass = 'se.deversity.codekarta.cli.KartaCli'
+ inputs.dir(file(d.input)).withPathSensitivity(PathSensitivity.RELATIVE)
+ outputs.file(layout.projectDirectory.file("docs/diagrams/${d.out}"))
+ args = ['--input', d.input,
+ '--output', 'docs/diagrams',
+ '--output-name', d.out] + d.extra
+ }
+}
+
+tasks.register('diagrams') {
+ group = 'documentation'
+ description = 'Regenerate every code-karta diagram under docs/diagrams'
+ dependsOn codekartaTasks
+}
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index c733664..0a93b62 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -70,6 +70,28 @@ Every package carries a `@NullMarked` `package-info.java` (JSpecify). Layering i
enforced by an ArchUnit test: `model` depends on nothing internal, only the
`Skill3App` root touches `cli`, and the sub-packages stay acyclic.
+## Generated structure diagrams
+
+The mermaid graph above is drawn by hand: it shows the pipeline as *intended*, including
+stages that are conceptual rather than a single class. The SVGs below are the other half —
+parsed straight out of `src/main/java` by [code-karta](https://github.com/PIsberg/codekarta),
+so they show the code as *written* and cannot quietly drift from it.
+
+Regenerate them with `./gradlew diagrams` and commit the result with the change that moved
+them. The task pins `code-karta-cli` from Maven Central, so it needs no local checkout, and
+the renderer is deterministic — a re-run with unchanged source produces byte-identical SVGs,
+which is what keeps them out of review diffs unless the structure really moved.
+
+| Diagram | What it answers |
+|---|---|
+| [](diagrams/model-class-diagram.svg) **Data carriers** (`model`) | What flows between stages: `Source` and its scoring fields, the immutable `ContextBundle`, `Cutoff`, and the `RunManifest` written as `run.json`. |
+| [](diagrams/llm-class-diagram.svg) **The provider seam** (`llm`) | Why `--llm-provider` works: `LocalLlmClient` and `AnthropicChatModel` both implement the one `ChatModel` interface every model-driven stage takes. |
+| [](diagrams/pipeline-class-diagram.svg) **Discovery and ingestion** (`pipeline`) | The collaborators behind discovery, and the `SearchClient` / `PageFetcher` seams that `FileCorpus` implements *both* of for offline runs. |
+| [**Call graph**](diagrams/pipeline-sequence-diagram.svg) (`pipeline`, stitched across files) | The real call sequence through discovery and ingestion, resolved across files by symbol solving. Large — open it directly rather than inline. |
+
+There is deliberately no JPMS module diagram: Skill3 has no `module-info.java`, so it would
+have nothing to show.
+
## Key design decisions
### Skills are post-cutoff deltas, not primers
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
new file mode 100644
index 0000000..1294472
--- /dev/null
+++ b/docs/DEVELOPMENT.md
@@ -0,0 +1,81 @@
+# Developing Skill3
+
+Build gates, the release process, and how the compile-time AI guardrails are
+maintained. For the design behind the code, see [ARCHITECTURE.md](ARCHITECTURE.md).
+
+## Development
+
+`./gradlew build` enforces a quality gate. Configuration lives in
+[`config/`](../config/) and [`build.gradle`](../build.gradle).
+
+| Tool | Scope | Config |
+|---|---|---|
+| **Error Prone** (`2.50.0`) | main sources, woven into `javac` | `build.gradle` |
+| **PMD** (`7.24.0`) | main sources | [`config/pmd-ruleset.xml`](../config/pmd-ruleset.xml) |
+| **SpotBugs** (`6.5.8`, effort `Max`) | main classes | [`config/spotbugs-exclude.xml`](../config/spotbugs-exclude.xml) |
+| **ArchUnit** (`1.4.0`) | layering / cycles | [`ArchitectureTest`](../src/test/java/se/deversity/skill3/ArchitectureTest.java) |
+| **JSpecify** (`1.0.0`) | nullness | `@NullMarked` `package-info.java` per package |
+| **async-test-lib** (`1.7.0-RC1`) | concurrency stress tests (`@AsyncTest`) | [`ConcurrencySafetyTest`](../src/test/java/se/deversity/skill3/ConcurrencySafetyTest.java) |
+| **JaCoCo** coverage gate | `check` fails below 75% instruction / 65% branch | `build.gradle` |
+
+Tests run on JUnit Jupiter 6; the hosted Claude provider uses the official
+`anthropic-java` SDK.
+
+ArchUnit keeps the layering honest: `model` is a dependency-free leaf, only the
+`Skill3App` composition root touches `cli`, and the sub-packages stay acyclic.
+
+### Releases
+
+Push a `v*` tag to cut a release: CI runs the full gate, builds the application
+distribution (`./gradlew build` → `build/distributions/skill3-.zip|tar`,
+launch scripts included), and publishes a GitHub Release with auto-generated notes
+and the distribution attached — see [`release.yml`](../.github/workflows/release.yml).
+
+```bash
+git tag v0.1.0 && git push origin v0.1.0
+```
+
+### AI guardrails (VibeTags)
+
+The codebase is annotated with [VibeTags](https://github.com/PIsberg/vibetags) —
+compile-time, `SOURCE`-retention annotations (zero runtime cost) that mark intent
+for AI tools, e.g.:
+
+- `@AIPrivacy` on the Brave API key (never log/echo it),
+- `@AICore` on `SkillMdPostProcessor` (guarantees spec compliance — change with care),
+- `@AISecure` on `NameSanitizer` / `BraveSearchClient`,
+- `@AIImmutable` on `ContextBundle`, `@AIContext` on `CutoffResolver`,
+- `@AIContract` on the three seams that make the pipeline testable without a network or a
+ model — `ChatModel`, `SearchClient`, `PageFetcher`,
+- `@AILoadBearing` on `FileCorpus` (it implements *both* discovery seams on purpose) and on
+ `InputVetter` (quarantine is mitigation, not amnesty; redaction is unconditional),
+- `@AISchemaSafe` on `RunManifest` — its component names are the `run.json` field names,
+- `@AIIdempotent` on `SkillMdPostProcessor.render()`, which the self-correction loop re-runs
+ on its own output,
+- `@AIDomainModel` + `@AIArchitecture` on `Source`, mirroring the layering `ArchitectureTest`
+ already enforces.
+
+On every compile the processor regenerates the guardrail regions in `CLAUDE.md`,
+`llms.txt` and `llms-full.txt`. **Never hand-edit between the `VIBETAGS-START` and
+`VIBETAGS-END` markers** — that region is rewritten from the annotations on the next
+compile. Everything outside them, including the hand-written briefing at the top of
+`CLAUDE.md`, survives untouched; change an annotation, not the generated text.
+
+Because [`.claude/rules/`](../.claude/rules/) exists, the layout is the *indexed* one: the
+root `CLAUDE.md` keeps only the always-on safety tier inline (privacy, core, security) and
+indexes the per-element detail into scoped rule files that a host tool loads when you open a
+matching source file. Delete that directory and the detail moves back inline. There is also a
+[`vibetags-usage`](../.claude/skills/vibetags-usage/SKILL.md) skill describing the full
+annotation set.
+
+### Diagrams
+
+`./gradlew diagrams` re-renders the SVGs under [`diagrams/`](diagrams/) from the source with
+[code-karta](https://github.com/PIsberg/codekarta), pinned from Maven Central. The renderer is
+deterministic, so re-running it with unchanged source is a no-op in `git status`. Regenerate
+and commit whenever a change moves the structure the diagrams describe — see
+[ARCHITECTURE.md](ARCHITECTURE.md#generated-structure-diagrams).
+
+---
+
+[← back to the README](../README.md)
diff --git a/docs/EXAMPLE-OUTPUT.md b/docs/EXAMPLE-OUTPUT.md
new file mode 100644
index 0000000..9d2f87a
--- /dev/null
+++ b/docs/EXAMPLE-OUTPUT.md
@@ -0,0 +1,45 @@
+# Example output
+
+A real `learn` run, start to finish.
+
+## Example output
+
+A skill3 output is a **delta**, not a primer: it covers only what changed *after* the target
+model's cutoff and explicitly tells the model to rely on existing knowledge for the rest.
+
+**MCP — `claude-opus-4-8`** ([`examples/SKILL-mcp-claude.md`](../examples/SKILL-mcp-claude.md)):
+the `QueryPlanner`'s protocol-focused queries (spec release, roadmap, security) surface the
+actual changelog, so the skill is a true protocol delta — the **2026-07-28 stateless release
+candidate** (SEP-2567/2575 remove the session header and `initialize` handshake; new
+`Mcp-Method`/`Mcp-Name` headers; `ttlMs`/`cacheScope` caching), the SEP-2577 deprecation of
+Roots/Sampling/Logging, the 2026 roadmap, and 2026 CVEs — with the pre-cutoff fundamentals
+treated as already known. ([`examples/SKILL-mcp.md`](../examples/SKILL-mcp.md) is the older
+local-model run, hand-edited for accuracy — kept for the model-quality contrast.)
+
+**Current events — `claude-opus-4-8`** ([`examples/SKILL-trump-claude.md`](../examples/SKILL-trump-claude.md)):
+proves the same machinery works for a non-technical topic. The `QueryPlanner` expanded
+`trump` into six facet queries (latest news, executive orders, tariffs, foreign policy, legal
+rulings, midterms), so the skill spans the full post-cutoff picture — the Iran war, the
+Venezuela strike, the Supreme Court striking down IEEPA tariffs and the Section 122/301/232
+pivot, ICE detention litigation, midterms — not just one story, all from sources dated after
+the cutoff. "When to use" points the model back to its existing knowledge for the baseline.
+The [local-model version](../examples/SKILL-trump.md) is kept alongside it.
+
+
+
+- **Caveat:** these are *raw, unverified* model summaries of post-cutoff pages — included to
+ demonstrate the pipeline, not as fact-checked references. Judge claims against the sources.
+
+- [`examples/SKILL-json-rpc.md`](../examples/SKILL-json-rpc.md) — an earlier locally-synthesized
+ skill, vetted clean by SkillSpector.
+
+Every generated skill ends with a provenance footer —
+`_Created with [skill3](https://github.com/PIsberg/skill3)._` — stamped deterministically
+by the generator (idempotently, even across self-correction revisions).
+
+The generated `SKILL.md` follows the
+[Agent Skills](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview)
+standard. Skill3 deterministically guarantees format compliance (name charset,
+reserved-word stripping, description limits) regardless of what the LLM emits.
+
+[← back to the README](../README.md)
diff --git a/docs/INSTALL.md b/docs/INSTALL.md
new file mode 100644
index 0000000..c51a61f
--- /dev/null
+++ b/docs/INSTALL.md
@@ -0,0 +1,69 @@
+# Installing Skill3
+
+Requirements, build, and the one-time setup for SkillSpector and a Brave Search key.
+For what to run once it is installed, see [USAGE.md](USAGE.md).
+
+## Requirements
+
+- **JDK 25** (compiled with `--release 25`). Gradle provisions the JDK 25 toolchain
+ automatically (auto-detected or downloaded via the Foojay resolver), so you don't
+ need JDK 25 on `JAVA_HOME` — any JDK that runs Gradle will do.
+- A **local LLM** exposed over an OpenAI-compatible API (e.g. [Ollama](https://ollama.com)).
+- **Python 3.12–3.14** (only for `setup`; SkillSpector's supported range).
+- A **[Brave Search API](https://brave.com/search/api/) key** for discovery.
+
+## Build
+
+```bash
+./gradlew build # compile + full quality gate (analysis) + tests
+./gradlew test # tests only (JUnit + ArchUnit)
+./gradlew run --args="..." # run the CLI
+```
+
+`build` runs the complete quality gate — see [Development](#development).
+
+---
+
+## Setup
+
+### 1. Install SkillSpector (one-time)
+
+```bash
+./gradlew run --args="setup"
+```
+
+This provisions a local Python venv and installs SkillSpector into it. `learn`
+runs SkillSpector with `--no-llm` so vetting stays fully local (static analysis only).
+
+### 2. Get a Brave Search key
+
+Discovery uses the [Brave Search API](https://brave.com/search/api/) — the **only
+external service `learn` needs**.
+
+1. Create an account at .
+2. Subscribe to a plan. The **Free** tier (a few thousand queries/month) is enough
+ to try Skill3; a card may be required for verification even on the free plan.
+3. Create a subscription token (your API key).
+4. Provide it one of two ways:
+
+```bash
+# Option A — environment variable (picked up automatically)
+export BRAVE_SEARCH_API_KEY="your-token"
+
+# Option B — per run
+./gradlew run --args="learn mcp --llm-model qwen2.5-coder:7b --brave-key your-token"
+```
+
+The token is sent in the `X-Subscription-Token` header. If no key is found,
+`learn` stops early with a clear message; the key is treated as a secret
+(`@AIPrivacy` — never logged).
+
+> You don't strictly need a key to evaluate the pipeline: the example in
+> [`examples/`](../examples/) was produced from seeded source URLs, and the tests
+> stub discovery behind the `SearchClient` interface. For a real run with **no
+> key and no network at all**, supply your own sources with `--input-file` (see
+> [Offline discovery](#offline-discovery-with---input-file)).
+
+---
+
+[← back to the README](../README.md)
diff --git a/docs/USAGE.md b/docs/USAGE.md
new file mode 100644
index 0000000..d1e6b00
--- /dev/null
+++ b/docs/USAGE.md
@@ -0,0 +1,143 @@
+# Using Skill3
+
+Every flag of `learn`, offline discovery with `--input-file`, choosing a synthesis
+model, and how the cutoff drives the search window. To install first, see
+[INSTALL.md](INSTALL.md); for why the pipeline is shaped this way, see
+[ARCHITECTURE.md](ARCHITECTURE.md).
+
+## Usage
+
+### Learn a skill
+
+```bash
+./gradlew run --args="learn mcp \
+ --target-model claude-opus-4-8 \
+ --llm-model qwen2.5-coder:7b \
+ --brave-key $BRAVE_SEARCH_API_KEY"
+```
+
+Common options for `learn`:
+
+| Option | Meaning | Default |
+|---|---|---|
+| `--target-model ` | Model the skill is *for*; used only to look up a knowledge cutoff. | `claude-opus-4-8` |
+| `--cutoff-time ` | Explicit cutoff override (wins over `--target-model`). | — |
+| `--strict-cutoff` | Hard-exclude sources at/before the cutoff. | off |
+| `--llm-model ` | Synthesis model name. | **required** |
+| `--llm-provider
` | `local` \| `openai` \| `anthropic`. | `local` |
+| `--llm-endpoint ` | OpenAI-compatible endpoint (local/openai). | `http://localhost:11434` |
+| `--llm-key ` | Key for hosted providers (`openai`: `LLM_API_KEY`; `anthropic`: `ANTHROPIC_API_KEY`). | env |
+| `--max-tokens ` | Max output tokens for synthesis. | `8192` |
+| `--temperature ` | Sampling temperature (local/openai only). | server default |
+| `--rich-context` | Feed more sources/excerpts to the model (suits big-context models). | off |
+| `--authoritative ` | Comma-separated hosts ranked first (e.g. `modelcontextprotocol.io,github.com`). | — |
+| `--verify` / `--no-verify` | Re-ground every claim against the sources (accuracy gate, one extra model call). | on for `openai`/`anthropic`, off for `local` |
+| `--brave-key ` | Brave Search key (or `BRAVE_SEARCH_API_KEY`). | env |
+| `--input-file ` | Offline discovery: a user-curated corpus file used instead of Brave (no key/network). See [Offline discovery](#offline-discovery-with---input-file). | — |
+| `--dry-run` | Stop after discovery + ranking; print the sources, dates and scores; write nothing. | off |
+| `--no-cache` | Bypass the on-disk cache of search results and fetched pages (`~/.skill3/cache`, 7-day TTL). | off |
+| `--output-dir ` | Where the skill is written. | `./skills/` |
+
+Output: `./skills//SKILL.md` (+ an `index.html` preview and a `run.json`
+provenance manifest recording the queries, the exact sources and scores that backed the
+skill, the verify/vet outcome, and per-phase timings).
+
+Discovery and model calls retry transient failures (connection errors, `429`, `5xx`) with
+exponential backoff (honoring `Retry-After`), and search results + fetched pages are cached
+under `~/.skill3/cache` (7-day TTL) so re-running a topic skips the network — pass `--no-cache`
+to force fresh fetches.
+
+### Offline discovery with `--input-file`
+
+`--input-file` replaces Brave with a **user-curated corpus file** you fill in
+yourself — the same role Brave plays (supplying source documents), but offline:
+no key, no network, fully reproducible. It slots in behind the same
+`SearchClient`/`PageFetcher` seams (`FileCorpus`), so everything downstream —
+date extraction, authority scoring, consensus, freshness, synthesis and vetting —
+runs exactly as it would for live pages.
+
+```bash
+./gradlew run --args="learn mcp \
+ --llm-model qwen2.5-coder:7b \
+ --input-file ./my-sources.txt"
+```
+
+**File format.** Documents are separated by a line that reads exactly
+`=== SOURCE ===`. Each starts with `key: value` headers (`url` required;
+`title` and `date` as `yyyy-MM-dd` optional), then a blank line, then the body.
+The body may be plain text, Markdown (fenced ```` ``` ```` code blocks and `#`
+headings are recognised), or raw HTML:
+
+````
+=== SOURCE ===
+url: https://modelcontextprotocol.io/specification
+title: MCP Specification (2026-03 revision)
+date: 2026-03-01
+
+# Resources
+The _meta field is now accepted on every request in the 2026-03 revision.
+
+```
+client.call("tools/list");
+```
+
+=== SOURCE ===
+url: https://github.com/org/repo/releases
+date: 2026-04-01
+
+Release notes describing the new behaviour and flags...
+````
+
+The whole file is treated as the curated result set (every document is used —
+the model's planned queries don't filter it down). Anything before the first
+`=== SOURCE ===` marker is ignored, so you can keep a comment at the top. A
+ready-to-copy template lives at
+[`examples/input-corpus-sample.txt`](../examples/input-corpus-sample.txt).
+
+### Choosing a synthesis model
+
+Synthesis is the quality bottleneck (see the examples below — the *same* sources, very
+different skills). Three providers, in order of fidelity to the local-first design:
+
+1. **Bigger local model (default, keeps the no-key design).** Just pull a stronger Ollama
+ model — no code, no key:
+ ```bash
+ ./gradlew run --args="learn mcp --llm-model qwen2.5-coder:32b --brave-key $BRAVE_SEARCH_API_KEY"
+ ```
+2. **Any OpenAI-compatible gateway** (OpenRouter, Together, Groq, …) — opt-in, breaks the
+ no-key property only when you use it:
+ ```bash
+ ./gradlew run --args="learn mcp --llm-provider openai \
+ --llm-endpoint https://openrouter.ai/api --llm-model \
+ --llm-key $LLM_API_KEY --rich-context --brave-key $BRAVE_SEARCH_API_KEY"
+ ```
+3. **Claude (native Anthropic SDK)** — highest quality. Uses the official
+ `anthropic-java` SDK and the Messages API (not an OpenAI shim):
+ ```bash
+ export ANTHROPIC_API_KEY=sk-ant-...
+ ./gradlew run --args="learn mcp --llm-provider anthropic \
+ --llm-model claude-opus-4-8 --rich-context --brave-key $BRAVE_SEARCH_API_KEY"
+ ```
+ `--temperature` is ignored for `anthropic` (Opus 4.8 rejects sampling parameters).
+
+### How the cutoff drives the search window
+
+The resolved cutoff (from `--target-model`, or `--cutoff-time` if given) becomes
+the **start** of the Brave discovery window; today is the end. For
+`claude-opus-4-8` (cutoff `2026-01`) a run today searches:
+
+```
+Cutoff: claude-opus-4-8 (2026-01)
+Search window: 2026-01-01to2026-06-22
+```
+
+So discovery skips what the model already knows and surfaces only what's new since
+its cutoff. Widen it for a given run with `--cutoff-time` (e.g. `--cutoff-time 2024-01`).
+
+> **Output quality scales with the synthesis model.** A small model (e.g.
+> `qwen2.5:3b`) hallucinates and conflates unrelated tools; a capable coder model
+> (e.g. `qwen2.5-coder:7b` or larger) produces accurate content from the same sources.
+
+---
+
+[← back to the README](../README.md)
diff --git a/docs/diagrams/llm-class-diagram.svg b/docs/diagrams/llm-class-diagram.svg
new file mode 100644
index 0000000..7a2081b
--- /dev/null
+++ b/docs/diagrams/llm-class-diagram.svg
@@ -0,0 +1,270 @@
+
+
diff --git a/docs/diagrams/model-class-diagram.svg b/docs/diagrams/model-class-diagram.svg
new file mode 100644
index 0000000..f40b86c
--- /dev/null
+++ b/docs/diagrams/model-class-diagram.svg
@@ -0,0 +1,153 @@
+
+
diff --git a/docs/diagrams/pipeline-class-diagram.svg b/docs/diagrams/pipeline-class-diagram.svg
new file mode 100644
index 0000000..d2aeef7
--- /dev/null
+++ b/docs/diagrams/pipeline-class-diagram.svg
@@ -0,0 +1,388 @@
+
+
diff --git a/docs/diagrams/pipeline-sequence-diagram.svg b/docs/diagrams/pipeline-sequence-diagram.svg
new file mode 100644
index 0000000..19e74a0
--- /dev/null
+++ b/docs/diagrams/pipeline-sequence-diagram.svg
@@ -0,0 +1,397 @@
+
+
diff --git a/llms-full.txt b/llms-full.txt
index 29821f7..be0598e 100644
--- a/llms-full.txt
+++ b/llms-full.txt
@@ -42,6 +42,19 @@ The following elements are well-tested core functionality. Make changes with ext
- **Note**: Accuracy gate that re-grounds claims against the sources. Only worthwhile with a capable model — a weak model rewrites rather than grounds. Keep the prompt strict about supported-claims-only and announced-vs-shipped.
+## 🔐 Contract-Frozen Signatures
+The following elements have frozen public API signatures. Internal implementation may be changed, but you MUST NOT alter method names, parameter types, parameter order, return types, or checked exceptions.
+
+### se.deversity.skill3.llm.ChatModel
+- **Reason**: The single seam every model-driven stage binds to — QueryPlanner, Synthesizer, Verifier and the self-correction Reviser all take this one interface, which is what lets one --llm-provider choice apply uniformly. Test fakes implement it directly, so changing the signature breaks every unit test that avoids a live model.
+
+### se.deversity.skill3.pipeline.PageFetcher
+- **Reason**: Fetch seam. Keeping page retrieval behind it is what lets extraction, date parsing and scoring be tested against HTML fixtures with no network, and it is the boundary at which --input-file replaces the network entirely.
+
+### se.deversity.skill3.pipeline.SearchClient
+- **Reason**: Discovery seam. BraveSearchClient (live) and FileCorpus (--input-file) both implement it, and isCuratedCorpus() is what tells the pipeline to skip LLM query planning. Removing the default method, or changing what it returns, silently re-enables planning for a corpus that is already the curated result set.
+
+
## 🧵 Thread-Safe by Design
These elements are explicitly designed to be thread-safe via the named strategy. Preserve the synchronization invariant on every change.
@@ -58,6 +71,29 @@ The following types are immutable. Never introduce non-final fields, setters, or
- **Note**: Immutable record; the sources list is defensively copied in the compact constructor.
+## Architectural Boundary Constraints
+Strict architectural layering must be respected. No illegal references or imports.
+
+### se.deversity.skill3.model.Source
+- **Belongs to Layer**: model
+- **Prohibited References**: se.deversity.skill3.pipeline, se.deversity.skill3.llm, se.deversity.skill3.cli, se.deversity.skill3.skillspector, se.deversity.skill3.web, se.deversity.skill3.net
+
+
+## Schema & Serialization Safety
+Schema and serialization compatibility must be strictly preserved.
+
+### se.deversity.skill3.model.RunManifest
+- Schema and serialization safety. Restrict changing serialization formats, database fields, or API models without a migration path.
+
+
+## ♻️ Idempotency Guarantees
+These operations are idempotent — calling multiple times must produce the same result as calling once.
+
+### se.deversity.skill3.llm.SkillMdPostProcessor.render(java.lang.String,se.deversity.skill3.model.ContextBundle,java.time.LocalDate)
+- Idempotency guaranteed. Multiple invocations must produce the same result as a single invocation.
+- **Reason**: SelfCorrectionLoop re-runs render() on its own output, so a revised draft passes through repeatedly. Every guarantee here must converge: exactly one frontmatter block and exactly one provenance footer, no matter how many revision rounds ran.
+
+
## 🔐 Security-Critical Code
These elements are security-critical. Do not weaken security properties. Every change requires security review.
@@ -95,4 +131,27 @@ The following elements must remain pure functions without side effects or mutati
### se.deversity.skill3.llm.NameSanitizer.sanitize(java.lang.String)
- **Requirement**: Mathematically pure function. No side effects.
+
+
+## Framework-Free Domain Entities
+The following elements are pure Domain Models. Do not import Spring, JPA/Hibernate, Jackson, or other framework packages.
+
+### se.deversity.skill3.model.Source
+- **Domain Boundary**: Framework-agnostic Domain Entity.
+
+
+## Load-Bearing Oddities
+These look wrong, redundant, or over-defensive and are deliberate. Refactoring is allowed only while the stated invariant survives.
+
+### se.deversity.skill3.pipeline.FileCorpus
+- This code is deliberate, not accidental.
+- **Invariant**: FileCorpus implements BOTH discovery seams — SearchClient and PageFetcher — and LearnCommand injects the same instance into both slots. That is the design, not a layering slip: it is what makes an offline --input-file run take the identical downstream path as a live Brave run, so the two modes cannot diverge.
+- **Breaks if changed**: the class is split into two collaborators, or either interface is dropped — offline runs then follow a different path from live ones and stop proving anything about the real pipeline
+- Edits are allowed as long as the invariant survives.
+
+### se.deversity.skill3.skillspector.InputVetter
+- This code is deliberate, not accidental.
+- **Invariant**: A quarantined source is dropped from the set handed to the synthesizer, but its finding is still recorded and still trips the run gate. Redaction runs FIRST and unconditionally, so a secret never reaches the model even when SkillSpector is unavailable — and when it is unavailable nothing is gated, because absence of findings is observed, never asserted.
+- **Breaks if changed**: quarantining is treated as resolving the finding, redaction is made conditional on the scanner being present, or a skipped scan is reported as clean
+- Edits are allowed as long as the invariant survives.
diff --git a/llms.txt b/llms.txt
index 09a0e7b..1b0429e 100644
--- a/llms.txt
+++ b/llms.txt
@@ -19,12 +19,26 @@ AI tools reading this file should respect the guardrails defined below. These ru
- [SkillMdPostProcessor](se.deversity.skill3.llm.SkillMdPostProcessor): Sensitivity: High. Note: Deterministically guarantees SKILL.md spec compliance; model output is never trusted. Changes risk emitting invalid frontmatter — keep the parsing and frontmatter synthesis covered by SkillMdPostProcessorTest.
- [Verifier](se.deversity.skill3.llm.Verifier): Sensitivity: High. Note: Accuracy gate that re-grounds claims against the sources. Only worthwhile with a capable model — a weak model rewrites rather than grounds. Keep the prompt strict about supported-claims-only and announced-vs-shipped.
+## 🔐 Contract-Frozen Signatures
+- [ChatModel](se.deversity.skill3.llm.ChatModel): The single seam every model-driven stage binds to — QueryPlanner, Synthesizer, Verifier and the self-correction Reviser all take this one interface, which is what lets one --llm-provider choice apply uniformly. Test fakes implement it directly, so changing the signature breaks every unit test that avoids a live model.
+- [PageFetcher](se.deversity.skill3.pipeline.PageFetcher): Fetch seam. Keeping page retrieval behind it is what lets extraction, date parsing and scoring be tested against HTML fixtures with no network, and it is the boundary at which --input-file replaces the network entirely.
+- [SearchClient](se.deversity.skill3.pipeline.SearchClient): Discovery seam. BraveSearchClient (live) and FileCorpus (--input-file) both implement it, and isCuratedCorpus() is what tells the pipeline to skip LLM query planning. Removing the default method, or changing what it returns, silently re-enables planning for a corpus that is already the curated result set.
+
## 🧵 Thread-Safe by Design
- [RetrievalService](se.deversity.skill3.pipeline.RetrievalService): Strategy: IMMUTABLE. Note: Collaborators (PageFetcher/HttpClient, DateExtractor, AuthorityScorer) are stateless/immutable; each fetch task builds its own Source and results are merged on the caller thread. Keep it that way — do not share mutable state between fetch tasks. The opt-in `sequential` mode only removes concurrency (fetches run on the caller thread); it cannot weaken the invariant — serial execution is strictly safer than the parallel default it replaces.
## ❄️ Immutable Types
- [ContextBundle](se.deversity.skill3.model.ContextBundle): immutable type — Immutable record; the sources list is defensively copied in the compact constructor.
+## Architectural Boundary Constraints
+- [Source](se.deversity.skill3.model.Source): Belongs to layer: `model`. Prohibited from referencing: [se.deversity.skill3.pipeline, se.deversity.skill3.llm, se.deversity.skill3.cli, se.deversity.skill3.skillspector, se.deversity.skill3.web, se.deversity.skill3.net]
+
+## Schema & Serialization Safety
+- [RunManifest](se.deversity.skill3.model.RunManifest): Schema/serialization safety guaranteed. Prohibit altering data formats or fields without migration plan. Reason: Serialized verbatim to run.json with a default ObjectMapper — the component names ARE the on-disk field names. Renaming, reordering into a different shape, or introducing a type that needs a Jackson module silently changes or breaks the provenance file that answers 'what produced this SKILL.md?'.
+
+## ♻️ Idempotency Guarantees
+- [SkillMdPostProcessor.render(java.lang.String,se.deversity.skill3.model.ContextBundle,java.time.LocalDate)](se.deversity.skill3.llm.SkillMdPostProcessor.render(java.lang.String,se.deversity.skill3.model.ContextBundle,java.time.LocalDate)): Idempotency guaranteed. Multiple invocations must produce the same result as one. Reason: SelfCorrectionLoop re-runs render() on its own output, so a revised draft passes through repeatedly. Every guarantee here must converge: exactly one frontmatter block and exactly one provenance footer, no matter how many revision rounds ran.
+
## 🔐 Security-Critical Code
- [AnthropicChatModel](se.deversity.skill3.llm.AnthropicChatModel): Security-critical code [Anthropic API credential handling and hosted-provider network egress]. Do not weaken security properties. Flag any change for security review.
- [LlmProviderFactory](se.deversity.skill3.llm.LlmProviderFactory): Security-critical code [LLM provider credential resolution and model selection]. Do not weaken security properties. Flag any change for security review.
@@ -36,4 +50,29 @@ AI tools reading this file should respect the guardrails defined below. These ru
## Deterministic Pure Functions
- [NameSanitizer.sanitize(java.lang.String)](se.deversity.skill3.llm.NameSanitizer.sanitize(java.lang.String)): Must remain a pure function. Forbid assignments to enclosing state, fields, or static members.
+
+## Framework-Free Domain Entities
+- [Source](se.deversity.skill3.model.Source): Pure Domain Model. Banned imports: [Spring, JPA, Hibernate, Jackson, etc.]. No external framework imports permitted.
+
+## Load-Bearing Oddities
+- [FileCorpus](se.deversity.skill3.pipeline.FileCorpus): Looks removable but is deliberate. Invariant: FileCorpus implements BOTH discovery seams — SearchClient and PageFetcher — and LearnCommand injects the same instance into both slots. That is the design, not a layering slip: it is what makes an offline --input-file run take the identical downstream path as a live Brave run, so the two modes cannot diverge. Breaks if changed: the class is split into two collaborators, or either interface is dropped — offline runs then follow a different path from live ones and stop proving anything about the real pipeline
+- [InputVetter](se.deversity.skill3.skillspector.InputVetter): Looks removable but is deliberate. Invariant: A quarantined source is dropped from the set handed to the synthesizer, but its finding is still recorded and still trips the run gate. Redaction runs FIRST and unconditionally, so a secret never reaches the model even when SkillSpector is unavailable — and when it is unavailable nothing is gated, because absence of findings is observed, never asserted. Breaks if changed: quarantining is treated as resolving the finding, redaction is made conditional on the scanner being present, or a skipped scan is reported as clean
+
+## What Skill3 is
+
+Skill3 relearns a technical skill for an AI agent: it discovers documentation, scores it for
+authority and freshness against a target model's knowledge cutoff, synthesizes an Agent Skills
+`SKILL.md`, and vets both the input corpus and the output skill. A generated skill is a
+**post-cutoff delta, not a primer**, and discovery is **topic-agnostic** — the model plans the
+searches, so nothing is hardcoded per topic.
+
+## Documentation
+
+- [README](README.md): what it is, the knowledge-cutoff premise, and the pipeline at a glance.
+- [Install](docs/INSTALL.md): requirements, build, SkillSpector setup, Brave Search key.
+- [Usage](docs/USAGE.md): every `learn` flag, offline `--input-file` runs, model choice, and how the cutoff drives the search window.
+- [Architecture](docs/ARCHITECTURE.md): pipeline structure, trust boundaries, failure handling, and diagrams generated from the source.
+- [Specification](docs/SPEC.md): the full behavioural spec.
+- [Development](docs/DEVELOPMENT.md): build gates, release process, and how these guardrails are maintained.
+- [Example output](docs/EXAMPLE-OUTPUT.md): complete runs, start to finish.
diff --git a/src/main/java/se/deversity/skill3/llm/ChatModel.java b/src/main/java/se/deversity/skill3/llm/ChatModel.java
index 57a975f..ffc5206 100644
--- a/src/main/java/se/deversity/skill3/llm/ChatModel.java
+++ b/src/main/java/se/deversity/skill3/llm/ChatModel.java
@@ -1,8 +1,14 @@
package se.deversity.skill3.llm;
+import se.deversity.vibetags.annotations.AIContract;
+
import java.io.IOException;
/** Minimal chat abstraction so synthesis can be unit-tested without a live LLM. */
+@AIContract(reason = "The single seam every model-driven stage binds to — QueryPlanner, "
+ + "Synthesizer, Verifier and the self-correction Reviser all take this one interface, "
+ + "which is what lets one --llm-provider choice apply uniformly. Test fakes implement "
+ + "it directly, so changing the signature breaks every unit test that avoids a live model.")
@FunctionalInterface
public interface ChatModel {
diff --git a/src/main/java/se/deversity/skill3/llm/SkillMdPostProcessor.java b/src/main/java/se/deversity/skill3/llm/SkillMdPostProcessor.java
index b6dc51e..1f1461b 100644
--- a/src/main/java/se/deversity/skill3/llm/SkillMdPostProcessor.java
+++ b/src/main/java/se/deversity/skill3/llm/SkillMdPostProcessor.java
@@ -2,6 +2,7 @@
import se.deversity.skill3.model.ContextBundle;
import se.deversity.vibetags.annotations.AICore;
+import se.deversity.vibetags.annotations.AIIdempotent;
import java.time.LocalDate;
import java.util.Arrays;
@@ -36,6 +37,10 @@ public final class SkillMdPostProcessor {
private SkillMdPostProcessor() {
}
+ @AIIdempotent(reason = "SelfCorrectionLoop re-runs render() on its own output, so a "
+ + "revised draft passes through repeatedly. Every guarantee here must converge: exactly "
+ + "one frontmatter block and exactly one provenance footer, no matter how many "
+ + "revision rounds ran.")
public static String render(String raw, ContextBundle bundle, LocalDate learnedDate) {
String content = unwrapFencedDocument(stripCodeFence(raw == null ? "" : raw.strip()));
String frontmatter = "";
diff --git a/src/main/java/se/deversity/skill3/model/RunManifest.java b/src/main/java/se/deversity/skill3/model/RunManifest.java
index 323519f..ba6e9bf 100644
--- a/src/main/java/se/deversity/skill3/model/RunManifest.java
+++ b/src/main/java/se/deversity/skill3/model/RunManifest.java
@@ -1,6 +1,7 @@
package se.deversity.skill3.model;
import org.jspecify.annotations.Nullable;
+import se.deversity.vibetags.annotations.AISchemaSafe;
import java.util.List;
import java.util.Map;
@@ -13,6 +14,10 @@
* only (dates as ISO strings), so it serializes with a default {@code ObjectMapper} and no extra
* Jackson modules. Lists are defensively copied so the record stays immutable.
*/
+@AISchemaSafe(reason = "Serialized verbatim to run.json with a default ObjectMapper — the "
+ + "component names ARE the on-disk field names. Renaming, reordering into a different "
+ + "shape, or introducing a type that needs a Jackson module silently changes or breaks "
+ + "the provenance file that answers 'what produced this SKILL.md?'.")
public record RunManifest(
String generatedBy,
String skill,
diff --git a/src/main/java/se/deversity/skill3/model/Source.java b/src/main/java/se/deversity/skill3/model/Source.java
index fdd890c..c4791f1 100644
--- a/src/main/java/se/deversity/skill3/model/Source.java
+++ b/src/main/java/se/deversity/skill3/model/Source.java
@@ -3,11 +3,18 @@
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
+import se.deversity.vibetags.annotations.AIArchitecture;
+import se.deversity.vibetags.annotations.AIDomainModel;
/**
* A single discovered documentation source, enriched as it flows through the
* ingestion pipeline. Plain mutable data carrier — stages set the scoring fields.
*/
+@AIDomainModel
+@AIArchitecture(belongsTo = "model",
+ cannotReference = {"se.deversity.skill3.pipeline", "se.deversity.skill3.llm",
+ "se.deversity.skill3.cli", "se.deversity.skill3.skillspector",
+ "se.deversity.skill3.web", "se.deversity.skill3.net"})
public class Source {
/** Source URL (also the identity key for consensus by host). */
diff --git a/src/main/java/se/deversity/skill3/pipeline/FileCorpus.java b/src/main/java/se/deversity/skill3/pipeline/FileCorpus.java
index c5a4e14..2cc35e4 100644
--- a/src/main/java/se/deversity/skill3/pipeline/FileCorpus.java
+++ b/src/main/java/se/deversity/skill3/pipeline/FileCorpus.java
@@ -11,6 +11,7 @@
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import se.deversity.vibetags.annotations.AILoadBearing;
/**
* Offline discovery from a user-curated input file — a no-network alternative to
@@ -48,6 +49,14 @@
* file may start with a comment. A body line that needs to read literally
* {@code === SOURCE ===} is the one thing the format cannot represent.
*/
+@AILoadBearing(
+ invariant = "FileCorpus implements BOTH discovery seams — SearchClient and PageFetcher — "
+ + "and LearnCommand injects the same instance into both slots. That is the design, "
+ + "not a layering slip: it is what makes an offline --input-file run take the "
+ + "identical downstream path as a live Brave run, so the two modes cannot diverge.",
+ breaksIf = "the class is split into two collaborators, or either interface is dropped — "
+ + "offline runs then follow a different path from live ones and stop proving anything "
+ + "about the real pipeline")
public final class FileCorpus implements SearchClient, PageFetcher {
/** A line that, trimmed, equals this starts a new document. */
diff --git a/src/main/java/se/deversity/skill3/pipeline/PageFetcher.java b/src/main/java/se/deversity/skill3/pipeline/PageFetcher.java
index 4b302a7..b8774d5 100644
--- a/src/main/java/se/deversity/skill3/pipeline/PageFetcher.java
+++ b/src/main/java/se/deversity/skill3/pipeline/PageFetcher.java
@@ -1,8 +1,13 @@
package se.deversity.skill3.pipeline;
+import se.deversity.vibetags.annotations.AIContract;
+
import java.io.IOException;
/** Fetches raw HTML for a URL; implemented by {@link HttpPageFetcher}. */
+@AIContract(reason = "Fetch seam. Keeping page retrieval behind it is what lets extraction, "
+ + "date parsing and scoring be tested against HTML fixtures with no network, and it is "
+ + "the boundary at which --input-file replaces the network entirely.")
@FunctionalInterface
public interface PageFetcher {
diff --git a/src/main/java/se/deversity/skill3/pipeline/SearchClient.java b/src/main/java/se/deversity/skill3/pipeline/SearchClient.java
index 4c86656..1cced94 100644
--- a/src/main/java/se/deversity/skill3/pipeline/SearchClient.java
+++ b/src/main/java/se/deversity/skill3/pipeline/SearchClient.java
@@ -1,9 +1,15 @@
package se.deversity.skill3.pipeline;
+import se.deversity.vibetags.annotations.AIContract;
+
import java.io.IOException;
import java.util.List;
/** Discovery search abstraction; implemented by {@link BraveSearchClient}. */
+@AIContract(reason = "Discovery seam. BraveSearchClient (live) and FileCorpus (--input-file) "
+ + "both implement it, and isCuratedCorpus() is what tells the pipeline to skip LLM query "
+ + "planning. Removing the default method, or changing what it returns, silently re-enables "
+ + "planning for a corpus that is already the curated result set.")
@FunctionalInterface
public interface SearchClient {
diff --git a/src/main/java/se/deversity/skill3/skillspector/InputVetter.java b/src/main/java/se/deversity/skill3/skillspector/InputVetter.java
index f7f090e..9a5b228 100644
--- a/src/main/java/se/deversity/skill3/skillspector/InputVetter.java
+++ b/src/main/java/se/deversity/skill3/skillspector/InputVetter.java
@@ -15,6 +15,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
+import se.deversity.vibetags.annotations.AILoadBearing;
/**
* Vets the untrusted retrieved corpus before it reaches the synthesizer LLM.
@@ -38,6 +39,14 @@
*
Sources are the pipeline's mutable carriers, so redaction is applied in place: the
* synthesizer that runs next only ever sees the sanitized, non-quarantined text.
*/
+@AILoadBearing(
+ invariant = "A quarantined source is dropped from the set handed to the synthesizer, but "
+ + "its finding is still recorded and still trips the run gate. Redaction runs "
+ + "FIRST and unconditionally, so a secret never reaches the model even when "
+ + "SkillSpector is unavailable — and when it is unavailable nothing is gated, "
+ + "because absence of findings is observed, never asserted.",
+ breaksIf = "quarantining is treated as resolving the finding, redaction is made "
+ + "conditional on the scanner being present, or a skipped scan is reported as clean")
public final class InputVetter {
/** Per-source scan filenames are {@code source-N.txt}; this maps a finding's file back to N. */