@dtmd/temper 0.0.6 → 0.0.8

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 (50) hide show
  1. package/README.md +84 -53
  2. package/bin/temper.js +52 -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 +765 -48
  6. package/dist/src/builtins.js +767 -68
  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 +92 -6
  12. package/dist/src/declarations.js +448 -102
  13. package/dist/src/dial.d.ts +75 -0
  14. package/dist/src/dial.js +82 -0
  15. package/dist/src/emit.d.ts +82 -1
  16. package/dist/src/emit.js +402 -56
  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 +21 -0
  21. package/dist/src/generated/CollectionAddressRow.js +2 -0
  22. package/dist/src/generated/Declarations.d.ts +17 -0
  23. package/dist/src/generated/EmbeddedMember.d.ts +3 -3
  24. package/dist/src/generated/FeatureValue.d.ts +2 -2
  25. package/dist/src/generated/Features.d.ts +52 -3
  26. package/dist/src/generated/KindFactRow.d.ts +38 -7
  27. package/dist/src/generated/MentionRow.d.ts +5 -3
  28. package/dist/src/generated/NestedMemberRow.d.ts +32 -0
  29. package/dist/src/generated/PayloadMember.d.ts +6 -0
  30. package/dist/src/generated/RegistrationRow.d.ts +35 -0
  31. package/dist/src/generated/RegistrationRow.js +2 -0
  32. package/dist/src/generated/RequirementRow.d.ts +4 -2
  33. package/dist/src/generated/SatisfiesRow.d.ts +2 -1
  34. package/dist/src/generated/SettingsRow.d.ts +25 -0
  35. package/dist/src/generated/SettingsRow.js +2 -0
  36. package/dist/src/generated/Shape.d.ts +15 -0
  37. package/dist/src/generated/Shape.js +2 -0
  38. package/dist/src/generated/TemplateRow.d.ts +24 -0
  39. package/dist/src/generated/TemplateRow.js +2 -0
  40. package/dist/src/generated/ValueType.d.ts +11 -2
  41. package/dist/src/generated/Verifier.d.ts +20 -0
  42. package/dist/src/generated/Verifier.js +2 -0
  43. package/dist/src/generated/index.d.ts +6 -0
  44. package/dist/src/index.d.ts +8 -8
  45. package/dist/src/index.js +3 -3
  46. package/dist/src/kind.d.ts +160 -29
  47. package/dist/src/kind.js +47 -16
  48. package/dist/src/prose.d.ts +81 -25
  49. package/dist/src/prose.js +91 -21
  50. package/package.json +10 -2
@@ -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
@@ -153,42 +207,100 @@ function contentRow(content) {
153
207
  return { regions };
154
208
  }
155
209
  /**
156
- * One kind's fact row — the `at` locus supplies `governs_root`/`governs_glob`,
157
- * `templates` names the embedded kinds (among `allKinds`) declared within it, and
158
- * `content` lowers a declared layout (absent for a `file`-content kind).
210
+ * Lower a kind's declared {@link CollectionAddress} into its `collection_address` row —
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.
214
+ */
215
+ function collectionAddressRow(facts) {
216
+ if (facts.collectionAddress === undefined)
217
+ return undefined;
218
+ return {
219
+ manifest: facts.collectionAddress.manifest,
220
+ key_path: facts.collectionAddress.keyPath,
221
+ entry_shape: facts.collectionAddress.entryShape,
222
+ };
223
+ }
224
+ /**
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`.
159
232
  */
