@dtmd/temper 0.0.10 → 0.0.12

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.
@@ -29,11 +29,11 @@ export interface Admission {
29
29
  readonly admits: readonly KindDefinition<never>[];
30
30
  }
31
31
  /**
32
- * The declared enforcement-mode vocabulary — how firmly the `PreToolUse` guard
33
- * binds a tool call, split by where the finding goes.
34
- * `block`: denies the call.
35
- * `warn` (default): allows the call and surfaces the finding in-band, into the
36
- * live context. `note`: allows the call and records the finding out-of-band
32
+ * The declared enforcement-mode vocabulary — how firmly a guard binds an
33
+ * intercepted action, split by where the finding goes.
34
+ * `block`: denies the action.
35
+ * `warn` (default): allows the action and surfaces the finding in-band, into the
36
+ * live context. `note`: allows the action and records the finding out-of-band
37
37
  * only — the next report, never the session.
38
38
  */
39
39
  export type EnforcementMode = "note" | "warn" | "block";
@@ -300,6 +300,12 @@ export interface Hook {
300
300
  /** The tool-name filter a tool-scoped event fires on (`"*"`/`""`/absent = all). */
301
301
  readonly matcher?: string;
302
302
  }
303
+ /**
304
+ * The manifest Claude Code's harness-level settings reside in — the file the assembly's
305
+ * residual settings keys fold into as opaque residue, the same manifest the `hook` kind's
306
+ * registrations surface inside (code.claude.com/docs/en/settings, retrieved 2026-07-16).
307
+ */
308
+ export declare const SETTINGS_MANIFEST = "settings.json";
303
309
  /**
304
310
  * `hook` — a `settings.json` `hooks.<Event>` registration member: a fields-only kind (no
305
311
  * body slot), its members discovered off the `.claude/settings.json` manifest at the
@@ -569,11 +575,6 @@ export declare const pluginManifest: KindDefinition<PluginManifest>;
569
575
  *
570
576
  * Deliberately absent as undecidable: whether the `description` reads well, whether
571
577
  * `keywords` aid discovery, whether `name` names the pack aptly.
572
- *
573
- * Authoring notes the clauses cannot carry: leave `version` unset while iterating, so the
574
- * commit SHA drives updates and users are not stranded on a stale pin; set it once the
575
- * plugin has a release cycle, and bump it every time — pushing commits without bumping is
576
- * a no-op. Reach for `defaultEnabled: false` when the plugin costs money or scope on load.
577
578
  */
578
579
  export declare const pluginManifestDefaultContract: readonly Clause[];
579
580
  /**
@@ -713,14 +714,6 @@ export declare const marketplace: KindDefinition<Marketplace>;
713
714
  * there is no predicate that decides it, and a clause that guessed would fire on true
714
715
  * negatives. The enumerated deny list is the decidable subset; the impersonation rule rides
715
716
  * as guidance below.
716
- *
717
- * Authoring notes the clauses cannot carry: a relative-path `source` resolves against a
718
- * *local copy* of the marketplace, so it silently fails to resolve for users who added the
719
- * marketplace by direct URL to `marketplace.json` — only that one file is downloaded. Reach
720
- * for `github`, `url`, or `npm` when the catalog is distributed by URL. Where a git source
721
- * pins both `ref` and `sha`, the `sha` is the effective pin. A marketplace entry's
722
- * `defaultEnabled` beats the same field in the plugin's own `plugin.json`, while `version`
723
- * runs the other way — `plugin.json` wins.
724
717
  */
725
718
  export declare const marketplaceDefaultContract: readonly Clause[];
726
719
  /**
@@ -803,13 +796,6 @@ export declare const settingsLocalDefaultContract: readonly Clause[];
803
796
  * vagueness/no-op detection (semantic); gerund naming (judgment). Nothing
804
797
  * decidable is held: the name's hyphen placement and the platform's "no XML
805
798
  * tags in the description" are the two `shape` clauses below.
806
- *
807
- * Authoring notes the clauses cannot carry: prefer gerund or noun-phrase
808
- * names (`processing-pdfs`, `pdf-processing`) over vague ones (`helper`,
809
- * `utils`); `disable-model-invocation: true` for side-effectful workflows you
810
- * want to time yourself; `user-invocable: false` for background knowledge
811
- * that is not a command; `metadata` is the sanctioned home for versioning —
812
- * there is no top-level `version` field.
813
799
  */
814
800
  export declare const skillDefaultContract: readonly Clause[];
815
801
  /**
@@ -830,12 +816,16 @@ export declare const skillDefaultContract: readonly Clause[];
830
816
  */
831
817
  export declare const supportingDocDefaultContract: readonly Clause[];
832
818
  /**
833
- * The default contract for `command` — `skillDefaultContract`'s clauses minus `nameMatchesDir`: a
834
- * command is a lone file with no parent directory to match, so the one clause
835
- * that ranges over the directory relationship does not apply; every other
836
- * documented skill-schema recommendation, name-requiredness included, still
837
- * governs a command by the same import (code.claude.com/docs/en/skills,
838
- * retrieved 2026-07-15).
819
+ * The default contract for `command` — deliberately empty. A command is a lone
820
+ * markdown file whose frontmatter is entirely optional, matching the Claude Code
821
+ * runtime's documented flexibility. Where a skill requires `name` and
822
+ * `description` by the Agent Skills open standard, a command file may carry any,
823
+ * all, or none of them — identity comes from the filename stem, not a field
824
+ * (code.claude.com/docs/en/slash-commands, retrieved 2026-07-22).
825
+ *
826
+ * A command that passes here holds no contract constraint and loads everywhere
827
+ * the format is honored. The strictness stays with the skill kind, which enforces
828
+ * the portable profile for skill directories.
839
829
  */
