Skip to content

Commit 26be588

Browse files
dmealingclaude
andcommitted
docs(plan): FR-004 plan #1 — prompt.* metatype + conformance fixtures (TS)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 655502b commit 26be588

1 file changed

Lines changed: 272 additions & 0 deletions

File tree

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
# FR-004 Plan #1`prompt.*` metatype + loader + conformance fixtures (TS reference)
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans (or subagent-driven-development) to implement task-by-task. Steps use checkbox (`- [ ]`) syntax. TDD throughout. Execute in an **isolated worktree** (superpowers:using-git-worktrees), not the main checkout — FR-003 is in flight in a separate worktree.
4+
5+
**Goal:** Add the `prompt` base metatype (subtypes `template`, `fragment`) to the TypeScript reference loader, plus shared conformance fixtures, without breaking the green C# conformance suite.
6+
7+
**Architecture:** `prompt` is a new base type modelled exactly on the recently-added `origin` (subtype→class dispatch, attr-only children). It gets its own concern folder `src/prompt/`. The loader is data-driven from constants + a registration loop in `core-types.ts`; the canonical serializer needs no changes. Shared `prompt-*` fixtures are added to `fixtures/conformance/`; because C#/Java don't support `prompt.*` yet, the fixtures are registered in C#'s `conformance-expected-failures.json` allowlist so its suite stays green. (Java's conformance harness is not yet in `main` — it's the in-flight H3b worktree work — so no Java gating is needed here; coordinate when H3b lands.)
8+
9+
**Scope boundary (deferred to later plans):** payload-as-projection (Layer 1, needs FR-003 §5), the render engine + section-format grammar + filesystem/RDB providers + golden harness (Plan #2), and porting `prompt.*` to the C#/Java loaders. This plan is the metatype vocabulary + loader recognition + validation + fixtures only.
10+
11+
**Tech Stack:** TypeScript (ESM), Bun test runner. Run tests from `server/typescript/packages/metadata/`.
12+
13+
**Reference files (read before starting):**
14+
- Base types: `server/typescript/packages/metadata/src/shared/base-types.ts` (`SUBTYPE_BASE`, `BASE_TYPES`)
15+
- Mirror pattern (origin): `src/persistence/origin/{origin-constants.ts,meta-origin.ts,origin-schema.ts}`
16+
- Registration: `src/core-types.ts` (`def()` @105, `wildcard()` @97, origin block @262-271)
17+
- Attr schema type: `src/registry.ts` (`AttrSchema` @32); attr value-type constants: `src/core/attr/attr-constants.ts`
18+
- Node base: `src/shared/meta-data.ts` (`ownAttr(name)` @231)
19+
- Exports: `src/index.ts`
20+
- Conformance: `fixtures/conformance/` (mirror `origin-passthrough-simple/`, `error-origin-bad-aggregate-fn/`), runner `server/typescript/packages/metadata/test/conformance.test.ts`, spec `spec/conformance-tests.md`
21+
- C# gate: `server/csharp/MetaObjects.Conformance.Tests/conformance-expected-failures.json`
22+
23+
---
24+
25+
## Task 1: `prompt.*` recognized, validated, and accessor-backed in the TS loader
26+
27+
**Files:**
28+
- Create: `server/typescript/packages/metadata/src/prompt/prompt-constants.ts`
29+
- Create: `server/typescript/packages/metadata/src/prompt/meta-prompt.ts`
30+
- Create: `server/typescript/packages/metadata/src/prompt/prompt-schema.ts`
31+
- Modify: `server/typescript/packages/metadata/src/shared/base-types.ts`
32+
- Modify: `server/typescript/packages/metadata/src/core-types.ts`
33+
- Modify: `server/typescript/packages/metadata/src/index.ts`
34+
- Test: `server/typescript/packages/metadata/test/prompt.test.ts`
35+
36+
- [ ] **Step 1: Write the failing test.** Create `test/prompt.test.ts`. (Mirror an existing loader test for `loadFromString`/`loadFromDirectory` usage — check `test/` for the exact loader entrypoint and adapt; the assertions below are the contract.)
37+
38+
```ts
39+
import { describe, it, expect } from "bun:test";
40+
import { Loader } from "../src/index.js"; // adjust to the test helper other tests use
41+
import { TYPE_PROMPT } from "../src/shared/base-types.js";
42+
import { PROMPT_SUBTYPE_TEMPLATE, PROMPT_SUBTYPE_FRAGMENT } from "../src/prompt/prompt-constants.js";
43+
44+
const good = {
45+
"metadata.root": {
46+
package: "acme::ai",
47+
children: [
48+
{ "object.entity": { name: "Npc", children: [
49+
{ "field.string": { name: "name" } },
50+
{ "identity.primary": { "@fields": "name" } },
51+
] } },
52+
{ "prompt.fragment": { name: "combatRules", "@textRef": "rules/combat#core" } },
53+
{ "prompt.template": { name: "npcTurn",
54+
"@payloadRef": "NpcPromptPayload", "@textRef": "npc/turn#main",
55+
"@outputFormat": "xml" } },
56+
],
57+
},
58+
};
59+
60+
describe("prompt metatype", () => {
61+
it("registers prompt as a base type", () => {
62+
expect(TYPE_PROMPT).toBe("prompt");
63+
});
64+
65+
it("loads prompt.template and prompt.fragment with no errors", () => {
66+
const res = Loader().loadFromString(JSON.stringify(good)); // adapt to actual API
67+
expect(res.errors).toEqual([]);
68+
const tmpl = res.root.find(PROMPT_SUBTYPE_TEMPLATE, "npcTurn"); // adapt finder to actual API
69+
expect(tmpl?.payloadRef).toBe("NpcPromptPayload");
70+
expect(tmpl?.textRef).toBe("npc/turn#main");
71+
expect(tmpl?.outputFormat).toBe("xml");
72+
const frag = res.root.find(PROMPT_SUBTYPE_FRAGMENT, "combatRules");
73+
expect(frag?.textRef).toBe("rules/combat#core");
74+
});
75+
76+
it("errors when prompt.template omits required @payloadRef", () => {
77+
const bad = structuredClone(good);
78+
delete (bad["metadata.root"].children[2] as any)["prompt.template"]["@payloadRef"];
79+
const res = Loader().loadFromString(JSON.stringify(bad));
80+
expect(res.errors.length).toBeGreaterThan(0);
81+
});
82+
});
83+
```
84+
85+
- [ ] **Step 2: Run it; confirm it fails.** Run: `cd server/typescript/packages/metadata && bun test test/prompt.test.ts`. Expected: FAIL — `TYPE_PROMPT` is not exported / loader reports unknown type `prompt.template`.
86+
87+
- [ ] **Step 3: Add the base type.** In `src/shared/base-types.ts`, add `export const TYPE_PROMPT = "prompt";` next to `TYPE_ORIGIN`, and append `TYPE_PROMPT` to the `BASE_TYPES` array.
88+
89+
- [ ] **Step 4: Create `src/prompt/prompt-constants.ts`.**
90+
91+
```ts
92+
import { SUBTYPE_BASE } from "../shared/base-types.js";
93+
94+
export const PROMPT_SUBTYPE_TEMPLATE = "template";
95+
export const PROMPT_SUBTYPE_FRAGMENT = "fragment";
96+
97+
export const PROMPT_SUBTYPES = [
98+
SUBTYPE_BASE,
99+
PROMPT_SUBTYPE_TEMPLATE,
100+
PROMPT_SUBTYPE_FRAGMENT,
101+
] as const;
102+
export type PromptSubType = (typeof PROMPT_SUBTYPES)[number];
103+
104+
// Reserved @-attr names (no '@' in the constant; prefix is applied at wire time).
105+
export const PROMPT_ATTR_PAYLOAD_REF = "payloadRef";
106+
export const PROMPT_ATTR_TEXT_REF = "textRef";
107+
export const PROMPT_ATTR_OUTPUT_FORMAT = "outputFormat";
108+
export const PROMPT_ATTR_REQUIRED_SLOTS = "requiredSlots";
109+
export const PROMPT_ATTR_MAX_CHARS = "maxChars";
110+
export const PROMPT_ATTR_MAX_TOKENS = "maxTokens";
111+
export const PROMPT_ATTR_OWNER = "owner";
112+
export const PROMPT_ATTR_SINCE = "since";
113+
```
114+
115+
- [ ] **Step 5: Create `src/prompt/meta-prompt.ts`** (mirror `meta-origin.ts` — base + subtype classes, accessors via `ownAttr`).
116+
117+
```ts
118+
import { MetaData } from "../shared/meta-data.js";
119+
import {
120+
PROMPT_ATTR_PAYLOAD_REF, PROMPT_ATTR_TEXT_REF, PROMPT_ATTR_OUTPUT_FORMAT,
121+
PROMPT_ATTR_REQUIRED_SLOTS, PROMPT_ATTR_MAX_CHARS, PROMPT_ATTR_MAX_TOKENS,
122+
} from "./prompt-constants.js";
123+
124+
const str = (v: unknown): string | undefined => (typeof v === "string" ? v : undefined);
125+
const num = (v: unknown): number | undefined => (typeof v === "number" ? v : undefined);
126+
127+
export class MetaPrompt extends MetaData {
128+
get textRef(): string | undefined { return str(this.ownAttr(PROMPT_ATTR_TEXT_REF)); }
129+
}
130+
131+
export class MetaPromptTemplate extends MetaPrompt {
132+
get payloadRef(): string | undefined { return str(this.ownAttr(PROMPT_ATTR_PAYLOAD_REF)); }
133+
get outputFormat(): string | undefined { return str(this.ownAttr(PROMPT_ATTR_OUTPUT_FORMAT)); }
134+
get requiredSlots(): unknown { return this.ownAttr(PROMPT_ATTR_REQUIRED_SLOTS); }
135+
get maxChars(): number | undefined { return num(this.ownAttr(PROMPT_ATTR_MAX_CHARS)); }
136+
get maxTokens(): number | undefined { return num(this.ownAttr(PROMPT_ATTR_MAX_TOKENS)); }
137+
}
138+
139+
export class MetaPromptFragment extends MetaPrompt {}
140+
```
141+
142+
- [ ] **Step 6: Create `src/prompt/prompt-schema.ts`** (mirror `origin-schema.ts`; `required: true` is what drives the missing-attr error). Confirm value-type constant names in `src/core/attr/attr-constants.ts` (use the string one and the int one; if a string-array subtype exists, use it for `requiredSlots`, else `ATTR_SUBTYPE_STRING`).
143+
144+
```ts
145+
import type { AttrSchema } from "../registry.js";
146+
import { ATTR_SUBTYPE_STRING, ATTR_SUBTYPE_INT } from "../core/attr/attr-constants.js";
147+
import { SUBTYPE_BASE } from "../shared/base-types.js";
148+
import {
149+
PROMPT_SUBTYPE_TEMPLATE, PROMPT_SUBTYPE_FRAGMENT,
150+
PROMPT_ATTR_PAYLOAD_REF, PROMPT_ATTR_TEXT_REF, PROMPT_ATTR_OUTPUT_FORMAT,
151+
PROMPT_ATTR_REQUIRED_SLOTS, PROMPT_ATTR_MAX_CHARS, PROMPT_ATTR_MAX_TOKENS,
152+
PROMPT_ATTR_OWNER, PROMPT_ATTR_SINCE,
153+
} from "./prompt-constants.js";
154+
155+
const templateAttrs: AttrSchema[] = [
156+
{ name: PROMPT_ATTR_PAYLOAD_REF, valueType: ATTR_SUBTYPE_STRING, required: true,
157+
description: "The object.value projection this template renders against." },
158+
{ name: PROMPT_ATTR_TEXT_REF, valueType: ATTR_SUBTYPE_STRING, required: true,
159+
description: "Logical reference to the template body text." },
160+
{ name: PROMPT_ATTR_OUTPUT_FORMAT, valueType: ATTR_SUBTYPE_STRING, required: false,
161+
description: "Expected output format, e.g. xml | json | text." },
162+
{ name: PROMPT_ATTR_REQUIRED_SLOTS, valueType: ATTR_SUBTYPE_STRING, required: false,
163+
description: "Slots that must resolve at render time (drives verify)." },
164+
{ name: PROMPT_ATTR_MAX_CHARS, valueType: ATTR_SUBTYPE_INT, required: false,
165+
description: "Size budget in characters." },
166+
{ name: PROMPT_ATTR_MAX_TOKENS, valueType: ATTR_SUBTYPE_INT, required: false,
167+
description: "Size budget in tokens." },
168+
{ name: PROMPT_ATTR_OWNER, valueType: ATTR_SUBTYPE_STRING, required: false, description: "Governance: owner." },
169+
{ name: PROMPT_ATTR_SINCE, valueType: ATTR_SUBTYPE_STRING, required: false, description: "Governance: since version." },
170+
];
171+
172+
const fragmentAttrs: AttrSchema[] = [
173+
{ name: PROMPT_ATTR_TEXT_REF, valueType: ATTR_SUBTYPE_STRING, required: true,
174+
description: "Logical reference to the fragment body text." },
175+
{ name: PROMPT_ATTR_OWNER, valueType: ATTR_SUBTYPE_STRING, required: false, description: "Governance: owner." },
176+
{ name: PROMPT_ATTR_SINCE, valueType: ATTR_SUBTYPE_STRING, required: false, description: "Governance: since version." },
177+
];
178+
179+
export const PROMPT_ATTRS_MAP = new Map<string, AttrSchema[]>([
180+
[SUBTYPE_BASE, []],
181+
[PROMPT_SUBTYPE_TEMPLATE, [...templateAttrs]],
182+
[PROMPT_SUBTYPE_FRAGMENT, [...fragmentAttrs]],
183+
]);
184+
```
185+
186+
- [ ] **Step 7: Wire registration in `src/core-types.ts`** (mirror the origin block @262-271).
187+
- Add to base-type imports: `TYPE_PROMPT`.
188+
- Add imports: `import { MetaPrompt, MetaPromptTemplate, MetaPromptFragment } from "./prompt/meta-prompt.js";`, `import { PROMPT_SUBTYPES, PROMPT_SUBTYPE_TEMPLATE, PROMPT_SUBTYPE_FRAGMENT } from "./prompt/prompt-constants.js";`, `import { PROMPT_ATTRS_MAP } from "./prompt/prompt-schema.js";`
189+
- Near the `ORIGIN_CLASS_MAP` definition add:
190+
```ts
191+
const PROMPT_CLASS_MAP = new Map([
192+
[PROMPT_SUBTYPE_TEMPLATE, MetaPromptTemplate],
193+
[PROMPT_SUBTYPE_FRAGMENT, MetaPromptFragment],
194+
]);
195+
```
196+
- After the origin registration loop add:
197+
```ts
198+
// prompt — LLM prompt construction (template, fragment). Only attr children.
199+
// Subtype→class dispatch: template → MetaPromptTemplate, fragment → MetaPromptFragment,
200+
// base (and any unmapped subtype) → MetaPrompt.
201+
for (const subType of PROMPT_SUBTYPES) {
202+
const NodeClass = PROMPT_CLASS_MAP.get(subType) ?? MetaPrompt;
203+
const promptAttrs = PROMPT_ATTRS_MAP.get(subType) ?? [];
204+
registry.register(
205+
def(TYPE_PROMPT, subType, `Prompt (${subType})`, [wildcard(TYPE_ATTR)], NodeClass, promptAttrs),
206+
);
207+
}
208+
```
209+
210+
- [ ] **Step 8: Export from `src/index.ts`** (mirror origin lines): `export * from "./prompt/prompt-constants.js";` and `export { MetaPrompt, MetaPromptTemplate, MetaPromptFragment } from "./prompt/meta-prompt.js";` (plus the `import type` line if the file maintains a type-only import block like origin's).
211+
212+
- [ ] **Step 9: Run the test; confirm it passes.** Run: `cd server/typescript/packages/metadata && bun test test/prompt.test.ts`. Expected: PASS (all three cases). If the finder/loader API names in Step 1 don't match the real API, fix the test to use the real entrypoints (do NOT weaken the assertions).
213+
214+
- [ ] **Step 10: Run the whole metadata package + typecheck.** Run: `cd server/typescript/packages/metadata && bun test && bunx tsc --noEmit` (or the package's typecheck script). Expected: all green — no regressions from adding the base type.
215+
216+
- [ ] **Step 11: Commit.**
217+
218+
```bash
219+
git add server/typescript/packages/metadata/src/prompt server/typescript/packages/metadata/src/shared/base-types.ts server/typescript/packages/metadata/src/core-types.ts server/typescript/packages/metadata/src/index.ts server/typescript/packages/metadata/test/prompt.test.ts
220+
git commit -m "feat(metadata): add prompt.* base metatype (template, fragment) [FR-004]"
221+
```
222+
223+
---
224+
225+
## Task 2: Shared conformance fixtures + C# expected-failures gate
226+
227+
**Files:**
228+
- Create: `fixtures/conformance/prompt-template-simple/{input/meta.ai.json,expected.json}`
229+
- Create: `fixtures/conformance/prompt-fragment-and-template/{input/meta.ai.json,expected.json}`
230+
- Create: `fixtures/conformance/error-prompt-template-missing-payload-ref/{input/meta.ai.json,expected-errors.json}`
231+
- Modify: `server/csharp/MetaObjects.Conformance.Tests/conformance-expected-failures.json`
232+
233+
- [ ] **Step 1: Author the happy-path fixture inputs.** Create `prompt-template-simple/input/meta.ai.json` with a `metadata.root` (package `acme::ai`) containing an `object.entity` "Npc" (one `field.string` "name" + `identity.primary` `@fields: "name"`) and a `prompt.template` "npcTurn" with `@payloadRef`, `@textRef`, `@outputFormat: "xml"`. Create `prompt-fragment-and-template/input/meta.ai.json` adding a `prompt.fragment` "combatRules" (`@textRef`) alongside the template. (Match the JSON shape of `origin-passthrough-simple/input/`.)
234+
235+
- [ ] **Step 2: Generate `expected.json` from actual canonical output.** Per `spec/conformance-tests.md` §"Adding a new fixture": run the TS conformance test for each new fixture, which fails with an actual-vs-expected diff; set `expected.json` to the actual canonical output (fused-key form; `@fields` normalized to array; `@`-attrs alphabetical; 2-space indent; trailing newline).
236+
Run: `cd server/typescript/packages/metadata && bun test test/conformance.test.ts -t "prompt-template-simple"` then `... -t "prompt-fragment-and-template"`. Paste the actual canonical output into each `expected.json`. Re-run: both PASS.
237+
238+
- [ ] **Step 3: Author the error fixture.** Create `error-prompt-template-missing-payload-ref/input/meta.ai.json` (a `prompt.template` missing `@payloadRef`). For `expected-errors.json`, use the array-of-`{code}` format (confirmed from `error-origin-bad-aggregate-fn/expected-errors.json`). Determine the exact code emitted for a missing required attr by reading `src/attr-schema-validate.ts` (the required-attr check) — set `expected-errors.json` to `[{ "code": "<that code>" }]`.
239+
Run: `cd server/typescript/packages/metadata && bun test test/conformance.test.ts -t "error-prompt-template-missing-payload-ref"`. Expected: PASS. If the emitted code differs, set the fixture to the actual emitted code.
240+
241+
- [ ] **Step 4: Run the full TS conformance suite.** Run: `cd server/typescript/packages/metadata && bun test test/conformance.test.ts`. Expected: all fixtures green, including the three new `prompt-*`.
242+
243+
- [ ] **Step 5: Gate C# so its suite stays green.** Add the three fixture directory names to the `fixtures` array in `server/csharp/MetaObjects.Conformance.Tests/conformance-expected-failures.json`:
244+
245+
```json
246+
{
247+
"language": "csharp",
248+
"fixtures": [
249+
"prompt-template-simple",
250+
"prompt-fragment-and-template",
251+
"error-prompt-template-missing-payload-ref"
252+
]
253+
}
254+
```
255+
256+
- [ ] **Step 6: Verify C# stays green.** Run: `cd server/csharp && dotnet test 2>&1 | tail -20`. Expected: pass. The C# loader will not recognize `prompt.*`; the expected-failures allowlist absorbs those three. **If a `prompt.*` fixture throws inside C# rather than being caught as a known failure** (e.g. unknown nested type throws instead of collecting an error), stop and reportthe allowlist may key on collected-error fixtures only, and the C# parser's unknown-nested-type behavior needs confirming before this gating approach is final.
257+
258+
- [ ] **Step 7: Commit.**
259+
260+
```bash
261+
git add fixtures/conformance/prompt-template-simple fixtures/conformance/prompt-fragment-and-template fixtures/conformance/error-prompt-template-missing-payload-ref server/csharp/MetaObjects.Conformance.Tests/conformance-expected-failures.json
262+
git commit -m "test(conformance): add prompt.* fixtures + gate C# expected-failures [FR-004]"
263+
```
264+
265+
---
266+
267+
## Self-review notes
268+
269+
- **Spec coverage:** FR-004 §2 (prompt.template/fragment + reserved attrs) → Task 1. FR-004 §9 (loader/serializer conformance for prompt vocabulary) → Task 2. Deferred: §1 payload-as-projection, §3–§6 (addressing/render/variants/verify beyond required-attr), §7 codegenPlan #2 and FR-003-dependent follow-ups, explicitly out of scope here.
270+
- **No-break guarantee:** Task 2 Step 56 keep C# green; Java has no `main` harness yet (coordinate at H3b). The serializer is data-driven (no change). The canonical serializer §key-order is unaffected`prompt.*` nodes only carry `name` + `@`-attrs + (no children beyond attrs).
271+
- **Open risk flagged inline:** C# unknown-nested-type behavior (Task 2 Step 6) — verified at runtime, with a stop-and-report fallback rather than a guess.
272+
- **`@requiredSlots` typing:** left as `ATTR_SUBTYPE_STRING` pending the §16 design (array vs csv); accessor returns raw `unknown`. Revisit in Plan #2.

0 commit comments

Comments
 (0)