@dtmd/temper 0.0.12 → 0.0.13

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 CHANGED
@@ -16,8 +16,9 @@ installed:
16
16
  npx @dtmd/temper check --harness .
17
17
  ```
18
18
 
19
- It validates every skill, rule, and agent against the documented Anthropic
20
- schemas and best practices, then reports what is malformed, what Claude Code
19
+ It validates every member Claude Code reads, from skills and rules to hooks,
20
+ MCP servers, and plugin manifests, against the documented Anthropic schemas
21
+ and best practices, then reports what is malformed, what Claude Code
21
22
  silently ignores, and what a requirement you declared would strand. Every
22
23
  finding arrives with its guidance attached.
23
24
 
@@ -314,6 +314,16 @@ export declare const SETTINGS_MANIFEST = "settings.json";
314
314
  * temper ships — the read side of 0021's manifest-authoring surface.
315
315
  */
316
316
  export declare const hook: KindDefinition<Hook>;
317
+ /**
318
+ * The tap hook registration's `hooks.<Event>` key-path and `Hook` field triple — the
319
+ * provider fact {@link tapHookRows} (`declarations.ts`) fills rather than authors inline.
320
+ * A tap hook always fires a `command` handler (`type` fixed), so only the tap's own
321
+ * `command` string and the per-event `matcher` {@link TELEMETRY_EVENT_HOOKS} names vary.
322
+ */
323
+ export declare function tapHookRegistration(command: string, matcher: string): {
324
+ readonly keyPath: string;
325
+ readonly fields: Array<[string, unknown]>;
326
+ };
317
327
  /**
318
328
  * A Claude Code MCP server — a fields-only registration member surfacing inside
319
329
  * `.mcp.json`, keyed by name under `mcpServers`. It owns no artifact of its own; its
@@ -115,6 +115,9 @@ export const memory = kind({
115
115
  * registrations surface inside (code.claude.com/docs/en/settings, retrieved 2026-07-16).
116
116
  */
117
117
  export const SETTINGS_MANIFEST = "settings.json";
118
+ /** The `hooks.<Event>` collection address's key-path — the `hook` kind's own address, and
119
+ * the tap hook registration's, since a tap hook is an ordinary `hook` entry. */
120
+ const HOOK_KEY_PATH = "hooks.<Event>";
118
121
  /**
119
122
  * `hook` — a `settings.json` `hooks.<Event>` registration member: a fields-only kind (no
120
123
  * body slot), its members discovered off the `.claude/settings.json` manifest at the
@@ -128,9 +131,25 @@ export const hook = kind({
128
131
  unitShape: "file",
129
132
  registration: [{ via: "event", field: "event" }],
130
133
  shape: "fields",
131
- collectionAddress: { manifest: SETTINGS_MANIFEST, keyPath: "hooks.<Event>", entryShape: "group-array(hooks;matcher)" },
134
+ collectionAddress: { manifest: SETTINGS_MANIFEST, keyPath: HOOK_KEY_PATH, entryShape: "group-array(hooks;matcher)" },
132
135
  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).",
133
136
  });
137
+ /**
138
+ * The tap hook registration's `hooks.<Event>` key-path and `Hook` field triple — the
139
+ * provider fact {@link tapHookRows} (`declarations.ts`) fills rather than authors inline.
140
+ * A tap hook always fires a `command` handler (`type` fixed), so only the tap's own
141
+ * `command` string and the per-event `matcher` {@link TELEMETRY_EVENT_HOOKS} names vary.
142
+ */
143
+ export function tapHookRegistration(command, matcher) {
144
+ return {
145
+ keyPath: HOOK_KEY_PATH,
146
+ fields: [
147
+ ["type", "command"],
148
+ ["command", command],
149
+ ["matcher", matcher],
150
+ ],
151
+ };
152
+ }
134
153
  /**
135
154
  * `mcpServer` — a `.mcp.json` `mcpServers.*` registration member: a fields-only kind (no
136
155
  * body slot), its members discovered off the `.mcp.json` manifest at the `mcpServers.*`
@@ -275,7 +275,7 @@ export declare const script: (path: string) => Verifier;
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
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.
278
+ * into it. See `TELEMETRY_EVENT_HOOKS` in `builtins.ts` for the vocabulary.
279
279
  */
280
280
  export declare const telemetry: (events: readonly string[]) => Verifier;
281
281
  /**
@@ -209,7 +209,7 @@ export const script = (path) => ({ species: "script", path });
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
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.
212
+ * into it. See `TELEMETRY_EVENT_HOOKS` in `builtins.ts` for the vocabulary.
213
213
  */
214
214
  export const telemetry = (events) => ({ species: "telemetry", events });
215
215
  /** An identity helper — types a requirement literal at the keystroke. */
@@ -70,8 +70,10 @@ export declare function buildTapHookDedupeKey(event: string, matcher: string): s
70
70
  * records every fire and read time joins raw events to members, so however many
71
71
  * verifiers name an event it takes exactly one hook — the derived-aggregate precedent
72
72
  * the permission union sets ({@link permissionUnion}). Each row runs {@link TAP_COMMAND}
73
- * under the event's documented matcher ({@link TELEMETRY_EVENT_HOOKS}); an event-name
74
- * outside that table is the roster's inadmissibility finding, never a row.
73
+ * under the event's documented matcher ({@link TELEMETRY_EVENT_HOOKS}), its key-path and
74
+ * field triple sourced from the provider face ({@link tapHookRegistration},
75
+ * `builtins.ts`); an event-name outside that table is the roster's inadmissibility
76
+ * finding, never a row.
75
77
  */
76
78
  export declare function tapHookRows(harness: Harness): RegistrationRow[];
77
79
  /**
@@ -10,7 +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
+ import { SETTINGS_MANIFEST, TELEMETRY_EVENT_HOOKS, tapHookRegistration } from "./builtins.js";
14
14
  /**
15
15
  * Compile one `Clause` into its lock row: the shared `key`/`field`/`severity`/
16
16
  * `guidance`/`cite` columns — the clause's four channels surviving erasure
@@ -223,20 +223,19 @@ function collectionAddressRow(facts) {
223
223
  };
224
224
  }
225
225
  /**
226
- * One kind's fact row — an `at` locus supplies `governs_root`/`governs_glob` and a
227
- * nested-file kind neither (its path composes from its host's unit and the host
228
- * template's pattern, so it governs no glob). A file locus's `commitment` class rides
229
- * the same spelling, absent for the committed default. `templates` names the embedded kinds the
230
- * corpus admits over it, and `content` lowers a declared layout (absent for a
231
- * `file`-content kind). A registration kind extends the row with its `shape` marker and
232
- * `collection_address`. Advisory `guidance`/`cite` pair rides alongside (decision 0045).
226
+ * One kind's fact row — an `at` locus supplies `governs_root`/`governs_glob` and any
227
+ * other locus neither (a nested-file kind's path composes from its host's unit and the
228
+ * host template's pattern; an embedded kind owns no unit at all both govern no glob).
229
+ * A file locus's `commitment` class rides the same spelling, absent for the committed
230
+ * default. `templates` names the embedded kinds the corpus admits over it, and `content`
231
+ * lowers a declared layout (absent for a `file`-content kind). A registration kind
232
+ * extends the row with its `shape` marker and `collection_address`. Advisory
233
+ * `guidance`/`cite` pair rides alongside, locus-optional so an embedded kind's own
234
+ * counsel reaches the lock the same way a nested-file kind's already does (decision
235
+ * 0045) — callable for any locus; [`kindFactKindsInPlay`] decides which embedded kinds
236
+ * actually take a row.
233
237
  */
234
238
  function kindFactRow(facts, admissions) {
235
- if (facts.locus.kind === "embedded") {
236
- // An embedded kind inherits its world residue through its host; it owns no unit at
237
- // all, so it takes no kind-fact row. Callers filter these out before this point.
238
- throw new Error(`kind \`${facts.name}\` is embedded — it carries no locus-bearing kind fact.`);
239
- }
240
239
  const governs = facts.locus.kind === "at" ? facts.locus : undefined;