840
830
  export declare const commandDefaultContract: readonly Clause[];
841
831
  /**
@@ -865,16 +855,6 @@ export declare const agentDefaultContract: readonly Clause[];
865
855
  * own: `required` is the one
866
856
  * presence predicate, and its absence is not itself a predicate.)
867
857
  * https://code.claude.com/docs/en/memory#path-specific-rules (retrieved 2026-07-15)
868
- *
869
- * What the clauses cannot carry, as guidance: keep a rule to facts Claude
870
- * should hold whenever the rule is in scope — concrete enough to verify ("use
871
- * 2-space indentation", not "format code properly"). If an entry is a
872
- * multi-step procedure or only matters occasionally, it belongs in a skill
873
- * (on-demand) rather than a rule (always-on). Prefer path-scoped rules when
874
- * one convention governs scattered paths; prefer per-directory CLAUDE.md when
875
- * directory owners maintain their own. Treat rules like code: prune them when
876
- * behavior drifts, and test a change by watching whether Claude's behavior
877
- * actually shifts.
878
858
  */
879
859
  export declare const ruleDefaultContract: readonly Clause[];
880
860
  /**
@@ -887,21 +867,6 @@ export declare const ruleDefaultContract: readonly Clause[];
887
867
  * schema to gate — manufacturing a required field or a forbidden-key list
888
868
  * would fake a check the format does not carry. The single clause is a context-cost
889
869
  * budget; everything else the contract could say is guidance.
890
- *
891
- * What the clauses cannot carry, as guidance: a `paths:` frontmatter block
892
- * belongs on a `.claude/rules/*.md` file, not on `CLAUDE.md` — the memory
893
- * docs document `paths` only for rules, so a rules-style header on
894
- * `CLAUDE.md` is dead configuration. Split a large file with `@path` imports
895
- * (resolved relative to the importing file, absolute allowed, recursion
896
- * capped at four hops; wrap a path in backticks to mention it without
897
- * importing). If the repo already ships an `AGENTS.md` for other agents,
898
- * don't duplicate it — create a `CLAUDE.md` that `@AGENTS.md`-imports it (or
899
- * symlink, except on Windows where the import is the recommended bridge).
900
- * Mind the loading asymmetry: every ancestor `CLAUDE.md` loads in full at
901
- * launch, while files in subdirectories load only when Claude reads a file
902
- * there — so a rule that must always hold belongs above the working
903
- * directory, not below it. Personal, un-shared notes go in `CLAUDE.local.md`
904
- * (gitignored), appended after `CLAUDE.md` at its level.
905
870
  */
906
871
  export declare const memoryAnthropicDefaultContract: readonly Clause[];
907
872
  /**
@@ -920,12 +885,29 @@ export declare const memoryAnthropicDefaultContract: readonly Clause[];
920
885
  * vocabulary's when/enumOf/type extensions, addressing still cannot spell a path into the
921
886
  * handler array (e.g., `hooks.<Event>[0].type`). A clause over it would range over a field
922
887
  * the read never surfaces, so it is no clause at all — the addressing-reach gap remains.
923
- * What the clauses cannot carry, as guidance: keep a handler's `type` among
924
- * `command`/`http`/`mcp_tool`/`prompt`/`agent`; a `command` handler needs a `command`, an
925
- * `http` handler a `url`; the `matcher` filters tool-scoped events and is inert on events
926
- * that carry no tool (`UserPromptSubmit`, `Stop`, and their siblings).
927
888
  */
928
889
  export declare const hookDefaultContract: readonly Clause[];
