From a57f83e30d9610691afbbb840d436c6aa5cfe5e7 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Wed, 5 Aug 2026 00:34:56 +0000 Subject: [PATCH] Centralize generic LLM schema assets --- .../generic}/schemas/test_artifact.v1.json | 0 .../schemas/test_validator_decision.v1.json | 0 assets/package.go | 15 + docs/roadmap/assets.md | 366 +++++++++ docs/roadmap/implementation.md | 695 ++++++++++++++++++ internal/framework/llm/schema_registry.go | 18 +- 6 files changed, 1089 insertions(+), 5 deletions(-) rename {internal/framework/llm/assets => assets/generic}/schemas/test_artifact.v1.json (100%) rename {internal/framework/llm/assets => assets/generic}/schemas/test_validator_decision.v1.json (100%) create mode 100644 assets/package.go create mode 100644 docs/roadmap/assets.md create mode 100644 docs/roadmap/implementation.md diff --git a/internal/framework/llm/assets/schemas/test_artifact.v1.json b/assets/generic/schemas/test_artifact.v1.json similarity index 100% rename from internal/framework/llm/assets/schemas/test_artifact.v1.json rename to assets/generic/schemas/test_artifact.v1.json diff --git a/internal/framework/llm/assets/schemas/test_validator_decision.v1.json b/assets/generic/schemas/test_validator_decision.v1.json similarity index 100% rename from internal/framework/llm/assets/schemas/test_validator_decision.v1.json rename to assets/generic/schemas/test_validator_decision.v1.json diff --git a/assets/package.go b/assets/package.go new file mode 100644 index 0000000..1a1fbdf --- /dev/null +++ b/assets/package.go @@ -0,0 +1,15 @@ +// Package assets exposes embedded LLM-facing content. +package assets + +import ( + "embed" + "io/fs" +) + +//go:embed generic +var embedded embed.FS + +// FS returns the embedded read-only asset filesystem. +func FS() fs.FS { + return embedded +} diff --git a/docs/roadmap/assets.md b/docs/roadmap/assets.md new file mode 100644 index 0000000..3cea3fb --- /dev/null +++ b/docs/roadmap/assets.md @@ -0,0 +1,366 @@ +# Centralized LLM Assets + +## Purpose + +Centralize Notarius's embedded LLM-facing assets in a top-level `assets/` +package so prompt declarations, prompt fragments, private structured-response +schemas, shared prompt text, and built-in LLM profiles are easy to find, +inspect, compare, and revise. + +This is an ownership and repository-layout change, not a prompt redesign or a +change to any durable artifact contract. Modules continue to own all behavior +associated with their assets. + +## Motivation + +The D&D module family currently distributes its LLM assets across the packages +that consume them. Twelve D&D prompt consumers collectively embed 46 prompt +files. Their private response schemas, shared entity-reconciliation schema, +shared prompt fragments, and built-in `dnd-extraction` profile are likewise +spread across chunk, extract, normalize, shared, and registrar packages. + +Co-location made each original module self-contained, but the growing family +now makes prompt inspection and cross-module editing unnecessarily difficult. +Prompt authors commonly need to: + +- review all sibling prompts together; +- preserve exact shared bytes and cache-compatible message prefixes; +- compare declarations, instructions, tasks, and response schemas; +- identify duplicated or drifting language; and +- iterate on prompt content without navigating implementation packages. + +A centralized, predictable asset tree makes those operations straightforward +while leaving executable behavior with the module packages. + +## Decisions + +### Top-Level Asset Package + +All production LLM-facing assets will live beneath a repository-root +`assets/` directory. That directory will also be a Go package named `assets` +because Go embed patterns cannot embed files outside the embedding package's +directory tree. + +The package will contain one Go source file, `assets/package.go`, unless a +future Go toolchain constraint makes that impractical. Its entire production +API will expose the embedded, read-only filesystem. The intended shape is +equivalent to: + +```go +package assets + +import ( + "embed" + "io/fs" +) + +//go:embed dnd generic +var embedded embed.FS + +func FS() fs.FS { return embedded } +``` + +The exact embed patterns may reflect the final asset tree, but the package must +retain this minimal character. Adding a new asset domain may require extending +the embed directive; it must not require adding domain behavior to the package. + +### Strict Data-Only Boundary + +The `assets` package is a content container and dependency leaf. No business, +domain, framework, or PromptKit behavior may live there. + +In particular, the package must not own: + +- prompt IDs, schema IDs, schema names, or schema versions; +- prompt manifests, message order, cache controls, or input construction; +- module, schema, profile, or PromptKit registration; +- asset selection rules or module-to-path catalogs; +- parsing, validation, normalization, transformation, or hashing; +- checkpoint or fingerprint policy; +- fallback-profile precedence or model defaults; or +- imports of Notarius internal packages, PromptKit, or module packages. + +It should expose no path-aware lookup helpers. Consumers obtain the root +read-only filesystem and use the standard library's `fs.Sub` to select the +smallest subtree they own. A module must not depend on an unrelated module's +asset subtree merely because both are available from the same embedded +filesystem. + +The package's top-level location and exported accessor are an intentional +tradeoff in favor of human discoverability. They do not establish a supported +public extension API or transfer semantic ownership away from modules. + +### Behavioral Ownership Remains With Modules + +Each LLM-backed module continues to own: + +- its prompt and response-schema identity constants; +- its ordered prompt-asset manifest; +- prompt input preparation and rendered message order; +- its response-schema definition and loading; +- prompt and schema registration with the shared LLM asset registry; +- output decoding and interpretation; +- its component fingerprint composition; and +- tests of its observable prompt and schema behavior. + +The D&D shared package continues to own the manifest mechanism and the declared +set of reusable D&D prompt fragments. The D&D registrar continues to own family +composition and registration of the built-in fallback profile. The generic LLM +framework continues to own filesystem flattening, duplicate detection, schema +loading, PromptKit construction, and content hashing. + +Physical storage is therefore centralized without creating a central package +that understands or composes every module. + +### Asset Scope + +The centralized tree includes embedded content whose direct purpose is to +configure or constrain an LLM interaction: + +- PromptKit prompt declarations; +- module-owned prompt fragments; +- byte-identical shared prompt fragments; +- private JSON Schemas for structured LLM response envelopes; +- shared private response schemas such as D&D entity reconciliation; and +- built-in fallback LLM profiles. + +The migration also includes the generic private LLM response schemas currently +embedded by the LLM framework. The framework may depend on the content-only +asset package for those generic files, but it must refer only to its own scoped +subtree and must not acquire D&D knowledge. + +The following embedded files remain with their current owners: + +- durable artifact schemas owned by D&D codec packages; +- framework-owned durable schemas such as chunk-map and evidence-context + contracts; +- the D&D spell catalog and other domain data that is not itself a prompt, + private LLM response schema, or built-in LLM profile; +- maintained configuration examples; and +- operator-supplied PromptKit profiles and other runtime files. + +Private LLM response schemas must remain clearly distinguishable from durable +artifact schemas. Centralization must not turn a model transport envelope into +an external Notarius contract. + +## Target Asset Tree + +The target tree groups assets first by domain and then by conceptual module. +Directory names use readable kebab case; Go package names, module keys, prompt +IDs, and schema identities do not change to match the directory spelling. + +```text +assets/ + package.go + generic/ + schemas/ + dnd/ + shared/ + prompts/ + profiles/ + entity-reconciliation/ + schemas/ + scenes/ + prompts/ + schemas/ + spells/ + prompts/ + schemas/ + npcs/ + extract/ + prompts/ + schemas/ + normalize/ + prompts/ + combat-turns/ + prompts/ + schemas/ + enemy-events/ + prompts/ + schemas/ + item-events/ + prompts/ + schemas/ + npc-interactions/ + prompts/ + schemas/ + scene-descriptions/ + prompts/ + schemas/ + locations/ + extract/ + prompts/ + schemas/ + normalize/ + prompts/ + location-occurrences/ + prompts/ + schemas/ +``` + +Each prompt-bearing leaf retains predictable filenames for its declaration, +task, instructions, and any lane-specific fragments. Existing filenames should +be preserved where practical so this migration does not create gratuitous +content churn. Shared content must have one physical source file rather than a +copy in each consuming subtree. + +Future LLM-backed domains and modules follow the same taxonomy. A future module +with both extraction and LLM-backed normalization assets uses explicit +`extract/` and `normalize/` children; a module with only one LLM-backed +operation may keep its prompt and schema directories directly beneath the +module directory. + +## Filesystem And Registration Model + +Consumers use `assets.FS()` plus `fs.Sub` to obtain a domain-, module-, or +asset-class-specific filesystem. Existing manifests continue to enumerate the +exact files used by each prompt. Registration continues through +`llm.AssetRegistry`, which presents PromptKit with its existing flattened +prompt, schema, and fallback-profile filesystems. + +The virtual PromptKit layout is stable even though repository source paths +move. In particular: + +- prompt IDs and declaration names remain unchanged; +- schema keys, IDs, versions, names, and registered filenames remain unchanged; +- fallback profile IDs, contents, and precedence remain unchanged; +- shared fragments remain mounted at the paths expected by existing prompt + declarations; and +- duplicate-path and missing-file failures continue to occur during + preparation. + +No consumer receives a precomposed global prompt catalog from the `assets` +package. Module manifests and the D&D registrar remain the explicit composition +points. + +## Prompt Caching And Fingerprints + +The migration must preserve every prompt's rendered roles, bytes, input +placement, message order, and cache-control metadata. Merely moving source +files must not reduce backend prompt-cache compatibility or alter model-visible +content. + +Each component fingerprint must remain scoped to the exact assets declared by +that component. An edit to one lane must not invalidate checkpoints belonging +to unrelated lanes. The implementation must not replace manifest-scoped +hashing with a digest of the complete root asset filesystem. + +Current asset hashes include source paths as well as bytes, so moving the files +may produce a one-time checkpoint fingerprint change for affected components. +That safe invalidation is acceptable. Preserving old internal fingerprints is +not a reason to retain duplicate files, compatibility shims, or legacy asset +paths. After migration, changing an unrelated asset must not affect a +component's fingerprint. + +## Architectural Documentation + +Central storage refines the accepted domain-packaging decision in ADR-0004, +which currently says domain-specific prompt fragments and schemas live within +the domain implementation tree. Implementation must add a new ADR recording +the content-only root package, module ownership rules, alternatives, and +consequences, and mark only the conflicting asset-co-location portion of +ADR-0004 as superseded. Its remaining domain-first module packaging decision +continues to apply. + +Once implemented, the canonical architecture and internal documentation must +describe current behavior: + +- architecture documents the data-only asset-package boundary and dependency + direction; +- LLM internals document embedding, scoped filesystem consumption, + registration, and private-schema ownership; +- D&D internals document the domain asset taxonomy, shared-fragment ownership, + manifest-scoped fingerprints, and the convention for new modules; and +- the internal component overview links to the focused owner without + duplicating volatile paths. + +The roadmap remains the only documentation of this unimplemented target until +the migration lands. + +## Work Scope + +Completing this feature requires: + +- creating the top-level asset package and target directory structure; +- moving all in-scope files without editing their semantic content; +- replacing package-local `embed.FS` declarations for migrated assets with + scoped views of the central filesystem; +- updating module manifests, response-schema loaders, shared-fragment + resolution, generic schema loading, fallback-profile registration, and asset + hashes to use those scoped views; +- removing obsolete module-local prompt, private-schema, and profile asset + directories and their embedding files; +- preserving module-local behavioral declarations and registration methods; +- updating tests and documentation at their durable ownership boundaries; and +- recording the architectural decision described above. + +## Verification Expectations + +Verification should protect behavior and architectural boundaries rather than +freeze prompt prose or count files. The completed migration must demonstrate +that: + +- all registered prompts and private response schemas prepare successfully; +- all built-in fallback profiles remain available with unchanged effective + configuration; +- rendered prompt messages and cache controls are unchanged for representative + extraction, chunking, and normalization prompts; +- shared prompt fragments come from one physical source and render identically + wherever reused; +- schema loading retains the existing identities and content digests; +- duplicate, missing, or malformed assets still fail at the established + preparation boundary; +- component fingerprints use only their declared assets; +- D&D family and production composition tests pass; +- no package-local migrated copies remain; and +- `assets/` contains only the single minimal Go source file and in-scope data + files, with no business logic or internal/PromptKit dependencies. + +Tests must follow the testing policy: do not add exact prompt-prose snapshots, +asset-count assertions, source-path snapshots, or other change-detector tests. +Use existing preparation, rendering, registration, schema, fingerprint, and +composition boundaries to protect meaningful behavior. + +Repository-wide validation includes: + +```sh +go test ./... +go vet ./... +go build ./cmd/notarius +``` + +## Non-Goals + +This work does not: + +- rewrite, consolidate, or otherwise tune prompt content; +- change prompt message order, caching policy, or profile defaults; +- change private or durable schema shapes; +- move durable schemas or the spell catalog into the root package; +- add runtime prompt overrides or live filesystem reloading; +- expose asset paths as configuration or a supported public API; +- replace the existing LLM asset registry or PromptKit filesystem boundary; +- centralize module behavior, registration, or identity constants; or +- preserve reuse of checkpoints created with the pre-migration internal asset + paths. + +An optional development-time filesystem overlay may be considered separately +if prompt iteration without rebuilding becomes valuable. It requires distinct +precedence, provenance, fingerprint, reproducibility, and security decisions +and is not implied by this repository-layout change. + +## Target End State + +Prompt authors can inspect every embedded LLM interaction asset from one +predictable top-level tree. Reusable prompt content has one physical owner, and +private LLM schemas are visibly separated from durable artifact contracts. + +The `assets` Go package consists only of `package.go`, embeds the content, and +exposes one read-only filesystem accessor. It contains no application logic and +has no Notarius-internal or PromptKit dependencies. Modules select scoped +subtrees and retain complete ownership of prompt semantics, schema identity, +registration, interpretation, and fingerprints. The framework remains +domain-neutral, the D&D registrar remains the family composition root, and all +observable prompt, schema, profile, and pipeline behavior is unchanged apart +from the accepted one-time invalidation of checkpoints whose fingerprints +include moved source paths. diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md new file mode 100644 index 0000000..d92814f --- /dev/null +++ b/docs/roadmap/implementation.md @@ -0,0 +1,695 @@ +# Centralized LLM Assets Implementation Plan + +## Objective + +Implement the target state defined in [Centralized LLM Assets](assets.md): move +embedded LLM-facing content into a minimal top-level `assets` package while +leaving prompt semantics, schema identities, registration, interpretation, and +fingerprint composition with their current framework and module owners. + +This plan is ordered. Implement each stage in sequence and complete its focused +validation before beginning the next stage. Do not retire either roadmap when +the stages are complete; roadmap cleanup is a separate maintainer decision. + +## Plan-Wide Constraints + +These constraints apply to every stage: + +- Treat [the feature roadmap](assets.md) as the canonical source for scope, + ownership, target layout, and non-goals. +- Follow the architecture, documentation, and testing policies under + `docs/policy/`. +- Move asset files without changing their bytes. Use Git rename detection or + content hashes to investigate any move reported as a content edit. +- Move a coherent asset set and update every consumer in the same stage. Never + leave a second authoritative copy in the old package. +- Keep `assets/package.go` as the only Go source file under `assets/`. Do not add + tests, generated Go files, path constants, module descriptors, lookup + helpers, registration, validation, hashing, or other behavior to that + package. +- The root package may import only `embed` and `io/fs`. It exposes one + read-only `FS() fs.FS` accessor and contains no Notarius-internal or PromptKit + dependency. +- A consuming package must import the root package consistently as + `rootassets`, call `fs.Sub(rootassets.FS(), "")`, and operate + only on its own subtree. The alias distinguishes content from local registry + parameters commonly named `assets`. Do not pass the global filesystem + through the application or introduce a global asset catalog. +- Retain prompt IDs, schema keys, IDs, versions, names, registered filenames, + profile IDs and contents, prompt message order, rendered bytes, and cache + controls. +- Keep `promptAssetRoot = "assets/prompts"` and each manifest's `ModuleDir` + unchanged where they define the synthesized PromptKit filesystem. Only the + source paths inside the manifest change. +- Register a scoped module filesystem's `schemas` directory as the schema root + so the flattened PromptKit schema filenames remain unchanged. +- Keep prompt fingerprints manifest-scoped. Accept the feature roadmap's + one-time fingerprint change caused by new source paths; do not preserve old + paths through compatibility files or hash the complete root filesystem. +- Do not move codec schemas, chunk-map or evidence-context durable schemas, the + spell catalog, examples, or operator-provided profiles. +- Do not rewrite prompts or schemas during this migration. +- Update existing tests only where their setup refers to the old filesystem or + path. Add a test only for a meaningful behavior not already protected. Do not + add prompt-prose snapshots, asset counts, source-path snapshots, exact shared + prefix lengths, or other change-detector tests. + +For each moved set, inspect `git diff --summary` and the ordinary diff before +finishing the stage. Asset files should appear as exact renames unless their +parent directory changes prevent Git from displaying the move immediately; +their content must nevertheless be identical. + +## Stage 1: Establish The Data-Only Package And Move Generic LLM Schemas + +### Goal + +Create the minimal root package and use it for the two generic private response +schemas currently embedded by the LLM framework. This proves the dependency +direction and scoped-filesystem pattern before any D&D assets move. + +### Changes + +1. Create `assets/package.go` with: + + - a package comment explaining that it exposes embedded LLM-facing content; + - imports of only `embed` and `io/fs`; + - an unexported `embed.FS` populated initially with `//go:embed generic`; + - one exported `FS() fs.FS` accessor returning the embedded filesystem; and + - no other declarations or behavior. + +2. Move these files without editing their contents: + + - `internal/framework/llm/assets/schemas/test_artifact.v1.json` to + `assets/generic/schemas/test_artifact.v1.json`; + - `internal/framework/llm/assets/schemas/test_validator_decision.v1.json` to + `assets/generic/schemas/test_validator_decision.v1.json`. + +3. Update `internal/framework/llm/schema_registry.go`: + + - remove its `embed` import, embed directive, and package-local embedded + filesystem; + - import the root `assets` package; + - create its existing package-private schema filesystem from + `fs.Sub(rootassets.FS(), "generic")`; + - keep the existing fail-fast initialization behavior for an invalid + compile-time subtree, using a small private helper in the LLM package if + needed; and + - change only the two source paths from `assets/schemas/...` to + `schemas/...`. Preserve every schema definition identity and the existing + defensive loading behavior. + +4. Remove the now-empty `internal/framework/llm/assets/` tree. + +Do not introduce a reusable root-package subdirectory helper. The private +framework helper, if used, is an initialization mechanism for the existing +framework registry and does not belong in `assets`. + +### Acceptance Criteria + +- `assets/package.go` is the sole Go file beneath `assets/` and has the exact + minimal responsibility above. +- The two generic schemas exist only in `assets/generic/schemas/`. +- `RegisteredResponseSchemas`, lookup, schema identities, and schema content + digests behave exactly as before. +- The generic framework imports no D&D package or path. +- No unrelated D&D asset has moved. + +### Validation + +```sh +gofmt -w assets/package.go internal/framework/llm/schema_registry.go +go test ./assets ./internal/framework/llm +go vet ./assets ./internal/framework/llm +git diff --check +git diff --summary +``` + +This stage is sized for one gpt-5.6-terra implementation prompt. + +## Stage 2: Centralize Shared D&D Prompts, Reconciliation Schema, And Profile + +### Goal + +Create the shared D&D asset foundation required by every later D&D migration, +including the single physical copy of shared prompt wording, the shared private +entity-reconciliation schema, and the built-in fallback profile. + +### Changes + +1. Extend the embed directive in `assets/package.go` to embed both `generic` and + `dnd`. Make no other root-package API change. + +2. Move the seven files under + `internal/modules/dnd/shared/assets/prompts/` unchanged to + `assets/dnd/shared/prompts/`. + +3. Update `internal/modules/dnd/shared/assets.go`: + + - remove the local embed declaration and `embed` import; + - import the root `assets` package; + - obtain a scoped filesystem with + `fs.Sub(rootassets.FS(), "dnd/shared")` inside the shared package; + - update `sharedPromptPaths` from `assets/prompts/` to + `prompts/`; + - continue resolving only the names explicitly declared by a + `PromptAssetManifest`; and + - retain the existing validation, virtual `sharedassets` mount names, + defensive copying, ordering, and manifest-scoped hashing. + + Propagate subtree errors through the existing `PromptFS` and `Hash` error + paths. Do not panic for D&D module asset preparation and do not move the + manifest implementation into the root package. + +4. Update the shared asset tests to obtain the scoped shared filesystem through + the shared package's private mechanism instead of referring to the removed + `embeddedAssets`. Retain the behavioral coverage for missing files, unknown + names, exact declared parts, and deterministic hashing without asserting the + repository tree or prompt prose. + +5. Move + `internal/modules/dnd/shared/entityreconcile/assets/schemas/dnd_entity_reconcile_llm.v1.json` + unchanged to + `assets/dnd/entity-reconciliation/schemas/dnd_entity_reconcile_llm.v1.json`. + In `entityreconcile/schema.go`, scope the root filesystem to + `dnd/entity-reconciliation`, change `SchemaAssetPath` to + `schemas/dnd_entity_reconcile_llm.v1.json`, and use the same scoped + filesystem for loading and registration. Register the `schemas` root so the + flattened filename remains `dnd_entity_reconcile_llm.v1.json`. Delete the + obsolete `entityreconcile/assets.go`. + +6. Move + `internal/modules/dnd/register/assets/profiles/dnd-extraction.yaml` + unchanged to `assets/dnd/profiles/dnd-extraction.yaml`. Update + `register/profiles.go` to scope `rootassets.FS()` to `dnd/profiles` and + register that filesystem at `.`. Preserve the profile's virtual filename, + ID, contents, fallback precedence, and digest behavior. + +7. Remove the now-empty local shared/profile asset directories. Do not touch + module-local prompt or schema assets in this stage. + +### Acceptance Criteria + +- Every shared D&D prompt fragment has one physical source under + `assets/dnd/shared/prompts/`. +- Existing D&D manifests still select shared files explicitly and mount them + at the same virtual names. +- Entity reconciliation loads and registers the same private schema identity + and bytes. +- The `dnd-extraction` fallback profile resolves with unchanged effective + content and precedence. +- Full D&D registration succeeds while the remaining modules still consume + their package-local assets. +- `assets/package.go` remains the only Go file in the root package. + +### Validation + +```sh +gofmt -w assets/package.go internal/modules/dnd/shared/assets.go internal/modules/dnd/shared/entityreconcile/schema.go internal/modules/dnd/register/profiles.go +go test ./assets ./internal/framework/llm ./internal/modules/dnd/shared/... ./internal/modules/dnd/register +git diff --check +git diff --summary +``` + +This stage is sized for one gpt-5.6-terra implementation prompt. + +## Stage 3: Migrate Scene Planning And Scene Descriptions + +### Goal + +Move the two scene-oriented prompt/schema sets and establish the repeatable +module-consumer pattern used in subsequent stages. + +### Changes + +Move these directories without editing asset contents: + +| Current source | Target | +| --- | --- | +| `internal/modules/dnd/chunk/scenes/assets/prompts/` | `assets/dnd/scenes/prompts/` | +| `internal/modules/dnd/chunk/scenes/assets/schemas/` | `assets/dnd/scenes/schemas/` | +| `internal/modules/dnd/extract/scenedescriptions/assets/prompts/` | `assets/dnd/scene-descriptions/prompts/` | +| `internal/modules/dnd/extract/scenedescriptions/assets/schemas/` | `assets/dnd/scene-descriptions/schemas/` | + +For both consuming packages: + +1. Delete the package-local `assets.go` embed file. +2. In `prompt_assets.go`, add a private `moduleAssetFS() (fs.FS, error)` that + scopes the root filesystem to the package's exact target subtree. +3. Change manifest source paths from `assets/prompts/` to + `prompts/`. Do not change `ModuleDir`, the declared file order, shared + file order, or `promptAssetRoot`. +4. Resolve the scoped filesystem in `RegisterPromptAssets`, pass it to the + existing manifest, and register its `schemas` root. +5. Resolve the same scoped filesystem in the synchronized prompt fingerprint + initializer and hash only the manifest's declared assets. +6. Update the response-schema loader to use the scoped filesystem and + `schemas/`, preserving all identity constants. +7. Update focused tests that directly used `embeddedAssets` or old source paths + to use `moduleAssetFS`. Preserve tests of prepared inputs, message placement, + cache control, diagnostics redaction, schema compatibility, and + registration. + +Keep scene planning in the chunk module and scene-description semantics in the +extractor; the common asset location must not merge their behavior. + +### Acceptance Criteria + +- Both scene asset sets exist only in their target root directories. +- Both packages use scoped filesystems and retain their own manifests, schema + definitions, registration, and fingerprints. +- Prepared prompt IDs, rendered messages, schema identities, and PromptKit + virtual filenames are unchanged. +- Scene chunking and scene-description extraction tests pass, including D&D + family registration. + +### Validation + +```sh +gofmt -w internal/modules/dnd/chunk/scenes/*.go internal/modules/dnd/extract/scenedescriptions/*.go +go test ./internal/modules/dnd/chunk/scenes ./internal/modules/dnd/extract/scenedescriptions ./internal/modules/dnd/register +git diff --check +git diff --summary +``` + +This stage is sized for one gpt-5.6-terra implementation prompt. + +## Stage 4: Migrate The NPC Prompt Family + +### Goal + +Centralize NPC extraction, NPC normalization, and NPC-interaction assets while +preserving their distinct module behavior and shared-prefix contracts. + +### Changes + +Move these directories unchanged: + +| Current source | Target | +| --- | --- | +| `internal/modules/dnd/extract/npcs/assets/prompts/` | `assets/dnd/npcs/extract/prompts/` | +| `internal/modules/dnd/extract/npcs/assets/schemas/` | `assets/dnd/npcs/extract/schemas/` | +| `internal/modules/dnd/normalize/npcs/assets/prompts/` | `assets/dnd/npcs/normalize/prompts/` | +| `internal/modules/dnd/extract/npcinteractions/assets/prompts/` | `assets/dnd/npc-interactions/prompts/` | +| `internal/modules/dnd/extract/npcinteractions/assets/schemas/` | `assets/dnd/npc-interactions/schemas/` | + +Apply the module-consumer pattern established in Stage 3 to all three packages: + +- delete each local embed-only `assets.go`; +- add a private scoped filesystem accessor in the owning `prompt_assets.go`; +- make manifest and response-schema paths relative to the scoped subtree; +- retain virtual prompt roots, module directories, IDs, manifest ordering, and + shared file selection; +- use the scoped filesystem for registration, schema loading, and + manifest-scoped fingerprinting; and +- update focused tests only for the filesystem/path move. + +NPC normalization has no module-private response schema; it must continue using +the shared entity-reconciliation schema migrated in Stage 2. NPC extraction and +NPC interactions retain their own private response schemas. Do not combine the +three manifests or add NPC behavior to the root package. + +### Acceptance Criteria + +- NPC extraction, normalization, and interaction assets exist only in the + target directories. +- The modules retain three independent manifests and their existing semantic + responsibilities. +- NPC normalization still registers and loads the shared reconciliation + schema rather than a copied schema. +- Extraction prompt prefix order, normalization ordering, generated-reference + grounding, cache controls, schema identities, and diagnostics behavior are + unchanged. +- Focused and family registration tests pass. + +### Validation + +```sh +gofmt -w internal/modules/dnd/extract/npcs/*.go internal/modules/dnd/normalize/npcs/*.go internal/modules/dnd/extract/npcinteractions/*.go +go test ./internal/modules/dnd/extract/npcs ./internal/modules/dnd/normalize/npcs ./internal/modules/dnd/extract/npcinteractions ./internal/modules/dnd/register +git diff --check +git diff --summary +``` + +This stage is sized for one gpt-5.6-terra implementation prompt. + +## Stage 5: Migrate The Location Prompt Family + +### Goal + +Centralize location extraction, location normalization, and +location-occurrence assets while preserving registry grounding and shared +entity-reconciliation behavior. + +### Changes + +Move these directories unchanged: + +| Current source | Target | +| --- | --- | +| `internal/modules/dnd/extract/locations/assets/prompts/` | `assets/dnd/locations/extract/prompts/` | +| `internal/modules/dnd/extract/locations/assets/schemas/` | `assets/dnd/locations/extract/schemas/` | +| `internal/modules/dnd/normalize/locations/assets/prompts/` | `assets/dnd/locations/normalize/prompts/` | +| `internal/modules/dnd/extract/locationoccurrences/assets/prompts/` | `assets/dnd/location-occurrences/prompts/` | +| `internal/modules/dnd/extract/locationoccurrences/assets/schemas/` | `assets/dnd/location-occurrences/schemas/` | + +Apply the Stage 3 module-consumer pattern to all three packages. In particular: + +- delete local embed-only files; +- scope each package to only its target subtree; +- update manifest and schema source paths without altering identities or + virtual PromptKit paths; +- keep fingerprints limited to each manifest's module and selected shared + assets; and +- update focused tests that refer to the old filesystem. + +Location normalization continues to use the shared entity-reconciliation +schema. Location occurrence extraction retains its own private response schema +and its existing required location-registry input. Do not conflate the +locations registry artifact with occurrence events. + +### Acceptance Criteria + +- All three location-oriented asset sets exist only under their target root + directories. +- Extraction, normalization, and occurrence modules retain separate manifests, + schema ownership, prompt inputs, and fingerprints. +- Reconciliation and location-registry grounding behavior is unchanged. +- Prompt preparation, schema compatibility, normalization, reference handoff, + and family registration tests pass. + +### Validation + +```sh +gofmt -w internal/modules/dnd/extract/locations/*.go internal/modules/dnd/normalize/locations/*.go internal/modules/dnd/extract/locationoccurrences/*.go +go test ./internal/modules/dnd/extract/locations ./internal/modules/dnd/normalize/locations ./internal/modules/dnd/extract/locationoccurrences ./internal/modules/dnd/register +git diff --check +git diff --summary +``` + +This stage is sized for one gpt-5.6-terra implementation prompt. + +## Stage 6: Migrate Spell And Combat-Turn Assets + +### Goal + +Move the spell and combat-turn LLM assets while preserving their distinct +catalog, NPC-grounding, and combat-scene eligibility behavior. + +### Changes + +Move these directories unchanged: + +| Current source | Target | +| --- | --- | +| `internal/modules/dnd/extract/spells/assets/prompts/` | `assets/dnd/spells/prompts/` | +| `internal/modules/dnd/extract/spells/assets/schemas/` | `assets/dnd/spells/schemas/` | +| `internal/modules/dnd/extract/combatturns/assets/prompts/` | `assets/dnd/combat-turns/prompts/` | +| `internal/modules/dnd/extract/combatturns/assets/schemas/` | `assets/dnd/combat-turns/schemas/` | + +Apply the Stage 3 module-consumer pattern to both extractors. Preserve the +spell manifest's catalog fragment and optional NPC registry placement. Preserve +combat-turn routing so the LLM runs only for exact combat scene +classifications; this refactor must not move eligibility logic into the assets +package or prompt declaration. + +Do not move or modify +`internal/modules/dnd/spells/catalog/assets/dnd_5e_2014_srd_spells.json`; it is +domain reference data, not an LLM interaction asset. + +### Acceptance Criteria + +- Spell and combat-turn prompts and private response schemas exist only in the + new root subtrees. +- Both packages retain independent manifests, response-schema definitions, + registrations, and component fingerprints. +- Spell catalog overlay identity, NPC grounding, combat eligibility, prompt + message order, cache controls, schema identities, and diagnostics remain + unchanged. +- The spell catalog stays with its existing package. +- Focused and family registration tests pass. + +### Validation + +```sh +gofmt -w internal/modules/dnd/extract/spells/*.go internal/modules/dnd/extract/combatturns/*.go +go test ./internal/modules/dnd/extract/spells ./internal/modules/dnd/extract/combatturns ./internal/modules/dnd/spells/catalog ./internal/modules/dnd/register +git diff --check +git diff --summary +``` + +This stage is sized for one gpt-5.6-terra implementation prompt. + +## Stage 7: Migrate Item-Event And Enemy-Event Assets + +### Goal + +Complete the module asset migration with item-event and enemy-event extraction, +leaving every D&D LLM prompt and private response schema under the root asset +tree. + +### Changes + +Move these directories unchanged: + +| Current source | Target | +| --- | --- | +| `internal/modules/dnd/extract/itemevents/assets/prompts/` | `assets/dnd/item-events/prompts/` | +| `internal/modules/dnd/extract/itemevents/assets/schemas/` | `assets/dnd/item-events/schemas/` | +| `internal/modules/dnd/extract/enemyevents/assets/prompts/` | `assets/dnd/enemy-events/prompts/` | +| `internal/modules/dnd/extract/enemyevents/assets/schemas/` | `assets/dnd/enemy-events/schemas/` | + +Apply the Stage 3 module-consumer pattern to both packages. Preserve the +enemy-event grounding fragment, required generated references, combat-scene +eligibility, and observation semantics. Preserve item-event categories and +prompt structure. Do not change deterministic normalizers or durable codecs. + +### Acceptance Criteria + +- Item-event and enemy-event LLM assets exist only in their target directories. +- Both extractors retain their module-owned manifests, schema definitions, + registrations, response interpretation, and fingerprints. +- Prompt preparation, reference grounding, scene gating, schema compatibility, + normalization, and family registration behavior is unchanged. +- No D&D prompt, private LLM schema, shared fragment, or built-in profile + remains beneath an `internal/modules/dnd/**/assets/` directory. + +### Validation + +```sh +gofmt -w internal/modules/dnd/extract/itemevents/*.go internal/modules/dnd/extract/enemyevents/*.go +go test ./internal/modules/dnd/extract/itemevents ./internal/modules/dnd/extract/enemyevents ./internal/modules/dnd/register +git diff --check +git diff --summary +``` + +This stage is sized for one gpt-5.6-terra implementation prompt. + +## Stage 8: Audit Asset Boundaries And Consolidate Behavioral Verification + +### Goal + +Review the completed physical migration as one system, remove obsolete +embedding surfaces, and ensure existing behavior-level tests protect the +meaningful asset contracts without introducing structural change detectors. + +### Changes + +1. Inventory every remaining `//go:embed` directive and every directory named + `assets` under `internal/`. Classify each remaining file against the feature + roadmap. Expected retained local embeds include durable codec/framework + schemas and the spell catalog; no migrated LLM interaction asset may remain. +2. Confirm all prompt-bearing consumers import the root package, scope it with + `fs.Sub`, and expose no root filesystem beyond their package. +3. Confirm every old embed-only module `assets.go` has been deleted. Keep files + named `assets.go` that still own actual manifest behavior, such as the D&D + shared implementation; file names alone are not grounds for deletion. +4. Inspect all `PromptAssetManifest` values and response-schema definitions: + + - source paths are relative to the correct scoped filesystem; + - virtual `ModuleDir`, prompt IDs, schema identities, and registration names + are unchanged; + - shared fragments remain explicitly enumerated in order; and + - hashes consume only the manifest's selected files. + +5. Run and review the existing family-wide prompt-prefix, schema-compatibility, + profile, registration, and composition tests. Repair coverage only if a + meaningful behavior lost protection during the move. Prefer existing public + or package-level preparation boundaries; do not test the literal root tree + shape, exact prose, file counts, or private call sequences. +6. Verify that only `assets/package.go` is Go code in the root asset tree and + that its imports and declarations still satisfy the strict data-only rule. + +Useful non-test inspection commands include: + +```sh +rg -n '//go:embed' --glob '*.go' . +find internal -type f -path '*/assets/*' | sort +find assets -type f | sort +find assets -name '*.go' -print +go list -f '{{join .Imports "\n"}}' ./assets +``` + +These are manual audit checks, not assertions to encode in permanent tests. + +### Acceptance Criteria + +- The root tree contains every in-scope LLM asset and no out-of-scope durable + contract or domain-data asset. +- Old module-local LLM assets and embed declarations are gone. +- There is one physical copy of each shared prompt fragment. +- Modules and framework consumers access only scoped subtrees. +- PromptKit preparation, response schemas, fallback profiles, family + registration, and production composition remain operational. +- No low-value structural or prose change-detector test has been added. + +### Validation + +```sh +go test ./assets ./internal/framework/llm/... ./internal/modules/dnd/... ./internal/cli/... +go vet ./assets ./internal/framework/llm/... ./internal/modules/dnd/... +git diff --check +git diff --summary +``` + +This audit and focused remediation stage is sized for one gpt-5.6-terra +implementation prompt. + +## Stage 9: Record The Decision And Update Canonical Documentation + +### Goal + +Document the implemented ownership boundary in its canonical homes without +duplicating volatile details or presenting the root package as a public +extension contract. + +### Changes + +1. Add `docs/adr/0011-centralize-llm-assets.md` in the policy-required Nygard + format. Record: + + - why prompt-author discoverability now outweighs package-local physical + co-location; + - the data-only top-level package and single-filesystem API; + - continued module ownership of semantics and registration; + - scoped consumer access and dependency direction; + - exclusion of durable schemas and non-LLM domain data; + - the accepted public import path and one-time checkpoint invalidation; + - alternatives of package-local assets, `internal/llmassets`, a behavioral + central registry, and runtime filesystem overlays; and + - the exact portion of ADR-0004 that this decision supersedes. + +2. Change only ADR-0004's status metadata to state that its asset-co-location + rule is superseded by ADR-0011 while its domain-first module packaging + decision remains accepted. Do not rewrite its accepted decision text. +3. Update `docs/policy/architecture.md` at the layer/domain and LLM boundaries: + + - define the root package as a content-only dependency leaf; + - prohibit business logic and internal/PromptKit dependencies there; + - state that physical centralization does not transfer semantic ownership + from modules; and + - keep generic framework code domain-neutral even when it reads its own + scoped generic assets from the shared container. + +4. Update `docs/internal/llm.md` to describe the implemented root filesystem, + scoped consumers, asset registry flattening, private schema distinction, and + manifest-scoped fingerprints. Link to architecture and the ADR instead of + repeating their rationale. +5. Update `docs/internal/dnd.md` to document the convention for locating new + D&D LLM assets, shared-fragment ownership, extract/normalize subtrees, and + module-owned manifests. Preserve its existing canonical prompt-order and + caching guidance. +6. Update `docs/internal/overview.md` with a concise current component entry for + `assets/` and links to the focused LLM/D&D documents. Do not duplicate the + full directory tree. +7. Search current documentation for claims that LLM assets are physically + package-local and revise only the canonical owners. Do not change user, + operator, configuration, or integration contracts unless inspection finds a + directly false statement caused by this internal-only move. + +### Acceptance Criteria + +- ADR-0011 records the durable decision and ADR-0004's status identifies the + limited supersession without altering its historical decision. +- Architecture contains the normative data-only boundary. +- Internal documentation accurately describes the implemented mechanics and + convention for future modules. +- No document outside `docs/roadmap/` describes unimplemented behavior. +- Documentation does not duplicate prompt contents, schema definitions, + module inventories, or volatile asset path lists unnecessarily. +- Relative links resolve to existing files. + +### Validation + +```sh +rg -n 'package-specific assets|module-owned assets|internal/modules/dnd/.*/assets|internal/framework/llm/assets' README.md docs +git diff --check +go test ./internal/framework/llm/... ./internal/modules/dnd/... +``` + +Manually verify changed Markdown links because the repository currently has no +dedicated documentation link checker. + +This documentation stage is sized for one gpt-5.6-terra implementation prompt. + +## Stage 10: Final Repository Verification + +### Goal + +Perform a clean final review of the complete migration, correct any remaining +in-scope defect, and demonstrate that the repository builds and tests as a +coherent whole. + +### Changes + +1. Re-read the feature roadmap and inspect the complete diff from the base + commit, not only the most recent stage. +2. Confirm every target-tree entry is present and every excluded asset remains + with its original owner. +3. Confirm moved prompt, schema, and profile files have unchanged content. +4. Confirm `assets/package.go` is still the sole Go file under `assets`, exposes + only `FS() fs.FS`, and imports only `embed` and `io/fs`. +5. Confirm no consumer uses an unrelated subtree and no generic package names a + D&D asset path. +6. Confirm all old local LLM embed declarations, files, and directories are + gone, without deleting retained durable schemas or the spell catalog. +7. Review test changes for compliance with the testing policy and remove any + redundant structural or exact-content assertion introduced during the + migration. +8. Review documentation changes for canonical ownership, current-state + wording, correct links, and absence of duplicated volatile content. +9. Run formatting and repository-wide validation. Fix only defects within this + roadmap's scope; report unrelated pre-existing failures separately. + +### Acceptance Criteria + +- Every target-end-state and verification expectation in `assets.md` is met. +- The complete diff contains no prompt, schema, profile, module key, profile + default, durable contract, or pipeline behavior change beyond source + relocation and the accepted internal fingerprint invalidation. +- The working tree contains no accidental generated files or obsolete empty + asset directories. +- All repository-wide checks pass. + +### Validation + +```sh +go fmt ./... +go test ./... +go vet ./... +go build ./cmd/notarius +git diff --check +git status --short +``` + +Review `git diff --stat`, `git diff --summary`, and the full diff before +reporting completion. Do not delete `docs/roadmap/assets.md` or +`docs/roadmap/implementation.md` in this stage. + +This final verification stage is sized for one gpt-5.6-terra implementation +prompt. + +## Open Questions + +None. The feature roadmap and this plan resolve the package API, target tree, +asset scope, consumer pattern, fingerprint policy, documentation ownership, +and accepted compatibility consequences needed for implementation. diff --git a/internal/framework/llm/schema_registry.go b/internal/framework/llm/schema_registry.go index d4f8d25..f67f3db 100644 --- a/internal/framework/llm/schema_registry.go +++ b/internal/framework/llm/schema_registry.go @@ -2,17 +2,17 @@ package llm import ( "crypto/sha256" - "embed" "encoding/hex" "encoding/json" "fmt" "io/fs" "sort" "strings" + + rootassets "gitea.maximumdirect.net/eric/notarius/assets" ) -//go:embed assets/schemas/*.json -var schemaAssets embed.FS +var schemaAssets = mustSubFS(rootassets.FS(), "generic") // ResponseSchemaKey identifies one structured response schema. type ResponseSchemaKey string @@ -49,17 +49,25 @@ var responseSchemaRegistry = map[ResponseSchemaKey]ResponseSchema{ ID: "notarius.test_artifact", Version: schemaVersionV1, Name: "notarius_test_artifact_v1", - AssetPath: "assets/schemas/test_artifact.v1.json", + AssetPath: "schemas/test_artifact.v1.json", }), TestValidatorDecisionSchemaKey: mustLoadResponseSchema(schemaAssets, ResponseSchemaDefinition{ Key: TestValidatorDecisionSchemaKey, ID: "notarius.test_validator_decision", Version: schemaVersionV1, Name: "notarius_test_validator_decision_v1", - AssetPath: "assets/schemas/test_validator_decision.v1.json", + AssetPath: "schemas/test_validator_decision.v1.json", }), } +func mustSubFS(fsys fs.FS, dir string) fs.FS { + sub, err := fs.Sub(fsys, dir) + if err != nil { + panic(fmt.Errorf("scope embedded assets to %s: %w", dir, err)) + } + return sub +} + // RegisteredResponseSchemas returns all registered response schemas sorted by key. func RegisteredResponseSchemas() []ResponseSchema { keys := make([]string, 0, len(responseSchemaRegistry))