@dtmd/temper 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +84 -53
  2. package/bin/temper.js +0 -0
  3. package/dist/src/assembly.d.ts +15 -1
  4. package/dist/src/assembly.js +2 -1
  5. package/dist/src/builtins.d.ts +682 -66
  6. package/dist/src/builtins.js +664 -92
  7. package/dist/src/claude-code.d.ts +2 -2
  8. package/dist/src/claude-code.js +1 -1
  9. package/dist/src/contract.d.ts +180 -29
  10. package/dist/src/contract.js +128 -15
  11. package/dist/src/declarations.d.ts +72 -5
  12. package/dist/src/declarations.js +389 -107
  13. package/dist/src/dial.d.ts +75 -0
  14. package/dist/src/dial.js +82 -0
  15. package/dist/src/emit.d.ts +35 -1
  16. package/dist/src/emit.js +369 -63
  17. package/dist/src/generated/AssemblyFactRow.d.ts +2 -2
  18. package/dist/src/generated/BoundRow.d.ts +2 -2
  19. package/dist/src/generated/ClauseRow.d.ts +73 -3
  20. package/dist/src/generated/CollectionAddressRow.d.ts +4 -0
  21. package/dist/src/generated/EmbeddedMember.d.ts +3 -3
  22. package/dist/src/generated/FeatureValue.d.ts +2 -2
  23. package/dist/src/generated/Features.d.ts +52 -3
  24. package/dist/src/generated/KindFactRow.d.ts +24 -7
  25. package/dist/src/generated/MentionRow.d.ts +5 -3
  26. package/dist/src/generated/NestedMemberRow.d.ts +32 -0
  27. package/dist/src/generated/PayloadMember.d.ts +6 -0
  28. package/dist/src/generated/RequirementRow.d.ts +4 -2
  29. package/dist/src/generated/SatisfiesRow.d.ts +2 -1
  30. package/dist/src/generated/Shape.d.ts +15 -0
  31. package/dist/src/generated/Shape.js +2 -0
  32. package/dist/src/generated/TemplateRow.d.ts +24 -0
  33. package/dist/src/generated/TemplateRow.js +2 -0
  34. package/dist/src/generated/ValueType.d.ts +11 -2
  35. package/dist/src/generated/Verifier.d.ts +20 -0
  36. package/dist/src/generated/Verifier.js +2 -0
  37. package/dist/src/generated/index.d.ts +3 -0
  38. package/dist/src/index.d.ts +8 -8
  39. package/dist/src/index.js +3 -3
  40. package/dist/src/kind.d.ts +135 -29
  41. package/dist/src/kind.js +35 -8
  42. package/dist/src/prose.d.ts +81 -25
  43. package/dist/src/prose.js +91 -21
  44. package/package.json +4 -4
@@ -9,7 +9,7 @@
9
9
  * in lockstep.
10
10
  */
11
11
  import { fileURLToPath } from "node:url";
12
- import { resolveLeaf } from "./prose.js";
12
+ import { isTextSpan, resolveLeaf } from "./prose.js";
13
13
  /**
14
14
  * Compile one `Clause` into its lock row: the shared `key`/`field`/`severity`/
15
15
  * `guidance`/`cite` columns — the clause's four channels surviving erasure
@@ -17,7 +17,7 @@ import { resolveLeaf } from "./prose.js";
17
17
  * predicate carries them, the `count`/`target`/`degree` argument columns a
18
18
  * requirement's own set-/edge-scope demand needs, and the
19
19
  * `bound`/`charset`/`keys`/`values` argument columns a kind's own node-scope
20
- * floor clause needs (`min_len`/`max_len`/`max_lines`'s bound,
20
+ * floor clause needs (`min_len`/`max_len`/`extent`'s bound, `extent`'s unit,
21
21
  * `allowed_chars`'s charset, `forbidden_keys`'s keys, `deny`'s values) — so
22
22
  * the lock encodes the floor losslessly, not identity+severity alone. `kind`
23
23
  * is supplied only for a kind's own `expect` clause; a requirement's nested
@@ -25,6 +25,37 @@ import { resolveLeaf } from "./prose.js";
25
25
  */
