Skip to content

Commit c1c66c9

Browse files
tomas-samekTomáš Samek
andauthored
build(archetype): gate bundled agent-skill docs against canonical drift (#408) (#410)
Co-authored-by: Tomáš Samek <jerry.samek@gmail.com>
1 parent d09a154 commit c1c66c9

6 files changed

Lines changed: 230 additions & 47 deletions

File tree

tiko-archetype/pom.xml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@
1616
<name>Tiko DI - Archetype</name>
1717
<description>Maven archetype that scaffolds a runnable single-module Tiko DI project</description>
1818

19+
<!-- Test-only deps: the doc-sync gate (#408) verifies the archetype-bundled agent-skill
20+
docs stay in sync with their canonical repo-root sources. Versions are BOM-managed. -->
21+
<dependencies>
22+
<dependency>
23+
<groupId>org.junit.jupiter</groupId>
24+
<artifactId>junit-jupiter</artifactId>
25+
<scope>test</scope>
26+
</dependency>
27+
<dependency>
28+
<groupId>org.assertj</groupId>
29+
<artifactId>assertj-core</artifactId>
30+
<scope>test</scope>
31+
</dependency>
32+
</dependencies>
33+
1934
<build>
2035
<extensions>
2136
<extension>
@@ -33,6 +48,38 @@
3348
</plugin>
3449
</plugins>
3550
</pluginManagement>
51+
52+
<plugins>
53+
<!-- The maven-archetype packaging lifecycle does not bind compile/test phases
54+
(archetypes are resource-only), so the doc-sync gate's test (#408) is wired
55+
explicitly: compile src/test/java, then run it under surefire. -->
56+
<plugin>
57+
<groupId>org.apache.maven.plugins</groupId>
58+
<artifactId>maven-compiler-plugin</artifactId>
59+
<executions>
60+
<execution>
61+
<id>doc-sync-test-compile</id>
62+
<phase>test-compile</phase>
63+
<goals>
64+
<goal>testCompile</goal>
65+
</goals>
66+
</execution>
67+
</executions>
68+
</plugin>
69+
<plugin>
70+
<groupId>org.apache.maven.plugins</groupId>
71+
<artifactId>maven-surefire-plugin</artifactId>
72+
<executions>
73+
<execution>
74+
<id>doc-sync-test</id>
75+
<phase>test</phase>
76+
<goals>
77+
<goal>test</goal>
78+
</goals>
79+
</execution>
80+
</executions>
81+
</plugin>
82+
</plugins>
3683
<!--
3784
The maven-archetype packaging binds archetype:integration-test to the
3885
integration-test phase by default. The IT regenerates the archetype against

tiko-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@
1212
</includes>
1313
</fileSet>
1414

15-
<!-- AI assistant context — committed by default; users can ignore via .gitignore. -->
16-
<fileSet filtered="true" encoding="UTF-8">
15+
<!-- AI assistant context — committed by default; users can ignore via .gitignore.
16+
NOT filtered: the bundled skills are verbatim copies of the canonical repo skills
17+
(kept in sync by ArchetypeBundledSkillsInSyncTest) and contain literal ${VAR:default}
18+
config examples that must pass through Velocity untouched. They use no archetype
19+
properties, so filtering would only risk mangling those examples. -->
20+
<fileSet filtered="false" encoding="UTF-8">
1721
<directory>.ai-skills</directory>
1822
<includes>
1923
<include>**/*.md</include>

tiko-archetype/src/main/resources/archetype-resources/.ai-skills/tiko-build/SKILL.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ description: Use when scaffolding or extending a service built on Tiko DI. Decis
88
> Tiko orchestrates, it doesn't bundle — direct access, compile-time safe,
99
> nothing wrapped.
1010
11-
This file is the **operational distillation** of the orchestrator-model doc
12-
([`docs/orchestrator-model.md`](https://github.com/tomas-samek/tiko-di/blob/main/docs/orchestrator-model.md)
13-
in the framework repo). That long doc is the source of truth for prose; this
14-
file is the shape an agent reads to act.
11+
This file is the **operational distillation** of
12+
[`docs/orchestrator-model.md`](https://github.com/tomas-samek/tiko-di/blob/main/docs/orchestrator-model.md). The long
13+
doc is the source of truth for prose; this file is the shape an agent reads
14+
to act. **When adding a recipe, update both** — the table here, and the
15+
recipe section there.
1516

1617
## The rule
1718

@@ -21,7 +22,7 @@ When a user says *"I need X"*, classify X into one of three buckets:
2122
|---|---|---|
2223
| **Core** | container, scopes, event bus, compile-time wiring, lifecycle | Use a tiko primitive directly. |
2324
| **Plug in** | any integration with an external system (HTTP, DB, cache, templating, scheduling, retry, observability, security, SDK clients) | Bring the library; expose it as a `@Produces` value; consume as a constructor parameter. |
24-
| **Open** | extending the event model itself (new async modes, scheduling-as-event, retry-as-loop) | Open an issue against tiko-di. Don't invent. |
25+
| **Open** | extending the event model itself (new async modes, scheduling-as-event, retry-as-loop) | File an issue. Don't invent. |
2526

2627
**Default if uncertain: Plug in.** Never search for "tiko's equivalent of
2728
Spring's X" — that frame is the failure mode this skill exists to prevent.
@@ -136,7 +137,6 @@ public record DbConfig(
136137
@Default("4") int poolSize) {}
137138
```
138139

139-
#[[
140140
The `application.yml` that binds against them — section names match the prefix
141141
and field names, `${VAR:default}` for environment overrides, `poolSize`
142142
camelCase exactly as declared:
@@ -151,7 +151,6 @@ app: # @Configuration(prefix = "app")
151151
password: ${DB_PASSWORD:}
152152
poolSize: 4 # exact key — NOT pool-size / pool_size
153153
```
154-
]]#
155154
156155
Read it with `Tiko.create(ConfigSources.classpath("application.yml"))` and
157156
inject `AppConfig` (or a nested record) as a constructor parameter.

tiko-archetype/src/main/resources/archetype-resources/.ai-skills/tiko-cookbook-extension/SKILL.md

Lines changed: 59 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ description: Use when adding a new integration recipe to the tiko-build cookbook
55

66
# tiko-cookbook-extension
77

8-
This file is the **operational distillation** of the cookbook-extension
9-
doc in the framework repo
10-
(https://github.com/tomas-samek/tiko-di/blob/main/docs/cookbook-extension.md).
11-
The long doc is the source of truth for prose; this file is the shape an
12-
agent reads to act.
8+
This file is the **operational distillation** of
9+
[`docs/cookbook-extension.md`](https://github.com/tomas-samek/tiko-di/blob/main/docs/cookbook-extension.md). The long
10+
doc is the source of truth for prose; this file is the shape an agent
11+
reads to act. When this skill teaches the cookbook to grow, both files
12+
need to grow together — update both, or the future-you reading the agent
13+
side won't know why the human side disagrees.
1314

1415
## The rule — ask, don't fabricate
1516

@@ -45,7 +46,9 @@ and write the question to the user.
4546
## When to use this skill
4647

4748
- The user wants to integrate tiko with a library or framework the
48-
[tiko-build cookbook](../tiko-build/SKILL.md) doesn't cover.
49+
[tiko-build cookbook](../tiko-build/SKILL.md) and
50+
[`docs/orchestrator-model.md`](https://github.com/tomas-samek/tiko-di/blob/main/docs/orchestrator-model.md) don't
51+
cover.
4952
- A contributor wants to add a new recipe to the cookbook.
5053
- You (the agent) notice the cookbook is silent on a question the user
5154
is asking and want to propose adding it.
@@ -55,14 +58,15 @@ the user didn't ask for, generic refactoring of the cookbook.
5558

5659
## Step 1 — gate: does this even belong in the cookbook?
5760

58-
Check the proposed addition against the three buckets
59-
(https://github.com/tomas-samek/tiko-di/blob/main/docs/orchestrator-vocabulary.md):
61+
Check the proposed addition against
62+
[`docs/orchestrator-vocabulary.md`](https://github.com/tomas-samek/tiko-di/blob/main/docs/orchestrator-vocabulary.md)'s
63+
three buckets:
6064

6165
| Bucket | What to do |
6266
|---|---|
63-
| **Core** (container, scopes, event bus, compile-time wiring, lifecycle) | **Not a cookbook entry.** Surface to the user — this is a framework-feature discussion, not an integration recipe. File an issue against `tomas-samek/tiko-di` if it's a real gap. |
67+
| **Core** (container, scopes, event bus, compile-time wiring, lifecycle) | **Not a cookbook entry.** Surface to the user — this is a framework-feature discussion, not an integration recipe. File an issue if it's a real gap. |
6468
| **Plug in** (HTTP, DB, cache, templates, scheduling, retry, SDK clients, security) | **Proceed to step 2.** This is what the cookbook is for. |
65-
| **Open design questions** (extends the event model itself — new async modes, scheduling-as-event, retry-as-loop) | **Not a cookbook entry.** Surface to the user — open an issue against `tomas-samek/tiko-di`; the cookbook can't make this decision. |
69+
| **Open design questions** (extends the event model itself — new async modes, scheduling-as-event, retry-as-loop) | **Not a cookbook entry.** Surface to the user — open an issue against tiko-di; the cookbook can't make this decision. |
6670

6771
If you're not sure which bucket — that's a step-1 ask. Don't pick.
6872

@@ -82,11 +86,12 @@ Before any code, get answers from the user:
8286
needed. If not, ask whether the recipe should add one and what
8387
`close()`-equivalent it should call.
8488
4. **Singleton vs per-request.** Most plug-in libraries are
85-
`SINGLETON`. If the user has a use case for `EVENT`-scoped, ask
86-
explicitly — the proxy generation has rules they should know about.
87-
5. **Overlap with existing recipes.** Read the cookbook table in
88-
[`tiko-build/SKILL.md`](../tiko-build/SKILL.md). If the new library
89-
competes with an existing recipe (e.g. Vert.x vs Javalin), ask:
89+
`SINGLETON`. If the user has a use case for `EVENT`-scoped (e.g.
90+
per-request DB connection wrappers), ask explicitly — the proxy
91+
generation has rules they should know about.
92+
5. **Overlap with existing recipes.** Search the cookbook table in
93+
`tiko-build/SKILL.md`. If the new library competes with an existing
94+
recipe (e.g. user wants Vert.x and the cookbook has Javalin), ask:
9095
replace? augment with a *choose-one* note? sit alongside?
9196
6. **Out-of-scope concerns.** Distributed tracing? Connection pooling
9297
knobs? Async semantics? Ask which the user cares about for this
@@ -99,8 +104,8 @@ writing the recipe with it.
99104
## Step 3 — the canonical recipe template
100105

101106
Every cookbook entry follows the same five-element shape. Mirror it.
102-
The three worked examples below are from the canonical cookbook in the
103-
framework repo — read them before writing yours.
107+
The three worked examples below are from the canonical cookbook — read
108+
them before writing yours.
104109

105110
### The shape
106111

@@ -112,7 +117,7 @@ framework repo — read them before writing yours.
112117
| **Why-this-instead-of-bundling** | One sentence in the orchestrator-model voice. Not "tiko's equivalent of X." Name the tiko-native primitive. |
113118
| **Reference link** | Either a worked example in `tiko-examples/` if one exists, or the upstream library doc. |
114119

115-
### Worked example 1 — HikariCP `DataSource`
120+
### Worked example 1 — HikariCP `DataSource` (canonical, see `docs/orchestrator-model.md` §3.1)
116121

117122
```java
118123
@Component(scope = Scope.SINGLETON)
@@ -134,10 +139,10 @@ public class DataSourceFactory {
134139
}
135140
```
136141

137-
`HikariDataSource` is `AutoCloseable` → no `@PreDestroy` needed.
138-
Reference: https://github.com/tomas-samek/tiko-di/blob/main/tiko-examples/15_quickstart/src/main/java/io/tiko/examples/quickstart/DataSourceFactory.java
142+
`HikariDataSource` is `AutoCloseable` → no `@PreDestroy` needed. Reference:
143+
[`DataSourceFactory.java`](https://github.com/tomas-samek/tiko-di/blob/main/tiko-examples/15_quickstart/src/main/java/io/tiko/examples/quickstart/DataSourceFactory.java).
139144

140-
### Worked example 2 — Javalin HTTP server
145+
### Worked example 2 — Javalin HTTP server (canonical, see `docs/orchestrator-model.md` §3.5)
141146

142147
```java
143148
@Component(scope = Scope.SINGLETON)
@@ -157,9 +162,9 @@ public class JavalinFactory {
157162

158163
Javalin isn't AutoCloseable in current versions → explicit `@PreDestroy`.
159164
Routes register in `Main`. Reference:
160-
https://github.com/tomas-samek/tiko-di/blob/main/tiko-examples/15_quickstart/src/main/java/io/tiko/examples/quickstart/JavalinFactory.java
165+
[`JavalinFactory.java`](https://github.com/tomas-samek/tiko-di/blob/main/tiko-examples/15_quickstart/src/main/java/io/tiko/examples/quickstart/JavalinFactory.java).
161166

162-
### Worked example 3 — Caffeine cache
167+
### Worked example 3 — Caffeine cache (canonical, see `docs/orchestrator-model.md` §3.4)
163168

164169
```java
165170
@Component(scope = Scope.SINGLETON)
@@ -176,7 +181,8 @@ public class UserCacheFactory {
176181

177182
No lifecycle hook — Caffeine holds no external resources. The
178183
`name = "users"` qualifier disambiguates if the project later adds a
179-
second cache.
184+
second cache. Reference: orchestrator-model §3.4 (no example app yet —
185+
this is fine for a recipe).
180186

181187
## Step 4 — anti-pattern check
182188

@@ -202,21 +208,25 @@ fires, stop and reconsider:
202208

203209
## Step 5 — where the recipe lands
204210

205-
If you're contributing back to the framework, the recipe lands in
206-
**two files together** in the `tomas-samek/tiko-di` repo:
211+
Two files, both updated together:
207212

208-
1. `docs/orchestrator-model.md` — the long-form prose entry. Goes under
209-
§3 (Plug-in cookbook). Numbered section (§3.N). Full code snippet +
210-
lifecycle note + reference link + one-sentence "why-this-instead-of-bundling."
211-
2. `.ai-skills/tiko-build/SKILL.md` — the operational distillation. Add
212-
a row to the **Cookbook table** and (if the library replaces a Spring
213-
reflex) a row to the **Anti-pattern redirect table**. Cross-link the
214-
§3.N anchor in `orchestrator-model.md`.
213+
1. **[`docs/orchestrator-model.md`](https://github.com/tomas-samek/tiko-di/blob/main/docs/orchestrator-model.md)**
214+
the long-form prose entry. Goes under §3 (Plug-in cookbook). Numbered
215+
section (§3.N). Full code snippet + lifecycle note + reference link
216+
+ the one-sentence "why-this-instead-of-bundling."
217+
2. **[`.ai-skills/tiko-build/SKILL.md`](../tiko-build/SKILL.md)**
218+
the operational distillation. Add a row to the **Cookbook table** and
219+
(if the library replaces a Spring reflex) a row to the **Anti-pattern
220+
redirect table**. Cross-link the §3.N anchor in `orchestrator-model.md`.
215221

216-
Open a PR against `tomas-samek/tiko-di` with both file updates.
222+
Don't update one without the other. The two files drift if you do.
217223

218-
If the recipe is project-internal (you're not contributing back), the
219-
same template applies — just keep it in your project's own docs.
224+
The archetype's local copy of `tiko-build/SKILL.md`
225+
(`.ai-skills/tiko-build/SKILL.md`) updates automatically on the next
226+
release because it's copied from the repo's
227+
`tiko-archetype/src/main/resources/archetype-resources/.ai-skills/tiko-build/SKILL.md`
228+
at build time — but a recipe addition lands in the **repo's** SKILL.md
229+
*and* the archetype's copy in the same PR.
220230

221231
## Quality criteria — minimal beats comprehensive
222232

@@ -231,7 +241,7 @@ Default to minimal:
231241
- **One sentence on why.** Why this is plug-in, not bundled.
232242

233243
If a recipe deserves expansion (advanced configuration, error handling,
234-
multi-tenancy), spin it out as a follow-up. Ship the minimum first.
244+
multi-tenancy), spin it out as a follow-up PR. Ship the minimum first.
235245

236246
## What this skill does NOT do
237247

@@ -240,4 +250,15 @@ multi-tenancy), spin it out as a follow-up. Ship the minimum first.
240250
- Replace user judgment on overlapping recipes. When two recipes
241251
compete, the user picks; the skill surfaces the choice.
242252
- Open a PR automatically. The agent writes the files, the user reviews
243-
and opens / merges — same as any other change.
253+
and merges — same as any other change.
254+
255+
## Reading list
256+
257+
- [`docs/orchestrator-model.md`](https://github.com/tomas-samek/tiko-di/blob/main/docs/orchestrator-model.md) — the
258+
canonical cookbook. The shape every new entry mirrors.
259+
- [`docs/orchestrator-vocabulary.md`](https://github.com/tomas-samek/tiko-di/blob/main/docs/orchestrator-vocabulary.md)
260+
the three-bucket gate from step 1.
261+
- [`tiko-build/SKILL.md`](../tiko-build/SKILL.md) — the operational
262+
distillation. Where the table row lands.
263+
- [`docs/mcp-design.md`](https://github.com/tomas-samek/tiko-di/blob/main/docs/mcp-design.md) — sibling principle
264+
(per-partes serving) for the other agent-facing surface.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package io.tiko.archetype;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.nio.file.Files;
6+
import java.util.List;
7+
import org.junit.jupiter.params.ParameterizedTest;
8+
import org.junit.jupiter.params.provider.MethodSource;
9+
10+
/**
11+
* Drift gate (#408): every archetype-bundled agent skill must equal
12+
* {@link ArchetypeDocSync#forArchetype} applied to its canonical repo-root source. Fails the build
13+
* the moment a bundled copy falls behind canonical, so the {@code #401}–{@code #406} silent drift
14+
* cannot recur. Regenerate with {@link ArchetypeDocSync#main}.
15+
*/
16+
class ArchetypeBundledSkillsInSyncTest {
17+
18+
static List<String> syncedSkills() {
19+
return ArchetypeDocSync.SYNCED_SKILLS;
20+
}
21+
22+
@ParameterizedTest(name = "{0}")
23+
@MethodSource("syncedSkills")
24+
void bundledSkillIsInSyncWithCanonical(String skill) throws Exception {
25+
String expected = ArchetypeDocSync.forArchetype(Files.readString(ArchetypeDocSync.canonical(skill)));
26+
String actual = Files.readString(ArchetypeDocSync.bundled(skill));
27+
28+
assertThat(normalize(actual))
29+
.as(
30+
"Bundled .ai-skills/%s/SKILL.md has drifted from its canonical source. Regenerate it from"
31+
+ " the tiko-archetype/ directory: `mvn -q test-compile` then `java -cp target/test-classes"
32+
+ " io.tiko.archetype.ArchetypeDocSync`.",
33+
skill)
34+
.isEqualTo(normalize(expected));
35+
}
36+
37+
/** Compare line-ending-agnostically — the repo stores LF but Windows checkouts may be CRLF. */
38+
private static String normalize(String markdown) {
39+
return markdown.replace("\r\n", "\n");
40+
}
41+
}

0 commit comments

Comments
 (0)