@dtmd/temper 0.0.15 → 0.0.16

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.
@@ -907,10 +907,11 @@ export declare const hookDefaultContract: readonly Clause[];
907
907
  * verifier names (`contract.ts`'s `telemetry`, the `roster.rs` admissibility set); the
908
908
  * `event` is the `hooks.<Event>` key the tap registers under, and the `matcher` scopes
909
909
  * the fire to the telemetry-relevant subset — each an external fact
910
- * (code.claude.com/docs/en/hooks, retrieved 2026-07-17):
910
+ * (code.claude.com/docs/en/hooks, retrieved 2026-08-26):
911
911
  *
912
912
  * - `InstructionsLoaded` fires on a rule/memory load; its matcher filters the load
913
- * reason, and `path_glob_match` is the lazy per-path load the coverage tap reads.
913
+ * reason across all documented reasons (session_start, nested_traversal, path_glob_match,
914
+ * include, compact), so always-on members that load at session start are recorded.
914
915
  * - `SkillInvoked` is a skill invocation, surfaced under `PostToolUse` with the tool-name
915
916
  * matcher `Skill` — the tap's own read of a skill call.
916
917
  * - `UserPromptExpansion` fires on a command expansion; its matcher filters the command
@@ -756,6 +756,11 @@ export const skillDefaultContract = [
756
756
  guidance: "The optional `paths` scope gates every invocation channel until Claude reads a file its globs match; each entry is a glob (brace expansion supported). An unparseable pattern — an unclosed `[`, say — is invalid under globset and silently matches nothing, so the gate never opens and the skill never registers, with no error surfaced. Fix the pattern or drop the field.",
757
757
  cite: "https://code.claude.com/docs/en/memory#path-specific-rules (retrieved 2026-07-15)",
758
758
  }),
759
+ clause(mentionReachable("paths", "paths"), {
760
+ severity: "advisory",
761
+ guidance: "A mention of a gated member is actionable only where that member can be invoked. A `paths` gate removes its member from every invocation channel until Claude reads a matching file, and invoking a gated member from outside its gate hard-errors (`Unknown skill`) — the harness then tells the user it doesn't exist. So a skill that loads where its target cannot be invoked hands Claude an obligation it cannot act on. Two remedies: scope this skill's `paths` to the target's gate, or ungate the target. Advisory because the containment test is literal — every glob here must appear verbatim in the gate — so a semantically narrower glob (`src/**/*.ts` inside `src/**`) false-fires; retune or drop this clause in your own contract when it does.",
762
+ cite: "https://code.claude.com/docs/en/skills (retrieved 2026-07-16; gating hard-error verified against 2.1.211)",
763
+ }),
759
764
  ];
760
765
  /**
761
766
  * The default contract for `supporting-doc` — one clause, because the format documents
@@ -951,10 +956,11 @@ export const hookDefaultContract = [
951
956
  * verifier names (`contract.ts`'s `telemetry`, the `roster.rs` admissibility set); the
952
957
  * `event` is the `hooks.<Event>` key the tap registers under, and the `matcher` scopes
953
958
  * the fire to the telemetry-relevant subset — each an external fact
954
- * (code.claude.com/docs/en/hooks, retrieved 2026-07-17):
959
+ * (code.claude.com/docs/en/hooks, retrieved 2026-08-26):
955
960
  *
956
961
  * - `InstructionsLoaded` fires on a rule/memory load; its matcher filters the load
957
- * reason, and `path_glob_match` is the lazy per-path load the coverage tap reads.
962
+ * reason across all documented reasons (session_start, nested_traversal, path_glob_match,
963
+ * include, compact), so always-on members that load at session start are recorded.
958
964
  * - `SkillInvoked` is a skill invocation, surfaced under `PostToolUse` with the tool-name
959
965
  * matcher `Skill` — the tap's own read of a skill call.
960
966
  * - `UserPromptExpansion` fires on a command expansion; its matcher filters the command
@@ -963,7 +969,7 @@ export const hookDefaultContract = [
963
969
  * capturing every one.
964
970
  */
