a. Overview
In this lab
- Add five AI steps to the BoxOffice pipeline of lab 11: a pull-request reviewer, a test generator checked by mutation testing, an agent that proposes a fix when CI fails, release notes on every tag, and an explained canary verdict.
- Run every step through one small, provider-neutral client: a local model with Ollama (free, the default), Claude through the official SDK, or any OpenAI-compatible endpoint through LiteLLM.
- Harden the workflows: permissions, triggers, secrets, and a prompt-injection test that the reviewer must survive.
- Measure it: review precision, cost per PR, CI minutes and time to merge over a week of pull requests.
Lab 11 gave BoxOffice a pipeline that builds, tests, scans, signs and rolls out each change behind a canary. Lab 12 put a language model inside the product. This lab puts language models inside the pipeline β and treats them with the same discipline as any other dependency: least privilege, budgets, validation of every output, and numbers that say whether they help. In the story of the course it builds chapter 14: the decisions of code review, AI-generated tests, agents in CI, release and operate and security & governance.
The rule for the whole lab: AI proposes, rules and humans dispose. No step in this lab can merge, approve, deploy or roll back on its own. The model writes comments, patches and explanations; deterministic checks (tests, mutation score, policy thresholds) and people decide.
Before you start
| Time | 6β8 hours for parts 0β6 (one part per sitting is fine), plus ~15 minutes a day for one week in part 7 |
| You need | the tools of lab 0; a GitHub account (the repository on GitHub, the gh CLI logged in); for the free model, ~6 GB of free RAM and ~5 GB of disk (8 GB machines: see Troubleshooting) |
| Directory | every command runs from the root of your boxoffice/ repository |
| Costs | zero on the default path: the model runs on your machine (Ollama) or on the GitHub runner. What can cost money is listed in What costs money |
git --version # git 2.40+
gh --version # gh 2.x
gh auth status # "Logged in to github.com account <you>"
uv --version # uv 0.5+ (runs every Python script of this lab)
java -version # 21 or newer
mvn -v # Apache Maven 3.9+
docker info > /dev/null && echo "Docker is running" # mvn verify starts Testcontainers (parts 2-3)
ollama --version # installed in step 0.1
Start state β the end of lab 12: your boxoffice repository, pushed to GitHub, with
π boxoffice/
βββ .github/workflows/ci.yaml # lab 11: "name: CI", runs `mvn -B verify` for each service
βββ docker-compose.yaml infra/ load/ scripts/ results/
βββ π services/catalog/ services/inventory/ services/auth/ services/gateway/ ...
βββ π k8s/ ... # lab 10-11 (only needed for the optional live canary, step 5.4)
This lab uses mostly services/catalog (a Spring Boot 4.1.1 service on port 8082, see lab 0) and the CI workflow of lab 11. The Kubernetes cluster and the LiteLLM proxy of lab 12 are optional.
Catch up
Skipped or broke lab 12? Download boxoffice-after-lab-12.zip, unzip it and cd boxoffice. Then put it on GitHub β the AI steps run in GitHub Actions:
git init -b main && git add -A && git commit -m "boxoffice after lab 12"
gh repo create boxoffice --public --source . --push
This lab adds .github/ai/ (scripts, golden set, canary snapshots), .github/actions/ollama/, five ai-*.yaml workflows, CONVENTIONS.md, docs/adr/0001-schema-per-service.md, Dockerfile.ai-tools, k8s/catalog/analysis-explained.yaml and a unit test of catalog, and replaces services/catalog/pom.xml. All are shown in full on these pages and are all in boxoffice-after-lab-13.zip. If your CI workflow is not named CI, change workflows: [CI] in ai-fix.yaml (part 3).
Acronyms used in this lab
PR pull request Β· CI continuous integration Β· LLM large language model Β· ADR architecture decision record Β· PIT the mutation-testing tool for Java (pitest.org) Β· SHA the hash that names a Git commit Β· pp percentage points (5 % β 10 % is +5 pp) Β· SDK software development kit.
Slides
Click the slides and use β β to move through them; F goes full screen and P opens the presenter view, with the notes. Open the slides in a new tab
Theory connection
This lab pairs with the class AI-Assisted Delivery. Read its Code review and Security & governance pages before parts 1 and 6. It builds on the Delivery page of the cloud-native class and on the LLM gateway of the AI systems class.
What you will build
flowchart LR
dev([Developer]) -->|"opens PR"| pr["Pull request"]
pr --> rev["1 Β· AI review<br/>inline comments + cost"]
pr -->|"label ai-tests"| gen["2 Β· AI tests<br/>PIT must improve"]
pr --> ci["CI (lab 11)<br/>build Β· test Β· scan"]
ci -->|"failure"| fix["3 Β· AI fix agent<br/>draft PR"]
ci -->|"merge + tag"| rel["4 Β· AI release notes<br/>draft release"]
rel --> canary["Canary (lab 11)"]
canary --> expl["5 Β· Policy verdict<br/>+ AI explanation"]
expl -->|"promote / rollback"| prod([Production])
sec["6 Β· Hardening<br/>permissions Β· injection test"] -.-> rev
sec -.-> fix
meas["7 Β· Measure it<br/>precision Β· $/PR Β· minutes"] -.-> rev Every AI step has the same shape β a trigger, context gathering, one model call (or a bounded agent loop) with structured output, validation, and an action with the smallest possible permission:
| Part | Trigger | Context | Output | Validated by | Action (permission) |
|---|---|---|---|---|---|
| 1. AI review | pull_request | numbered diff, CONVENTIONS.md, ADRs | JSON findings | added lines only, thresholds, dedup | review comments (pull-requests: write) |
| 2. AI tests | label ai-tests | class, neighbours, existing tests | a test class | compiles, passes twice, PIT score rises | draft PR (separate job) |
| 3. Fix agent | workflow_run failed | failed log, repository | a patch | mvn verify, no test removed, src/ only | draft PR (separate job) |
| 4. Release notes | tag v* | commits, diffstat, migrations | JSON items with refs | every item cites a real commit | draft release (contents: write) |
| 5. Canary | analysis step | Prometheus numbers | explanation | must repeat the policy verdict | none: the policy decides |
| 6. Security | β | β | β | golden set + injection cases | β |
| 7. Measure it | weekly | PR history | report | β | β |
0. Setup: one client, three ways to reach a model
All scripts live in .github/ai/ of your repository and import one module, llm.py. It hides the provider behind a single function, complete_json(system, user, schema), that returns JSON validated against a schema plus the token usage and its cost. The provider is chosen with environment variables β the same names locally and in GitHub Actions (as repository variables):
| Variable | Default | Meaning |
|---|---|---|
LLM_PROVIDER | ollama | ollama (local, free), anthropic (Claude) or openai (any OpenAI-compatible endpoint, e.g. LiteLLM) |
LLM_MODEL | qwen2.5-coder-16k on ollama, claude-opus-5-5 on anthropic | the model id |
LLM_BASE_URL | http://localhost:11434/v1 on ollama | endpoint of ollama / openai |
LLM_API_KEY, ANTHROPIC_API_KEY | β | secrets, only for the paid or remote paths |
0.1 Pick a model
-
Install Ollama and start it:
# macOS brew install ollama && brew services start ollama # Linux curl -fsSL https://ollama.com/install.sh | shCheck that the server answers:
curl -s http://localhost:11434/api/versionprints{"version":"..."}. On Linux without systemd, start it by hand in another terminal withollama serve. -
Get the model β after you saved the files of step 0.2 (the script is one of them):
It downloads
qwen2.5-coder:7b(4.7 GB, once) and createsqwen2.5-coder-16k, the same model with a 16,384-token context window. Ollama's default window is 4,096 tokens, and a longer prompt is cut silently β the reviewer would lose its instructions on a big diff. You should see, at the end:
Nothing else to configure: ollama is the default provider. In GitHub Actions the workflows install Ollama on the runner with the composite action .github/actions/ollama (below) β free, but slow: a CPU-only runner needs minutes per call, where your laptop needs seconds. Expect lower review quality than a frontier model; part 6 measures how much lower.
- Create an API key in the Claude Console and set a monthly spend limit for its workspace (a few dollars is plenty for this lab).
- Locally:
export LLM_PROVIDER=anthropicandexport ANTHROPIC_API_KEY=<the key you just created>(never commit it; the lab 0.gitignorealready ignores.env). -
In GitHub: Settings β Secrets and variables β Actions, or with
gh:
The client calls the Messages API through the official anthropic SDK with structured outputs (output_config.format with a JSON schema), prompt caching of the system prompt, and the server-side refusal fallback (a beta) on models that support it. Check the models overview for current model ids and prices.
The LiteLLM proxy of lab 12 exposes an OpenAI-compatible endpoint in front of any provider. Locally: export LLM_PROVIDER=openai LLM_BASE_URL=http://localhost:4000/v1 LLM_MODEL=<a model alias of your proxy> and export LLM_API_KEY=<your LiteLLM virtual key>; curl -s -H "Authorization: Bearer $LLM_API_KEY" $LLM_BASE_URL/models lists the aliases.
GitHub-hosted runners cannot reach localhost on your laptop: in CI this path needs a LiteLLM proxy reachable from the internet (or a self-hosted runner). Set the repository variables LLM_PROVIDER=openai, LLM_BASE_URL, LLM_MODEL and the secret LLM_API_KEY (a virtual key with its own budget).
What about GitHub Models?
Earlier versions of this lab could use GitHub Models, a free inference API for Actions. GitHub retired it on 30 July 2026 (GitHub docs), so the free path is now Ollama.
What costs money
| Item | Cost |
|---|---|
| Ollama on your machine | free (electricity, ~5 GB of disk) |
| GitHub Actions minutes | free for public repositories; in a private repository they count against your plan's monthly minutes (GitHub billing) β the Ollama path uses several runner minutes per AI job |
Claude API (LLM_PROVIDER=anthropic, and path A of part 3) | paid per token; protect it with a workspace spend limit. Each job prints its cost in US$ |
| A LiteLLM proxy on the internet | whatever its upstream provider charges |
Course policy for this lab
Use your own key or a local model, never a teammate's or a company's. Never paste secrets, real customer data or personal data into prompts. Any commit that contains AI-generated code carries the trailer AI-assisted: <tool> (<model>) and every AI-opened PR carries the label ai-generated.
0.2 The shared client
Save the files below at the paths in their titles (the folder .github/ai/ is new). To avoid copying twenty files by hand, you can also take them from boxoffice-after-lab-13.zip β the state at the end of this lab β and copy only what you need:
curl -fsSL -o /tmp/lab13.zip https://insper.github.io/platform/2027.1/hands-on/13/boxoffice-after-lab-13.zip
unzip -oq /tmp/lab13.zip -d /tmp/lab13 # outside your repository: nothing to clean up in git status
cp -R /tmp/lab13/boxoffice/.github/ai /tmp/lab13/boxoffice/.github/actions .github/
#!/usr/bin/env bash
# Prepare the free local model of lab 13 (run it from the repository root):
#
# bash .github/ai/ollama-model.sh
#
# Pulls qwen2.5-coder:7b (4.7 GB) and creates `qwen2.5-coder-16k`: the same weights with a
# 16,384-token context window. Ollama's default window is 4,096 tokens, and a longer prompt
# (conventions + a big diff, or an agent's growing conversation) is cut SILENTLY.
set -euo pipefail
BASE="${OLLAMA_BASE_MODEL:-qwen2.5-coder:7b}"
NAME="${OLLAMA_MODEL_NAME:-qwen2.5-coder-16k}"
CTX="${OLLAMA_NUM_CTX:-16384}"
ollama pull "$BASE"
modelfile="$(mktemp)"
printf 'FROM %s\nPARAMETER num_ctx %s\n' "$BASE" "$CTX" > "$modelfile"
ollama create "$NAME" -f "$modelfile"
rm -f "$modelfile"
echo "ready: LLM_PROVIDER=ollama LLM_MODEL=$NAME (context $CTX tokens)"
name: Local model (Ollama)
description: >-
Free path of lab 13. Installs Ollama on the runner and prepares qwen2.5-coder-16k.
CPU only: expect minutes per call, not seconds.
runs:
using: composite
steps:
- name: Install and start Ollama
shell: bash
run: |
curl -fsSL https://ollama.com/install.sh | sh
if ! curl -sf http://localhost:11434/api/version > /dev/null; then
(ollama serve > "$RUNNER_TEMP/ollama.log" 2>&1 &)
fi
for _ in $(seq 1 30); do
curl -sf http://localhost:11434/api/version && break
sleep 1
done
- name: Prepare the model
shell: bash
run: bash .github/ai/ollama-model.sh
| .github/ai/llm.py | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | |
What to notice in llm.py:
| Design decision | Why |
|---|---|
system is stable, user carries the run's data | The system prompt (instructions + conventions) is identical on every run, so it can be cached: on Claude, cached input tokens are billed at a fraction of the normal price. The diff, logs and metrics change every run and go in user. |
| Structured output + schema validation | Claude constrains the answer to the JSON schema (output_config.format); the OpenAI-compatible path (Ollama, LiteLLM) uses JSON mode and validates with one repair attempt. Either way the caller gets a dict it can check, never free text it must parse. |
Every failure becomes an LLMError | A refusal, a truncated answer (max_tokens), no API key, a model server that is down: each job decides explicitly what happens then (usually: fail open, skip the AI step and say so in the summary). |
Usage with prices | Every job prints tokens and dollars in its summary. You cannot manage a cost you do not see. The prices are list prices when this lab was written β update PRICES from the provider's page. A local model shows n/a. |
| No retries here | The SDKs already retry 429 and 5xx with backoff; a second retry layer would multiply calls during an outage (the retry storm of Reliability). |
Checkpoint. Run the smoke test at the bottom of llm.py (the first uv run downloads the Python packages, ~10 s):
uv run --no-project --python 3.12 --with-requirements .github/ai/requirements.txt python .github/ai/llm.py
You should see (the greeting varies; the first call also loads the model into memory, so it takes longer):
provider=ollama model=qwen2.5-coder-16k
{"greeting": "Hello, BoxOffice team!"}
| Model | Calls | Input | Cache read | Cache write | Output | Time | Cost |
|---|---|---|---|---|---|---|---|
| `qwen2.5-coder-16k` | 1 | 81 | 0 | 0 | 15 | 1.9 s | n/a (local or unknown model) |
LLM error: APIConnectionError: Connection error. means Ollama is not running; NotFoundError ... model 'qwen2.5-coder-16k' not found means step 0.1 was skipped. See Troubleshooting.
0.3 The context: conventions and decisions
The reviewer is only as good as what it knows about your system. A generic model knows Java; it does not know that order must never write to the inventory schema. Write that knowledge down once β for humans first β in CONVENTIONS.md at the root of the repository:
# BoxOffice conventions
Read by humans and by the AI reviewer. Keep it short: every line costs tokens on every review.
## Architecture rules (blocker if violated)
- One PostgreSQL schema per service (`catalog`, `inventory`, `orders`, `payment`, ...).
A service reads and writes ONLY its own schema. Another service's data is reached
through its HTTP API or its events, never with SQL.
- Seat state (holds, sales) is owned by `inventory`. Any change to a seat goes through
inventory, inside a transaction that locks the row (`SELECT ... FOR UPDATE`) or uses
optimistic locking. Double booking is the worst bug this system can have.
- `order` publishes events through the transactional outbox, never directly to Kafka
inside a database transaction.
- Every outbound HTTP call (service-to-service or to the payment provider) has a connect
and a read timeout, and calls to `payment` are idempotent (Idempotency-Key header).
- Services trust identity only from the `X-User-*` headers set by the gateway.
## Code
- Java 21, Spring Boot 4.1, Spring Cloud 2025.1; DTOs are `record`s; no Lombok.
- Money is `long` cents or `BigDecimal`, never `double`.
- SQL uses bind parameters; never build SQL by string concatenation.
- No secrets in code or in application.yaml: read them from environment variables.
- Errors are RFC 9457 problem+json; `POST` that creates returns 201 with `Location`.
- Flyway migrations are append-only: never edit a migration that was merged.
## Tests
- Business rules in `inventory`, `order` and `payment` need unit tests for the error paths.
- No `Thread.sleep` in tests; inject a `Clock` for time.
Architecture decision records (ADRs) in docs/adr/ are also sent to the reviewer (the first 3,000 characters of each). An ADR is a short note of one decision: context, decision, consequences. Start with the one BoxOffice made in lab 1:
# ADR 0001 β One database schema per service
- Status: accepted
- Date: 2027-02-15
## Context
All BoxOffice services share one PostgreSQL instance (`db`, database `boxoffice`). Nothing in
PostgreSQL stops `order` from reading or updating `inventory.seats` directly, and a shortcut like
that would couple the two services' release cycles and bypass inventory's locking rules.
## Decision
Each service owns one schema (`catalog`, `inventory`, `orders`, `payment`, `notification`, `auth`)
and its Flyway migrations. A service reads and writes ONLY its own schema. Data owned by another
service is reached through that service's HTTP API or its events.
## Consequences
- A query across services becomes an API call or a read model built from events (slower, explicit).
- Each schema can move to its own database later without changing other services.
- Reviewers (human and AI) treat SQL that names another service's schema as a blocker.
0.4 Repository settings
Once per repository, from the repository root:
gh label create ai-generated --color 8250df --description "opened or written by an AI step"
gh label create ai-tests --color 0e8a16 --description "run the AI test generator on this PR"
# Workflow permissions: read-only by default, and allow Actions to open (draft) pull requests
gh api --method PUT "repos/{owner}/{repo}/actions/permissions/workflow" \
-f default_workflow_permissions=read -F can_approve_pull_request_reviews=true
{owner}/{repo} is filled in by gh from the current repository. The last command is the same as Settings β Actions β General β Workflow permissions: Read repository contents and Allow GitHub Actions to create and approve pull requests (GitHub has one checkbox for both; no workflow of this lab ever approves). Check it with gh api "repos/{owner}/{repo}/actions/permissions/workflow":
Parts
- AI pull-request review β structured findings as inline comments, noise control, cost in the job summary.
- AI-generated tests, judged by mutation testing β keep a generated test only if PIT says it catches more bugs.
- An agent that fixes the failing build β headless Claude Code or a 150-line agent loop; a draft PR for a human.
- AI release notes β grounded in commits, published as a draft.
- An explained canary verdict β the policy decides, the model explains.
- Security hardening β triggers, permissions, secrets, prompt injection, and a golden-set eval.
- Measure it β precision, $/PR, CI minutes and time to merge over a week.
- Quiz.
Troubleshooting
Each part has its own list for its specific errors; these are the ones that can appear anywhere.
| Symptom | Cause | Fix |
|---|---|---|
AI review skipped / LLM error: APIConnectionError: Connection error. | Ollama is not running (or LLM_BASE_URL points elsewhere) | curl -s http://localhost:11434/api/version; start it (brew services start ollama, sudo systemctl start ollama or ollama serve) |
NotFoundError ... model 'qwen2.5-coder-16k' not found | the 16k model was not created | bash .github/ai/ollama-model.sh; ollama list must show it |
| A local call takes minutes, or the machine swaps | not enough free RAM for a 7B model with a 16k window | close other programs; on an 8 GB machine use the 3B model: OLLAMA_BASE_MODEL=qwen2.5-coder:3b bash .github/ai/ollama-model.sh (same name, smaller model, lower quality) |
TypeError: Could not resolve authentication method | LLM_PROVIDER=anthropic without ANTHROPIC_API_KEY | export the key, or unset LLM_PROVIDER to go back to Ollama |
uv: command not found / No interpreter found for Python 3.12 | uv missing (lab 0) | install uv (lab 0); uv python install 3.12 |
| A workflow does not start at all | the file is not on the default branch yet (workflow_run, labels), or Actions are disabled | merge the workflows into main first; Settings β Actions β General β Allow all actions |
HTTP 403: Resource not accessible by integration | the job lacks a permission, or the repository default is read-only and the workflow did not declare it | keep the permissions: blocks of the files on these pages; run step 0.4 |
GitHub Actions is not permitted to create or approve pull requests | step 0.4 skipped | run the gh api --method PUT ... command of step 0.4 |
| A CI job with Ollama hits its timeout | CPU-only runner, large prompt (in a private repository the runner may also have less memory) | smaller diffs; the 3B model (add env: { OLLAMA_BASE_MODEL: "qwen2.5-coder:3b" } to the step that uses ./.github/actions/ollama); or a paid provider for that job |
Best practices in this lab
- AI proposes, rules decide β
.github/ai/review.pypostsevent: COMMENT, never an approval, and exits 0 when the model fails; why: Code review. - Privilege split by job β
ai-fix.yamlruns the model with a read-only token and opens the draft PR from a job with no model; why: Agents in CI. - Generated tests prove their value β
gen_tests.pykeeps a test only if it passes twice and adds 5 pp of mutation score; why: Testing. - The verdict is code; the model only explains β
canary_explain.pyreturns the exit code of the policy and discards an explanation that disagrees; why: Release & operate. - The AI tools have their own regression test β
eval_review.pyandai-eval.yamlrun the golden set when the prompt, conventions or model change; why: Security & governance.
Clean up
# stop the AI workflows from running on every PR (re-enable with `gh workflow enable`)
for w in ai-review.yaml ai-tests.yaml ai-fix.yaml ai-release-notes.yaml ai-eval.yaml; do gh workflow disable "$w"; done
# delete the branches the bots opened (their PRs were merged or closed)
git ls-remote --heads origin 'ai-fix/*' 'ai-tests/*' | awk '{print $2}' | sed 's|refs/heads/||' \
| xargs -r -n1 git push origin --delete
ollama stop qwen2.5-coder-16k # frees the RAM now; `ollama rm qwen2.5-coder-16k qwen2.5-coder:7b` frees the disk
If you used Claude, revoke the API key in the Claude Console (or keep it with its spend limit) and gh secret delete ANTHROPIC_API_KEY. Keep the files: they are part of your repository and of the team project.
Deliverable
-
.github/ai/withllm.py,diffutil.py,review.py,gen_tests.py,fix_agent.py,release_notes.py,canary_explain.py,eval_review.py,metrics.py,ollama-model.sh,requirements.txt, theevals/golden set and thecanary/snapshots. -
CONVENTIONS.mdwith at least five rules specific to your BoxOffice, and at least one ADR indocs/adr/. - Workflows
ai-review.yaml,ai-tests.yaml,ai-fix.yaml,ai-release-notes.yamlandai-eval.yaml, each with an explicitpermissions:block, passingactionlint(part 6). - One PR where the reviewer posted inline comments, with the cost table in the job summary.
- One draft PR opened by the test generator with a higher mutation score, or the job summary explaining why the tests were rejected.
- One draft PR opened by the fix agent for a build you broke on purpose (or the local run of part 3), reviewed (approved or rejected) by you with a written reason.
- A draft release with AI notes for a
v*tag. - The three canary verdicts (healthy, regression, low traffic) of part 5.
- The injection PR of part 6 and an
ai-evalrun, with the eval table of your model. -
results/lab13.mdwith the measurement table of part 7 and your conclusion: keep, tune or remove each AI step? - Tag the commit:
git tag lab13 && git push --tags; quiz answered.
Self-check before you submit: git status is clean; gh workflow list shows the five AI workflows active; gh run list --limit 20 shows at least one successful run of AI review, AI tests, AI release notes and AI reviewer eval; git log -p | grep -c 'sk-ant-' prints 0 (no Claude key was ever committed).
End state
π boxoffice/
βββ π .github/ai/ # new: llm.py, diffutil.py, review.py, gen_tests.py, fix_agent.py,
β βββ # release_notes.py, canary_explain.py, eval_review.py, metrics.py,
β βββ # ollama-model.sh, requirements.txt, evals/ (5 cases), canary/ (3 snapshots)
βββ .github/actions/ollama/action.yml # new: the free model on the runner
βββ .github/workflows/ai-review.yaml ai-tests.yaml ai-fix.yaml ai-release-notes.yaml ai-eval.yaml # new
βββ CONVENTIONS.md # new
βββ docs/adr/0001-schema-per-service.md # new
βββ Dockerfile.ai-tools # new (optional live canary, step 5.4)
βββ k8s/catalog/analysis-explained.yaml # new (optional, step 5.4)
βββ services/catalog/pom.xml # replaced: lab 11 pom + PIT plugin
βββ services/catalog/src/test/java/boxoffice/catalog/CatalogServiceTest.java # new
βββ results/lab13.md
This is the last lab: the same pipeline, conventions and measurements carry into the team project.