26
26
  function clauseRow(clause, kind) {
27
27
  const { predicate } = clause;
28
+ // For a `when` clause, extract the guard predicate's key and arguments,
29
+ // then convert the body to nested rows.
30
+ if (predicate.key === "when") {
31
+ const guardPredicate = clause.when_guard;
32
+ if (!guardPredicate) {
33
+ throw new Error("when clause missing guard predicate");
34
+ }
35
+ // Build a row for the guard predicate's arguments, then copy those fields
36
+ // into this row alongside the guard_predicate and body.
37
+ const guardRow = clauseRow({ predicate: guardPredicate, severity: clause.severity }, undefined);
38
+ return {
39
+ kind,
40
+ predicate: "when",
41
+ severity: clause.severity,
42
+ guidance: clause.guidance,
43
+ cite: clause.cite,
44
+ field: guardRow.field,
45
+ guard_predicate: guardRow.predicate,
46
+ value_type: guardRow.value_type,
47
+ shape: guardRow.shape,
48
+ bound: guardRow.bound,
49
+ unit: guardRow.unit,
50
+ charset: guardRow.charset,
51
+ keys: guardRow.keys,
52
+ values: guardRow.values,
53
+ range: guardRow.range,
54
+ section: guardRow.section,
55
+ sections: guardRow.sections,
56
+ body: clause.when_body ? clause.when_body.map((c) => clauseRow(c, undefined)) : undefined,
57
+ };
58
+ }
28
59
  return {
29
60
  kind,
30
61
  predicate: predicate.key,
@@ -42,10 +73,14 @@ function clauseRow(clause, kind) {
42
73
  outgoing: edgeBoundArgs(predicate.args, "outgoing"),
43
74
  }
44
75
  : undefined,
76
+ gate: predicate.key === "mention-reachable" ? predicate.gate : undefined,
77
+ value_type: predicate.key === "type" && predicate.value_type ? [...predicate.value_type] : undefined,
78
+ shape: predicate.key === "shape" ? predicate.shape : undefined,
45
79
  bound: nodeScopeBoundArgs(predicate),
46
- // The generated rows carry mutable columns; the predicate's `charset`/`keys`/
47
- // `values` are read-only, so copy each into a fresh array/object — the same
48
- // bytes, a shape the row will accept.
80
+ unit: predicate.key === "extent" ? predicate.unit : undefined,
81
+ // The generated rows carry mutable columns; the predicate's `value_type`/
82
+ // `charset`/`keys`/`values` are read-only, so copy each into a fresh
83
+ // array/object — the same bytes, a shape the row will accept.
49
84
  charset: predicate.key === "allowed_chars" && predicate.charset !== undefined
50
85
  ? {
51
86
  ranges: predicate.charset.ranges ? [...predicate.charset.ranges] : undefined,
@@ -62,13 +97,16 @@ function clauseRow(clause, kind) {
62
97
  section: predicate.key === "section_contains" && predicate.section !== undefined
63
98
  ? { heading: predicate.section.heading, marker: predicate.section.marker }
64
99
  : undefined,
100
+ sections: predicate.key === "require_sections" && predicate.sections
101
+ ? [...predicate.sections]
102
+ : undefined,
65
103
  };
66
104
  }
67
- /** `min_len`/`max_len`/`max_lines`'s scalar bound off their shared `min`/`max`
105
+ /** `min_len`/`max_len`/`extent`'s scalar bound off their shared `min`/`max`
68
106
  * args keys — `undefined` for every other predicate, and for these three when
69
107
  * neither endpoint is present. */
70
108
  function nodeScopeBoundArgs(predicate) {
71
- if (predicate.key !== "min_len" && predicate.key !== "max_len" && predicate.key !== "max_lines") {
109
+ if (predicate.key !== "min_len" && predicate.key !== "max_len" && predicate.key !== "extent") {
72
110
  return undefined;
73
111
  }
74
112
  const min = predicate.args?.min;
@@ -83,10 +121,10 @@ function edgeBoundArgs(args, direction) {
83
121
  return min === undefined && max === undefined ? undefined : { min, max };
84
122
  }
85
123
  /**
86
- * The lock label for a kind's declared unit shape: `file`/`directory` verbatim,
87
- * or `named-field(<identityField>)` for the third mode — the same `<name>(<field>)`
88
- * call syntax [`registrationLabel`] uses, so the id source round-trips through the
89
- * row rather than degrading to a bare, unreconstructable `"named-field"`.
124
+ * The lock label for a kind's declared unit shape: `file`/`directory`/`starred-segment`
125
+ * verbatim, or `named-field(<identityField>)` for the field-sourced mode — the same
126
+ * `<name>(<field>)` call syntax [`registrationLabel`] uses, so the id source round-trips
127
+ * through the row rather than degrading to a bare, unreconstructable `"named-field"`.
90
128
  */
91
129
  function unitShapeLabel(facts) {
92
130
  if (facts.unitShape !== "named-field")
@@ -108,6 +146,10 @@ function registrationLabel(registration) {
108
146
  return `event(${registration.field})`;
109
147
  case "connection":
110
148
  return "connection";
149
+ case "enablement":
150
+ return "enablement";
151
+ case "registry":
152
+ return "registry";
111
153
  }
112
154
  }
113
155
  /** The lock labels for a kind's declared registration **set**, in declaration order —
@@ -120,16 +162,28 @@ export function compareStrings(a, b) {
120
162
  return a < b ? -1 : a > b ? 1 : 0;
121
163
  }
122
164
  /**
123
- * A host kind's declared nesting templates — the embedded kinds among `allKinds`
124
- * whose `withinHosts` names it, name-sorted. `undefined` when the host nests nothing, so the row omits the column
125
- * rather than carrying an empty array.
165
+ * A host kind's nesting templates, from its two declaration loci. The kind's own
166
+ * declared templates carry each layer — child kind, plus a file layer's path pattern.
167
+ * An adopting corpus that admits its own embedded kinds over the host overrides the
168
+ * *embedded* grain only: an admission names a host and a child kind but no path, so it
169
+ * can only speak for the pathless (embedded) layer. The path-carrying file layers the
170
+ * host declares are the host's own facts and stand, joined with the admitted rows —
171
+ * else composing a body over a host wipes its declared file layer and the engine, finding
172
+ * no file template, refuses the host's nested file children.
173
+ *
174
+ * `undefined` when nothing is declared or admitted, so the row omits the column rather
175
+ * than carrying an empty array.
126
176
  */
127
- function templatesFor(hostName, allKinds) {
128
- const names = allKinds
129
- .filter((facts) => facts.locus.kind === "embedded" && facts.locus.withinHosts.includes(hostName))
130
- .map((facts) => facts.name)
131
- .sort(compareStrings);
132
- return names.length > 0 ? names : undefined;
177
+ function templatesFor(facts, admissions) {
178
+ const declared = (facts.templates ?? []).map((template) => ({
179
+ kind: template.kind.key,
180
+ path: template.path,
181
+ }));
182
+ const admitted = [...(admissions.get(facts.name) ?? [])].sort(compareStrings);
183
+ if (admitted.length === 0)
184
+ return declared.length > 0 ? declared : undefined;
185
+ const fileLayers = declared.filter((template) => template.path !== undefined);
186
+ return [...fileLayers, ...admitted.map((kind) => ({ kind }))];
133
187
  }
134
188
  /**
135
189
  * Lower a kind's declared {@link Layout} into its `content` row — one flat
@@ -154,54 +208,99 @@ function contentRow(content) {
154
208
  }
155
209
  /**
156
210
  * Lower a kind's declared {@link CollectionAddress} into its `collection_address` row —
157
- * `keyPath` spelled as the wire's snake_case `key_path`. `undefined` for a file-locus
158
- * kind, so its row omits the column and stays byte-identical.
211
+ * `keyPath` spelled as the wire's snake_case `key_path`, and `entryShape` passed through
212
+ * as a string wire format (`object`, `scalar(field)`, or `group-array(member_key;lifted_fields)`).
213
+ * `undefined` for a file-locus kind, so its row omits the column and stays byte-identical.
159
214
  */
160
215
  function collectionAddressRow(facts) {
161
216
  if (facts.collectionAddress === undefined)
162
217
  return undefined;
163
- return { manifest: facts.collectionAddress.manifest, key_path: facts.collectionAddress.keyPath };
218
+ return {
219
+ manifest: facts.collectionAddress.manifest,
220
+ key_path: facts.collectionAddress.keyPath,
221
+ entry_shape: facts.collectionAddress.entryShape,
222
+ };
164
223
  }
165
224
  /**
166
- * One kind's fact row — the `at` locus supplies `governs_root`/`governs_glob`,
167
- * `templates` names the embedded kinds (among `allKinds`) declared within it, and
168
- * `content` lowers a declared layout (absent for a `file`-content kind). A registration
169
- * kind extends the row with its `shape` marker and `collection_address`.
225
+ * One kind's fact row — an `at` locus supplies `governs_root`/`governs_glob` and a
226
+ * nested-file kind neither (its path composes from its host's unit and the host
227
+ * template's pattern, so it governs no glob). A file locus's `commitment` class rides
228
+ * the same spelling, absent for the committed default. `templates` names the embedded kinds the
229
+ * corpus admits over it, and `content` lowers a declared layout (absent for a
230
+ * `file`-content kind). A registration kind extends the row with its `shape` marker and
231
+ * `collection_address`.
170
232
  */
171
- function kindFactRow(facts, allKinds) {
172
- if (facts.locus.kind !== "at") {
173
- // An embedded kind inherits its world residue through its host; it carries no
174
- // `at` locus, so it takes no kind-fact row. Callers filter these out before this point.
233
+ function kindFactRow(facts, admissions) {
234
+ if (facts.locus.kind === "embedded") {
235
+ // An embedded kind inherits its world residue through its host; it owns no unit at
236
+ // all, so it takes no kind-fact row. Callers filter these out before this point.
175
237
  throw new Error(`kind \`${facts.name}\` is embedded — it carries no locus-bearing kind fact.`);
176
238
  }
239
+ const governs = facts.locus.kind === "at" ? facts.locus : undefined;
177
240
  return {
178
241
  name: facts.name,
179
242
  provider: facts.provider,
180
- governs_root: facts.locus.root,
181
- governs_glob: facts.locus.glob,
243
+ governs_root: governs?.root,
244
+ governs_glob: governs?.glob,
245
+ commitment: governs?.commitment,
182
246
  format: facts.format,
183
247
  unit_shape: unitShapeLabel(facts),
184
248
  registration: registrationLabels(facts.registration),
185
- templates: templatesFor(facts.name, allKinds),
249
+ templates: templatesFor(facts, admissions),
186
250
  content: contentRow(facts.content),
187
251
  shape: facts.shape,
188
252
  collection_address: collectionAddressRow(facts),
189
253
  };
190
254
  }
191
- /** Every kind in play, at any locus — member kinds ∪ expect kinds ∪ their embedded children. */
255
+ /**
256
+ * Every kind in play, at any locus — member kinds ∪ expect kinds ∪ their embedded
257
+ * children — name-sorted, so every family derived from it inherits one stable order.
258
+ *
259
+ * The embedded children close transitively over both channels a host names one through:
260
+ * `admit` (the adopting corpus's declaration) and a path-less `templates` entry (the
261
+ * embedded layer). An embedded kind reaches the lock through its host's `templates`
262
+ * column rather than a row of its own, so those channels are the only way one is in play
263
+ * at all — and its declared edge fields are still owed their assembly facts.
264
+ *
265
+ * Only *embedded* children are drawn in. A path-carrying template is the nested-file
266
+ * layer, whose child owns a unit and reaches the lock through `expect` like any other
267
+ * unit kind; pulling one in here would forge it a kind-fact row it never declared.
268
+ */
192
269
  function kindsInPlay(harness) {
193
270
  const byName = new Map();
271
+ const pending = [];
272
+ const admit = (facts) => {
273
+ if (byName.has(facts.name))
274
+ return;
275
+ byName.set(facts.name, facts);
276
+ pending.push(facts);
277
+ };
194
278
  for (const member of harness.members)
195
- byName.set(member.facts.name, member.facts);
279
+ admit(member.facts);
196
280
  for (const binding of harness.expect)
197
- byName.set(binding.kind.facts.name, binding.kind.facts);
198
- return [...byName.values()];
281
+ admit(binding.kind.facts);
282
+ for (const { admits } of harness.admit)
283
+ for (const child of admits)
284
+ admit(child.facts);
285
+ for (let facts = pending.pop(); facts !== undefined; facts = pending.pop()) {
286
+ for (const template of facts.templates ?? []) {
287
+ if (template.kind.facts.locus.kind === "embedded")
288
+ admit(template.kind.facts);
289
+ }
290
+ }
291
+ return [...byName.values()].sort((a, b) => compareStrings(a.name, b.name));
199
292
  }
200
- /** The distinct locus-bearing (`at`) kinds in play, name-sorted. */
293
+ /** The distinct discoverable (`at`) kinds in play. */
201
294
  function atLocusKindsInPlay(allKinds) {
202
- return allKinds
203
- .filter((facts) => facts.locus.kind === "at")
204
- .sort((a, b) => compareStrings(a.name, b.name));
295
+ return allKinds.filter((facts) => facts.locus.kind === "at");
296
+ }
297
+ /**
298
+ * The distinct unit-owning kinds in play — every locus but `embedded`. The kinds that
299
+ * take a fact row: a nested-file kind owns a file the engine must place, and places it
300
+ * off its row, though it governs no glob to be discovered at.
301
+ */
302
+ function unitKindsInPlay(allKinds) {
303
+ return allKinds.filter((facts) => facts.locus.kind !== "embedded");
205
304
  }
206
305
  /** The requirement rows — assembly `require` and every member's `requires`, one namespace. */
207
306
  function requirementRows(harness) {
@@ -227,34 +326,56 @@ function requirementRows(harness) {
227
326
  .sort(([a], [b]) => compareStrings(a, b))
228
327
  .map(([name, requirement]) => ({
229
328
  name,
230
- kind: requirement.kind?.key,
329
+ kind: typeof requirement.kind === "string" ? requirement.kind : requirement.kind?.key,
231
330
  required: requirement.required ?? false,
232
331
  clauses: (requirement.clauses ?? []).map((clause) => clauseRow(clause)),
233
- verified_by: requirement.verifiedBy,
332
+ verifier: verifierRow(requirement.verifier),
234
333
  prose: requirement.prose,
235
334
  }));
236
335
  }
237
336
  /**
238
- * The assembly-scope facts, in a stable order: the root member's declared
239
- * `mode` (always present"The root
240
- * member": harness-wide declarations are root-member fields), then one edge
241
- * row per kind edge field.
337
+ * Lower a typed verifier to its species-tagged wire row — `species` plus the
338
+ * variant's own payload. The generated row carries a mutable `events` column, so
339
+ * the telemetry species copies its read-only names into a fresh array (the same
340
+ * read-only→mutable copy `charset`/`keys`/`values` make above).
341
+ */
342
+ function verifierRow(verifier) {
343
+ if (verifier === undefined)
344
+ return undefined;
345
+ return verifier.species === "script"
346
+ ? { species: "script", path: verifier.path }
347
+ : { species: "telemetry", events: [...verifier.events] };
348
+ }
349
+ /**
350
+ * The assembly-scope facts, in a stable order: the root member's declared `mode`, then
351
+ * one edge row per kind edge field.
352
+ *
353
+ * `kinds` is every kind in play at *any* locus, not just the unit-owning ones that take
354
+ * a kind-fact row: an edge is a declared relationship at any grain, so an embedded
355
+ * kind's edge fields are owed their rows even though the kind itself reaches the lock
356
+ * through its host's `templates` column alone.
242
357
  */
243
358
  function assemblyFactRows(harness, kinds) {
244
359
  const facts = [{ fact: "mode", value: harness.mode }];
245
360
  for (const kind of kinds) {
246
361
  for (const edge of kind.edgeFields ?? []) {
247
- facts.push({ fact: "edge", from: kind.name, field: edge.field, to: edge.to });
362
+ facts.push({ fact: "edge", from: kind.name, field: edge.field, to: [...edge.to] });
248
363
  }
249
364
  }
250
365
  return facts;
251
366
  }
252
- /** The `satisfies` rows — every member's fill claims, member-then-requirement sorted. */
367
+ /**
368
+ * The `satisfies` rows — every member's fill claims, member-then-requirement sorted.
369
+ * The `member` is the filler's own `kind:name` address, the same identity
370
+ * `mentionRows` writes, so the read side joins on a kind-qualified label a same-named
371
+ * member of another kind can never collide with.
372
+ */
253
373
  function satisfiesRows(harness) {
254
374
  const rows = [];
255
375
  for (const member of harness.members) {
376
+ const address = `${member.kind}:${member.name}`;
256
377
  for (const requirement of member.satisfies) {
257
- rows.push({ member: member.name, requirement });
378
+ rows.push({ member: address, requirement });
258
379
  }
259
380
  }
260
381
  return rows.sort((a, b) => compareStrings(a.member, b.member) || compareStrings(a.requirement, b.requirement));
@@ -262,25 +383,33 @@ function satisfiesRows(harness) {
262
383
  /**
263
384
  * The `mention` rows — every member's authored `n` targets, member-then-target
264
385
  * sorted. `text`-kind prose contributes one row per mention, keyed to the
265
- * member's own `kind:name` address; `blocks()`-kind prose additionally
266
- * contributes one row per mention inside a `Text`-authored leaf, keyed to that
267
- * leaf's own `<member>/<kind>/<key>/<child-path>` address
268
- * ([`embeddedLeafMentionRows`]) — a `file()` body names none. Recorded off the
269
- * raw authored address, unconditionally — resolution is `emit`'s own refusal
386
+ * member's own `kind:name` address. A `blocks()` composed body keys each child
387
+ * to what it is: a prose span's mentions are host-level, keyed to the member's
388
+ * own `kind:name` address like a `text` body; an embedded value's `Text`-leaf
389
+ * mentions are keyed to that leaf's own `<member>/<kind>/<key>/<child-path>`
390
+ * address ([`embeddedLeafMentionRows`]). A `file()` body names none. Recorded off
391
+ * the raw authored address, unconditionally — resolution is `emit`'s own refusal
270
392
  * (`emit.ts`), not this row's concern.
271
393
  */
272
394
  function mentionRows(harness) {
273
395
  const rows = [];
274
396
  for (const member of harness.members) {
397
+ const address = `${member.kind}:${member.name}`;
275
398
  if (member.prose?.kind === "text") {
276
- const address = `${member.kind}:${member.name}`;
277
399
  for (const mention of member.prose.mentions) {
278
400
  rows.push({ member: address, target: mention.target.address });
279
401
  }
280
402
  }
281
403
  if (member.prose?.kind === "blocks") {
282
404
  for (const value of member.prose.values) {
283
- rows.push(...embeddedLeafMentionRows(member.name, value));
405
+ if (isTextSpan(value)) {
406
+ for (const mention of value.mentions) {
407
+ rows.push({ member: address, target: mention.target.address });
408
+ }
409
+ }
410
+ else {
411
+ rows.push(...embeddedLeafMentionRows(member.name, value));
412
+ }
284
413
  }
285
414
  }
286
415
  }
@@ -324,93 +453,140 @@ function embeddedLeafMentionRows(hostName, value) {
324
453
  * path resolved against the stating module ({@link fileURLToPath} over the include's own
325
454
  * `moduleUrl`), never the workspace — the engine reads, splices, and fingerprints it.
326
455
  * Member order stays authored (never target-sorted): the body's include slots ride the
327
- * same order, so the engine pairs the k-th slot with the k-th row. Only member-level
328
- * `text` prose carries includes (a `file()`/`blocks()` body names none; an embedded leaf
329
- * is refused at {@link resolveLeaf}).
456
+ * same order, so the engine pairs the k-th slot with the k-th row. Includes ride a `text`
457
+ * body and a composed body's prose spans alike, in authored order across the interleave; an
458
+ * embedded leaf carries none (refused at {@link resolveLeaf}) and a `file()` body names none.
330
459
  */
331
460
  function includeRows(harness) {
332
461
  const rows = [];
462
+ const push = (address, include) => {
463
+ rows.push({ member: address, source_path: fileURLToPath(new URL(include.path, include.moduleUrl)) });
464
+ };
333
465
  for (const member of harness.members) {
334
- if (member.prose?.kind !== "text")
335
- continue;
336
466
  const address = `${member.kind}:${member.name}`;
337
- for (const include of member.prose.includes) {
338
- rows.push({ member: address, source_path: fileURLToPath(new URL(include.path, include.moduleUrl)) });
467
+ if (member.prose?.kind === "text") {
468
+ for (const include of member.prose.includes)
469
+ push(address, include);
470
+ }
471
+ else if (member.prose?.kind === "blocks") {
472
+ for (const value of member.prose.values) {
473
+ if (!isTextSpan(value))
474
+ continue;
475
+ for (const include of value.includes)
476
+ push(address, include);
477
+ }
339
478
  }
340
479
  }
341
480
  return rows;
342
481
  }
482
+ /**
483
+ * One composed embedded value's key in an {@link EdgePlacements} table — its host's
484
+ * `kind:name` address plus the value's own kind and key, the same triple the
485
+ * `nested_member` row it feeds is identified by.
486
+ */
487
+ export function placementKey(host, kind, key) {
488
+ return `${host}${kind}${key}`;
489
+ }
343
490
  /**
344
491
  * One host member's declared embedded-member value as its declaration row —
345
492
  * each `Text`-authored leaf resolved to its final stored string
346
- * ([`NestedMemberRow`]), mention-resolution-checked against `mentionable` the
347
- * identical way `emit.ts`'s `renderMemberToml` checks the same leaf on its way
348
- * into the rendered fence.
493
+ * ([`NestedMemberRow`]), mention-resolution-checked against `scope` the identical
494
+ * way `emit.ts`'s `renderMemberToml` checks the same leaf on its way into the
495
+ * rendered fence — plus the `placed_edges` record `emit` observed while rendering the
496
+ * same value, which is the only way an edge's placement reaches the engine.
349
497
  */
350
- function nestedMemberRow(host, value, mentionable) {
498
+ function nestedMemberRow(host, value, scope, placements, extents) {
351
499
  const context = (childPath) => `member.${value.kind} ${value.key}: leaf \`${childPath}\``;
352
500
  const leaves = {};
353
501
  for (const [field, leaf] of Object.entries(value.leaves)) {
354
- leaves[field] = resolveLeaf(leaf, mentionable, context(field));
502
+ leaves[field] = resolveLeaf(leaf, scope, context(field));
355
503
  }
356
504
  const collections = {};
357
505
  for (const [collection, entries] of Object.entries(value.collections)) {
358
506
  collections[collection] = entries.map((entry) => {
359
507
  const entryLeaves = {};
360
508
  for (const [field, leaf] of Object.entries(entry.leaves)) {
361
- entryLeaves[field] = resolveLeaf(leaf, mentionable, context(`${collection}.${entry.key}.${field}`));
509
+ entryLeaves[field] = resolveLeaf(leaf, scope, context(`${collection}.${entry.key}.${field}`));
362
510
  }
363
511
  return { key: entry.key, leaves: entryLeaves };
364
512
  });
365
513
  }
366
- return { host, kind: value.kind, key: value.key, leaves, collections };
514
+ const key = placementKey(host, value.kind, value.key);
515
+ const placed = placements?.get(key);
516
+ const extent = extents?.get(key);
517
+ return {
518
+ host,
519
+ kind: value.kind,
520
+ key: value.key,
521
+ leaves,
522
+ collections,
523
+ // Omitted, never `undefined`-valued: an absent column is the wire's own spelling of
524
+ // "no format placement was observed here", so a value with no edge to place keeps
525
+ // the row it has always written. The generated row carries a mutable column and the
526
+ // placement record is read-only, so the copy is a fresh array.
527
+ ...(placed === undefined ? {} : { placed_edges: [...placed] }),
528
+ // The rendered span, on the same omitted-not-null discipline: a value no render
529
+ // observed (compiled without `emit`'s measurement) keeps the row it has always
530
+ // written, and its `extent` reads as undecidable rather than a captured zero.
531
+ ...(extent === undefined ? {} : { rendered_lines: extent.lines, rendered_chars: extent.chars }),
532
+ };
367
533
  }
368
534
  /**
369
- * Every in-play embedded kind's declared host kinds, keyed by kind name — the map
370
- * {@link nestedMemberRows} checks a `blocks()` value's kind against. An embedded kind
371
- * reaches this map through `kindsInPlay` (a member of it, or an `expect` binding), and
372
- * its `withinHosts` is the set of host kinds it templates within; a value whose kind is
373
- * absent here, or whose hosts miss the hosting member's kind, is untemplated nesting.
535
+ * The corpus's `admit` declarations, indexed by host kind name — the one source both
536
+ * {@link templatesFor} and {@link nestedMemberRows} read. Admission carries kind values,
537
+ * so an admitted kind is imported and thereby in play; repeated hosts union.
538
+ *
539
+ * # Throws
540
+ * If an admission names a non-embedded kind: a body composes embedded members, and a
541
+ * file-locus kind owns a file instead — admitting one declares a nesting no locus backs.
374
542
  */
375
- function embeddedHostsByKind(harness) {
543
+ function admissionsByHost(harness) {
376
544
  const map = new Map();
377
- for (const facts of kindsInPlay(harness)) {
378
- if (facts.locus.kind === "embedded") {
379
- map.set(facts.name, new Set(facts.locus.withinHosts));
545
+ for (const { host, admits } of harness.admit) {
546
+ const admitted = map.get(host.key) ?? new Set();
547
+ for (const child of admits) {
548
+ if (child.facts.locus.kind !== "embedded") {
549
+ throw new Error(`host kind \`${host.key}\` admits \`${child.key}\`, which is not an embedded kind — ` +
550
+ `a composed body admits embedded members only ` +
551
+ `(specs/model/representation.md, "nesting").`);
552
+ }
553
+ admitted.add(child.key);
380
554
  }
555
+ map.set(host.key, admitted);
381
556
  }
382
557
  return map;
383
558
  }
384
559
  /**
385
560
  * The `nested_member` rows — every host member's `blocks()`-declared embedded-member
386
- * values, host-then-kind-then-key sorted. Only `blocks`-kind prose carries them (a
387
- * `file()`/`text` body names none); the fence rendering itself is unchanged
561
+ * values, host-then-kind-then-key sorted. Only a composed body's embedded values carry
562
+ * them (a `file()`/`text` body — and a composed body's prose spans — name none); the
563
+ * fence rendering itself is unchanged
388
564
  * (`emit.ts`'s `resolveBody`) — this row is a second *read* of the same authored
389
565
  * value, never a second copy the engine reads back (0018).
390
566
  *
391
- * Refuses an untemplated nesting before a byte is written: a value's kind must be an
392
- * in-play embedded kind whose `withinHosts` names the hosting member's kind. `templates`
393
- * derives solely from `withinHosts` ({@link templatesFor}), so a value the host never
394
- * templates would reach the lock as a `nested_member` row no `templates` column admits,
395
- * to be unmodeled without a word — nesting is the host template's own declaration, so an
396
- * unadmitted nested member is an unresolved input, not output to write over.
567
+ * Refuses an unadmitted nesting before a byte is written: the corpus must admit the
568
+ * value's kind over the hosting member's kind. `templates` derives from that same
569
+ * admission ({@link templatesFor}), so an unadmitted value would reach the lock as a
570
+ * `nested_member` row no `templates` column admits, to be unmodeled without a word —
571
+ * admission is the adopting corpus's own declaration, so an unadmitted nested member is
572
+ * an unresolved input, not output to write over.
397
573
  */
398
- function nestedMemberRows(harness, mentionable) {
399
- const hostsByKind = embeddedHostsByKind(harness);
574
+ function nestedMemberRows(harness, admissions, scope, placements, extents) {
400
575
  const rows = [];
401
576
  for (const member of harness.members) {
402
577
  if (member.prose?.kind !== "blocks")
403
578
  continue;
404
579
  const host = `${member.kind}:${member.name}`;
405
580
  for (const value of member.prose.values) {
406
- const hosts = hostsByKind.get(value.kind);
407
- if (hosts === undefined || !hosts.has(member.kind)) {
581
+ if (isTextSpan(value))
582
+ continue;
583
+ if (!admissions.get(member.kind)?.has(value.kind)) {
408
584
  throw new Error(`member \`${member.name}\`: embedded value \`${value.key}\` is of kind ` +
409
585
  `\`${value.kind}\`, which does not nest within host kind \`${member.kind}\` — a ` +
410
- `\`blocks()\` value's kind must be an in-play embedded kind whose \`withinHosts\` ` +
411
- `names the host (specs/model/representation.md, "nesting").`);
586
+ `\`blocks()\` value's kind must be one the harness \`admit\`s over the host kind ` +
587
+ `(specs/model/representation.md, "nesting").`);
412
588
  }
413
- rows.push(nestedMemberRow(host, value, mentionable));
589
+ rows.push(nestedMemberRow(host, value, scope, placements, extents));
414
590
  }
415
591
  }
416
592
  return rows.sort((a, b) => compareStrings(a.host, b.host) || compareStrings(a.kind, b.kind) || compareStrings(a.key, b.key));
@@ -448,6 +624,76 @@ export function registrationRows(harness) {
448
624
  })
449
625
  .sort((a, b) => compareStrings(a.kind, b.kind) || compareStrings(a.key, b.key));
450
626
  }
627
+ /** The tap invocation the synthesized telemetry hooks run — the sibling verb of the
628
+ * session-start reporter, appending one event record to the per-machine log
629
+ * (`src/tap.rs`). Every synthesized hook's `command` field carries it verbatim. */
630
+ const TAP_COMMAND = "temper tap";
631
+ /**
632
+ * The `settings.json` lifecycle event and matcher one documented telemetry event-name
633
+ * projects its tap hook at. The event-name is the author-facing token a telemetry
634
+ * verifier names (`contract.ts`'s `telemetry`, the `roster.rs` admissibility set); the
635
+ * `event` is the `hooks.<Event>` key the tap registers under, and the `matcher` scopes
636
+ * the fire to the telemetry-relevant subset — each an external fact
637
+ * (code.claude.com/docs/en/hooks, retrieved 2026-07-17):
638
+ *
639
+ * - `InstructionsLoaded` fires on a rule/memory load; its matcher filters the load
640
+ * reason, and `path_glob_match` is the lazy per-path load the coverage tap reads.
641
+ * - `Skill` is a skill invocation, surfaced under `PostToolUse` with the tool-name
642
+ * matcher `Skill` — the tap's own read of a skill call.
643
+ * - `UserPromptExpansion` fires on a command expansion; its matcher filters the command
644
+ * name, `.*` capturing every one.
645
+ * - `PostToolUse` fires after any tool call; its matcher filters the tool name, `.*`
646
+ * capturing every one.
647
+ */
648
+ const TELEMETRY_EVENT_HOOKS = {
649
+ InstructionsLoaded: { event: "InstructionsLoaded", matcher: "path_glob_match" },
650
+ Skill: { event: "PostToolUse", matcher: "Skill" },
651
+ UserPromptExpansion: { event: "UserPromptExpansion", matcher: ".*" },
652
+ PostToolUse: { event: "PostToolUse", matcher: ".*" },
653
+ };
654
+ /**
655
+ * The synthesized tap-hook `registration` rows — one deduped `hooks.<Event>`
656
+ * registration per (lifecycle event, matcher) any telemetry verifier names. Scans the
657
+ * same requirement sources {@link requirementRows} reads (assembly `require` ∪ each
658
+ * member's `requires`), keeps the telemetry-species verifiers ({@link Verifier}), and
659
+ * unions the lifecycle events they name into one dumb registration apiece: the tap
660
+ * records every fire and read time joins raw events to members, so however many
661
+ * verifiers name an event it takes exactly one hook — the derived-aggregate precedent
662
+ * the permission union sets ({@link permissionUnion}). Each row runs {@link TAP_COMMAND}
663
+ * under the event's documented matcher ({@link TELEMETRY_EVENT_HOOKS}); an event-name
664
+ * outside that table is the roster's inadmissibility finding, never a row.
665
+ */
666
+ export function tapHookRows(harness) {
667
+ const deduped = new Map();
668
+ const collect = (requirement) => {
669
+ if (requirement.verifier?.species !== "telemetry")
670
+ return;
671
+ for (const name of requirement.verifier.events) {
672
+ const mapping = TELEMETRY_EVENT_HOOKS[name];
673
+ if (mapping !== undefined)
674
+ deduped.set(`${mapping.event}${mapping.matcher}`, mapping);
675
+ }
676
+ };
677
+ for (const requirement of Object.values(harness.require))
678
+ collect(requirement);
679
+ for (const member of harness.members) {
680
+ for (const requirement of Object.values(member.requires))
681
+ collect(requirement);
682
+ }
683
+ return [...deduped.values()]
684
+ .sort((a, b) => compareStrings(a.event, b.event) || compareStrings(a.matcher, b.matcher))
685
+ .map(({ event, matcher }) => ({
686
+ kind: "hook",
687
+ key: event,
688
+ manifest: SETTINGS_MANIFEST,
689
+ key_path: "hooks.<Event>",
690
+ fields: [
691
+ ["type", "command"],
692
+ ["command", TAP_COMMAND],
693
+ ["matcher", matcher],
694
+ ],
695
+ }));
696
+ }
451
697
  /**
452
698
  * The manifest Claude Code's harness-level settings reside in — the file the assembly's
453
699
  * residual settings keys fold into as opaque residue, the same manifest the `hook` kind's
@@ -477,20 +723,56 @@ export function declaredRequirements(harness) {
477
723
  }
478
724
  /**
479
725
  * Every address a mention may name — declared requirement names ∪ each member's
480
- * `kind:name`. Shared by `emit.ts` (a member-level `Text` body's mentions) and
481
- * this module (an embedded member's `Text` leaves) — the one resolution-check
482
- * set, so a leaf mention and a member mention are held to the identical bar.
726
+ * `kind:name` ∪ each `blocks()`-declared embedded member's host-scoped
727
+ * `<host-kind>:<host-name>/<kind>/<key>` address. Shared by `emit.ts` (a
728
+ * member-level `Text` body's mentions) and this module (an embedded member's
729
+ * `Text` leaves) — the one resolution-check set, so a leaf mention and a member
730
+ * mention are held to the identical bar. The embedded address is host-scoped,
731
+ * never a flat `<kind>:<key>` — flat would force corpus-wide key uniqueness on
732
+ * embedded kinds.
483
733
  */
484
734
  export function declaredAddresses(harness) {
485
735
  const set = declaredRequirements(harness);
486
- for (const member of harness.members)
736
+ for (const member of harness.members) {
487
737
  set.add(`${member.kind}:${member.name}`);
738
+ if (member.prose?.kind !== "blocks")
739
+ continue;
740
+ for (const value of member.prose.values) {
741
+ if (isTextSpan(value))
742
+ continue;
743
+ set.add(`${member.kind}:${member.name}/${value.kind}/${value.key}`);
744
+ }
745
+ }
488
746
  return set;
489
747
  }
490
- /** Compile a harness into its seven declaration families — the erased program. */
491
- export function compileDeclarations(harness) {
748
+ /**
749
+ * Every discoverable (`at`-locus) kind the program declares — the deferral signal a
750
+ * dangling mention is measured against (`prose.ts`'s `defersToGate`): a mention naming
751
+ * one of these whose member is not a composed value defers to `check`, while a mention
752
+ * naming no declared kind refuses at emit. Member kinds ∪ `expect` kinds; an embedded
753
+ * kind is excluded — its members are composed within a host, never discovered, so a
754
+ * flat `kind:name` mention of one has no discovery locus to defer to.
755
+ */
756
+ export function declaredAtLocusKinds(harness) {
757
+ return new Set(atLocusKindsInPlay(kindsInPlay(harness)).map((facts) => facts.name));
758
+ }
759
+ /** The full {@link MentionScope} the program resolves a mention against — its addresses and its deferral kinds. */
760
+ export function mentionScope(harness) {
761
+ return { mentionable: declaredAddresses(harness), deferrableKinds: declaredAtLocusKinds(harness) };
762
+ }
763
+ /**
764
+ * Compile a harness into its seven declaration families — the erased program.
765
+ *
766
+ * `placements` and `extents` are `emit`'s record of what each embedded value's format
767
+ * rendered (`emit.ts`'s `edgePlacements`) and the span it spanned (`renderedExtents`); this
768
+ * pass compiles declarations and never renders, so it observes neither itself. Omitted,
769
+ * every `nested_member` row omits its `placed_edges`/`rendered_lines`/`rendered_chars`
770
+ * columns — honest (nothing observed a render) but undecidable for a `format-places-edges`
771
+ * or `extent` clause, so a whole compile goes through `emit`, never this alone.
772
+ */
773
+ export function compileDeclarations(harness, placements, extents) {
492
774
  const allKinds = kindsInPlay(harness);
493
- const kinds = atLocusKindsInPlay(allKinds);
775
+ const admissions = admissionsByHost(harness);
494
776
  const clauses = [];
495
777
  for (const binding of [...harness.expect].sort((a, b) => compareStrings(a.kind.key, b.kind.key))) {
496
778
  for (const clause of binding.clauses) {
@@ -498,15 +780,15 @@ export function compileDeclarations(harness) {
498
780
  }
499
781
  }
500
782
  return {
501
- kinds: kinds.map((facts) => kindFactRow(facts, allKinds)),
783
+ kinds: unitKindsInPlay(allKinds).map((facts) => kindFactRow(facts, admissions)),
502
784
  clauses,
503
785
  requirements: requirementRows(harness),
504
- assembly: assemblyFactRows(harness, kinds),
786
+ assembly: assemblyFactRows(harness, allKinds),
505
787
  satisfies: satisfiesRows(harness),
506
788
  mentions: mentionRows(harness),
507
789
  includes: includeRows(harness),
508
- nested_members: nestedMemberRows(harness, declaredAddresses(harness)),
509
- registrations: registrationRows(harness),
790
+ nested_members: nestedMemberRows(harness, admissions, mentionScope(harness), placements, extents),
791
+ registrations: [...registrationRows(harness), ...tapHookRows(harness)],
510
792
  settings: settingsRows(harness),
511
793
  };
512
794
  }