965
971
  export const TELEMETRY_EVENT_HOOKS = {
966
- InstructionsLoaded: { event: "InstructionsLoaded", matcher: "path_glob_match" },
972
+ InstructionsLoaded: { event: "InstructionsLoaded", matcher: ".*" },
967
973
  SkillInvoked: { event: "PostToolUse", matcher: "Skill" },
968
974
  UserPromptExpansion: { event: "UserPromptExpansion", matcher: ".*" },
969
975
  ToolUse: { event: "PostToolUse", matcher: ".*" },
@@ -13,6 +13,15 @@ import type { Declarations, Payload, RegistrationRow, SettingsRow } from "./gene
13
13
  export type { AssemblyFactRow, ClauseRow, Declarations, KindFactRow, RequirementRow, SatisfiesRow, } from "./generated/index.js";
14
14
  /** The stable-sort ordering every declaration row family shares. */
15
15
  export declare function compareStrings(a: string, b: string): number;
16
+ /**
17
+ * Build a `Map<K, V>` from an iterable of key-value pairs, refusing on duplicate
18
+ * identity keys. A map the caller builds must have unique keys; a duplicate is
19
+ * corruption, not a shadowing rule.
20
+ *
21
+ * @throws Error when a key appears more than once in the iterable, naming the
22
+ * colliding key.
23
+ */
24
+ export declare function uniqueMap<K, V>(entries: Iterable<[K, V]>): Map<K, V>;
16
25
  /**
17
26
  * One composed embedded value's key in an {@link EdgePlacements} table — its host's
18
27
  * `kind:name` address plus the value's own kind and key, the same triple the
@@ -162,6 +162,24 @@ function registrationLabels(registration) {
162
162
  export function compareStrings(a, b) {
163
163
  return a < b ? -1 : a > b ? 1 : 0;
164
164
  }
165
+ /**
166
+ * Build a `Map<K, V>` from an iterable of key-value pairs, refusing on duplicate
167
+ * identity keys. A map the caller builds must have unique keys; a duplicate is
168
+ * corruption, not a shadowing rule.
169
+ *
170
+ * @throws Error when a key appears more than once in the iterable, naming the
171
+ * colliding key.
172
+ */
173
+ export function uniqueMap(entries) {
174
+ const map = new Map();
175
+ for (const [key, value] of entries) {
176
+ if (map.has(key)) {
177
+ throw new Error(`duplicate identity key \`${key}\``);
178
+ }
179
+ map.set(key, value);
180
+ }
181
+ return map;
182
+ }
165
183
  /**
166
184
  * A host kind's nesting templates, from its two declaration loci. The kind's own
167
185
  * declared templates carry each layer — child kind, plus a file layer's path pattern.
@@ -309,24 +327,16 @@ function kindFactKindsInPlay(allKinds) {
309
327
  }
310
328
  /** The requirement rows — assembly `require` and every member's `requires`, one namespace. */
311
329
  function requirementRows(harness) {
312
- const merged = new Map();
313
- const publish = (name, requirement, source) => {
314
- const existing = merged.get(name);
315
- if (existing !== undefined && existing !== requirement) {
316
- // One namespace, one fill mechanism; a cross-publisher name collision is an
317
- // admissibility finding, never a shadowing rule.
318
- throw new Error(`requirement \`${name}\` is published twice (${source} collides with an earlier ` +
319
- `publisher) — a name collision across publishers is an admissibility finding.`);
320
- }
321
- merged.set(name, requirement);
322
- };
323
- for (const [name, requirement] of Object.entries(harness.require))
324
- publish(name, requirement, "the assembly");
330
+ const entries = [];
331
+ for (const [name, requirement] of Object.entries(harness.require)) {
332
+ entries.push([name, requirement]);
333
+ }
325
334
  for (const member of harness.members) {
326
335
  for (const [name, requirement] of Object.entries(member.requires)) {
327
- publish(name, requirement, `member \`${member.name}\``);
336
+ entries.push([name, requirement]);
328
337
  }
329
338
  }
339
+ const merged = uniqueMap(entries);
330
340
  return [...merged.entries()]
331
341
  .sort(([a], [b]) => compareStrings(a, b))
332
342
  .map(([name, requirement]) => ({
@@ -632,7 +642,7 @@ export function registrationRows(harness) {
632
642
  /** The tap invocation the synthesized telemetry hooks run — the sibling verb of the
633
643
  * session-start reporter, appending one event record to the per-machine log
634
644
  * (`src/tap.rs`). Every synthesized hook's `command` field carries it verbatim. */
635
- const TAP_COMMAND = "temper tap";
645
+ const TAP_COMMAND = "temper tap \"$CLAUDE_PROJECT_DIR\"";
636
646
  /**
637
647
  * Builds a collision-safe dedup key for a tap hook (event, matcher) pair.
638
648
  * Two distinct pairs cannot produce the same key (e.g., ("Foo", "BarBaz") ≠ ("FooBar", "Baz")).
package/dist/src/emit.js CHANGED
@@ -12,7 +12,7 @@ import { fileURLToPath } from "node:url";
12
12
  import { readFileSync } from "node:fs";
13
13
  import { checkMentions, isTextSpan, renderText, resolveLeaf } from "./prose.js";
14
14
  import { permissionUnion } from "./needs.js";
15
- import { compareStrings, compileDeclarations, declaredAddresses, declaredAtLocusKinds, declaredRequirements, encodeSeam, placementKey, registrationRows, settingsRows, tapHookRows, } from "./declarations.js";
15
+ import { compareStrings, compileDeclarations, declaredAddresses, declaredAtLocusKinds, declaredRequirements, encodeSeam, placementKey, registrationRows, settingsRows, tapHookRows, uniqueMap, } from "./declarations.js";
16
16
  /** The {@link MentionScope} a set of {@link ResolveOptions} names — its two sets, each defaulting to empty. */
17
17
  function scopeOf(options) {
18
18
  return {
@@ -209,6 +209,7 @@ function edgeTargetFacts(host, value, leaves, options) {
209
209
  address: lookup,
210
210
  kind: target.kind,
211
211
  path: relativeProjection(projectionPath(host), projectionPath(target)),
212
+ repoRootedPath: projectionPath(target),
212
213
  };
213
214
  }
214
215
  return targets;
@@ -349,7 +350,7 @@ function placedEdges(host, value, options) {
349
350
  * values `nestedMemberRows` does, so every edge-bearing row it builds has an observation.
350
351
  */
351
352
  function edgePlacements(harness, options) {
352
- const placements = new Map();
353
+ const entries = [];
353
354
  for (const member of harness.members) {
354
355
  if (member.prose?.kind !== "blocks")
355
356
  continue;
@@ -358,11 +359,11 @@ function edgePlacements(harness, options) {
358
359
  continue;
359
360
  const placed = placedEdges(member, value, options);
360
361
  if (placed !== undefined) {
361
- placements.set(placementKey(`${member.kind}:${member.name}`, value.kind, value.key), placed);
362
+ entries.push([placementKey(`${member.kind}:${member.name}`, value.kind, value.key), placed]);
362
363
  }
363
364
  }
364
365
  }
365
- return placements;
366
+ return uniqueMap(entries);
366
367
  }
367
368
  /**
368
369
  * The line count of a rendered block, matching the engine's `str::lines()`: a single
@@ -390,7 +391,7 @@ function renderedLineCount(block) {
390
391
  * distinction between an observed empty and an unobserved absence).
391
392
  */
392
393
  function renderedExtents(harness, options) {
393
- const extents = new Map();
394
+ const entries = [];
394
395
  for (const member of harness.members) {
395
396
  if (member.prose?.kind !== "blocks")
396
397
  continue;
@@ -398,15 +399,18 @@ function renderedExtents(harness, options) {
398
399
  if (isTextSpan(value))
399
400
  continue;
400
401
  const block = renderMemberBlock(member, value, options);
401
- extents.set(placementKey(`${member.kind}:${member.name}`, value.kind, value.key), {
402
- lines: renderedLineCount(block),
403
- // Unicode scalar values, matching Rust's `chars().count()` — iterating a string
404
- // yields code points, so a surrogate pair counts once, the way it does file-side.
405
- chars: [...block].length,
406
- });
402
+ entries.push([
403
+ placementKey(`${member.kind}:${member.name}`, value.kind, value.key),
404
+ {
405
+ lines: renderedLineCount(block),
406
+ // Unicode scalar values, matching Rust's `chars().count()` — iterating a string
407
+ // yields code points, so a surrogate pair counts once, the way it does file-side.
408
+ chars: [...block].length,
409
+ },
410
+ ]);
407
411
  }
408
412
  }
409
- return extents;
413
+ return uniqueMap(entries);
410
414
  }
411
415
  /**
412
416
  * Render a member-level `Text` body to its final bytes: its mentions are
@@ -550,7 +554,7 @@ function settingsResidue(harness) {
550
554
  * spells a member address, so an edge field and a mention name a member the same way.
551
555
  */
552
556
  function memberTable(harness) {
553
- return new Map(harness.members.map((member) => [`${member.kind}:${member.name}`, member]));
557
+ return uniqueMap(harness.members.map((member) => [`${member.kind}:${member.name}`, member]));
554
558
  }
555
559
  /** The harness's projected members as payload members, deterministically kind-then-name ordered. */
556
560
  function orderedMembers(harness, options) {
@@ -345,8 +345,7 @@ export interface ResolvedEmbeddedMemberCollectionEntry {
345
345
  * The closed set of facts an embedded format may place about one edge field's
346
346
  * target — derived at emit off the resolved target member, never authored at the
347
347
  * instance and never fabricated, so a rendered reference is true by construction.
348
- * The set is exactly these four; a fifth fact is a spec question, not a
349
- * convenience.
348
+ * The set is exactly these five facts; no convenience additions beyond.
350
349
  */
351
350
  export interface EdgeTargetFacts {
352
351
  /** The target member's identity within its kind. */
@@ -355,8 +354,10 @@ export interface EdgeTargetFacts {
355
354
  readonly address: string;
356
355
  /** The target member's kind. */
357
356
  readonly kind: string;
358
- /** The target's projection, relative to the host member's own projection. */
357
+ /** The target's projection, relative to the host member's own projection — used by a render hook to spell a link from the host body. */
359
358
  readonly path: string;
359
+ /** The target's projection rooted at the repository — used by a render hook consuming edge targets from repo root. */
360
+ readonly repoRootedPath: string;
360
361
  }
361
362
  /**
362
363
  * An {@link EmbeddedMemberValue} after every leaf (top-level and each
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtmd/temper",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "The temper authoring face — the six-noun model as typed modules: harness(), kind<T>(), clause values, needs, and file()/text/blocks(). Emit compiles to the declaration rows the engine reads, a byte-faithful projection, and the lock.",
5
5
  "license": "(MIT OR Apache-2.0)",
6
6
  "repository": {
@@ -52,7 +52,7 @@
52
52
  "temper": "bin/temper.js"
53
53
  },
54
54
  "optionalDependencies": {
55
- "@dtmd/temper-linux-x64": "0.0.15",
56
- "@dtmd/temper-win32-x64": "0.0.15"
55
+ "@dtmd/temper-linux-x64": "0.0.16",
56
+ "@dtmd/temper-win32-x64": "0.0.16"
57
57
  }
58
58
  }