160
- function kindFactRow(facts, allKinds) {
161
- if (facts.locus.kind !== "at") {
162
- // An embedded kind inherits its world residue through its host; it carries no
163
- // `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.
164
237
  throw new Error(`kind \`${facts.name}\` is embedded — it carries no locus-bearing kind fact.`);
165
238
  }
239
+ const governs = facts.locus.kind === "at" ? facts.locus : undefined;
166
240
  return {
167
241
  name: facts.name,
168
242
  provider: facts.provider,
169
- governs_root: facts.locus.root,
170
- governs_glob: facts.locus.glob,
243
+ governs_root: governs?.root,
244
+ governs_glob: governs?.glob,
245
+ commitment: governs?.commitment,
171
246
  format: facts.format,
172
247
  unit_shape: unitShapeLabel(facts),
173
248
  registration: registrationLabels(facts.registration),
174
- templates: templatesFor(facts.name, allKinds),
249
+ templates: templatesFor(facts, admissions),
175
250
  content: contentRow(facts.content),
251
+ shape: facts.shape,
252
+ collection_address: collectionAddressRow(facts),
176
253
  };
177
254
  }
178
- /** 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
+ */
179
269
  function kindsInPlay(harness) {
180
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
+ };
181
278
  for (const member of harness.members)
182
- byName.set(member.facts.name, member.facts);
279
+ admit(member.facts);
183
280
  for (const binding of harness.expect)
184
- byName.set(binding.kind.facts.name, binding.kind.facts);
185
- 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));
186
292
  }
187
- /** The distinct locus-bearing (`at`) kinds in play, name-sorted. */
293
+ /** The distinct discoverable (`at`) kinds in play. */
188
294
  function atLocusKindsInPlay(allKinds) {
189
- return allKinds
190
- .filter((facts) => facts.locus.kind === "at")
191
- .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");
192
304
  }
193
305
  /** The requirement rows — assembly `require` and every member's `requires`, one namespace. */
194
306
  function requirementRows(harness) {
@@ -214,34 +326,56 @@ function requirementRows(harness) {
214
326
  .sort(([a], [b]) => compareStrings(a, b))
215
327
  .map(([name, requirement]) => ({
216
328
  name,
217
- kind: requirement.kind?.key,
329
+ kind: typeof requirement.kind === "string" ? requirement.kind : requirement.kind?.key,
218
330
  required: requirement.required ?? false,
219
331
  clauses: (requirement.clauses ?? []).map((clause) => clauseRow(clause)),
220
- verified_by: requirement.verifiedBy,
332
+ verifier: verifierRow(requirement.verifier),
221
333
  prose: requirement.prose,
222
334
  }));
223
335
  }
224
336
  /**
225
- * The assembly-scope facts, in a stable order: the root member's declared
226
- * `mode` (always present"The root
227
- * member": harness-wide declarations are root-member fields), then one edge
228
- * 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.
229
357
  */
230
358
  function assemblyFactRows(harness, kinds) {
231
359
  const facts = [{ fact: "mode", value: harness.mode }];
232
360
  for (const kind of kinds) {
233
361
  for (const edge of kind.edgeFields ?? []) {
234
- 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] });
235
363
  }
236
364
  }
237
365
  return facts;
238
366
  }
