From e39aa243b9eabb5506427945d3aeb1113c70b338 Mon Sep 17 00:00:00 2001 From: Peter Isberg Date: Sat, 1 Aug 2026 13:41:50 +0200 Subject: [PATCH] feat: give agents a briefing, split the docs, and generate the diagrams from source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLAUDE.md was 100% generated. An agent opening this repo learned the security guardrails and nothing else — not what Skill3 is, not the two invariants the whole design serves, not where anything lives, not how to build it. Three changes, one theme: what an agent loads on every session should orient it. **A hand-written briefing.** CLAUDE.md now opens with what Skill3 is, the two invariants (a skill is a post-cutoff delta, not a primer; discovery is topic-agnostic), a table of where to read what, the build commands, and the five things that will bite someone changing this code. It sits outside the VIBETAGS markers, so every compile leaves it alone. **Guardrails split by tier.** Adding .claude/rules/ switches VibeTags to its indexed layout: the root keeps only the always-on safety tier inline (privacy, core, security) and indexes per-element detail into 20 scoped files that load when a matching source file is opened. That needs 1.0.0-RC8 — before it, the indexed root kept *nothing* inline, so an @AIPrivacy rule only loaded once an agent opened the file holding the key. RC1 -> RC8 also brings correct XML escaping in the generated regions. **README split by task.** 446 lines covering pitch, install, usage, development and sample output became a 171-line landing page plus docs/INSTALL.md, docs/USAGE.md, docs/DEVELOPMENT.md and docs/EXAMPLE-OUTPUT.md. Someone installing no longer reads past the release process. All 57 relative links verified to resolve. **Diagrams parsed, not drawn.** `./gradlew diagrams` renders four SVGs from src/main/java with code-karta, pinned from Maven Central so no local checkout is needed. The mermaid graph in ARCHITECTURE.md stays: it shows the pipeline as intended, these show it as written. Verified byte-identical across re-runs, so they stay out of diffs unless the structure really moved. No module diagram — Skill3 has no module-info.java, so it would show nothing. **Eight new annotations**, each recording an invariant the code cannot state itself: @AIContract on the three seams that make the pipeline testable without a network or a model; @AILoadBearing on FileCorpus (implements both discovery seams on purpose) and InputVetter (quarantine is mitigation, not amnesty; redaction is unconditional); @AISchemaSafe on RunManifest (its components are the run.json field names); @AIIdempotent on SkillMdPostProcessor.render(), which the self-correction loop re-runs on its own output; @AIDomainModel and @AIArchitecture on Source, mirroring the layering ArchitectureTest already enforces. Build green: Error Prone, PMD, SpotBugs, ArchUnit and the JaCoCo gate all pass. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0153QiTQsRQEikh9pHa5zrpC --- .claude/rules/.gitkeep | 2 + ...deversity-skill3-llm-AnthropicChatModel.md | 11 + .../se-deversity-skill3-llm-ChatModel.md | 11 + ...deversity-skill3-llm-LlmProviderFactory.md | 11 + .../se-deversity-skill3-llm-LocalLlmClient.md | 15 + .../se-deversity-skill3-llm-NameSanitizer.md | 14 + ...versity-skill3-llm-SkillMdPostProcessor.md | 15 + .../rules/se-deversity-skill3-llm-Verifier.md | 11 + ...se-deversity-skill3-model-ContextBundle.md | 11 + .../se-deversity-skill3-model-RunManifest.md | 10 + .../rules/se-deversity-skill3-model-Source.md | 14 + ...rsity-skill3-pipeline-BraveSearchClient.md | 15 + ...eversity-skill3-pipeline-CutoffResolver.md | 11 + ...rsity-skill3-pipeline-DiscoveryProvider.md | 11 + ...se-deversity-skill3-pipeline-FileCorpus.md | 12 + ...versity-skill3-pipeline-HttpPageFetcher.md | 11 + ...e-deversity-skill3-pipeline-PageFetcher.md | 11 + ...-deversity-skill3-pipeline-QueryPlanner.md | 11 + ...ersity-skill3-pipeline-RetrievalService.md | 11 + ...-deversity-skill3-pipeline-SearchClient.md | 11 + ...versity-skill3-skillspector-InputVetter.md | 12 + CLAUDE.md | 120 ++++-- README.md | 300 +------------ build.gradle | 59 ++- docs/ARCHITECTURE.md | 22 + docs/DEVELOPMENT.md | 81 ++++ docs/EXAMPLE-OUTPUT.md | 45 ++ docs/INSTALL.md | 69 +++ docs/USAGE.md | 143 +++++++ docs/diagrams/llm-class-diagram.svg | 270 ++++++++++++ docs/diagrams/model-class-diagram.svg | 153 +++++++ docs/diagrams/pipeline-class-diagram.svg | 388 +++++++++++++++++ docs/diagrams/pipeline-sequence-diagram.svg | 397 ++++++++++++++++++ llms-full.txt | 59 +++ llms.txt | 39 ++ .../se/deversity/skill3/llm/ChatModel.java | 6 + .../skill3/llm/SkillMdPostProcessor.java | 5 + .../deversity/skill3/model/RunManifest.java | 5 + .../se/deversity/skill3/model/Source.java | 7 + .../deversity/skill3/pipeline/FileCorpus.java | 9 + .../skill3/pipeline/PageFetcher.java | 5 + .../skill3/pipeline/SearchClient.java | 6 + .../skill3/skillspector/InputVetter.java | 9 + 43 files changed, 2115 insertions(+), 323 deletions(-) create mode 100644 .claude/rules/.gitkeep create mode 100644 .claude/rules/se-deversity-skill3-llm-AnthropicChatModel.md create mode 100644 .claude/rules/se-deversity-skill3-llm-ChatModel.md create mode 100644 .claude/rules/se-deversity-skill3-llm-LlmProviderFactory.md create mode 100644 .claude/rules/se-deversity-skill3-llm-LocalLlmClient.md create mode 100644 .claude/rules/se-deversity-skill3-llm-NameSanitizer.md create mode 100644 .claude/rules/se-deversity-skill3-llm-SkillMdPostProcessor.md create mode 100644 .claude/rules/se-deversity-skill3-llm-Verifier.md create mode 100644 .claude/rules/se-deversity-skill3-model-ContextBundle.md create mode 100644 .claude/rules/se-deversity-skill3-model-RunManifest.md create mode 100644 .claude/rules/se-deversity-skill3-model-Source.md create mode 100644 .claude/rules/se-deversity-skill3-pipeline-BraveSearchClient.md create mode 100644 .claude/rules/se-deversity-skill3-pipeline-CutoffResolver.md create mode 100644 .claude/rules/se-deversity-skill3-pipeline-DiscoveryProvider.md create mode 100644 .claude/rules/se-deversity-skill3-pipeline-FileCorpus.md create mode 100644 .claude/rules/se-deversity-skill3-pipeline-HttpPageFetcher.md create mode 100644 .claude/rules/se-deversity-skill3-pipeline-PageFetcher.md create mode 100644 .claude/rules/se-deversity-skill3-pipeline-QueryPlanner.md create mode 100644 .claude/rules/se-deversity-skill3-pipeline-RetrievalService.md create mode 100644 .claude/rules/se-deversity-skill3-pipeline-SearchClient.md create mode 100644 .claude/rules/se-deversity-skill3-skillspector-InputVetter.md create mode 100644 docs/DEVELOPMENT.md create mode 100644 docs/EXAMPLE-OUTPUT.md create mode 100644 docs/INSTALL.md create mode 100644 docs/USAGE.md create mode 100644 docs/diagrams/llm-class-diagram.svg create mode 100644 docs/diagrams/model-class-diagram.svg create mode 100644 docs/diagrams/pipeline-class-diagram.svg create mode 100644 docs/diagrams/pipeline-sequence-diagram.svg 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. */