holonovel 2026.9.7 → 2026.9.10

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.
@@ -506,7 +506,7 @@ export declare class StateManager {
506
506
  narrative_tag?: string;
507
507
  }>;
508
508
  codex: Map<string, CodexEntry>;
509
- readonly dataFormat = "15312d5bcb533c32b852179fdbff0b8ee6e21473af3c1981f01fd7074709f6ad";
509
+ readonly dataFormat = "b2665b830a4076bc83619d7495e199f525ba72b040ec294bc5152c3c5d526d0d";
510
510
  staleData: Map<string, string>;
511
511
  corruptData: Map<string, string>;
512
512
  private restoredThisSession;
@@ -1,2 +1,2 @@
1
1
  export declare const PACKAGE_FORMAT = "2e36c2ccb9283086dd91056cd4571fd648854a4b83df4f391150e3942bcb85c8";
2
- export declare const DATA_FORMAT = "15312d5bcb533c32b852179fdbff0b8ee6e21473af3c1981f01fd7074709f6ad";
2
+ export declare const DATA_FORMAT = "b2665b830a4076bc83619d7495e199f525ba72b040ec294bc5152c3c5d526d0d";
@@ -1,5 +1,5 @@
1
1
  // Generated by scripts/lib/contract-fingerprint.ts — do not edit by hand.
2
2
  // Regenerated by build-order whenever the spec changes.
3
3
  export const PACKAGE_FORMAT = "2e36c2ccb9283086dd91056cd4571fd648854a4b83df4f391150e3942bcb85c8";
4
- export const DATA_FORMAT = "15312d5bcb533c32b852179fdbff0b8ee6e21473af3c1981f01fd7074709f6ad";
4
+ export const DATA_FORMAT = "b2665b830a4076bc83619d7495e199f525ba72b040ec294bc5152c3c5d526d0d";
5
5
  //# sourceMappingURL=contract-fingerprints.js.map
package/dist/index.js CHANGED
@@ -54,7 +54,7 @@ state.buildFingerprint.lastSpecReview = new Date().toISOString();
54
54
  // ── Server ─────────────────────────────────────────────────────────
55
55
  const server = new McpServer({
56
56
  name: "holonovel",
57
- version: "2026.09.07",
57
+ version: "2026.09.10",
58
58
  });
59
59
  // REQ-426c — MCP Apps capability negotiation: the server declares the
60
60
  // `io.modelcontextprotocol/ui` extension in its capabilities; a client that