890
+ /**
891
+ * The `settings.json` lifecycle event and matcher one documented telemetry event-name
892
+ * projects its tap hook at. The event-name is the author-facing token a telemetry
893
+ * verifier names (`contract.ts`'s `telemetry`, the `roster.rs` admissibility set); the
894
+ * `event` is the `hooks.<Event>` key the tap registers under, and the `matcher` scopes
895
+ * the fire to the telemetry-relevant subset — each an external fact
896
+ * (code.claude.com/docs/en/hooks, retrieved 2026-07-17):
897
+ *
898
+ * - `InstructionsLoaded` fires on a rule/memory load; its matcher filters the load
899
+ * reason, and `path_glob_match` is the lazy per-path load the coverage tap reads.
900
+ * - `SkillInvoked` is a skill invocation, surfaced under `PostToolUse` with the tool-name
901
+ * matcher `Skill` — the tap's own read of a skill call.
902
+ * - `UserPromptExpansion` fires on a command expansion; its matcher filters the command
903
+ * name, `.*` capturing every one.
904
+ * - `ToolUse` fires after any tool call; its matcher filters the tool name, `.*`
905
+ * capturing every one.
906
+ */
907
+ export declare const TELEMETRY_EVENT_HOOKS: Readonly<Record<string, {
908
+ readonly event: string;
909
+ readonly matcher: string;
910
+ }>>;
929
911
  /**
930
912
  * The default contract for `mcpServer` — Anthropic's documented `.mcp.json` contract
931
913
  * (code.claude.com/docs/en/mcp, retrieved 2026-07-15). A server surfaces at `mcpServers.*`,
@@ -48,6 +48,7 @@ export const skill = kind({
48
48
  registration: [{ via: "user-invoked" }, { via: "description-trigger", field: "description" }],
49
49
  identityField: "name",
50
50
  templates: [{ kind: supportingDoc, path: "*.md" }],
51
+ guidance: "prefer gerund or noun-phrase names (`processing-pdfs`, `pdf-processing`) over vague ones (`helper`, `utils`); `disable-model-invocation: true` for side-effectful workflows you want to time yourself; `user-invocable: false` for background knowledge that is not a command; `metadata` is the sanctioned home for versioning — there is no top-level `version` field.",
51
52
  });
52
53
  /**
53
54
  * `command` — `.claude/commands/*.md`, the skill surface's legacy file placement
@@ -90,6 +91,7 @@ export const rule = kind({
90
91
  format: "yaml-frontmatter",
91
92
  unitShape: "file",
92
93
  registration: [{ via: "paths-match", field: "paths" }],
94
+ guidance: "keep a rule to facts Claude should hold whenever the rule is in scope — concrete enough to verify ('use 2-space indentation', not 'format code properly'). If an entry is a multi-step procedure or only matters occasionally, it belongs in a skill (on-demand) rather than a rule (always-on). Prefer path-scoped rules when one convention governs scattered paths; prefer per-directory CLAUDE.md when directory owners maintain their own. Treat rules like code: prune them when behavior drifts, and test a change by watching whether Claude's behavior actually shifts.",
93
95
  });
94
96
  /**
95
97
  * `memory` — a root `<name>.md` (`CLAUDE.md`, `AGENTS.md`), a lone file loaded
@@ -105,7 +107,14 @@ export const memory = kind({
105
107
  locus: { kind: "at", root: ".", glob: "**/CLAUDE.md" },
106
108
  unitShape: "file",
107
109
  registration: [{ via: "always" }],
110
+ guidance: "a `paths:` frontmatter block belongs on a `.claude/rules/*.md` file, not on `CLAUDE.md` — the memory docs document `paths` only for rules, so a rules-style header on `CLAUDE.md` is dead configuration. Split a large file with `@path` imports (resolved relative to the importing file, absolute allowed, recursion capped at four hops; wrap a path in backticks to mention it without importing). If the repo already ships an `AGENTS.md` for other agents, don't duplicate it — create a `CLAUDE.md` that `@AGENTS.md`-imports it (or symlink, except on Windows where the import is the recommended bridge). Mind the loading asymmetry: every ancestor `CLAUDE.md` loads in full at launch, while files in subdirectories load only when Claude reads a file there — so a rule that must always hold belongs above the working directory, not below it. Personal, un-shared notes go in `CLAUDE.local.md` (gitignored), appended after `CLAUDE.md` at its level.",
108
111
  });
112
+ /**
113
+ * The manifest Claude Code's harness-level settings reside in — the file the assembly's
114
+ * residual settings keys fold into as opaque residue, the same manifest the `hook` kind's
115
+ * registrations surface inside (code.claude.com/docs/en/settings, retrieved 2026-07-16).
116
+ */
117
+ export const SETTINGS_MANIFEST = "settings.json";
109
118
  /**
110
119
  * `hook` — a `settings.json` `hooks.<Event>` registration member: a fields-only kind (no
111
120
  * body slot), its members discovered off the `.claude/settings.json` manifest at the
@@ -115,11 +124,12 @@ export const memory = kind({
115
124
  */
116
125
  export const hook = kind({
117
126
  name: "hook",
118
- locus: { kind: "at", root: ".claude", glob: "settings.json" },
127
+ locus: { kind: "at", root: ".claude", glob: SETTINGS_MANIFEST },
119
128
  unitShape: "file",
120
129
  registration: [{ via: "event", field: "event" }],
121
130
  shape: "fields",
122
- collectionAddress: { manifest: "settings.json", keyPath: "hooks.<Event>", entryShape: "group-array(hooks;matcher)" },
131
+ collectionAddress: { manifest: SETTINGS_MANIFEST, keyPath: "hooks.<Event>", entryShape: "group-array(hooks;matcher)" },
132
+ guidance: "keep a handler's `type` among `command`/`http`/`mcp_tool`/`prompt`/`agent`; a `command` handler needs a `command`, an `http` handler a `url`; the `matcher` filters tool-scoped events and is inert on events that carry no tool (`UserPromptSubmit`, `Stop`, and their siblings).",
123
133
  });
124
134
  /**
125
135
  * `mcpServer` — a `.mcp.json` `mcpServers.*` registration member: a fields-only kind (no
@@ -147,11 +157,11 @@ export const mcpServer = kind({
147
157
  */