239
- /** 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
+ */
240
373
  function satisfiesRows(harness) {
241
374
  const rows = [];
242
375
  for (const member of harness.members) {
376
+ const address = `${member.kind}:${member.name}`;
243
377
  for (const requirement of member.satisfies) {
244
- rows.push({ member: member.name, requirement });
378
+ rows.push({ member: address, requirement });
245
379
  }
246
380
  }
247
381
  return rows.sort((a, b) => compareStrings(a.member, b.member) || compareStrings(a.requirement, b.requirement));
@@ -249,25 +383,33 @@ function satisfiesRows(harness) {
249
383
  /**
250
384
  * The `mention` rows — every member's authored `n` targets, member-then-target
251
385
  * sorted. `text`-kind prose contributes one row per mention, keyed to the
252
- * member's own `kind:name` address; `blocks()`-kind prose additionally
253
- * contributes one row per mention inside a `Text`-authored leaf, keyed to that
254
- * leaf's own `<member>/<kind>/<key>/<child-path>` address
255
- * ([`embeddedLeafMentionRows`]) — a `file()` body names none. Recorded off the
256
- * 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
257
392
  * (`emit.ts`), not this row's concern.
258
393
  */
259
394
  function mentionRows(harness) {
260
395
  const rows = [];
261
396
  for (const member of harness.members) {
397
+ const address = `${member.kind}:${member.name}`;
262
398
  if (member.prose?.kind === "text") {
263
- const address = `${member.kind}:${member.name}`;
264
399
  for (const mention of member.prose.mentions) {
265
400
  rows.push({ member: address, target: mention.target.address });
266
401
  }
267
402
  }
268
403
  if (member.prose?.kind === "blocks") {
269
404
  for (const value of member.prose.values) {
270
- 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
+ }
271
413
  }
272
414
  }
273
415
  }
@@ -311,97 +453,263 @@ function embeddedLeafMentionRows(hostName, value) {
311
453
  * path resolved against the stating module ({@link fileURLToPath} over the include's own
312
454
  * `moduleUrl`), never the workspace — the engine reads, splices, and fingerprints it.
313
455
  * Member order stays authored (never target-sorted): the body's include slots ride the
314
- * same order, so the engine pairs the k-th slot with the k-th row. Only member-level
315
- * `text` prose carries includes (a `file()`/`blocks()` body names none; an embedded leaf
316
- * 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.
317
459
  */
318
460
  function includeRows(harness) {
319
461
  const rows = [];
462
+ const push = (address, include) => {
463
+ rows.push({ member: address, source_path: fileURLToPath(new URL(include.path, include.moduleUrl)) });
464
+ };
320
465
  for (const member of harness.members) {
321
- if (member.prose?.kind !== "text")
322
- continue;
323
466
  const address = `${member.kind}:${member.name}`;
324
- for (const include of member.prose.includes) {
325
- 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
+ }
326
478
  }
327
479
  }
328
480
  return rows;
329
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
+ }
330
490
  /**
331
491
  * One host member's declared embedded-member value as its declaration row —
332
492
  * each `Text`-authored leaf resolved to its final stored string
333
- * ([`NestedMemberRow`]), mention-resolution-checked against `mentionable` the
334
- * identical way `emit.ts`'s `renderMemberToml` checks the same leaf on its way
335
- * 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.
336
497
  */
337
- function nestedMemberRow(host, value, mentionable) {
498
+ function nestedMemberRow(host, value, scope, placements, extents) {
338
499
  const context = (childPath) => `member.${value.kind} ${value.key}: leaf \`${childPath}\``;
339
500
  const leaves = {};
340
501
  for (const [field, leaf] of Object.entries(value.leaves)) {
341
- leaves[field] = resolveLeaf(leaf, mentionable, context(field));
502
+ leaves[field] = resolveLeaf(leaf, scope, context(field));
342
503
  }
343
504
  const collections = {};
344
505
  for (const [collection, entries] of Object.entries(value.collections)) {
345
506
  collections[collection] = entries.map((entry) => {
346
507
  const entryLeaves = {};
347
508
  for (const [field, leaf] of Object.entries(entry.leaves)) {
348
- entryLeaves[field] = resolveLeaf(leaf, mentionable, context(`${collection}.${entry.key}.${field}`));
509
+ entryLeaves[field] = resolveLeaf(leaf, scope, context(`${collection}.${entry.key}.${field}`));
349
510
  }
350
511
  return { key: entry.key, leaves: entryLeaves };
351
512
  });
352
513
  }
353
- 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
+ };
354
533
  }
355
534
  /**
356
- * Every in-play embedded kind's declared host kinds, keyed by kind name — the map
357
- * {@link nestedMemberRows} checks a `blocks()` value's kind against. An embedded kind
358
- * reaches this map through `kindsInPlay` (a member of it, or an `expect` binding), and
359
- * its `withinHosts` is the set of host kinds it templates within; a value whose kind is
360
- * 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.
361
542
  */
362
- function embeddedHostsByKind(harness) {
543
+ function admissionsByHost(harness) {
363
544
  const map = new Map();
364
- for (const facts of kindsInPlay(harness)) {
365
- if (facts.locus.kind === "embedded") {
366
- 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);
367
554
  }
555
+ map.set(host.key, admitted);
368
556
  }
369
557
  return map;
370
558
  }
371
559
  /**
372
560
  * The `nested_member` rows — every host member's `blocks()`-declared embedded-member
373
- * values, host-then-kind-then-key sorted. Only `blocks`-kind prose carries them (a
374
- * `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
375
564
  * (`emit.ts`'s `resolveBody`) — this row is a second *read* of the same authored
376
565
  * value, never a second copy the engine reads back (0018).
377
566
  *
378
- * Refuses an untemplated nesting before a byte is written: a value's kind must be an
379
- * in-play embedded kind whose `withinHosts` names the hosting member's kind. `templates`
380
- * derives solely from `withinHosts` ({@link templatesFor}), so a value the host never
381
- * templates would reach the lock as a `nested_member` row no `templates` column admits,
382
- * to be unmodeled without a word — nesting is the host template's own declaration, so an
383
- * 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.
384
573
  */
385
- function nestedMemberRows(harness, mentionable) {
386
- const hostsByKind = embeddedHostsByKind(harness);
574
+ function nestedMemberRows(harness, admissions, scope, placements, extents) {
387
575
  const rows = [];
388
576
  for (const member of harness.members) {
389
577
  if (member.prose?.kind !== "blocks")
390
578
  continue;
391
579
  const host = `${member.kind}:${member.name}`;
392
580
  for (const value of member.prose.values) {
393
- const hosts = hostsByKind.get(value.kind);
394
- if (hosts === undefined || !hosts.has(member.kind)) {
581
+ if (isTextSpan(value))
582
+ continue;
583
+ if (!admissions.get(member.kind)?.has(value.kind)) {
395
584
  throw new Error(`member \`${member.name}\`: embedded value \`${value.key}\` is of kind ` +
396
585
  `\`${value.kind}\`, which does not nest within host kind \`${member.kind}\` — a ` +
397
- `\`blocks()\` value's kind must be an in-play embedded kind whose \`withinHosts\` ` +
398
- `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").`);
399
588
  }
400
- rows.push(nestedMemberRow(host, value, mentionable));
589
+ rows.push(nestedMemberRow(host, value, scope, placements, extents));
401
590
  }
402
591
  }
403
592
  return rows.sort((a, b) => compareStrings(a.host, b.host) || compareStrings(a.kind, b.kind) || compareStrings(a.key, b.key));
404
593
  }
594
+ /**
595
+ * The `registration` rows — every fields-only registration member (a hook, an MCP server)
596
+ * erased for the manifest write face, kind-then-key sorted so double emit is byte-stable.
597
+ * Each carries its identity (`kind`/`key`), its collection address (`manifest`/`keyPath`,
598
+ * the wire's snake_case `key_path`), and its folded typed fields — the entry value the
599
+ * engine's write face places under `key`. The one source `emit.ts`'s public
600
+ * {@link RegistrationFact} view also maps from, so the seam and the `EmitResult` sibling
601
+ * cannot disagree on what a manifest carries.
602
+ *
603
+ * # Throws
604
+ * If a fields-only member declares no collection address — it surfaces in no host manifest.
605
+ */
606
+ export function registrationRows(harness) {
607
+ return harness.members
608
+ .filter((member) => member.facts.shape === "fields")
609
+ .map((member) => {
610
+ const address = member.facts.collectionAddress;
611
+ if (address === undefined) {
612
+ throw new Error(`member \`${member.name}\`: a fields-only registration kind declares no ` +
613
+ `collection address — it surfaces in no host manifest (specs/model/pipeline.md, "The SDK").`);
614
+ }
615
+ return {
616
+ kind: member.kind,
617
+ key: member.name,
618
+ manifest: address.manifest,
619
+ key_path: address.keyPath,
620
+ // The generated row carries a mutable field list; the member's is read-only, so
621
+ // copy each pair into a fresh tuple — the same values, a shape the row accepts.
622
+ fields: member.fields.map(([name, value]) => [name, value]),
623
+ };
624
+ })
625
+ .sort((a, b) => compareStrings(a.kind, b.kind) || compareStrings(a.key, b.key));
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
+ }
697
+ /**
698
+ * The manifest Claude Code's harness-level settings reside in — the file the assembly's
699
+ * residual settings keys fold into as opaque residue, the same manifest the `hook` kind's
700
+ * registrations surface inside (code.claude.com/docs/en/settings, retrieved 2026-07-10).
701
+ */
702
+ const SETTINGS_MANIFEST = "settings.json";
703
+ /**
704
+ * The `settings` rows — the assembly's harness-level residual settings keys, each folded
705
+ * into the settings.json manifest's opaque residue at emit. Key-sorted so double emit is
706
+ * byte-stable. Seam-inbound: the value lives in the projected manifest, never the lock.
707
+ */
708
+ export function settingsRows(harness) {
709
+ return Object.entries(harness.settings)
710
+ .map(([key, value]) => ({ manifest: SETTINGS_MANIFEST, key, value }))
711
+ .sort((a, b) => compareStrings(a.key, b.key));
712
+ }
405
713
  /** Every requirement name a `satisfies` claim may fill — assembly `require` ∪ member `requires`. */
406
714
  export function declaredRequirements(harness) {
407
715
  const set = new Set();
@@ -415,20 +723,56 @@ export function declaredRequirements(harness) {
415
723
  }
416
724
  /**
417
725
  * Every address a mention may name — declared requirement names ∪ each member's
418
- * `kind:name`. Shared by `emit.ts` (a member-level `Text` body's mentions) and
419
- * this module (an embedded member's `Text` leaves) — the one resolution-check
420
- * 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.
421
733
  */
422
734
  export function declaredAddresses(harness) {
423
735
  const set = declaredRequirements(harness);
424
- for (const member of harness.members)
736
+ for (const member of harness.members) {
425
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
+ }
426
746
  return set;
427
747
  }
428
- /** Compile a harness into its seven declaration families — the erased program. */
429
- 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) {
430
774
  const allKinds = kindsInPlay(harness);
431
- const kinds = atLocusKindsInPlay(allKinds);
775
+ const admissions = admissionsByHost(harness);
432
776
  const clauses = [];
433
777
  for (const binding of [...harness.expect].sort((a, b) => compareStrings(a.kind.key, b.kind.key))) {
434
778
  for (const clause of binding.clauses) {
@@ -436,14 +780,16 @@ export function compileDeclarations(harness) {
436
780
  }
437
781
  }
438
782
  return {
439
- kinds: kinds.map((facts) => kindFactRow(facts, allKinds)),
783
+ kinds: unitKindsInPlay(allKinds).map((facts) => kindFactRow(facts, admissions)),
440
784
  clauses,
441
785
  requirements: requirementRows(harness),
442
- assembly: assemblyFactRows(harness, kinds),
786
+ assembly: assemblyFactRows(harness, allKinds),
443
787
  satisfies: satisfiesRows(harness),
444
788
  mentions: mentionRows(harness),
445
789
  includes: includeRows(harness),
446
- nested_members: nestedMemberRows(harness, declaredAddresses(harness)),
790
+ nested_members: nestedMemberRows(harness, admissions, mentionScope(harness), placements, extents),
791
+ registrations: [...registrationRows(harness), ...tapHookRows(harness)],
792
+ settings: settingsRows(harness),
447
793
  };
448
794
  }
449
795
  /** The SDK's pinned engine/interchange version — the JSON pipe rides it in lockstep. */