@dtmd/temper 0.0.4 → 0.0.7

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 (91) hide show
  1. package/README.md +10 -12
  2. package/bin/temper.js +52 -0
  3. package/dist/src/assembly.d.ts +32 -16
  4. package/dist/src/assembly.js +11 -10
  5. package/dist/src/builtins.d.ts +206 -41
  6. package/dist/src/builtins.js +258 -54
  7. package/dist/src/claude-code.d.ts +7 -9
  8. package/dist/src/claude-code.js +6 -8
  9. package/dist/src/contract.d.ts +47 -24
  10. package/dist/src/contract.js +35 -9
  11. package/dist/src/declarations.d.ts +42 -101
  12. package/dist/src/declarations.js +375 -47
  13. package/dist/src/emit.d.ts +56 -28
  14. package/dist/src/emit.js +177 -54
  15. package/dist/src/generated/AssemblyFactRow.d.ts +29 -0
  16. package/dist/src/generated/AssemblyFactRow.js +2 -0
  17. package/dist/src/generated/BoundRow.d.ts +14 -0
  18. package/dist/src/generated/BoundRow.js +2 -0
  19. package/dist/src/generated/CharsetRow.d.ts +15 -0
  20. package/dist/src/generated/CharsetRow.js +2 -0
  21. package/dist/src/generated/ClauseRow.d.ts +97 -0
  22. package/dist/src/generated/ClauseRow.js +1 -0
  23. package/dist/src/generated/CollectionAddressRow.d.ts +17 -0
  24. package/dist/src/generated/CollectionAddressRow.js +2 -0
  25. package/dist/src/generated/CollectionEntryRow.d.ts +23 -0
  26. package/dist/src/generated/CollectionEntryRow.js +2 -0
  27. package/dist/src/generated/CollectionEntryWire.d.ts +17 -0
  28. package/dist/src/generated/CollectionEntryWire.js +2 -0
  29. package/dist/src/generated/CountBoundRow.d.ts +13 -0
  30. package/dist/src/generated/CountBoundRow.js +2 -0
  31. package/dist/src/generated/Declarations.d.ts +89 -0
  32. package/dist/src/generated/Declarations.js +1 -0
  33. package/dist/src/generated/DegreeBoundRow.d.ts +15 -0
  34. package/dist/src/generated/DegreeBoundRow.js +1 -0
  35. package/dist/src/generated/EdgeBoundRow.d.ts +13 -0
  36. package/dist/src/generated/EdgeBoundRow.js +2 -0
  37. package/dist/src/generated/EmbeddedMember.d.ts +45 -0
  38. package/dist/src/generated/EmbeddedMember.js +1 -0
  39. package/dist/src/generated/EmbeddedMemberCollectionEntry.d.ts +21 -0
  40. package/dist/src/generated/EmbeddedMemberCollectionEntry.js +1 -0
  41. package/dist/src/generated/FeatureValue.d.ts +23 -0
  42. package/dist/src/generated/FeatureValue.js +1 -0
  43. package/dist/src/generated/Features.d.ts +82 -0
  44. package/dist/src/generated/Features.js +1 -0
  45. package/dist/src/generated/FencedBlock.d.ts +24 -0
  46. package/dist/src/generated/FencedBlock.js +2 -0
  47. package/dist/src/generated/IncludeRow.d.ts +18 -0
  48. package/dist/src/generated/IncludeRow.js +2 -0
  49. package/dist/src/generated/KindFactRow.d.ts +65 -0
  50. package/dist/src/generated/KindFactRow.js +1 -0
  51. package/dist/src/generated/LayoutRegionRow.d.ts +29 -0
  52. package/dist/src/generated/LayoutRegionRow.js +2 -0
  53. package/dist/src/generated/LayoutRow.d.ts +12 -0
  54. package/dist/src/generated/LayoutRow.js +1 -0
  55. package/dist/src/generated/MentionRow.d.ts +17 -0
  56. package/dist/src/generated/MentionRow.js +2 -0
  57. package/dist/src/generated/NestedMemberRow.d.ts +39 -0
  58. package/dist/src/generated/NestedMemberRow.js +1 -0
  59. package/dist/src/generated/Payload.d.ts +25 -0
  60. package/dist/src/generated/Payload.js +1 -0
  61. package/dist/src/generated/PayloadMember.d.ts +31 -0
  62. package/dist/src/generated/PayloadMember.js +2 -0
  63. package/dist/src/generated/RangeBoundRow.d.ts +15 -0
  64. package/dist/src/generated/RangeBoundRow.js +2 -0
  65. package/dist/src/generated/RegistrationRow.d.ts +35 -0
  66. package/dist/src/generated/RegistrationRow.js +2 -0
  67. package/dist/src/generated/RequirementRow.d.ts +40 -0
  68. package/dist/src/generated/RequirementRow.js +1 -0
  69. package/dist/src/generated/SatisfiesRow.d.ts +14 -0
  70. package/dist/src/generated/SatisfiesRow.js +2 -0
  71. package/dist/src/generated/Section.d.ts +21 -0
  72. package/dist/src/generated/Section.js +2 -0
  73. package/dist/src/generated/SectionContainsRow.d.ts +14 -0
  74. package/dist/src/generated/SectionContainsRow.js +2 -0
  75. package/dist/src/generated/SettingsRow.d.ts +25 -0
  76. package/dist/src/generated/SettingsRow.js +2 -0
  77. package/dist/src/generated/ValueType.d.ts +9 -0
  78. package/dist/src/generated/ValueType.js +2 -0
  79. package/dist/src/generated/index.d.ts +32 -0
  80. package/dist/src/generated/index.js +1 -0
  81. package/dist/src/index.d.ts +14 -18
  82. package/dist/src/index.js +10 -13
  83. package/dist/src/kind.d.ts +190 -43
  84. package/dist/src/kind.js +34 -30
  85. package/dist/src/needs.d.ts +8 -8
  86. package/dist/src/needs.js +7 -7
  87. package/dist/src/prose.d.ts +75 -28
  88. package/dist/src/prose.js +0 -0
  89. package/package.json +9 -1
  90. package/dist/src/genres.d.ts +0 -38
  91. package/dist/src/genres.js +0 -24