148
158
  export const installedPlugin = kind({
149
159
  name: "installed-plugin",
150
- locus: { kind: "at", root: ".claude", glob: "settings.json" },
160
+ locus: { kind: "at", root: ".claude", glob: SETTINGS_MANIFEST },
151
161
  unitShape: "file",
152
162
  registration: [{ via: "enablement", field: "enabled" }],
153
163
  shape: "fields",
154
- collectionAddress: { manifest: "settings.json", keyPath: "enabledPlugins.*", entryShape: "scalar(enabled)" },
164
+ collectionAddress: { manifest: SETTINGS_MANIFEST, keyPath: "enabledPlugins.*", entryShape: "scalar(enabled)" },
155
165
  // The marketplace half of the `<plugin>@<marketplace>` key is a declared edge to the
156
166
  // `known-marketplace` member it names (decision 0039). The half is not an authored field
157
167
  // — the engine splits it off the composite key at read (`src/kind.rs`, the read-time fold
@@ -190,11 +200,11 @@ export const installedPluginDefaultContract = [];
190
200
  */
191
201
  export const knownMarketplace = kind({
192
202
  name: "known-marketplace",
193
- locus: { kind: "at", root: ".claude", glob: "settings.json" },
203
+ locus: { kind: "at", root: ".claude", glob: SETTINGS_MANIFEST },
194
204
  unitShape: "file",
195
205
  registration: [{ via: "registry" }],
196
206
  shape: "fields",
197
- collectionAddress: { manifest: "settings.json", keyPath: "extraKnownMarketplaces.*", entryShape: "object" },
207
+ collectionAddress: { manifest: SETTINGS_MANIFEST, keyPath: "extraKnownMarketplaces.*", entryShape: "object" },
198
208
  });
199
209
  /**
200
210
  * The default contract for `known-marketplace` — **deliberately empty**. The format
@@ -222,6 +232,7 @@ export const pluginManifest = kind({
222
232
  unitShape: "named-field",
223
233
  registration: [],
224
234
  identityField: "name",
235
+ guidance: "leave `version` unset while iterating, so the commit SHA drives updates and users are not stranded on a stale pin; set it once the plugin has a release cycle, and bump it every time — pushing commits without bumping is a no-op. Reach for `defaultEnabled: false` when the plugin costs money or scope on load.",
225
236
  });
226
237
  /**
227
238
  * The default contract for `plugin-manifest` — the documented profile of `claude plugin
@@ -249,11 +260,6 @@ export const pluginManifest = kind({
249
260
  *
250
261
  * Deliberately absent as undecidable: whether the `description` reads well, whether
251
262
  * `keywords` aid discovery, whether `name` names the pack aptly.
252
- *
253
- * Authoring notes the clauses cannot carry: leave `version` unset while iterating, so the
254
- * commit SHA drives updates and users are not stranded on a stale pin; set it once the
255
- * plugin has a release cycle, and bump it every time — pushing commits without bumping is
256
- * a no-op. Reach for `defaultEnabled: false` when the plugin costs money or scope on load.
257
263
  */
258
264
  export const pluginManifestDefaultContract = [
259
265
  clause(required("name"), {
@@ -441,6 +447,7 @@ export const marketplace = kind({
441
447
  unitShape: "named-field",
442
448
  registration: [],
443
449
  identityField: "name",
450
+ guidance: "a relative-path `source` resolves against a *local copy* of the marketplace, so it silently fails to resolve for users who added the marketplace by direct URL to `marketplace.json` — only that one file is downloaded. Reach for `github`, `url`, or `npm` when the catalog is distributed by URL. Where a git source pins both `ref` and `sha`, the `sha` is the effective pin. A marketplace entry's `defaultEnabled` beats the same field in the plugin's own `plugin.json`, while `version` runs the other way — `plugin.json` wins.",
444
451
  });
445
452
  /**
446
453
  * The reserved marketplace names — reserved for official Anthropic use and refused to a
@@ -490,14 +497,6 @@ const RESERVED_MARKETPLACE_NAMES = [
490
497
  * there is no predicate that decides it, and a clause that guessed would fire on true
491
498
  * negatives. The enumerated deny list is the decidable subset; the impersonation rule rides
492
499
  * as guidance below.
493
- *
494
- * Authoring notes the clauses cannot carry: a relative-path `source` resolves against a
495
- * *local copy* of the marketplace, so it silently fails to resolve for users who added the
496
- * marketplace by direct URL to `marketplace.json` — only that one file is downloaded. Reach
497
- * for `github`, `url`, or `npm` when the catalog is distributed by URL. Where a git source
498
- * pins both `ref` and `sha`, the `sha` is the effective pin. A marketplace entry's
499
- * `defaultEnabled` beats the same field in the plugin's own `plugin.json`, while `version`
500
- * runs the other way — `plugin.json` wins.
501
500
  */
502
501
  export const marketplaceDefaultContract = [
503
502
  clause(required("name"), {
@@ -661,13 +660,6 @@ export const settingsLocalDefaultContract = [
661
660
  * vagueness/no-op detection (semantic); gerund naming (judgment). Nothing
662
661
  * decidable is held: the name's hyphen placement and the platform's "no XML
663
662
  * tags in the description" are the two `shape` clauses below.
664
- *
665
- * Authoring notes the clauses cannot carry: prefer gerund or noun-phrase
666
- * names (`processing-pdfs`, `pdf-processing`) over vague ones (`helper`,
667
- * `utils`); `disable-model-invocation: true` for side-effectful workflows you
668
- * want to time yourself; `user-invocable: false` for background knowledge
669
- * that is not a command; `metadata` is the sanctioned home for versioning —
670
- * there is no top-level `version` field.
671
663
  */
672
664
  export const skillDefaultContract = [
673
665
  clause(required("name"), {
@@ -770,14 +762,18 @@ export const supportingDocDefaultContract = [
770
762
  }),
771
763
  ];
772
764
  /**
773
- * The default contract for `command` — `skillDefaultContract`'s clauses minus `nameMatchesDir`: a
774
- * command is a lone file with no parent directory to match, so the one clause
775
- * that ranges over the directory relationship does not apply; every other
776
- * documented skill-schema recommendation, name-requiredness included, still
777
- * governs a command by the same import (code.claude.com/docs/en/skills,
778
- * retrieved 2026-07-15).
765
+ * The default contract for `command` — deliberately empty. A command is a lone
766
+ * markdown file whose frontmatter is entirely optional, matching the Claude Code
767
+ * runtime's documented flexibility. Where a skill requires `name` and
768
+ * `description` by the Agent Skills open standard, a command file may carry any,
769
+ * all, or none of them — identity comes from the filename stem, not a field
770
+ * (code.claude.com/docs/en/slash-commands, retrieved 2026-07-22).
771
+ *
772
+ * A command that passes here holds no contract constraint and loads everywhere
773
+ * the format is honored. The strictness stays with the skill kind, which enforces
774
+ * the portable profile for skill directories.
779
775
  */
780
- export const commandDefaultContract = skillDefaultContract.filter((entry) => entry.predicate.key !== "name-matches-dir");
776
+ export const commandDefaultContract = [];
781
777
  /**
782
778
  * The default contract for `agent` — Anthropic's documented subagent contract
783
779
  * (code.claude.com/docs/en/sub-agents, retrieved 2026-07-15): `name` and
@@ -826,16 +822,6 @@ export const agentDefaultContract = [
826
822
  * own: `required` is the one
827
823
  * presence predicate, and its absence is not itself a predicate.)
828
824
  * https://code.claude.com/docs/en/memory#path-specific-rules (retrieved 2026-07-15)
829
- *
830
- * What the clauses cannot carry, as guidance: keep a rule to facts Claude
831
- * should hold whenever the rule is in scope — concrete enough to verify ("use
832
- * 2-space indentation", not "format code properly"). If an entry is a
833
- * multi-step procedure or only matters occasionally, it belongs in a skill
834
- * (on-demand) rather than a rule (always-on). Prefer path-scoped rules when
835
- * one convention governs scattered paths; prefer per-directory CLAUDE.md when
836
- * directory owners maintain their own. Treat rules like code: prune them when
837
- * behavior drifts, and test a change by watching whether Claude's behavior
838
- * actually shifts.
839
825
  */
840
826
  export const ruleDefaultContract = [
841
827
  clause(forbiddenKeys(["description", "globs", "alwaysApply"]), {
@@ -869,21 +855,6 @@ export const ruleDefaultContract = [
869
855
  * schema to gate — manufacturing a required field or a forbidden-key list
870
856
  * would fake a check the format does not carry. The single clause is a context-cost
871
857
  * budget; everything else the contract could say is guidance.
872
- *
873
- * What the clauses cannot carry, as guidance: a `paths:` frontmatter block
874
- * belongs on a `.claude/rules/*.md` file, not on `CLAUDE.md` — the memory
875
- * docs document `paths` only for rules, so a rules-style header on
876
- * `CLAUDE.md` is dead configuration. Split a large file with `@path` imports
877
- * (resolved relative to the importing file, absolute allowed, recursion
878
- * capped at four hops; wrap a path in backticks to mention it without
879
- * importing). If the repo already ships an `AGENTS.md` for other agents,
880
- * don't duplicate it — create a `CLAUDE.md` that `@AGENTS.md`-imports it (or
881
- * symlink, except on Windows where the import is the recommended bridge).
882
- * Mind the loading asymmetry: every ancestor `CLAUDE.md` loads in full at
883
- * launch, while files in subdirectories load only when Claude reads a file
884
- * there — so a rule that must always hold belongs above the working
885
- * directory, not below it. Personal, un-shared notes go in `CLAUDE.local.md`
886
- * (gitignored), appended after `CLAUDE.md` at its level.
887
858
  */
888
859
  export const memoryAnthropicDefaultContract = [
889
860
  clause(extent("lines", 200), {
@@ -947,10 +918,6 @@ const DOCUMENTED_HOOK_EVENTS = [
947
918
  * vocabulary's when/enumOf/type extensions, addressing still cannot spell a path into the
948
919
  * handler array (e.g., `hooks.<Event>[0].type`). A clause over it would range over a field
949
920
  * the read never surfaces, so it is no clause at all — the addressing-reach gap remains.
950
- * What the clauses cannot carry, as guidance: keep a handler's `type` among
951
- * `command`/`http`/`mcp_tool`/`prompt`/`agent`; a `command` handler needs a `command`, an
952
- * `http` handler a `url`; the `matcher` filters tool-scoped events and is inert on events
953
- * that carry no tool (`UserPromptSubmit`, `Stop`, and their siblings).
954
921
  */
955
922
  export const hookDefaultContract = [
956
923
  clause(enumOf("event", DOCUMENTED_HOOK_EVENTS), {
@@ -959,6 +926,29 @@ export const hookDefaultContract = [
959
926
  cite: "https://code.claude.com/docs/en/hooks (retrieved 2026-07-15)",
960
927
  }),
961
928
  ];
929
+ /**
930
+ * The `settings.json` lifecycle event and matcher one documented telemetry event-name
931
+ * projects its tap hook at. The event-name is the author-facing token a telemetry
932
+ * verifier names (`contract.ts`'s `telemetry`, the `roster.rs` admissibility set); the
933
+ * `event` is the `hooks.<Event>` key the tap registers under, and the `matcher` scopes
934
+ * the fire to the telemetry-relevant subset — each an external fact
935
+ * (code.claude.com/docs/en/hooks, retrieved 2026-07-17):
936
+ *
937
+ * - `InstructionsLoaded` fires on a rule/memory load; its matcher filters the load
938
+ * reason, and `path_glob_match` is the lazy per-path load the coverage tap reads.
939
+ * - `SkillInvoked` is a skill invocation, surfaced under `PostToolUse` with the tool-name
940
+ * matcher `Skill` — the tap's own read of a skill call.
941
+ * - `UserPromptExpansion` fires on a command expansion; its matcher filters the command
942
+ * name, `.*` capturing every one.
943
+ * - `ToolUse` fires after any tool call; its matcher filters the tool name, `.*`
944
+ * capturing every one.
945
+ */
946
+ export const TELEMETRY_EVENT_HOOKS = {
947
+ InstructionsLoaded: { event: "InstructionsLoaded", matcher: "path_glob_match" },
948
+ SkillInvoked: { event: "PostToolUse", matcher: "Skill" },
949
+ UserPromptExpansion: { event: "UserPromptExpansion", matcher: ".*" },
950
+ ToolUse: { event: "PostToolUse", matcher: ".*" },
951
+ };
962
952
  /**
963
953
  * Every documented `.mcp.json` server transport — the closed set a server entry's `type`
964
954
  * is drawn from (code.claude.com/docs/en/mcp, retrieved 2026-07-15). `stdio` is the
@@ -274,9 +274,8 @@ export declare const script: (path: string) => Verifier;
274
274
  /**
275
275
  * A telemetry verifier — the named documented harness events the emitted tap records
276
276
  * to a local-locus log, judged by reading the field record. Each name must be a
277
- * documented harness lifecycle event (`InstructionsLoaded`, `SkillInvoked`,
278
- * `UserPromptExpansion`, `ToolUse`; code.claude.com/docs/en/hooks, retrieved
279
- * 2026-07-17) — the gate checks each resolves, never records into it.
277
+ * documented harness lifecycle event the gate checks each resolves, never records
278
+ * into it. See `TELEMETRY_EVENT_HOOKS` in `declarations.ts` for the vocabulary.
280
279
  */
281
280
  export declare const telemetry: (events: readonly string[]) => Verifier;
282
281
  /**
@@ -208,9 +208,8 @@ export const script = (path) => ({ species: "script", path });
208
208
  /**
209
209
  * A telemetry verifier — the named documented harness events the emitted tap records
210
210
  * to a local-locus log, judged by reading the field record. Each name must be a
211
- * documented harness lifecycle event (`InstructionsLoaded`, `SkillInvoked`,
212
- * `UserPromptExpansion`, `ToolUse`; code.claude.com/docs/en/hooks, retrieved
213
- * 2026-07-17) — the gate checks each resolves, never records into it.
211
+ * documented harness lifecycle event the gate checks each resolves, never records
212
+ * into it. See `TELEMETRY_EVENT_HOOKS` in `declarations.ts` for the vocabulary.
214
213
  */
215
214
  export const telemetry = (events) => ({ species: "telemetry", events });
216
215
  /** An identity helper — types a requirement literal at the keystroke. */
@@ -9,7 +9,6 @@
9
9
  * in lockstep.
10
10
  */
11
11
  import type { Harness } from "./assembly.js";
12
- import type { MentionScope } from "./prose.js";
13
12
  import type { Declarations, Payload, RegistrationRow, SettingsRow } from "./generated/index.js";
14
13
  export type { AssemblyFactRow, ClauseRow, Declarations, KindFactRow, RequirementRow, SatisfiesRow, } from "./generated/index.js";
15
14
  /** The stable-sort ordering every declaration row family shares. */
@@ -56,6 +55,12 @@ export type RenderedExtents = ReadonlyMap<string, RenderedExtent>;
56
55
  * If a fields-only member declares no collection address — it surfaces in no host manifest.
57
56
  */
58
57
  export declare function registrationRows(harness: Harness): RegistrationRow[];
58
+ /**
59
+ * Builds a collision-safe dedup key for a tap hook (event, matcher) pair.
60
+ * Two distinct pairs cannot produce the same key (e.g., ("Foo", "BarBaz") ≠ ("FooBar", "Baz")).
61
+ * @internal
62
+ */
63
+ export declare function buildTapHookDedupeKey(event: string, matcher: string): string;
59
64
  /**
60
65
  * The synthesized tap-hook `registration` rows — one deduped `hooks.<Event>`
61
66
  * registration per (lifecycle event, matcher) any telemetry verifier names. Scans the
@@ -97,8 +102,6 @@ export declare function declaredAddresses(harness: Harness): Set<string>;
97
102
  * flat `kind:name` mention of one has no discovery locus to defer to.
98
103
  */
99
104
  export declare function declaredAtLocusKinds(harness: Harness): Set<string>;
100
- /** The full {@link MentionScope} the program resolves a mention against — its addresses and its deferral kinds. */
101
- export declare function mentionScope(harness: Harness): MentionScope;
102
105
  /**
103
106
  * Compile a harness into its seven declaration families — the erased program.
104
107
  *
@@ -110,8 +113,6 @@ export declare function mentionScope(harness: Harness): MentionScope;
110
113
  * or `extent` clause, so a whole compile goes through `emit`, never this alone.
111
114
  */
112
115
  export declare function compileDeclarations(harness: Harness, placements?: EdgePlacements, extents?: RenderedExtents): Declarations;
113
- /** The SDK's pinned engine/interchange version — the JSON pipe rides it in lockstep. */
114
- export declare const SEAM_VERSION = 2;
115
116
  /**
116
117
  * Serialize the seam payload to the internal versioned JSON pipe — `encodeSeam`
117
118
  * stamps `version`, so the caller supplies the rest of the {@link Payload}.
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { fileURLToPath } from "node:url";
12
12
  import { isTextSpan, resolveLeaf } from "./prose.js";
13
+ import { SETTINGS_MANIFEST, TELEMETRY_EVENT_HOOKS } from "./builtins.js";
13
14
  /**
14
15
  * Compile one `Clause` into its lock row: the shared `key`/`field`/`severity`/
15
16
  * `guidance`/`cite` columns — the clause's four channels surviving erasure
@@ -228,7 +229,7 @@ function collectionAddressRow(facts) {
228
229
  * the same spelling, absent for the committed default. `templates` names the embedded kinds the
229
230
  * corpus admits over it, and `content` lowers a declared layout (absent for a
230
231
  * `file`-content kind). A registration kind extends the row with its `shape` marker and
231
- * `collection_address`.
232
+ * `collection_address`. Advisory `guidance`/`cite` pair rides alongside (decision 0045).
232
233
  */
233
234
  function kindFactRow(facts, admissions) {
234
235
  if (facts.locus.kind === "embedded") {
@@ -250,6 +251,8 @@ function kindFactRow(facts, admissions) {
250
251
  content: contentRow(facts.content),
251
252
  shape: facts.shape,
252
253
  collection_address: collectionAddressRow(facts),
254
+ guidance: facts.guidance,
255
+ cite: facts.cite,
253
256
  };
254
257
  }
255
258
  /**
@@ -629,28 +632,13 @@ export function registrationRows(harness) {
629
632
  * (`src/tap.rs`). Every synthesized hook's `command` field carries it verbatim. */
630
633
  const TAP_COMMAND = "temper tap";
631
634
  /**
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
- * - `SkillInvoked` 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
- * - `ToolUse` fires after any tool call; its matcher filters the tool name, `.*`
646
- * capturing every one.
635
+ * Builds a collision-safe dedup key for a tap hook (event, matcher) pair.
636
+ * Two distinct pairs cannot produce the same key (e.g., ("Foo", "BarBaz") ("FooBar", "Baz")).
637
+ * @internal
647
638
  */
648
- const TELEMETRY_EVENT_HOOKS = {
649
- InstructionsLoaded: { event: "InstructionsLoaded", matcher: "path_glob_match" },
650
- SkillInvoked: { event: "PostToolUse", matcher: "Skill" },
651
- UserPromptExpansion: { event: "UserPromptExpansion", matcher: ".*" },
652
- ToolUse: { event: "PostToolUse", matcher: ".*" },
653
- };
639
+ export function buildTapHookDedupeKey(event, matcher) {
640
+ return JSON.stringify([event, matcher]);
641
+ }
654
642
  /**
655
643
  * The synthesized tap-hook `registration` rows — one deduped `hooks.<Event>`
656
644
  * registration per (lifecycle event, matcher) any telemetry verifier names. Scans the
@@ -671,7 +659,7 @@ export function tapHookRows(harness) {
671
659
  for (const name of requirement.verifier.events) {
672
660
  const mapping = TELEMETRY_EVENT_HOOKS[name];
673
661
  if (mapping !== undefined)
674
- deduped.set(`${mapping.event}${mapping.matcher}`, mapping);
662
+ deduped.set(buildTapHookDedupeKey(mapping.event, mapping.matcher), mapping);
675
663
  }
676
664
  };
677
665
  for (const requirement of Object.values(harness.require))
@@ -694,12 +682,6 @@ export function tapHookRows(harness) {
694
682
  ],
695
683
  }));
696
684
  }
697
- /**
698
- * The manifest Claude Code's harness-level settings reside in — the file the assembly's
699
- * residual settings keys fold into as opaque residue, the same manifest the `hook` kind's
700
- * registrations surface inside (code.claude.com/docs/en/settings, retrieved 2026-07-10).
701
- */
702
- const SETTINGS_MANIFEST = "settings.json";
703
685
  /**
704
686
  * The `settings` rows — the assembly's harness-level residual settings keys, each folded
705
687
  * into the settings.json manifest's opaque residue at emit. Key-sorted so double emit is
@@ -757,7 +739,7 @@ export function declaredAtLocusKinds(harness) {
757
739
  return new Set(atLocusKindsInPlay(kindsInPlay(harness)).map((facts) => facts.name));
758
740
  }
759
741
  /** The full {@link MentionScope} the program resolves a mention against — its addresses and its deferral kinds. */
760
- export function mentionScope(harness) {
742
+ function mentionScope(harness) {
761
743
  return { mentionable: declaredAddresses(harness), deferrableKinds: declaredAtLocusKinds(harness) };
762
744
  }
763
745
  /**
@@ -793,7 +775,7 @@ export function compileDeclarations(harness, placements, extents) {
793
775
  };
794
776
  }
795
777
  /** The SDK's pinned engine/interchange version — the JSON pipe rides it in lockstep. */
796
- export const SEAM_VERSION = 2;
778
+ const SEAM_VERSION = 2;
797
779
  /**
798
780
  * Serialize the seam payload to the internal versioned JSON pipe — `encodeSeam`
799
781
  * stamps `version`, so the caller supplies the rest of the {@link Payload}.
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import type { Harness } from "./assembly.js";
12
12
  import type { Member } from "./kind.js";
13
- import type { Declarations, RenderedExtent } from "./declarations.js";
13
+ import type { Declarations } from "./declarations.js";
14
14
  import type { PayloadMember } from "./generated/index.js";
15
15
  export type { PayloadMember } from "./generated/index.js";
16
16
  /** What a mention may resolve against at emit. */
@@ -30,27 +30,6 @@ export interface ResolveOptions {
30
30
  */
31
31
  readonly members?: ReadonlyMap<string, Member>;
32
32
  }
33
- /**
34
- * Every composed embedded value's placed edge fields, keyed by the value's
35
- * {@link placementKey} — what `emit` hands {@link compileDeclarations} so each
36
- * `nested_member` row carries its own format's placement record. Iterates exactly the
37
- * values `nestedMemberRows` does, so every edge-bearing row it builds has an observation.
38
- */
39
- export declare function edgePlacements(harness: Harness, options: ResolveOptions): Map<string, string[]>;
40
- /**
41
- * Every composed embedded value's rendered extent — the line and character count of the
42
- * block `emit` projected for it — keyed by its {@link placementKey}, what `emit` hands
43
- * {@link compileDeclarations} so each `nested_member` row carries the span an `extent`
44
- * clause budgets. Iterates exactly the values {@link edgePlacements} does, rendering each
45
- * through the same {@link renderMemberBlock} the body projection uses (a hook is pure, so
46
- * the measured render and the projected one cannot disagree), never a second renderer.
47
- *
48
- * A value the SDK composes is always rendered here, so it always captures a span; a value
49
- * no format rendered — an embedded member read off a layout host's source — is lowered by
50
- * the engine, not this pass, and reaches its row with no span (the `placed_edges`
51
- * distinction between an observed empty and an unobserved absence).
52
- */
53
- export declare function renderedExtents(harness: Harness, options: ResolveOptions): Map<string, RenderedExtent>;
54
33
  /**
55
34
  * One fields-only registration member erased for the manifest write face: its key
56
35
  * (a hook's lifecycle event, an MCP server's name), the collection address it keys
package/dist/src/emit.js CHANGED
@@ -348,7 +348,7 @@ function placedEdges(host, value, options) {
348
348
  * `nested_member` row carries its own format's placement record. Iterates exactly the
349
349
  * values `nestedMemberRows` does, so every edge-bearing row it builds has an observation.
350
350
  */
351
- export function edgePlacements(harness, options) {
351
+ function edgePlacements(harness, options) {
352
352
  const placements = new Map();
353
353
  for (const member of harness.members) {
354
354
  if (member.prose?.kind !== "blocks")
@@ -389,7 +389,7 @@ function renderedLineCount(block) {
389
389
  * the engine, not this pass, and reaches its row with no span (the `placed_edges`
390
390
  * distinction between an observed empty and an unobserved absence).
391
391
  */
392
- export function renderedExtents(harness, options) {
392
+ function renderedExtents(harness, options) {
393
393
  const extents = new Map();
394
394
  for (const member of harness.members) {
395
395
  if (member.prose?.kind !== "blocks")
@@ -79,4 +79,13 @@ export type KindFactRow = {
79
79
  * file-locus kind, so an ordinary row stays byte-identical.
80
80
  */
81
81
  collection_address?: CollectionAddressRow;
82
+ /**
83
+ * Advisory authoring counsel for the kind as a whole — teaching at authoring time via
84
+ * `schema` hover or `explain`, carrying no predicate or severity (decision 0045).
85
+ */
86
+ guidance?: string;
87
+ /**
88
+ * External-fact source backing the guidance — a doc URL plus retrieved date.
89
+ */
90
+ cite?: string;
82
91
  };
@@ -210,6 +210,12 @@ export interface KindFacts {
210
210
  /** Fact 7, template — one {@link Template} per inner layer of nested members the kind
211
211
  * hosts; absent for a kind that nests nothing. */
212
212
  readonly templates?: readonly Template[];
213
+ /** Advisory authoring counsel for the kind as a whole — teaching at authoring time via
214
+ * `schema` hover or `explain`, carrying no predicate or severity (decision 0045). */
215
+ readonly guidance?: string;
216
+ /** External-fact source backing the guidance — a doc URL plus retrieved date,
217
+ * carried as data. */
218
+ readonly cite?: string;
213
219
  }
214
220
  /**
215
221
  * One authored member — a typed value in the library. Kind identity travels by
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtmd/temper",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
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.10",
56
- "@dtmd/temper-win32-x64": "0.0.10"
55
+ "@dtmd/temper-linux-x64": "0.0.12",
56
+ "@dtmd/temper-win32-x64": "0.0.12"
57
57
  }
58
58
  }