Skip to content

Commit 9284fa3

Browse files
dmealingclaude
andcommitted
docs: refine "when to escalate to subtype" with two real-world triggers
While doing the wizardsofodd toolcall refactor, found two cases that justify subtype escalation beyond just "different runtime node class": 1. **Existing subtype's required attrs don't apply** — template.output's @payloadRef AND @textRef are both required (renderable-template invariants). LLM tool-call envelopes need @toolname but have no text body; @textRef doesn't apply. Extending template.output with @toolname would force every toolcall to declare a stub @textRef. New subtype (template.toolcall, no @textRef required) is the honest fit. 2. **Load-time error detection when provider is missing** — the loader treats undeclared @-attrs as OPEN POLICY (silently accepted, no error or warning per attr-schema-validate.ts:15). Only subtype names are validated. So if a custom generator only does something useful when metadata is present, subtype registration is the only mechanism that catches "consumer forgot to wire the provider" at load time via ERR_UNKNOWN_SUBTYPE. With pure attr-extension, missing providers silently no-op. Also rewrote the "extend an existing subtype" default example to use @AuditTrail (a real cross-cutting concern) instead of @toolname (which turned out NOT to fit the extend pattern). Notes the extend → escalate transition explicitly. These were surfaced during real refactor work. No API change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 041be46 commit 9284fa3

1 file changed

Lines changed: 43 additions & 18 deletions

File tree

docs/features/extending-with-providers.md

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -174,29 +174,37 @@ in conformance tests, and in every consumer's mental model.
174174

175175
### Default: extend an existing subtype with attrs (`registry.extend`)
176176

177-
If the new concept is structurally identical to an existing subtype and only
178-
adds configuration, **extend the existing subtype with new attrs**. No new
179-
node class, no new cross-port mapping, no new childRules.
177+
If the new concept is structurally identical to an existing subtype, only
178+
adds configuration, and the existing subtype's required attrs ALL apply,
179+
**extend the existing subtype with new attrs**. No new node class, no new
180+
cross-port mapping, no new childRules.
180181

181-
Example — an LLM tool-call envelope is structurally identical to
182-
`template.output` (both have a typed payload + a rendered template + a
183-
parser). What makes it a *tool-call* is the configuration: the tool name,
184-
the retry-with-reminder string, the optional fallback shape. Express it as:
182+
Example — a `@audit-trail: true` flag on every `field.*` subtype, marking
183+
columns whose changes get logged:
185184

186185
```ts
187-
registry.extend(TYPE_TEMPLATE, "output", {
188-
attributes: [
189-
{ name: "toolName", valueType: ATTR_SUBTYPE_STRING, required: false },
190-
{ name: "retryReminder", valueType: ATTR_SUBTYPE_STRING, required: false },
191-
{ name: "fallback", valueType: ATTR_SUBTYPE_OBJECT, required: false },
192-
],
193-
});
186+
// "metaobjects-audit-trail" provider's registerTypes body:
187+
for (const subType of FIELD_SUBTYPES) {
188+
registry.extend(TYPE_FIELD, subType, {
189+
attributes: [
190+
{ name: "auditTrail", valueType: ATTR_SUBTYPE_BOOLEAN, required: false,
191+
description: "Log changes to this column in the audit-trail table." },
192+
],
193+
});
194+
}
194195
```
195196

196-
YAML authors then write `template.output:` with `@toolName: emit_x`. The
197-
custom generator scans `template.output` nodes and branches on
198-
`node.hasAttr("toolName")` to decide whether to emit a tool-call wrapper.
199-
**Same end result, zero new metamodel surface.**
197+
YAML authors then write `field.string: { "@auditTrail": true }`. A custom
198+
generator scans every field and emits audit-table wiring for those with
199+
`@auditTrail: true`. **Same end result, zero new metamodel surface.** This
200+
is exactly what the built-in `dbProvider` does to add `@column` and
201+
`@db.indexed` to every field subtype.
202+
203+
**When extend doesn't fit:** if the existing subtype has REQUIRED attrs
204+
that don't apply to your concept (e.g., `template.output` requires
205+
`@textRef` for the renderable body, which a non-renderable tool-call
206+
envelope doesn't have), escalation to a new subtype is honest. See the
207+
escalation criteria below.
200208

201209
### When abstracts shine: reusable constraint sets via `extends`
202210

@@ -256,10 +264,27 @@ overlay semantics, common patterns).
256264
- **Different `childRules`** that can't be expressed as additional attrs.
257265
Example: `field.enum` requires an `enum.values` attr and a constrained set
258266
of child types that plain `field.string` doesn't.
267+
- **The closest existing subtype has required attrs that don't apply to
268+
your concept.** Example: `template.output` requires `@payloadRef` AND
269+
`@textRef` (both are renderable-template invariants). An LLM tool-call
270+
envelope needs `@toolName` but has no renderable text body — `@textRef`
271+
doesn't apply. Extending `template.output` with `@toolName` would force
272+
every toolcall to declare a stub `@textRef`. A new subtype
273+
(`template.toolcall` with `@payloadRef` + `@toolName` required, no
274+
`@textRef`) is the honest fit.
259275
- **The semantic concept is so universal it deserves a name in the closed
260276
core vocabulary.** Example: `source.rdb` is a persistence-paradigm split —
261277
there will eventually be `source.kv`, `source.graph`, etc., and they all
262278
need first-class subtype identity for cross-port codegen routing.
279+
- **You need load-time error detection when the provider is missing.** The
280+
loader treats undeclared `@-attrs` as **open policy** — they're silently
281+
accepted, no error or warning (`attr-schema-validate.ts:15`). Only the
282+
*subtype* itself is validated against the registry. So if your concept's
283+
correctness depends on the provider being loaded (e.g., a custom generator
284+
must run when the metadata is present), subtype registration is the only
285+
mechanism that catches "consumer forgot to wire the provider" at load
286+
time via `ERR_UNKNOWN_SUBTYPE`. With pure attr-extension, missing
287+
providers silently no-op.
263288

264289
If the only argument for a new subtype is "the name reads nicer," add an
265290
attr instead. A subtype is a permanent commitment in every port's registry;

0 commit comments

Comments
 (0)