@@ -1,20 +1,21 @@
1
1
  /**
2
- * Declaration rows — the composed program's erased declarations
3
- * (`specs/architecture/20-surface.md`, "The seam — one implementation"; "The lock
4
- * and drift"). Every type erases at the seam: kinds, clauses, requirements, and
5
- * assembly facts compile to plain rows the engine reads. The **row shape** matches
6
- * the Rust lock's `[declaration]` families (`src/drift.rs` `Declarations`) — the
7
- * byte-parity lockstep two writers keep until single-writer lands
8
- * (`SDK-RECUT-CORPUS-FACE`). The same rows ride the internal versioned JSON pipe
9
- * ({@link declarationsToJson}) — not a designed IR, versioned in lockstep.
2
+ * Declaration rows — the composed program's erased declarations.
3
+ * Every type erases at the seam: kinds, clauses, requirements, and
4
+ * assembly facts compile to plain rows the engine reads. The **row shapes are the
5
+ * generated `ts-rs` bindings** (`./generated/`, derived from `src/drift.rs`), so a
6
+ * Rust-side row rename is a compile error here, never a silent shape drift. This
7
+ * module authors the builders that fill them; the same rows ride the internal
8
+ * versioned JSON pipe ({@link encodeSeam}) — not a designed IR, versioned
9
+ * in lockstep.
10
10
  */