241
240
  return {
242
241
  name: facts.name,
@@ -298,12 +297,15 @@ function atLocusKindsInPlay(allKinds) {
298
297
  return allKinds.filter((facts) => facts.locus.kind === "at");
299
298
  }
300
299
  /**
301
- * The distinct unit-owning kinds in play every locus but `embedded`. The kinds that
302
- * take a fact row: a nested-file kind owns a file the engine must place, and places it
303
- * off its row, though it governs no glob to be discovered at.
300
+ * The distinct kinds in play that take a kind-fact row: every non-embedded locus
301
+ * unconditionally (a nested-file kind owns a file the engine must place, and places it
302
+ * off its row, though it governs no glob to be discovered at), plus an embedded kind
303
+ * only when it declares `guidance` or `cite` of its own (decision 0045) — an embedded
304
+ * kind with neither has nothing for the row to carry, and its members already reach the
305
+ * corpus through their host's `templates` column alone (`kindsInPlay`), never the row.
304
306
  */
305
- function unitKindsInPlay(allKinds) {
306
- return allKinds.filter((facts) => facts.locus.kind !== "embedded");
307
+ function kindFactKindsInPlay(allKinds) {
308
+ return allKinds.filter((facts) => facts.locus.kind !== "embedded" || facts.guidance !== undefined || facts.cite !== undefined);
307
309
  }
308
310
  /** The requirement rows — assembly `require` and every member's `requires`, one namespace. */
309
311
  function requirementRows(harness) {
@@ -648,8 +650,10 @@ export function buildTapHookDedupeKey(event, matcher) {
648
650
  * records every fire and read time joins raw events to members, so however many
649
651
  * verifiers name an event it takes exactly one hook — the derived-aggregate precedent
650
652
  * the permission union sets ({@link permissionUnion}). Each row runs {@link TAP_COMMAND}
651
- * under the event's documented matcher ({@link TELEMETRY_EVENT_HOOKS}); an event-name
652
- * outside that table is the roster's inadmissibility finding, never a row.
653
+ * under the event's documented matcher ({@link TELEMETRY_EVENT_HOOKS}), its key-path and
654
+ * field triple sourced from the provider face ({@link tapHookRegistration},
655
+ * `builtins.ts`); an event-name outside that table is the roster's inadmissibility
656
+ * finding, never a row.
653
657
  */
654
658
  export function tapHookRows(harness) {
655
659
  const deduped = new Map();
@@ -670,17 +674,16 @@ export function tapHookRows(harness) {
670
674
  }
671
675
  return [...deduped.values()]
672
676
  .sort((a, b) => compareStrings(a.event, b.event) || compareStrings(a.matcher, b.matcher))
673
- .map(({ event, matcher }) => ({
674
- kind: "hook",
675
- key: event,
676
- manifest: SETTINGS_MANIFEST,
677
- key_path: "hooks.<Event>",
678
- fields: [
679
- ["type", "command"],
680
- ["command", TAP_COMMAND],
681
- ["matcher", matcher],
682
- ],
683
- }));
677
+ .map(({ event, matcher }) => {
678
+ const { keyPath, fields } = tapHookRegistration(TAP_COMMAND, matcher);
679
+ return {
680
+ kind: "hook",
681
+ key: event,
682
+ manifest: SETTINGS_MANIFEST,
683
+ key_path: keyPath,
684
+ fields,
685
+ };
686
+ });
684
687
  }
685
688
  /**
686
689
  * The `settings` rows — the assembly's harness-level residual settings keys, each folded
@@ -762,7 +765,7 @@ export function compileDeclarations(harness, placements, extents) {
762
765
  }
763
766
  }
764
767
  return {
765
- kinds: unitKindsInPlay(allKinds).map((facts) => kindFactRow(facts, admissions)),
768
+ kinds: kindFactKindsInPlay(allKinds).map((facts) => kindFactRow(facts, admissions)),
766
769
  clauses,
767
770
  requirements: requirementRows(harness),
768
771
  assembly: assemblyFactRows(harness, allKinds),
@@ -94,7 +94,7 @@ export type Features = {
94
94
  /**
95
95
  * The host member's own **nested members** — its declared [`EmbeddedMember`]s,
96
96
  * read off the lock's own `Declarations::nested_members` rows by this member's
97
- * `kind:name` address ([`nested_members_from_rows`]), never mined from
97
+ * `kind:name` address ([`nested_members_from_rows`](crate::drift::nested_members_from_rows)), never mined from
98
98
  * [`fenced_blocks`](Features::fenced_blocks) (0018, "the projection is not the
99
99
  * database"). Empty when the lock carries no row for this member.
100
100
  */
@@ -31,7 +31,7 @@ export type RequirementRow = {
31
31
  clauses: Array<ClauseRow>;
32
32
  /**
33
33
  * The typed verifier for the behavioral remainder, when declared — a
34
- * species-tagged [`Verifier`], resolved at admissibility, never run.
34
+ * species-tagged [`Verifier`](crate::compose::Verifier), resolved at admissibility, never run.
35
35
  */
36
36
  verifier?: Verifier;
37
37
  /**
@@ -9,8 +9,8 @@
9
9
  * Every type erases at the seam, and Turing-completeness
10
10
  * stays quarantined at authoring time.
11
11
  *
12
- * The first-party Claude Code provider face — the built-in `skill`/`rule`/
13
- * `memory` kinds — lives at the `./claude-code` subpath, never here.
12
+ * The first-party Claude Code provider face — the built-in kinds — lives at
13
+ * the `./claude-code` subpath, never here.
14
14
  */
15
15
  export type { Blocks, File, Include, Mention, Mentionable, Prose, Reference, Text } from "./prose.js";
16
16
  export { blocks, file, include, mentionOf, renderText, text } from "./prose.js";
package/dist/src/index.js CHANGED
@@ -9,8 +9,8 @@
9
9
  * Every type erases at the seam, and Turing-completeness
10
10
  * stays quarantined at authoring time.
11
11
  *
12
- * The first-party Claude Code provider face — the built-in `skill`/`rule`/
13
- * `memory` kinds — lives at the `./claude-code` subpath, never here.
12
+ * The first-party Claude Code provider face — the built-in kinds — lives at
13
+ * the `./claude-code` subpath, never here.
14
14
  */
15
15
  export { blocks, file, include, mentionOf, renderText, text } from "./prose.js";
16
16
  export { bash, capability, permissionUnion } from "./needs.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtmd/temper",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
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.12",
56
- "@dtmd/temper-win32-x64": "0.0.12"
55
+ "@dtmd/temper-linux-x64": "0.0.13",
56
+ "@dtmd/temper-win32-x64": "0.0.13"
57
57
  }
58
58
  }