@@ -62,22 +62,43 @@ const server = new McpServer({
62
62
  // appsNegotiated() below). REQ-426a — UI resources are served under the
63
63
  // `ui://` scheme as `text/html;profile=mcp-app`.
64
64
  server.server.registerCapabilities({ extensions: { "io.modelcontextprotocol/ui": {} } });
65
- // REQ-133 — forbidden-call audit: every tool handler is wrapped so that a
66
- // thrown `[FORBIDDEN]` records the call (badge, tool name, arguments,
67
- // violation_type: boundary) in the Novel audit log before propagating.
68
- // REQ-429 — server-wide action-discriminator surface: one tool per persisted
69
- // entity type within a twenty-eight-tool budget; every persisted type has a
70
- // list/get/info/status/knowledge action; uniform verb-noun/noun+action naming.
71
- // REQ-450 — TDQS-conformant tool definitions: every registered tool carries a
72
- // mutation-class annotation (read-only vs open-world) so callers see behavior
73
- // hints beyond the description.
65
+ const READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
66
+ const MUTATING = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
74
67
  const TOOL_ANNOTATIONS = {
75
- help: { readOnlyHint: true, openWorldHint: false },
68
+ help: READ_ONLY,
69
+ set_badge: MUTATING,
70
+ respond: MUTATING,
71
+ undo: MUTATING,
72
+ redo: MUTATING,
73
+ character: MUTATING,
74
+ npc: MUTATING,
75
+ world: MUTATING,
76
+ command: MUTATING,
77
+ combat: MUTATING,
78
+ scene: MUTATING,
79
+ countdown: MUTATING,
80
+ lore: MUTATING,
81
+ condition: MUTATING,
82
+ faction: MUTATING,
83
+ relationship: MUTATING,
84
+ vow: MUTATING,
85
+ fate: MUTATING,
86
+ ironsworn: MUTATING,
87
+ forged: MUTATING,
88
+ story: MUTATING,
89
+ note: MUTATING,
90
+ session: MUTATING,
91
+ adventure: MUTATING,
92
+ novel: MUTATING,
93
+ ruleset: MUTATING,
94
+ codex: MUTATING,
95
+ synthesis: MUTATING,
76
96
  };
77
- const DEFAULT_TOOL_ANNOTATIONS = { readOnlyHint: false, openWorldHint: true };
78
97
  const _registerTool = server.registerTool.bind(server);
79
98
  server.registerTool = ((name, config, handler) => {
80
- const annotations = TOOL_ANNOTATIONS[name] ?? DEFAULT_TOOL_ANNOTATIONS;
99
+ const annotations = config.annotations ?? TOOL_ANNOTATIONS[name];
100
+ if (!annotations)
101
+ throw new Error(`Tool '${name}' has no REQ-450 mutation-class annotation; add it to TOOL_ANNOTATIONS.`);
81
102
  return _registerTool(name, { ...config, annotations }, withForbiddenAudit(handler, name));
82
103
  });
83
104
  // ── §5.12 Narrative Architecture helpers (REQ-335 through REQ-366) ──
@@ -731,11 +752,18 @@ function gatedRulesetTool(slug, schema, toolName) {
731
752
  for (const slug of rulesets.installedSlugs()) {
732
753
  for (const schema of rulesets.toolSchemas(slug)) {
733
754
  const toolName = `${slug}_${schema.name}`;
755
+ // REQ-450 — ruleset-derived tools read indexed data (lookup/search/info:
756
+ // idempotent) or generate content (roll/table): neither mutates Novel
757
+ // state, so destructiveHint is false for every ruleset-derived kind.
758
+ const annotations = (schema.kind === "lookup" || schema.kind === "search" || schema.kind === "info")
759
+ ? { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
760
+ : { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
734
761
  try {
735
762
  server.registerTool(toolName, {
736
763
  title: schema.title ?? schema.name,
737
764
  description: `${schema.description ?? ""} (ruleset: ${slug})`,
738
765
  inputSchema: jsonSchemaToZod(schema.inputSchema),
766
+ annotations,
739
767
  }, gatedRulesetTool(slug, schema, toolName));
740
768
  }
741
769
  catch (e) {
@@ -1651,6 +1679,8 @@ server.registerTool("redo", {
1651
1679
  state.redo(novel, getBadge());
1652
1680
  return ok("Redo successful.");
1653
1681
  });
1682
+ // REQ-067 — Help and tool discovery: badge-filtered categorized task map with
1683
+ // query search and Novel-scoped category reassignment (T62/T118).
1654
1684
  server.registerTool("help", {
1655
1685
  title: "Help and Tool Discovery",
1656
1686
  description: "Show the available tools grouped by category (badge-filtered) or reassign a tool's category. Use when: the caller needs to discover tools, find one by keyword, or override a tool's category for a session. Do NOT use when: reading the current badge's guidance — use the badge_briefing prompt.",
@@ -1727,16 +1757,35 @@ server.registerTool("help", {
1727
1757
  return raw(top.map(m => `**${m.name}** — ${m.description}\nExample: ${m.example}`).join("\n\n"));
1728
1758
  }
1729
1759
  const builderCategories = BUILDER_CATEGORIES;
1760
+ // REQ-067b — GM category reassignment: an overridden tool leaves its
1761
+ // builder-assigned category and renders under the user-defined category.
1762
+ // Player badge always reflects builder-assigned categories.
1763
+ const overrides = novel?.help_category_overrides ?? {};
1764
+ const overriddenTools = new Set(Object.keys(overrides));
1730
1765
  let result = "## Holonovel MCP Server\n\n### Tool Categories\n\n";
1731
1766
  for (const [cat, tools] of Object.entries(builderCategories)) {
1732
1767
  let displayTools = [...tools];
1733
- if (!isGM) {
1734
- displayTools = tools.filter(t => !GMToolsSet.has(t) && !(t === "command" && novel?.ruleset));
1768
+ if (isGM) {
1769
+ displayTools = displayTools.filter(t => !overriddenTools.has(t));
1770
+ }
1771
+ else {
1772
+ displayTools = displayTools.filter(t => !GMToolsSet.has(t) && !(t === "command" && novel?.ruleset));
1735
1773
  }
1736
1774
  if (displayTools.length > 0) {
1737
1775
  result += `**${cat}:** ${displayTools.join(", ")}\n`;
1738
1776
  }
1739
1777
  }
1778
+ if (isGM) {
1779
+ const userCategories = new Map();
1780
+ for (const [tool, cat] of Object.entries(overrides)) {
1781
+ if (!userCategories.has(cat))
1782
+ userCategories.set(cat, []);
1783
+ userCategories.get(cat).push(tool);
1784
+ }
1785
+ for (const [cat, tools] of userCategories) {
1786
+ result += `**${cat}:** ${tools.join(", ")}\n`;
1787
+ }
1788
+ }
1740
1789
  result += "\nUse the intro prompt to get started, or badge_briefing for current badge guidance.";
1741
1790
  // Add world-model hint if populated
1742
1791
  if (novel && novel.world.rooms.size > 0) {