11
+ import { fileURLToPath } from "node:url";
12
+ import { resolveLeaf } from "./prose.js";
11
13
  /**
12
14
  * Compile one `Clause` into its lock row: the shared `key`/`field`/`severity`/
13
15
  * `guidance`/`cite` columns — the clause's four channels surviving erasure
14
- * (`10-contracts.md`, "The clause — the atom of a contract") — plus, when the
16
+ * — plus, when the
15
17
  * predicate carries them, the `count`/`target`/`degree` argument columns a
16
- * requirement's own set-/edge-scope demand needs (`10-contracts.md`, "Judged
17
- * at the node-set scope" / "Judged at the edge scope"), and the
18
+ * requirement's own set-/edge-scope demand needs, and the
18
19
  * `bound`/`charset`/`keys`/`values` argument columns a kind's own node-scope
19
20
  * floor clause needs (`min_len`/`max_len`/`max_lines`'s bound,
20
21
  * `allowed_chars`'s charset, `forbidden_keys`'s keys, `deny`'s values) — so
@@ -42,9 +43,25 @@ function clauseRow(clause, kind) {
42
43
  }
43
44
  : undefined,
44
45
  bound: nodeScopeBoundArgs(predicate),
45
- charset: predicate.key === "allowed_chars" ? predicate.charset : undefined,
46
- keys: predicate.key === "forbidden_keys" ? predicate.keys : undefined,
47
- values: predicate.key === "deny" ? predicate.values : undefined,
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.
49
+ charset: predicate.key === "allowed_chars" && predicate.charset !== undefined
50
+ ? {
51
+ ranges: predicate.charset.ranges ? [...predicate.charset.ranges] : undefined,
52
+ chars: predicate.charset.chars,
53
+ }
54
+ : undefined,
55
+ keys: predicate.key === "forbidden_keys" && predicate.keys ? [...predicate.keys] : undefined,
56
+ values: (predicate.key === "deny" || predicate.key === "enum") && predicate.values
57
+ ? [...predicate.values]
58
+ : undefined,
59
+ range: predicate.key === "range" && predicate.range !== undefined
60
+ ? { min: predicate.range.min, max: predicate.range.max }
61
+ : undefined,
62
+ section: predicate.key === "section_contains" && predicate.section !== undefined
63
+ ? { heading: predicate.section.heading, marker: predicate.section.marker }
64
+ : undefined,
48
65
  };
49
66
  }
50
67
  /** `min_len`/`max_len`/`max_lines`'s scalar bound off their shared `min`/`max`
@@ -65,11 +82,24 @@ function edgeBoundArgs(args, direction) {
65
82
  const max = args?.[`${direction}_max`];
66
83
  return min === undefined && max === undefined ? undefined : { min, max };
67
84
  }
68
- /** The lock label for a kind's declared registration. */
85
+ /**
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"`.
90
+ */
91
+ function unitShapeLabel(facts) {
92
+ if (facts.unitShape !== "named-field")
93
+ return facts.unitShape;
94
+ return `named-field(${facts.identityField})`;
95
+ }
96
+ /** The lock label for one declared registration channel. */
69
97
  function registrationLabel(registration) {
70
98
  switch (registration.via) {
71
99
  case "always":
72
100
  return "always";
101
+ case "user-invoked":
102
+ return "user-invoked";
73
103
  case "description-trigger":
74
104
  return `description-trigger(${registration.field})`;
75
105
  case "paths-match":
@@ -80,13 +110,69 @@ function registrationLabel(registration) {
80
110
  return "connection";
81
111
  }
82
112
  }
83
- /** One kind's fact row — the `at` locus supplies `governs_root`/`governs_glob`. */
84
- function kindFactRow(facts) {
113
+ /** The lock labels for a kind's declared registration **set**, in declaration order —
114
+ * `undefined` for an empty set, the same omit-the-column tolerance `templatesFor` takes. */
115
+ function registrationLabels(registration) {
116
+ return registration.length > 0 ? registration.map(registrationLabel) : undefined;
117
+ }
118
+ /** The stable-sort ordering every declaration row family shares. */
119
+ export function compareStrings(a, b) {
120
+ return a < b ? -1 : a > b ? 1 : 0;
121
+ }
122
+ /**
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.
126
+ */
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;
133
+ }
134
+ /**
135
+ * Lower a kind's declared {@link Layout} into its `content` row — one flat
136
+ * discriminator-plus-columns [`LayoutRegionRow`] per region, `memberKind` spelled as the
137
+ * wire's snake_case `member_kind`. `undefined` for a `file`-content kind (no declared
138
+ * content), so its row omits the column and stays byte-identical.
139
+ */
140
+ function contentRow(content) {
141
+ if (content === undefined)
142
+ return undefined;
143
+ const regions = content.regions.map((region) => {
144
+ switch (region.region) {
145
+ case "prose":
146
+ return { region: "prose", import: region.import };
147
+ case "field":
148
+ return { region: "field", slot: region.slot };
149
+ case "collection":
150
+ return { region: "collection", member_kind: region.memberKind, key: region.key };
151
+ }
152
+ });
153
+ return { regions };
154
+ }
155
+ /**
156
+ * 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.
159
+ */
160
+ function collectionAddressRow(facts) {
161
+ if (facts.collectionAddress === undefined)
162
+ return undefined;
163
+ return { manifest: facts.collectionAddress.manifest, key_path: facts.collectionAddress.keyPath };
164
+ }
165
+ /**
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`.
170
+ */
171
+ function kindFactRow(facts, allKinds) {
85
172
  if (facts.locus.kind !== "at") {
86
- // A genre inherits its world residue through its host; it carries no `at`
87
- // locus, so it takes no kind-fact row (`15-kinds.md`). Callers filter these
88
- // out before this point.
89
- throw new Error(`kind \`${facts.name}\` is a genre — it carries no locus-bearing kind fact.`);
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.
175
+ throw new Error(`kind \`${facts.name}\` is embedded — it carries no locus-bearing kind fact.`);
90
176
  }
91
177
  return {
92
178
  name: facts.name,
@@ -94,20 +180,28 @@ function kindFactRow(facts) {
94
180
  governs_root: facts.locus.root,
95
181
  governs_glob: facts.locus.glob,
96
182
  format: facts.format,
97
- unit_shape: facts.unitShape,
98
- registration: registrationLabel(facts.registration),
183
+ unit_shape: unitShapeLabel(facts),
184
+ registration: registrationLabels(facts.registration),
185
+ templates: templatesFor(facts.name, allKinds),
186
+ content: contentRow(facts.content),
187
+ shape: facts.shape,
188
+ collection_address: collectionAddressRow(facts),
99
189
  };
100
190
  }
101
- /** The distinct locus-bearing kinds in play — member kinds ∪ expect kinds, name-sorted. */
191
+ /** Every kind in play, at any locus — member kinds ∪ expect kinds ∪ their embedded children. */
102
192
  function kindsInPlay(harness) {
103
193
  const byName = new Map();
104
194
  for (const member of harness.members)
105
195
  byName.set(member.facts.name, member.facts);
106
196
  for (const binding of harness.expect)
107
197
  byName.set(binding.kind.facts.name, binding.kind.facts);
108
- return [...byName.values()]
198
+ return [...byName.values()];
199
+ }
200
+ /** The distinct locus-bearing (`at`) kinds in play, name-sorted. */
201
+ function atLocusKindsInPlay(allKinds) {
202
+ return allKinds
109
203
  .filter((facts) => facts.locus.kind === "at")
110
- .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
204
+ .sort((a, b) => compareStrings(a.name, b.name));
111
205
  }
112
206
  /** The requirement rows — assembly `require` and every member's `requires`, one namespace. */
113
207
  function requirementRows(harness) {
@@ -116,7 +210,7 @@ function requirementRows(harness) {
116
210
  const existing = merged.get(name);
117
211
  if (existing !== undefined && existing !== requirement) {
118
212
  // One namespace, one fill mechanism; a cross-publisher name collision is an
119
- // admissibility finding, never a shadowing rule (`10-contracts.md`).
213
+ // admissibility finding, never a shadowing rule.
120
214
  throw new Error(`requirement \`${name}\` is published twice (${source} collides with an earlier ` +
121
215
  `publisher) — a name collision across publishers is an admissibility finding.`);
122
216
  }
@@ -130,23 +224,24 @@ function requirementRows(harness) {
130
224
  }
131
225
  }
132
226
  return [...merged.entries()]
133
- .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
227
+ .sort(([a], [b]) => compareStrings(a, b))
134
228
  .map(([name, requirement]) => ({
135
229
  name,
136
230
  kind: requirement.kind?.key,
137
231
  required: requirement.required ?? false,
138
232
  clauses: (requirement.clauses ?? []).map((clause) => clauseRow(clause)),
139
233
  verified_by: requirement.verifiedBy,
234
+ prose: requirement.prose,
140
235
  }));
141
236
  }
142
237
  /**
143
- * The assembly-scope facts, in a stable order: authority (always declared — the
144
- * `shared` default anchors every harness until a surface-authority posture is
145
- * authored), then one edge row per kind edge field (`40-composition.md`;
146
- * `45-governance.md`).
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.
147
242
  */
148
- function assemblyFactRows(kinds) {
149
- const facts = [{ fact: "authority", value: "shared" }];
243
+ function assemblyFactRows(harness, kinds) {
244
+ const facts = [{ fact: "mode", value: harness.mode }];
150
245
  for (const kind of kinds) {
151
246
  for (const edge of kind.edgeFields ?? []) {
152
247
  facts.push({ fact: "edge", from: kind.name, field: edge.field, to: edge.to });
@@ -162,34 +257,267 @@ function satisfiesRows(harness) {
162
257
  rows.push({ member: member.name, requirement });
163
258
  }
164
259
  }
165
- return rows.sort((a, b) => (a.member < b.member ? -1 : a.member > b.member ? 1 : 0) ||
166
- (a.requirement < b.requirement ? -1 : a.requirement > b.requirement ? 1 : 0));
260
+ return rows.sort((a, b) => compareStrings(a.member, b.member) || compareStrings(a.requirement, b.requirement));
261
+ }
262
+ /**
263
+ * The `mention` rows — every member's authored `n` targets, member-then-target
264
+ * 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
270
+ * (`emit.ts`), not this row's concern.
271
+ */
272
+ function mentionRows(harness) {
273
+ const rows = [];
274
+ for (const member of harness.members) {
275
+ if (member.prose?.kind === "text") {
276
+ const address = `${member.kind}:${member.name}`;
277
+ for (const mention of member.prose.mentions) {
278
+ rows.push({ member: address, target: mention.target.address });
279
+ }
280
+ }
281
+ if (member.prose?.kind === "blocks") {
282
+ for (const value of member.prose.values) {
283
+ rows.push(...embeddedLeafMentionRows(member.name, value));
284
+ }
285
+ }
286
+ }
287
+ return rows.sort((a, b) => compareStrings(a.member, b.member) || compareStrings(a.target, b.target));
288
+ }
289
+ /**
290
+ * The mention rows one `blocks()`-declared embedded-member value's `Text` leaves
291
+ * contribute — top-level leaves addressed by their bare field name, a
292
+ * collection entry's leaves addressed `<collection>.<entry>.<field>` (one layer
293
+ * deep, matching the row's own shape) — each row keyed to the leaf's own
294
+ * structural address, the `<member>/<kind>/<key>/<child-path>` grammar
295
+ * `src/read.rs`'s `parse_leaf_address` resolves. A bare-string leaf names no
296
+ * mention.
297
+ */
298
+ function embeddedLeafMentionRows(hostName, value) {
299
+ const rows = [];
300
+ const addressed = (childPath) => `${hostName}/${value.kind}/${value.key}/${childPath}`;
301
+ for (const [field, leaf] of Object.entries(value.leaves)) {
302
+ if (typeof leaf === "string")
303
+ continue;
304
+ for (const mention of leaf.mentions) {
305
+ rows.push({ member: addressed(field), target: mention.target.address });
306
+ }
307
+ }
308
+ for (const [collection, entries] of Object.entries(value.collections)) {
309
+ for (const entry of entries) {
310
+ for (const [field, leaf] of Object.entries(entry.leaves)) {
311
+ if (typeof leaf === "string")
312
+ continue;
313
+ for (const mention of leaf.mentions) {
314
+ rows.push({ member: addressed(`${collection}.${entry.key}.${field}`), target: mention.target.address });
315
+ }
316
+ }
317
+ }
318
+ }
319
+ return rows;
320
+ }
321
+ /**
322
+ * The `include` rows — every member's `text`-body includes, in member-then-authored
323
+ * order. Each carries the host member's `kind:name` address and the include target's
324
+ * path resolved against the stating module ({@link fileURLToPath} over the include's own
325
+ * `moduleUrl`), never the workspace — the engine reads, splices, and fingerprints it.
326
+ * 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}).
330
+ */
331
+ function includeRows(harness) {
332
+ const rows = [];
333
+ for (const member of harness.members) {
334
+ if (member.prose?.kind !== "text")
335
+ continue;
336
+ 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)) });
339
+ }
340
+ }
341
+ return rows;
342
+ }
343
+ /**
344
+ * One host member's declared embedded-member value as its declaration row —
345
+ * 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.
349
+ */
350
+ function nestedMemberRow(host, value, mentionable) {
351
+ const context = (childPath) => `member.${value.kind} ${value.key}: leaf \`${childPath}\``;
352
+ const leaves = {};
353
+ for (const [field, leaf] of Object.entries(value.leaves)) {
354
+ leaves[field] = resolveLeaf(leaf, mentionable, context(field));
355
+ }
356
+ const collections = {};
357
+ for (const [collection, entries] of Object.entries(value.collections)) {
358
+ collections[collection] = entries.map((entry) => {
359
+ const entryLeaves = {};
360
+ for (const [field, leaf] of Object.entries(entry.leaves)) {
361
+ entryLeaves[field] = resolveLeaf(leaf, mentionable, context(`${collection}.${entry.key}.${field}`));
362
+ }
363
+ return { key: entry.key, leaves: entryLeaves };
364
+ });
365
+ }
366
+ return { host, kind: value.kind, key: value.key, leaves, collections };
367
+ }
368
+ /**
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.
374
+ */
375
+ function embeddedHostsByKind(harness) {
376
+ 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));
380
+ }
381
+ }
382
+ return map;
383
+ }
384
+ /**
385
+ * 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
388
+ * (`emit.ts`'s `resolveBody`) — this row is a second *read* of the same authored
389
+ * value, never a second copy the engine reads back (0018).
390
+ *
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.
397
+ */
398
+ function nestedMemberRows(harness, mentionable) {
399
+ const hostsByKind = embeddedHostsByKind(harness);
400
+ const rows = [];
401
+ for (const member of harness.members) {
402
+ if (member.prose?.kind !== "blocks")
403
+ continue;
404
+ const host = `${member.kind}:${member.name}`;
405
+ for (const value of member.prose.values) {
406
+ const hosts = hostsByKind.get(value.kind);
407
+ if (hosts === undefined || !hosts.has(member.kind)) {
408
+ throw new Error(`member \`${member.name}\`: embedded value \`${value.key}\` is of kind ` +
409
+ `\`${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").`);
412
+ }
413
+ rows.push(nestedMemberRow(host, value, mentionable));
414
+ }
415
+ }
416
+ return rows.sort((a, b) => compareStrings(a.host, b.host) || compareStrings(a.kind, b.kind) || compareStrings(a.key, b.key));
417
+ }
418
+ /**
419
+ * The `registration` rows — every fields-only registration member (a hook, an MCP server)
420
+ * erased for the manifest write face, kind-then-key sorted so double emit is byte-stable.
421
+ * Each carries its identity (`kind`/`key`), its collection address (`manifest`/`keyPath`,
422
+ * the wire's snake_case `key_path`), and its folded typed fields — the entry value the
423
+ * engine's write face places under `key`. The one source `emit.ts`'s public
424
+ * {@link RegistrationFact} view also maps from, so the seam and the `EmitResult` sibling
425
+ * cannot disagree on what a manifest carries.
426
+ *
427
+ * # Throws
428
+ * If a fields-only member declares no collection address — it surfaces in no host manifest.
429
+ */
430
+ export function registrationRows(harness) {
431
+ return harness.members
432
+ .filter((member) => member.facts.shape === "fields")
433
+ .map((member) => {
434
+ const address = member.facts.collectionAddress;
435
+ if (address === undefined) {
436
+ throw new Error(`member \`${member.name}\`: a fields-only registration kind declares no ` +
437
+ `collection address — it surfaces in no host manifest (specs/model/pipeline.md, "The SDK").`);
438
+ }
439
+ return {
440
+ kind: member.kind,
441
+ key: member.name,
442
+ manifest: address.manifest,
443
+ key_path: address.keyPath,
444
+ // The generated row carries a mutable field list; the member's is read-only, so
445
+ // copy each pair into a fresh tuple — the same values, a shape the row accepts.
446
+ fields: member.fields.map(([name, value]) => [name, value]),
447
+ };
448
+ })
449
+ .sort((a, b) => compareStrings(a.kind, b.kind) || compareStrings(a.key, b.key));
450
+ }
451
+ /**
452
+ * The manifest Claude Code's harness-level settings reside in — the file the assembly's
453
+ * residual settings keys fold into as opaque residue, the same manifest the `hook` kind's
454
+ * registrations surface inside (code.claude.com/docs/en/settings, retrieved 2026-07-10).
455
+ */
456
+ const SETTINGS_MANIFEST = "settings.json";
457
+ /**
458
+ * The `settings` rows — the assembly's harness-level residual settings keys, each folded
459
+ * into the settings.json manifest's opaque residue at emit. Key-sorted so double emit is
460
+ * byte-stable. Seam-inbound: the value lives in the projected manifest, never the lock.
461
+ */
462
+ export function settingsRows(harness) {
463
+ return Object.entries(harness.settings)
464
+ .map(([key, value]) => ({ manifest: SETTINGS_MANIFEST, key, value }))
465
+ .sort((a, b) => compareStrings(a.key, b.key));
466
+ }
467
+ /** Every requirement name a `satisfies` claim may fill — assembly `require` ∪ member `requires`. */
468
+ export function declaredRequirements(harness) {
469
+ const set = new Set();
470
+ for (const name of Object.keys(harness.require))
471
+ set.add(name);
472
+ for (const member of harness.members) {
473
+ for (const name of Object.keys(member.requires))
474
+ set.add(name);
475
+ }
476
+ return set;
477
+ }
478
+ /**
479
+ * 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.
483
+ */
484
+ export function declaredAddresses(harness) {
485
+ const set = declaredRequirements(harness);
486
+ for (const member of harness.members)
487
+ set.add(`${member.kind}:${member.name}`);
488
+ return set;
167
489
  }
