@dtmd/temper 0.0.7 → 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.
- package/README.md +84 -53
- package/bin/temper.js +0 -0
- package/dist/src/assembly.d.ts +15 -1
- package/dist/src/assembly.js +2 -1
- package/dist/src/builtins.d.ts +682 -66
- package/dist/src/builtins.js +664 -92
- package/dist/src/claude-code.d.ts +2 -2
- package/dist/src/claude-code.js +1 -1
- package/dist/src/contract.d.ts +180 -29
- package/dist/src/contract.js +128 -15
- package/dist/src/declarations.d.ts +72 -5
- package/dist/src/declarations.js +389 -107
- package/dist/src/dial.d.ts +75 -0
- package/dist/src/dial.js +82 -0
- package/dist/src/emit.d.ts +35 -1
- package/dist/src/emit.js +369 -63
- package/dist/src/generated/AssemblyFactRow.d.ts +2 -2
- package/dist/src/generated/BoundRow.d.ts +2 -2
- package/dist/src/generated/ClauseRow.d.ts +73 -3
- package/dist/src/generated/CollectionAddressRow.d.ts +4 -0
- package/dist/src/generated/EmbeddedMember.d.ts +3 -3
- package/dist/src/generated/FeatureValue.d.ts +2 -2
- package/dist/src/generated/Features.d.ts +52 -3
- package/dist/src/generated/KindFactRow.d.ts +24 -7
- package/dist/src/generated/MentionRow.d.ts +5 -3
- package/dist/src/generated/NestedMemberRow.d.ts +32 -0
- package/dist/src/generated/PayloadMember.d.ts +6 -0
- package/dist/src/generated/RequirementRow.d.ts +4 -2
- package/dist/src/generated/SatisfiesRow.d.ts +2 -1
- package/dist/src/generated/Shape.d.ts +15 -0
- package/dist/src/generated/Shape.js +2 -0
- package/dist/src/generated/TemplateRow.d.ts +24 -0
- package/dist/src/generated/TemplateRow.js +2 -0
- package/dist/src/generated/ValueType.d.ts +11 -2
- package/dist/src/generated/Verifier.d.ts +20 -0
- package/dist/src/generated/Verifier.js +2 -0
- package/dist/src/generated/index.d.ts +3 -0
- package/dist/src/index.d.ts +8 -8
- package/dist/src/index.js +3 -3
- package/dist/src/kind.d.ts +135 -29
- package/dist/src/kind.js +35 -8
- package/dist/src/prose.d.ts +81 -25
- package/dist/src/prose.js +91 -21
- package/package.json +2 -2
package/dist/src/declarations.js
CHANGED
|
@@ -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`/`
|
|
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
|
-
|
|
47
|
-
//
|
|
48
|
-
//
|
|
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`/`
|
|
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 !== "
|
|
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`
|
|
87
|
-
* or `named-field(<identityField>)` for the
|
|
88
|
-
* call syntax [`registrationLabel`] uses, so the id source round-trips
|
|
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
|
|
124
|
-
*
|
|
125
|
-
*
|
|
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(
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
158
|
-
*
|
|
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 {
|
|
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 —
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
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,
|
|
172
|
-
if (facts.locus.kind
|
|
173
|
-
// An embedded kind inherits its world residue through its host; it
|
|
174
|
-
//
|
|
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:
|
|
181
|
-
governs_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
|
|
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
|
-
/**
|
|
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
|
-
|
|
279
|
+
admit(member.facts);
|
|
196
280
|
for (const binding of harness.expect)
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
293
|
+
/** The distinct discoverable (`at`) kinds in play. */
|
|
201
294
|
function atLocusKindsInPlay(allKinds) {
|
|
202
|
-
return allKinds
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
332
|
+
verifier: verifierRow(requirement.verifier),
|
|
234
333
|
prose: requirement.prose,
|
|
235
334
|
}));
|
|
236
335
|
}
|
|
237
336
|
/**
|
|
238
|
-
*
|
|
239
|
-
* `
|
|
240
|
-
*
|
|
241
|
-
*
|
|
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
|
-
/**
|
|
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:
|
|
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
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
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
|
-
|
|
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.
|
|
328
|
-
*
|
|
329
|
-
*
|
|
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
|
-
|
|
338
|
-
|
|
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 `
|
|
347
|
-
*
|
|
348
|
-
*
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
*
|
|
370
|
-
* {@link
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
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
|
|
543
|
+
function admissionsByHost(harness) {
|
|
376
544
|
const map = new Map();
|
|
377
|
-
for (const
|
|
378
|
-
|
|
379
|
-
|
|
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
|
|
387
|
-
* `file()`/`text` body
|
|
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
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
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,
|
|
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
|
-
|
|
407
|
-
|
|
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
|
|
411
|
-
`
|
|
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,
|
|
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
|
|
481
|
-
*
|
|
482
|
-
*
|
|
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
|
-
/**
|
|
491
|
-
|
|
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
|
|
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:
|
|
783
|
+
kinds: unitKindsInPlay(allKinds).map((facts) => kindFactRow(facts, admissions)),
|
|
502
784
|
clauses,
|
|
503
785
|
requirements: requirementRows(harness),
|
|
504
|
-
assembly: assemblyFactRows(harness,
|
|
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,
|
|
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
|
}
|