diff --git a/bruno/neon-sprawl-server/skill-progression/Post skill progression grant.bru b/bruno/neon-sprawl-server/skill-progression/Post skill progression grant.bru
index 8bfa23d..355e773 100644
--- a/bruno/neon-sprawl-server/skill-progression/Post skill progression grant.bru
+++ b/bruno/neon-sprawl-server/skill-progression/Post skill progression grant.bru
@@ -6,6 +6,7 @@ meta {
docs {
NEO-38: single skill XP grant + allowlist + level-up metadata; see server README.
+ NEO-40: same contract; server adds comment-only telemetry hook markers (no request/response change).
}
post {
diff --git a/docs/decomposition/modules/E2_M2_XpAwardAndLevelEngine.md b/docs/decomposition/modules/E2_M2_XpAwardAndLevelEngine.md
index 36552b2..781f28d 100644
--- a/docs/decomposition/modules/E2_M2_XpAwardAndLevelEngine.md
+++ b/docs/decomposition/modules/E2_M2_XpAwardAndLevelEngine.md
@@ -19,7 +19,9 @@
**NEO-39 landed:** data-driven **`LevelCurve`** at `content/skills/prototype_level_curve.json` with schema `content/schemas/level-curve.schema.json`; CI validation extended in `scripts/validate_content.py`; server boot now fail-fast loads curve content and uses it for XP→level resolution in skill progression GET/POST paths (no code edit required for threshold tuning).
-**Still backlog within this module:** telemetry hook sites (**NEO-40**); callers in Slice 3 (**NEO-41**–**NEO-43**). Keep this paragraph and [documentation tracking](documentation_and_implementation_alignment.md) in sync as Slice 2 stories merge.
+**NEO-40 landed:** comment-only **telemetry hook sites** in `SkillProgressionSnapshotApi` (`POST …/skill-progression`) for future catalog events **`xp_grant`** and **`level_up`** ([implementation plan](../../plans/NEO-40-implementation-plan.md)); manual QA **[`NEO-40.md`](../../manual-qa/NEO-40.md)**.
+
+**Still backlog within this module:** callers in Slice 3 (**NEO-41**–**NEO-43**). Keep this paragraph and [documentation tracking](documentation_and_implementation_alignment.md) in sync as Slice 2 stories merge.
## Purpose
diff --git a/docs/manual-qa/NEO-40.md b/docs/manual-qa/NEO-40.md
new file mode 100644
index 0000000..c00365e
--- /dev/null
+++ b/docs/manual-qa/NEO-40.md
@@ -0,0 +1,32 @@
+# Manual QA — NEO-40 (progression telemetry hook sites)
+
+Reference: [implementation plan](../plans/NEO-40-implementation-plan.md), [NEO-38 manual QA](./NEO-38.md) (same server surface).
+
+## Preconditions
+
+- Same as NEO-38: run `NeonSprawl.Server` (`dotnet run` from `server/NeonSprawl.Server`; default `http://localhost:5253`).
+- This story adds **comments only**; behavior matches NEO-38.
+
+```bash
+BASE=http://localhost:5253
+ID=dev-local-1
+```
+
+## Verify API unchanged
+
+1. **Grant** (same as NEO-38 happy path):
+
+```bash
+curl -sS -i -X POST "${BASE}/game/players/${ID}/skill-progression" \
+ -H "Content-Type: application/json" \
+ -d '{"schemaVersion":1,"skillId":"salvage","amount":10,"sourceKind":"activity"}'
+```
+
+Expect **HTTP 200**, `granted` **true**, `progression` and `levelUps` present as before.
+
+2. **Level-up path** (optional, same as NEO-38 bonus): large grant on `intrusion` if XP crosses the curve threshold; expect `levelUps` non-empty when a boundary is crossed.
+
+## Code review check
+
+- Open `server/NeonSprawl.Server/Game/Skills/SkillProgressionSnapshotApi.cs` and confirm **comment blocks** mark **`xp_grant`** (after successful `TryApplyXpDelta`) and **`level_up`** (inside `nextLevel > prevLevel`), referencing **E9.M1** / no production ingest.
+- Confirm there are **no** new `ILogger` or metrics calls for these events.
diff --git a/docs/plans/NEO-40-implementation-plan.md b/docs/plans/NEO-40-implementation-plan.md
index 4067678..3d58f26 100644
--- a/docs/plans/NEO-40-implementation-plan.md
+++ b/docs/plans/NEO-40-implementation-plan.md
@@ -34,8 +34,8 @@
## Acceptance criteria checklist
-- [ ] Hook locations in **grant** + **level-up** path; documented here **and** in code comments referencing future **`xp_grant`** / **`level_up`** (and **time-to-first-level-up** intent where appropriate).
-- [ ] **No** mistaken high-volume logging to production pipelines (no new log statements for these hooks).
+- [x] Hook locations in **grant** + **level-up** path; documented here **and** in code comments referencing future **`xp_grant`** / **`level_up`** (and **time-to-first-level-up** intent where appropriate).
+- [x] **No** mistaken high-volume logging to production pipelines (no new log statements for these hooks).
## Technical approach
@@ -53,14 +53,14 @@
| Path | Purpose |
|------|---------|
-| **none** | Hook sites are **comment markers** on the existing NEO-38 route; no new types or files required. |
+| `docs/manual-qa/NEO-40.md` | Manual QA checklist (API parity + comment review). |
## Files to modify
| Path | Rationale |
|------|-----------|
| `server/NeonSprawl.Server/Game/Skills/SkillProgressionSnapshotApi.cs` | Insert **telemetry hook site** comments on the **successful grant** path (`xp_grant`) and **level-up** branch (`level_up`); optional one-line **time-to-first-level-up** intent. |
-| `docs/decomposition/modules/E2_M2_XpAwardAndLevelEngine.md` | **Only if** a single sentence improves traceability from module doc → hook file; otherwise **none** (YAGNI). |
+| `docs/decomposition/modules/E2_M2_XpAwardAndLevelEngine.md` | **NEO-40 landed** line + backlog sentence updated (replaces “telemetry still backlog”). |
## Tests
diff --git a/server/NeonSprawl.Server.Tests/Game/Skills/SkillProgressionGrantApiTests.cs b/server/NeonSprawl.Server.Tests/Game/Skills/SkillProgressionGrantApiTests.cs
index bd8f174..c6695e0 100644
--- a/server/NeonSprawl.Server.Tests/Game/Skills/SkillProgressionGrantApiTests.cs
+++ b/server/NeonSprawl.Server.Tests/Game/Skills/SkillProgressionGrantApiTests.cs
@@ -6,6 +6,7 @@ using Xunit;
namespace NeonSprawl.Server.Tests.Game.Skills;
+/// Regression tests for POST …/skill-progression (NEO-38). NEO-40 adds comment-only hook sites; JSON contract unchanged.
public sealed class SkillProgressionGrantApiTests
{
private static SkillProgressionGrantRequest Grant(
diff --git a/server/NeonSprawl.Server/Game/Skills/SkillProgressionSnapshotApi.cs b/server/NeonSprawl.Server/Game/Skills/SkillProgressionSnapshotApi.cs
index 0798e8c..598fc63 100644
--- a/server/NeonSprawl.Server/Game/Skills/SkillProgressionSnapshotApi.cs
+++ b/server/NeonSprawl.Server/Game/Skills/SkillProgressionSnapshotApi.cs
@@ -81,11 +81,20 @@ public static class SkillProgressionSnapshotApi
return Results.NotFound();
}
+ // --- Telemetry hook site (NEO-40): future E9.M1 catalog event `xp_grant` ---
+ // No ingest here until the telemetry catalog exists. Planned payload fields: playerId,
+ // skillId, amount, sourceKind; optional correlation/request ids when Slice 3 wires callers.
+
var prevLevel = levelCurve.LevelFromTotalXp(prevXp);
var nextLevel = levelCurve.LevelFromTotalXp(newXp);
var levelUps = new List();
if (nextLevel > prevLevel)
{
+ // --- Telemetry hook site (NEO-40): future E9.M1 catalog event `level_up` ---
+ // Planned fields: playerId, skillId, previousLevel, newLevel. No logging (comments-only).
+ // Time-to-first-level-up: needs first-seen / milestone persistence not present here; wire
+ // when catalog + storage exist (likely derived from `level_up` or a dedicated milestone).
+
levelUps.Add(
new SkillLevelUpJson
{