168
- /** Compile a harness into its five declaration families — the erased program. */
490
+ /** Compile a harness into its seven declaration families — the erased program. */
169
491
  export function compileDeclarations(harness) {
170
- const kinds = kindsInPlay(harness);
492
+ const allKinds = kindsInPlay(harness);
493
+ const kinds = atLocusKindsInPlay(allKinds);
171
494
  const clauses = [];
172
- for (const binding of [...harness.expect].sort((a, b) => (a.kind.key < b.kind.key ? -1 : a.kind.key > b.kind.key ? 1 : 0))) {
495
+ for (const binding of [...harness.expect].sort((a, b) => compareStrings(a.kind.key, b.kind.key))) {
173
496
  for (const clause of binding.clauses) {
174
497
  clauses.push(clauseRow(clause, binding.kind.key));
175
498
  }
176
499
  }
177
500
  return {
178
- kinds: kinds.map(kindFactRow),
501
+ kinds: kinds.map((facts) => kindFactRow(facts, allKinds)),
179
502
  clauses,
180
503
  requirements: requirementRows(harness),
181
- assembly: assemblyFactRows(kinds),
504
+ assembly: assemblyFactRows(harness, kinds),
182
505
  satisfies: satisfiesRows(harness),
506
+ mentions: mentionRows(harness),
507
+ includes: includeRows(harness),
508
+ nested_members: nestedMemberRows(harness, declaredAddresses(harness)),
509
+ registrations: registrationRows(harness),
510
+ settings: settingsRows(harness),
183
511
  };
184
512
  }
185
513
  /** The SDK's pinned engine/interchange version — the JSON pipe rides it in lockstep. */
186
514
  export const SEAM_VERSION = 2;
187
515
  /**
188
- * Serialize the declaration rows to the internal versioned JSON pipe
189
- * (`20-surface.md`, "The seam"). Not a designed IR — a stable public interchange
190
- * is admitted only when its consumer lands. Deterministic: insertion-ordered keys
191
- * and a trailing newline, so a re-emit is byte-identical (law 5).
516
+ * Serialize the seam payload to the internal versioned JSON pipe — `encodeSeam`
517
+ * stamps `version`, so the caller supplies the rest of the {@link Payload}.
518
+ * Deterministic: insertion-ordered keys and a trailing newline, so a re-emit is
519
+ * byte-identical.
192
520
  */
193
- export function declarationsToJson(declarations) {
194
- return JSON.stringify({ version: SEAM_VERSION, ...declarations }, null, 2) + "\n";
521
+ export function encodeSeam(payload) {
522
+ return JSON.stringify({ version: SEAM_VERSION, ...payload }, null, 2) + "\n";
195
523
  }
@@ -1,48 +1,63 @@
1
1
  /**
2
- * Emit — the compile from the six-noun face to the seam's JSON pipe
3
- * (`specs/architecture/20-surface.md`, "Emit — total, byte-reproducible, refusing";
4
- * "The seam — one implementation"). The SDK implements **no semantics**: emit
2
+ * Emit — the compile from the six-noun face to the seam's JSON pipe.
3
+ * The SDK implements **no semantics**: emit
5
4
  * produces plain data — the declaration rows the engine reads and, per projected
6
5
  * member, its ordered typed fields and resolved prose body. The engine is the
7
6
  * sole compiler of every projection and the whole lock; the SDK writes neither.
8
7
  * Emit is total (members are the only source), refuses before it produces a byte
9
8
  * on a broken source, and is byte-reproducible — double-emit verified at every
10
- * run (law 5).
9
+ * run.
11
10
  */
12
11
  import type { Harness } from "./assembly.js";
13
12
  import type { Declarations } from "./declarations.js";
14
- /** How a `file()` asset's module-relative path resolves at emit. */
13
+ import type { PayloadMember } from "./generated/index.js";
14
+ export type { PayloadMember } from "./generated/index.js";
15
+ /** What a mention may resolve against at emit. */
15
16
  export interface ResolveOptions {
16
- /** Base dir a `file()` module-relative path resolves against (default: cwd). */
17
- readonly baseDir?: string;
18
17
  /** The addresses a mention may name — resolution-checked; a mention cannot dangle. */
19
18
  readonly mentionable?: ReadonlySet<string>;
20
19
  }
21
- /** One projected member's erased payload — the engine derives its locus from the kind's own declaration row. */
22
- export interface PayloadMember {
23
- /** The kind's bare name — joins the payload's `declarations.kinds` family. */
20
+ /**
21
+ * One fields-only registration member erased for the manifest write face: its key
22
+ * (a hook's lifecycle event, an MCP server's name), the collection address it keys
23
+ * at, and its folded typed fields — the same declaration-row shape the engine write
24
+ * face reads back off a manifest (`json_manifest.rs`'s `RegistrationMember`). Carried
25
+ * from the composing program, never mined from a projection.
26
+ */
27
+ export interface RegistrationFact {
28
+ /** The erased registration kind — `hook`, `mcp-server` — joining `declarations.kinds`. */
24
29
  readonly kind: string;
25
- /** Identity within the kind. */
26
- readonly name: string;
27
- /** The kind's typed fields, flat and ordered — the projected frontmatter. */
30
+ /** The member's key among its collection's entries — a hook's event, a server's name. */
31
+ readonly key: string;
32
+ /** The manifest collection address the registration surfaces at. */
33
+ readonly collectionAddress: {
34
+ readonly manifest: string;
35
+ readonly keyPath: string;
36
+ };
37
+ /** The member's folded typed fields, in the author's declared order. */
28
38
  readonly fields: ReadonlyArray<readonly [string, unknown]>;
29
- /** The resolved prose body, byte-faithful. */
30
- readonly body: string;
31
- /** The resolved `file()` asset's absolute path; absent for `text`/`blocks` prose. */
32
- readonly source_path?: string;
33
39
  }
34
- /** Emit-time inputs beyond the harness — where a `file()` asset's module-relative path resolves against. */
35
- export interface EmitOptions {
36
- /** Base dir a `file()` asset's module-relative path resolves against (default: cwd). */
37
- readonly baseDir?: string;
40
+ /**
41
+ * One harness-level settings-residue key erased for the manifest write face: the manifest
42
+ * it surfaces in, its opaque key, and its value — the entry `emit` folds into the manifest's
43
+ * residue beside its registration members' collection segments. Carried from the composing
44
+ * program, never mined from a projection.
45
+ */
46
+ export interface SettingsResidue {
47
+ /** The host manifest the residue key surfaces in (`settings.json`). */
48
+ readonly manifest: string;
49
+ /** The opaque top-level manifest key with no member home. */
50
+ readonly key: string;
51
+ /** The key's opaque value, placed verbatim into the manifest's residue. */
52
+ readonly value: unknown;
38
53
  }
39
54
  /**
40
- * A full emit's compiled outputs — the whole seam the engine reads
41
- * (`20-surface.md`, "The seam"). A pure function of the harness, so [`emit`]
55
+ * A full emit's compiled outputs — the whole seam the engine reads.
56
+ * A pure function of the harness, so [`emit`]
42
57
  * double-verifies it.
43
58
  */
44
59
  export interface EmitResult {
45
- /** The declaration rows — the erased program the lock's five families carry. */
60
+ /** The declaration rows — the erased program the lock's seven families carry. */
46
61
  readonly declarations: Declarations;
47
62
  /** The projected members — the engine's sole input for every projection. */
48
63
  readonly members: readonly PayloadMember[];
@@ -53,17 +68,30 @@ export interface EmitResult {
53
68
  readonly seam: string;
54
69
  /**
55
70
  * The derived permission list — the union of every member's `needs`, deduped and
56
- * sorted (`20-surface.md`, "The permission list is derived, never authored").
71
+ * sorted.
57
72
  * Folds into the settings artifact once hook/MCP members land; carried here as
58
73
  * data until then.
59
74
  */
60
75
  readonly permissions: readonly string[];
76
+ /**
77
+ * The fields-only registration members erased for the manifest write face — each
78
+ * a name, its collection address, and its folded fields. Folds into the manifest
79
+ * artifacts once the engine write face lands; carried here as data until then, the
80
+ * way `permissions` is.
81
+ */
82
+ readonly registrations: readonly RegistrationFact[];
83
+ /**
84
+ * The harness-level settings residue erased for the manifest write face — each an opaque
85
+ * settings.json key and its value. Folds into the settings.json manifest's residue at
86
+ * emit, the way `registrations` builds its collection segments; carried here as data too.
87
+ */
88
+ readonly settings: readonly SettingsResidue[];
61
89
  }
62
90
  /**
63
91
  * Compile the whole face in one deterministic pass: the declaration rows (its
64
- * rollup and its five families) and every projected member's erased payload.
92
+ * rollup and its seven families) and every projected member's erased payload.
65
93
  * Prose resolves once (`file()` assets read in, mentions resolution-checked
66
94
  * against the harness's declared values). Double-emit verified — nondeterministic
67
- * authoring is a loud failure, never a silent churn (law 5).
95
+ * authoring is a loud failure, never a silent churn.
68
96
  */
69
- export declare function emit(harness: Harness, options?: EmitOptions): EmitResult;
97
+ export declare function emit(harness: Harness): EmitResult;