NEO-145: address review findings — tests and decomposition docs.
Rename cross-ref unit test; add loader freeze-overflow and missing-schema negatives; update E7.M4/CT.M1 alignment tables and strike through review.pull/185/head
parent
1b904b594d
commit
56bc22c70d
|
|
@ -63,7 +63,7 @@ Content tooling **Slice 1** — schemas + CI; **Slice 4** — server parity hard
|
|||
|
||||
**Quest completion bundles ([NEO-124](https://linear.app/neon-sprawl/issue/NEO-124)):** optional **`completionRewardBundle`** on each quest row via [`quest-reward-bundle.schema.json`](../../../content/schemas/quest-reward-bundle.schema.json) (`itemGrants` reuses [`reward-grant-row.schema.json`](../../../content/schemas/reward-grant-row.schema.json); `skillXpGrants` via [`quest-skill-xp-grant.schema.json`](../../../content/schemas/quest-skill-xp-grant.schema.json)). E7 Slice 2 requires bundles on all four frozen quest ids with exact freeze-table contents; cross-checks **`itemGrants[].itemId`** and **`skillXpGrants[].skillId`**; each skill XP target must allow **`mission_reward`** in **`allowedXpSourceKinds`**.
|
||||
|
||||
**Contract template catalogs ([NEO-144](https://linear.app/neon-sprawl/issue/NEO-144)):** the same script validates each row in `content/contracts/*_contract_templates.json` against [`content/schemas/contract-template.schema.json`](../../../content/schemas/contract-template.schema.json) (`completionRewardBundle` via [`quest-reward-bundle.schema.json`](../../../content/schemas/quest-reward-bundle.schema.json); `minFactionStanding` via [`faction-gate-rule.schema.json`](../../../content/schemas/faction-gate-rule.schema.json)), rejects **duplicate `id`** across files, cross-checks **`encounterTemplateId`** against encounter catalogs and bundle item/skill/faction refs against frozen catalogs, enforces the **frozen one-template** id set (`prototype_contract_clear_combat_pocket`) with exact freeze-table contents (including empty **`reputationGrants`**), and enforces **band-1 economy caps** (item qty ≤ 10, skill XP ≤ 25, rep ≤ 10 per grant row). Issue/audit shape: [`contract-seed.schema.json`](../../../content/schemas/contract-seed.schema.json) — minimal fixture smoke parse in CI (no catalog file yet).
|
||||
**Contract template catalogs ([NEO-144](https://linear.app/neon-sprawl/issue/NEO-144)):** the same script validates each row in `content/contracts/*_contract_templates.json` against [`content/schemas/contract-template.schema.json`](../../../content/schemas/contract-template.schema.json) (`completionRewardBundle` via [`quest-reward-bundle.schema.json`](../../../content/schemas/quest-reward-bundle.schema.json); `minFactionStanding` via [`faction-gate-rule.schema.json`](../../../content/schemas/faction-gate-rule.schema.json)), rejects **duplicate `id`** across files, cross-checks **`encounterTemplateId`** against encounter catalogs and bundle item/skill/faction refs against frozen catalogs, enforces the **frozen one-template** id set (`prototype_contract_clear_combat_pocket`) with exact freeze-table contents (including empty **`reputationGrants`**), and enforces **band-1 economy caps** (item qty ≤ 10, skill XP ≤ 25, rep ≤ 10 per grant row). Issue/audit shape: [`contract-seed.schema.json`](../../../content/schemas/contract-seed.schema.json) — minimal fixture smoke parse in CI (no catalog file yet). **Server startup ([NEO-145](https://linear.app/neon-sprawl/issue/NEO-145)):** the host mirrors the same gates via `ContractTemplateCatalogLoader` before listening (see [server README — Contract template catalog](../../../server/README.md#contract-template-catalog-contentcontracts-neo-145)).
|
||||
|
||||
**Decomposition vocabulary:** the same workflow runs [`scripts/check_decomposition_language.py`](../../../scripts/check_decomposition_language.py) (stdlib only) over `docs/decomposition/**/*.md` to catch wording that treats **combat** as a leveled **`SkillDef`** line instead of **gig** progression (see script docstring for patterns). Adjust the script if a future doc needs a documented exception.
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ Epic 7 **Slice 4** — `contract_issued`, `contract_complete`, reward anomalies.
|
|||
|
||||
**CI (NEO-144):** `content/contracts/prototype_contract_templates.json` validated by `scripts/validate_content.py` — schema, single-template roster, freeze table, encounter/item/skill/faction cross-refs, band-1 economy caps. Schemas: [`contract-template.schema.json`](../../../content/schemas/contract-template.schema.json), [`contract-seed.schema.json`](../../../content/schemas/contract-seed.schema.json).
|
||||
|
||||
**Server load (NEO-145):** host fail-fast load of `content/contracts/*_contract_templates.json` via `ContractTemplateCatalogLoader` + **`IContractTemplateRegistry`** — same E7M4 gates as CI at startup; [server README — Contract template catalog](../../../server/README.md#contract-template-catalog-contentcontracts-neo-145).
|
||||
|
||||
## Source anchors
|
||||
|
||||
- Master plan: [`neon_sprawl_vision.plan.md`](../../../neon_sprawl_vision.plan.md) — Epic 7.
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -118,7 +118,7 @@ Gameplay **content and curves** default to **boot load** with optional **dev-onl
|
|||
**E7.M3 note:** Epic 7 **Slice 3** backlog in Linear ([Epic 7 — Questing, Narrative, and Faction Reputation](https://linear.app/neon-sprawl/project/epic-7-questing-narrative-and-faction-reputation-a9416783ceee)): [NEO-133](https://linear.app/neon-sprawl/issue/NEO-133) → [NEO-143](https://linear.app/neon-sprawl/issue/NEO-143) **landed**; label **`E7.M3`**. See [E7M3-pre-production-backlog.md](../../plans/E7M3-pre-production-backlog.md), [E7_M3_FactionReputationLedger.md](E7_M3_FactionReputationLedger.md). Upstream: E7.M1 **Ready**, E7.M2 **Ready**. Prototype spine: two frozen factions; operator chain grants **+15 Grid Operators**; **`prototype_quest_grid_contract`** gated at **minStanding 15**; rep grants via **`completionRewardBundle.reputationGrants`**; accept deny **`faction_gate_blocked`**. **E7M3-11 / NEO-143** client capstone landed — [`NEO-143` manual QA](../../manual-qa/NEO-143.md), [client README — End-to-end faction reputation loop (NEO-143)](../../../client/README.md#end-to-end-faction-reputation-loop-neo-143), plan [NEO-143](../../plans/NEO-143-implementation-plan.md). Epic 7 Slice 3 client capstone complete.
|
||||
| E7.M4 | ContractMissionGenerator | E4.M1, E5.M3, E7.M3 | ContractTemplate, ContractSeed, ContractOutcome | Pre-production | In Progress |
|
||||
|
||||
**E7.M4 note:** Epic 7 **Slice 4** backlog in Linear ([Epic 7 — Questing, Narrative, and Faction Reputation](https://linear.app/neon-sprawl/project/epic-7-questing-narrative-and-faction-reputation-a9416783ceee)): [NEO-144](https://linear.app/neon-sprawl/issue/NEO-144) → [NEO-154](https://linear.app/neon-sprawl/issue/NEO-154); label **`E7.M4`**. See [E7M4-pre-production-backlog.md](../../plans/E7M4-pre-production-backlog.md), [E7_M4_ContractMissionGenerator.md](E7_M4_ContractMissionGenerator.md). Upstream: E5.M3 **Ready**, E7.M3 **Ready**; E4.M1 live zone read deferred (content-only **`zoneDifficultyBand`** in Slice 4 v1). Prototype spine: one template **`prototype_contract_clear_combat_pocket`** → **`prototype_combat_pocket`** encounter clear → repeat **`scrap_metal_bulk` ×5** + salvage XP; economy band caps at issue. Client capstone **E7M4-11** [NEO-154](https://linear.app/neon-sprawl/issue/NEO-154).
|
||||
**E7.M4 note:** Epic 7 **Slice 4** backlog in Linear ([Epic 7 — Questing, Narrative, and Faction Reputation](https://linear.app/neon-sprawl/project/epic-7-questing-narrative-and-faction-reputation-a9416783ceee)): [NEO-144](https://linear.app/neon-sprawl/issue/NEO-144) → [NEO-154](https://linear.app/neon-sprawl/issue/NEO-154); label **`E7.M4`**. See [E7M4-pre-production-backlog.md](../../plans/E7M4-pre-production-backlog.md), [E7_M4_ContractMissionGenerator.md](E7_M4_ContractMissionGenerator.md). **E7M4-01 / NEO-144** catalog + CI landed; **E7M4-02 / NEO-145** fail-fast server catalog load + **`IContractTemplateRegistry`** landed ([NEO-145 plan](../../plans/NEO-145-implementation-plan.md)). Upstream: E5.M3 **Ready**, E7.M3 **Ready**; E4.M1 live zone read deferred (content-only **`zoneDifficultyBand`** in Slice 4 v1). Prototype spine: one template **`prototype_contract_clear_combat_pocket`** → **`prototype_combat_pocket`** encounter clear → repeat **`scrap_metal_bulk` ×5** + salvage XP; economy band caps at issue. Client capstone **E7M4-11** [NEO-154](https://linear.app/neon-sprawl/issue/NEO-154).
|
||||
|
||||
### Epic 8 — Social / Guild
|
||||
|
||||
|
|
|
|||
|
|
@ -132,8 +132,8 @@ Working backlog for **Epic 7 — Slice 4** ([contract generator](../decompositio
|
|||
|
||||
**Acceptance criteria**
|
||||
|
||||
- [ ] Host exits on invalid contract catalog or broken encounter refs at startup.
|
||||
- [ ] Registry resolves template metadata by id.
|
||||
- [x] Host exits on invalid contract catalog or broken encounter refs at startup.
|
||||
- [x] Registry resolves template metadata by id.
|
||||
|
||||
**Client counterpart:** none (infrastructure-only).
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@ Fail-fast startup load of `content/contracts/*_contract_templates.json` with **C
|
|||
- **Contract catalog:** `Game/Contracts/` loader, catalog, `IContractTemplateRegistry`, `PrototypeE7M4ContractCatalogRules` (roster, freeze, cross-ref, band caps synced to `PROTOTYPE_E7M4_*`), DI + `Program.cs` load order (after quest catalog).
|
||||
- **Config:** `ContentPathsOptions` `ContractsDirectory` / `ContractTemplateSchemaPath`; `InMemoryWebApplicationFactory` pins repo `content/contracts`.
|
||||
- **Tests:** `ContractTemplateCatalogLoaderTests`, `ContractTemplateRegistryTests`, host boot via `Host_ShouldResolveContractCatalogFromDi` (839 tests green).
|
||||
- **Docs:** `server/README.md` contract catalog section.
|
||||
- **Docs:** `server/README.md` contract catalog section; decomposition alignment (E7_M4, CT_M1, register tables).
|
||||
- **Bruno:** `bruno/neon-sprawl-server/contract-catalog/` health smoke (startup-only; no contract HTTP in E7M4-02).
|
||||
|
||||
## Technical approach
|
||||
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@ NEO-145 delivers fail-fast startup loading of `content/contracts/*_contract_temp
|
|||
|----------|-----------|
|
||||
| `docs/plans/NEO-145-implementation-plan.md` | **Matches** — AC checklist complete; reconciliation section reflects shipped loader, registry, DI, tests, README. |
|
||||
| `docs/plans/E7M4-pre-production-backlog.md` (E7M4-02) | **Matches** — loader, registry, cross-ref, tests, README in scope; instance store/HTTP/Godot correctly out of scope. |
|
||||
| `docs/decomposition/modules/E7_M4_ContractMissionGenerator.md` | **Partially matches** — freeze table and CI (NEO-144) documented; no note that server fail-fast load landed (NEO-145). Plan marked this optional. |
|
||||
| `docs/decomposition/modules/CT_M1_ContentValidationPipeline.md` | **Partially matches** — Purpose mentions server-side smoke loading; contract paragraph still NEO-144/CI-only — should add NEO-145 server mirror. |
|
||||
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | **Partially matches** — E7.M4 row cites E7M4-01/NEO-144 only; should mention E7M4-02 server catalog load after merge. |
|
||||
| `docs/decomposition/modules/module_dependency_register.md` | **Partially matches** — E7.M4 In Progress spine unchanged; acceptable until alignment pass, but register note could cite NEO-145. |
|
||||
| `docs/decomposition/modules/E7_M4_ContractMissionGenerator.md` | ~~**Partially matches** — freeze table and CI (NEO-144) documented; no note that server fail-fast load landed (NEO-145). Plan marked this optional.~~ **Done.** Server load (NEO-145) paragraph added. |
|
||||
| `docs/decomposition/modules/CT_M1_ContentValidationPipeline.md` | ~~**Partially matches** — Purpose mentions server-side smoke loading; contract paragraph still NEO-144/CI-only — should add NEO-145 server mirror.~~ **Done.** NEO-145 server startup mirror sentence added. |
|
||||
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | ~~**Partially matches** — E7.M4 row cites E7M4-01/NEO-144 only; should mention E7M4-02 server catalog load after merge.~~ **Done.** E7M4-02 / NEO-145 row extended. |
|
||||
| `docs/decomposition/modules/module_dependency_register.md` | ~~**Partially matches** — E7.M4 In Progress spine unchanged; acceptable until alignment pass, but register note could cite NEO-145.~~ **Done.** E7.M4 note cites NEO-145. |
|
||||
| `docs/decomposition/modules/client_server_authority.md` | **N/A** — read-only catalog load; no authority boundary change. |
|
||||
| Full-stack epic decomposition | **N/A** — infrastructure-only; client counterpart correctly none (NEO-154 capstone). |
|
||||
|
||||
Register/tracking tables were **not** updated for NEO-145 in this branch — **should fix** (non-blocking) per plan §6 optional module note and alignment doc pattern from NEO-144.
|
||||
Register/tracking tables were **not** updated for NEO-145 in this branch — ~~**should fix** (non-blocking) per plan §6 optional module note and alignment doc pattern from NEO-144.~~ **Done.**
|
||||
|
||||
## Blocking issues
|
||||
|
||||
|
|
@ -33,19 +33,19 @@ None.
|
|||
|
||||
## Suggestions
|
||||
|
||||
1. **Update decomposition tracking for E7M4-02** — After merge, extend `documentation_and_implementation_alignment.md` E7.M4 row and optionally `module_dependency_register.md` / `E7_M4_ContractMissionGenerator.md` with one line: server fail-fast catalog load + `IContractTemplateRegistry` (NEO-145). Mirrors NEO-144 alignment follow-up.
|
||||
1. ~~**Update decomposition tracking for E7M4-02** — After merge, extend `documentation_and_implementation_alignment.md` E7.M4 row and optionally `module_dependency_register.md` / `E7_M4_ContractMissionGenerator.md` with one line: server fail-fast catalog load + `IContractTemplateRegistry` (NEO-145). Mirrors NEO-144 alignment follow-up.~~ **Done.**
|
||||
|
||||
2. **CT_M1 server-load paragraph** — Add a sentence under the contract-catalog CI bullet that the host mirrors the same gates at startup via `ContractTemplateCatalogLoader` (NEO-145), matching faction/quest catalog precedent in that doc.
|
||||
2. ~~**CT_M1 server-load paragraph** — Add a sentence under the contract-catalog CI bullet that the host mirrors the same gates at startup via `ContractTemplateCatalogLoader` (NEO-145), matching faction/quest catalog precedent in that doc.~~ **Done.**
|
||||
|
||||
3. **Band-cap negative through full loader** — Plan §5 lists band-cap violation via loader; shipped coverage tests `TryGetBandCapGateError` in isolation only. Consider one temp-fixture test that writes qty **11** and asserts `ContractTemplateCatalogLoader.Load` throws (same pattern as freeze/roster negatives) so the loader→rules wiring is exercised end-to-end.
|
||||
3. ~~**Band-cap negative through full loader** — Plan §5 lists band-cap violation via loader; shipped coverage tests `TryGetBandCapGateError` in isolation only. Consider one temp-fixture test that writes qty **11** and asserts `ContractTemplateCatalogLoader.Load` throws (same pattern as freeze/roster negatives) so the loader→rules wiring is exercised end-to-end.~~ **Done.** `Load_ShouldThrow_WhenItemQuantityExceedsPrototypeBundleLimits` exercises loader→freeze gate (qty 11 fails freeze before band-cap on the single-template roster); `TryGetBandCapGateError_ShouldReturnError_WhenItemQuantityExceedsBandCap` covers band-cap rule directly.
|
||||
|
||||
4. **Missing schema path test** — Plan §5 lists missing schema; only missing **directory** is tested. A temp layout with contracts dir present but schema file removed would close the gap cheaply.
|
||||
4. ~~**Missing schema path test** — Plan §5 lists missing schema; only missing **directory** is tested. A temp layout with contracts dir present but schema file removed would close the gap cheaply.~~ **Done.** `Load_ShouldThrow_WhenSchemaFileMissing` added.
|
||||
|
||||
## Nits
|
||||
|
||||
- Nit: `Load_ShouldThrow_WhenBundleItemIdUnknown` calls `TryGetCrossRefError` directly rather than `Load`; rename to `TryGetCrossRefError_ShouldReturnError_WhenBundleItemIdUnknown` (or route through the loader) for clarity.
|
||||
- ~~Nit: `Load_ShouldThrow_WhenBundleItemIdUnknown` calls `TryGetCrossRefError` directly rather than `Load`; rename to `TryGetCrossRefError_ShouldReturnError_WhenBundleItemIdUnknown` (or route through the loader) for clarity.~~ **Done.**
|
||||
|
||||
- Nit: Bruno folder `bruno/neon-sprawl-server/contract-catalog/` (health smoke) is a nice addition but not listed in the plan — harmless; optional one-line in plan reconciliation if you want doc parity.
|
||||
- ~~Nit: Bruno folder `bruno/neon-sprawl-server/contract-catalog/` (health smoke) is a nice addition but not listed in the plan — harmless; optional one-line in plan reconciliation if you want doc parity.~~ **Done.** Plan reconciliation notes Bruno health smoke.
|
||||
|
||||
- Nit: `AddContractTemplateCatalog` re-binds `ContentPathsOptions` (same as faction/quest extensions) — fine; no action needed.
|
||||
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ public class ContractTemplateCatalogLoaderTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_ShouldThrow_WhenBundleItemIdUnknown()
|
||||
public void TryGetCrossRefError_ShouldReturnError_WhenBundleItemIdUnknown()
|
||||
{
|
||||
// Arrange
|
||||
var prototype = PrototypeE7M4ContractCatalogRules.ExpectedTemplateFreeze["prototype_contract_clear_combat_pocket"];
|
||||
|
|
@ -239,6 +239,35 @@ public class ContractTemplateCatalogLoaderTests
|
|||
Assert.Contains("must allow sourceKind 'mission_reward' in allowedXpSourceKinds", ioe.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_ShouldThrow_WhenItemQuantityExceedsPrototypeBundleLimits()
|
||||
{
|
||||
// Arrange — qty 11 fails freeze before band-cap gate (same CI tamper path as validate_content.py)
|
||||
var (_, contractsDir, schemaPath) = CreateTempContentLayout();
|
||||
var overCap = ValidPrototypeCatalogJson.Replace("\"quantity\": 5", "\"quantity\": 11", StringComparison.Ordinal);
|
||||
File.WriteAllText(Path.Combine(contractsDir, "prototype_contract_templates.json"), overCap, Encoding.UTF8);
|
||||
// Act
|
||||
var ex = Record.Exception(() => LoadCatalog(contractsDir, schemaPath));
|
||||
// Assert
|
||||
var ioe = Assert.IsType<InvalidOperationException>(ex);
|
||||
Assert.Contains("completionRewardBundle must match E7M4 freeze table", ioe.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_ShouldThrow_WhenSchemaFileMissing()
|
||||
{
|
||||
// Arrange
|
||||
var (_, contractsDir, schemaPath) = CreateTempContentLayout();
|
||||
File.WriteAllText(Path.Combine(contractsDir, "prototype_contract_templates.json"), ValidPrototypeCatalogJson, Encoding.UTF8);
|
||||
File.Delete(schemaPath);
|
||||
// Act
|
||||
var ex = Record.Exception(() => LoadCatalog(contractsDir, schemaPath));
|
||||
// Assert
|
||||
var ioe = Assert.IsType<InvalidOperationException>(ex);
|
||||
Assert.Contains("missing schema file", ioe.Message, StringComparison.Ordinal);
|
||||
Assert.Contains(schemaPath, ioe.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetBandCapGateError_ShouldReturnError_WhenItemQuantityExceedsBandCap()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue