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 @@ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![CI](https://github.com/PIsberg/skill3/actions/workflows/ci.yml/badge.svg)](https://github.com/PIsberg/skill3/actions/workflows/ci.yml) [![Java 25](https://img.shields.io/badge/Java-25-orange?logo=openjdk)](https://adoptium.net/) -[![Static analysis: Error Prone · PMD · SpotBugs · ArchUnit](https://img.shields.io/badge/static%20analysis-Error%20Prone%20%C2%B7%20PMD%20%C2%B7%20SpotBugs%20%C2%B7%20ArchUnit-success)](#development) +[![Static analysis: Error Prone · PMD · SpotBugs · ArchUnit](https://img.shields.io/badge/static%20analysis-Error%20Prone%20%C2%B7%20PMD%20%C2%B7%20SpotBugs%20%C2%B7%20ArchUnit-success)](docs/DEVELOPMENT.md) [![Donate](https://img.shields.io/badge/Donate-PayPal-0079C1?logo=paypal&logoColor=white)](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. - -![Why the Trump demo is honest: with a 2026-01 cutoff, the only legitimate way to produce SKILL-trump.md is to run the real pipeline — Brave fetches post-cutoff sources, the local LLM synthesizes them, no current events fabricated from memory.](assets/trump-demo-note.png) - -- **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 | +|---|---| +| [Class diagram of the model package: Source, ContextBundle, Cutoff, RunManifest](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`. | +| [Class diagram of the llm package showing the ChatModel interface and its implementations](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. | +| [Class diagram of the pipeline package: discovery and ingestion collaborators](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. + +![Why the Trump demo is honest: with a 2026-01 cutoff, the only legitimate way to produce SKILL-trump.md is to run the real pipeline — Brave fetches post-cutoff sources, the local LLM synthesizes them, no current events fabricated from memory.](../assets/trump-demo-note.png) + +- **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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +se.deversity.skill3.llm + +implements + +implements + +model + +model + + AnthropicChatModel [CLASS] + + AnthropicChatModel + + OAUTH_BETA_HEADER: String + client: AnthropicClient + model: String + maxTokens: long + + withApiKey(String, String,… + withSubscription(String, S… + complete(String, String): … + + + ChatModel [INTERFACE] + + «interface» + ChatModel + + complete(String, String): … + + + EvidenceSelector [CLASS] + + EvidenceSelector + + topByRelevance(List<String… + score(String, Set<String>)… + topicTokens(String): Set<S… + + + LlmProviderFactory [CLASS] + + LlmProviderFactory + + isCapable(String): boolean + create(Config): ChatModel + resolveKey(String, String)… + + + LocalLlmClient [CLASS] + + LocalLlmClient + + DEFAULT_MAX_TOKENS: int + RETRY: HttpRetry + endpoint: String + model: String + http: HttpClient + maxTokens: int + …(+3 more) + + defaultClient(): HttpClient + complete(String, String): … + + + NameSanitizer [CLASS] + + NameSanitizer + + MAX: int + FALLBACK: String + + sanitize(String): String + + + PromptFraming [CLASS] + + PromptFraming + + MARKER_RUN: Pattern + + neutralizeMarkers(String):… + + + SkillMdPostProcessor [CLASS] + + SkillMdPostProcessor + + DESC_MAX: int + DESC_MIN: int + NAME: Pattern + DESCRIPTION: Pattern + MARKER: Pattern + LABEL: Pattern + …(+3 more) + + render(String, ContextBund… + withAttribution(String): S… + sanitizeDescription(String… + isUsableDescription(String… + deriveDescription(String, … + cleanBody(String): String + …(+6 more) + + + Synthesizer [CLASS] + + Synthesizer + + DEFAULT_MAX_SOURCES: int + DEFAULT_MAX_EXCERPTS: int + DEFAULT_MAX_CODE: int + SYSTEM: String + model: ChatModel + maxSources: int + …(+2 more) + + synthesize(ContextBundle):… + buildUserPrompt(ContextBun… + appendList(StringBuilder, … + + + Verifier [CLASS] + + Verifier + + MAX_SOURCES: int + MAX_EXCERPTS: int + MAX_CODE: int + SYSTEM: String + model: ChatModel + + verify(String, ContextBund… + sourcesBlock(ContextBundle… + + +Legend + +implements + +has + + + Created with https://github.com/PIsberg/codekarta + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +se.deversity.skill3.model + + ContextBundle [CLASS] + + ContextBundle + + + Cutoff [CLASS] + + Cutoff + + + RunManifest [CLASS] + + RunManifest + + + Source [CLASS] + + Source + + url: String + title: String + excerpts: List<String> + codeBlocks: List<String> + published: LocalDate + authority: double + …(+4 more) + + toString(): String + + + + Created with https://github.com/PIsberg/codekarta + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +se.deversity.skill3.pipeline + +implements + +implements + +delegate + +cache + +implements + +delegate + +cache + +implements + +implements + +implements + +consensus + +freshness + +search + +fetcher + +dates + +authority + + AuthorityScorer [CLASS] + + AuthorityScorer + + LOW: Set<String> + authoritative: Set<String> + + score(String): double + matches(String, String): b… + host(String): String + + + BraveSearchClient [CLASS] + + BraveSearchClient + + ENDPOINT: String + RETRY: HttpRetry + apiKey: String + freshness: String + http: HttpClient + mapper: ObjectMapper + + search(String, int): List<… + parse(String): List<String> + + + SearchClient [INTERFACE] + + «interface» + SearchClient + + search(String, int): List<… + isCuratedCorpus(): boolean + + + CachingPageFetcher [CLASS] + + CachingPageFetcher + + delegate: PageFetcher + cache: DiskCache + + fetch(String): String + + + PageFetcher [INTERFACE] + + «interface» + PageFetcher + + fetch(String): String + + + DiskCache [CLASS] + + DiskCache + + dir: Path + ttl: Duration + + get(String): Optional<Stri… + put(String, String): void + key(String): String + + + CachingSearchClient [CLASS] + + CachingSearchClient + + delegate: SearchClient + cache: DiskCache + qualifier: String + + search(String, int): List<… + isCuratedCorpus(): boolean + + + ConsensusValidator [CLASS] + + ConsensusValidator + + HIGH_AUTHORITY: double + minAgreement: int + + annotate(List<Source>): vo… + hostKey(Source): String + normalize(String): String + + + CutoffResolver [CLASS] + + CutoffResolver + + TABLE: Map<String,YearMont… + + resolve(String, String): C… + + + DateExtractor [CLASS] + + DateExtractor + + META_SELECTORS: String + UPDATED_TIME_SELECTOR: Str… + FALLBACK_FORMATS: DateTime… + JSON_LD_DATE: Pattern + + extract(Document): LocalDa… + parse(String): LocalDate + + + DiscoveryProvider [CLASS] + + DiscoveryProvider + + fromInputFile(Path): Sourc… + brave(String, String, Disk… + + + FileCorpus [CLASS] + + FileCorpus + + DELIMITER: String + HEADER: Pattern + HTML_TAG: Pattern + HEADING: Pattern + htmlByUrl: Map<String,Stri… + + load(Path): FileCorpus + parse(String): FileCorpus + search(String, int): List<… + isCuratedCorpus(): boolean + fetch(String): String + splitRecords(String): List… + …(+5 more) + + + FreshnessFilter [CLASS] + + FreshnessFilter + + RECENCY_POST_CUTOFF: double + RECENCY_POST_FLOOR: double + RECENCY_PRE_CUTOFF: double + RECENCY_UNDATED: double + cutoff: Cutoff + strict: boolean + …(+1 more) + + apply(List<Source>): List<… + recency(Source): double + postCutoffWeight(LocalDate… + + + HttpPageFetcher [CLASS] + + HttpPageFetcher + + UA: String + MAX_REDIRECTS: int + MAX_BYTES: int + RETRY: HttpRetry + http: HttpClient + + fetch(String): String + send(URI): HttpResponse<St… + boundedBody(): HttpRespons… + charset(String): Charset + validate(String): URI + isUniqueLocalV6(InetAddres… + + + IngestionPipeline [CLASS] + + IngestionPipeline + + consensus: ConsensusValida… + freshness: FreshnessFilter + + ingest(List<Source>): List… + + + QueryPlanner [CLASS] + + QueryPlanner + + DEFAULT_MAX_QUERIES: int + LEADING_MARKER: Pattern + SYSTEM: String + model: ChatModel + maxQueries: int + + plan(String, Cutoff, Local… + scopeAll(List<String>, Str… + scope(String, String): Str… + parse(String): List<String> + + + RetrievalService [CLASS] + + RetrievalService + + MAX_EXCERPTS: int + MAX_CODE_BLOCKS: int + MAX_CODE_BLOCK_CHARS: int + search: SearchClient + fetcher: PageFetcher + dates: DateExtractor + …(+2 more) + + retrieve(String, int): Lis… + retrieve(List<String>, int… + fetchAll(List<String>): Li… + fetchConcurrently(List<Str… + fetchSerially(List<String>… + fetchOne(String): FetchRes… + …(+2 more) + + +Legend + +implements + +has + + + Created with https://github.com/PIsberg/codekarta + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +catch(InterruptedException) + + +catch(InterruptedException) + + +catch(UnknownHostException) + + +catch(IOException) + + +catch(Exception) + + +catch(Exception) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +1: host + +2: matches + +3: matches + +1: parse + +1: key + +1: key + +1: hostKey + +1: host + +2: normalize + +3: normalize + +1: parse + +2: parse + +3: parse + +4: parse + +1: load + +1: parse + +1: splitRecords + +1: stripCr + +2: stripCr + +3: stripCr + +4: parseRecord + +1: escape + +2: escape + +1: markdownToHtml + +1: flushPara + +1: escape + +2: escape + +3: flushPara + +4: flushPara + +5: escape + +6: escape + +7: flushPara + +1: recency + +1: postCutoffWeight + +1: validate + +1: isUniqueLocalV6 + +2: send + +1: boundedBody + +1: charset + +3: validate + +1: parse + +2: scopeAll + +1: scope + +1: fetchAll + +1: fetchConcurrently + +1: fetchOne + +1: extractContent + +1: truncate + +2: fetchSerially + +1: fetchOne + + se.deversity.skill3.pipeline.AuthorityScorer + + se.deversity.skill3.pipeline + AuthorityScorer + + + se.deversity.skill3.pipeline.BraveSearchClient + + se.deversity.skill3.pipeline + BraveSearchClient + + + se.deversity.skill3.pipeline.CachingPageFetcher + + se.deversity.skill3.pipeline + CachingPageFetcher + + + DiskCache + + DiskCache + + + se.deversity.skill3.pipeline.CachingSearchClient + + se.deversity.skill3.pipeline + CachingSearchClient + + + se.deversity.skill3.pipeline.ConsensusValidator + + se.deversity.skill3.pipeline + ConsensusValidator + + + AuthorityScorer + + AuthorityScorer + + + se.deversity.skill3.pipeline.DateExtractor + + se.deversity.skill3.pipeline + DateExtractor + + + se.deversity.skill3.pipeline.DiscoveryProvider + + se.deversity.skill3.pipeline + DiscoveryProvider + + + FileCorpus + + FileCorpus + + + se.deversity.skill3.pipeline.FileCorpus + + se.deversity.skill3.pipeline + FileCorpus + + + se.deversity.skill3.pipeline.FreshnessFilter + + se.deversity.skill3.pipeline + FreshnessFilter + + + se.deversity.skill3.pipeline.HttpPageFetcher + + se.deversity.skill3.pipeline + HttpPageFetcher + + + se.deversity.skill3.pipeline.QueryPlanner + + se.deversity.skill3.pipeline + QueryPlanner + + + se.deversity.skill3.pipeline.RetrievalService + + se.deversity.skill3.pipeline + RetrievalService + + + Created with https://github.com/PIsberg/codekarta + + + 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. */