@vibe-hero/server 0.1.0

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.
Files changed (150) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +151 -0
  3. package/dist/catalog/bundled/claude-code/.gitkeep +0 -0
  4. package/dist/catalog/bundled/claude-code/context-management.yaml +302 -0
  5. package/dist/catalog/bundled/claude-code/planning.yaml +313 -0
  6. package/dist/catalog/bundled/claude-code/subagents.yaml +357 -0
  7. package/dist/catalog/bundled/general/.gitkeep +0 -0
  8. package/dist/catalog/bundled/general/_placeholder.yaml +39 -0
  9. package/dist/catalog/bundled/general/task-decomposition.yaml +390 -0
  10. package/dist/catalog/bundled/index.d.ts +39 -0
  11. package/dist/catalog/bundled/index.d.ts.map +1 -0
  12. package/dist/catalog/bundled/index.js +41 -0
  13. package/dist/catalog/bundled/index.js.map +1 -0
  14. package/dist/catalog/fetcher.d.ts +201 -0
  15. package/dist/catalog/fetcher.d.ts.map +1 -0
  16. package/dist/catalog/fetcher.js +452 -0
  17. package/dist/catalog/fetcher.js.map +1 -0
  18. package/dist/catalog/loader.d.ts +165 -0
  19. package/dist/catalog/loader.d.ts.map +1 -0
  20. package/dist/catalog/loader.js +241 -0
  21. package/dist/catalog/loader.js.map +1 -0
  22. package/dist/catalog/resolve.d.ts +85 -0
  23. package/dist/catalog/resolve.d.ts.map +1 -0
  24. package/dist/catalog/resolve.js +103 -0
  25. package/dist/catalog/resolve.js.map +1 -0
  26. package/dist/cli/getOffer.d.ts +38 -0
  27. package/dist/cli/getOffer.d.ts.map +1 -0
  28. package/dist/cli/getOffer.js +150 -0
  29. package/dist/cli/getOffer.js.map +1 -0
  30. package/dist/cli/index.d.ts +46 -0
  31. package/dist/cli/index.d.ts.map +1 -0
  32. package/dist/cli/index.js +88 -0
  33. package/dist/cli/index.js.map +1 -0
  34. package/dist/config.d.ts +34 -0
  35. package/dist/config.d.ts.map +1 -0
  36. package/dist/config.js +63 -0
  37. package/dist/config.js.map +1 -0
  38. package/dist/engine/elo.d.ts +76 -0
  39. package/dist/engine/elo.d.ts.map +1 -0
  40. package/dist/engine/elo.js +79 -0
  41. package/dist/engine/elo.js.map +1 -0
  42. package/dist/engine/graduation.d.ts +108 -0
  43. package/dist/engine/graduation.d.ts.map +1 -0
  44. package/dist/engine/graduation.js +161 -0
  45. package/dist/engine/graduation.js.map +1 -0
  46. package/dist/engine/lapse.d.ts +80 -0
  47. package/dist/engine/lapse.d.ts.map +1 -0
  48. package/dist/engine/lapse.js +125 -0
  49. package/dist/engine/lapse.js.map +1 -0
  50. package/dist/engine/selection.d.ts +84 -0
  51. package/dist/engine/selection.d.ts.map +1 -0
  52. package/dist/engine/selection.js +119 -0
  53. package/dist/engine/selection.js.map +1 -0
  54. package/dist/grading/deterministic.d.ts +102 -0
  55. package/dist/grading/deterministic.d.ts.map +1 -0
  56. package/dist/grading/deterministic.js +118 -0
  57. package/dist/grading/deterministic.js.map +1 -0
  58. package/dist/grading/freeform.d.ts +64 -0
  59. package/dist/grading/freeform.d.ts.map +1 -0
  60. package/dist/grading/freeform.js +85 -0
  61. package/dist/grading/freeform.js.map +1 -0
  62. package/dist/index.d.ts +52 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +91 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/observation/hookEvents.d.ts +113 -0
  67. package/dist/observation/hookEvents.d.ts.map +1 -0
  68. package/dist/observation/hookEvents.js +170 -0
  69. package/dist/observation/hookEvents.js.map +1 -0
  70. package/dist/observation/offers.d.ts +215 -0
  71. package/dist/observation/offers.d.ts.map +1 -0
  72. package/dist/observation/offers.js +327 -0
  73. package/dist/observation/offers.js.map +1 -0
  74. package/dist/observation/source.d.ts +133 -0
  75. package/dist/observation/source.d.ts.map +1 -0
  76. package/dist/observation/source.js +105 -0
  77. package/dist/observation/source.js.map +1 -0
  78. package/dist/profile/migrate.d.ts +122 -0
  79. package/dist/profile/migrate.d.ts.map +1 -0
  80. package/dist/profile/migrate.js +147 -0
  81. package/dist/profile/migrate.js.map +1 -0
  82. package/dist/profile/store.d.ts +84 -0
  83. package/dist/profile/store.d.ts.map +1 -0
  84. package/dist/profile/store.js +267 -0
  85. package/dist/profile/store.js.map +1 -0
  86. package/dist/schemas/common.d.ts +95 -0
  87. package/dist/schemas/common.d.ts.map +1 -0
  88. package/dist/schemas/common.js +106 -0
  89. package/dist/schemas/common.js.map +1 -0
  90. package/dist/schemas/content.d.ts +828 -0
  91. package/dist/schemas/content.d.ts.map +1 -0
  92. package/dist/schemas/content.js +219 -0
  93. package/dist/schemas/content.js.map +1 -0
  94. package/dist/schemas/profile.d.ts +599 -0
  95. package/dist/schemas/profile.d.ts.map +1 -0
  96. package/dist/schemas/profile.js +177 -0
  97. package/dist/schemas/profile.js.map +1 -0
  98. package/dist/schemas/tools.d.ts +1581 -0
  99. package/dist/schemas/tools.d.ts.map +1 -0
  100. package/dist/schemas/tools.js +286 -0
  101. package/dist/schemas/tools.js.map +1 -0
  102. package/dist/tools/config.d.ts +51 -0
  103. package/dist/tools/config.d.ts.map +1 -0
  104. package/dist/tools/config.js +104 -0
  105. package/dist/tools/config.js.map +1 -0
  106. package/dist/tools/gate.d.ts +50 -0
  107. package/dist/tools/gate.d.ts.map +1 -0
  108. package/dist/tools/gate.js +67 -0
  109. package/dist/tools/gate.js.map +1 -0
  110. package/dist/tools/guidance.d.ts +36 -0
  111. package/dist/tools/guidance.d.ts.map +1 -0
  112. package/dist/tools/guidance.js +117 -0
  113. package/dist/tools/guidance.js.map +1 -0
  114. package/dist/tools/listTopics.d.ts +55 -0
  115. package/dist/tools/listTopics.d.ts.map +1 -0
  116. package/dist/tools/listTopics.js +78 -0
  117. package/dist/tools/listTopics.js.map +1 -0
  118. package/dist/tools/offers.d.ts +60 -0
  119. package/dist/tools/offers.d.ts.map +1 -0
  120. package/dist/tools/offers.js +152 -0
  121. package/dist/tools/offers.js.map +1 -0
  122. package/dist/tools/placeholders.d.ts +27 -0
  123. package/dist/tools/placeholders.d.ts.map +1 -0
  124. package/dist/tools/placeholders.js +49 -0
  125. package/dist/tools/placeholders.js.map +1 -0
  126. package/dist/tools/recordObservation.d.ts +52 -0
  127. package/dist/tools/recordObservation.d.ts.map +1 -0
  128. package/dist/tools/recordObservation.js +87 -0
  129. package/dist/tools/recordObservation.js.map +1 -0
  130. package/dist/tools/startQuiz.d.ts +82 -0
  131. package/dist/tools/startQuiz.d.ts.map +1 -0
  132. package/dist/tools/startQuiz.js +180 -0
  133. package/dist/tools/startQuiz.js.map +1 -0
  134. package/dist/tools/status.d.ts +59 -0
  135. package/dist/tools/status.d.ts.map +1 -0
  136. package/dist/tools/status.js +133 -0
  137. package/dist/tools/status.js.map +1 -0
  138. package/dist/tools/submitAnswer.d.ts +156 -0
  139. package/dist/tools/submitAnswer.d.ts.map +1 -0
  140. package/dist/tools/submitAnswer.js +402 -0
  141. package/dist/tools/submitAnswer.js.map +1 -0
  142. package/dist/tools/types.d.ts +82 -0
  143. package/dist/tools/types.d.ts.map +1 -0
  144. package/dist/tools/types.js +48 -0
  145. package/dist/tools/types.js.map +1 -0
  146. package/dist/tools/us2/standing.d.ts +111 -0
  147. package/dist/tools/us2/standing.d.ts.map +1 -0
  148. package/dist/tools/us2/standing.js +143 -0
  149. package/dist/tools/us2/standing.js.map +1 -0
  150. package/package.json +62 -0
@@ -0,0 +1,119 @@
1
+ /**
2
+ * @file PURE item-selection engine (T011, OD-005 / research.md).
3
+ *
4
+ * Chooses the items for one quiz session for a single (topic × class) given the
5
+ * learner's current ability θ, the topic's candidate items (with FIXED authored
6
+ * difficulties), the target tier's next boundary, and a recent-item exclusion
7
+ * set. Difficulty-targets at the promotion bar so "passing a set" ≈ "ready to
8
+ * graduate", while an information-weighted window avoids always serving the
9
+ * hardest item.
10
+ *
11
+ * This module is IO-FREE and time-free (E5) and never mutates item difficulty
12
+ * (E3): items are read-only inputs.
13
+ *
14
+ * Selection rule (OD-005):
15
+ * target = min(θ + targetOffset, nextBoundary + hysteresisMargin)
16
+ * pool = items with |difficulty − target| ≤ selectWindow, excluding recents
17
+ * weight = p · (1 − p), p = expectedScore(θ, difficulty) // Fisher info ∝ p(1−p)
18
+ * pick `length` items by weight; ALWAYS include one "anchor" item within
19
+ * ±anchorWindow of θ if one exists in the pool.
20
+ *
21
+ * Determinism: this function NEVER calls `Math.random`. Sampling is deterministic
22
+ * given the inputs. By default it picks the top-weighted items (ties broken by
23
+ * item id for total order). Callers that want stochastic-but-reproducible
24
+ * sampling may inject a seeded {@link Rng}; the same seed ⇒ the same output.
25
+ *
26
+ * Source of truth: specs/001-vibe-hero-mvp/research.md (OD-005);
27
+ * constants in ../config.ts (ASSESSMENT_CONFIG).
28
+ */
29
+ import { ASSESSMENT_CONFIG } from "../config.js";
30
+ import { expectedScore } from "./elo.js";
31
+ /**
32
+ * Total ordering used to break ties deterministically: descending weight, then
33
+ * ascending item id. Guarantees a single canonical order for equal weights so
34
+ * the default (RNG-free) path is fully reproducible.
35
+ */
36
+ const byWeightThenId = (a, b) => b.weight - a.weight || (a.item.id < b.item.id ? -1 : a.item.id > b.item.id ? 1 : 0);
37
+ /**
38
+ * Draw `count` items from `pool` proportional to weight using an injected RNG,
39
+ * without replacement. Deterministic for a given RNG sequence. Falls back to
40
+ * uniform choice if all remaining weights are zero.
41
+ */
42
+ const weightedSampleWithoutReplacement = (pool, count, rng) => {
43
+ const remaining = [...pool];
44
+ const picked = [];
45
+ while (picked.length < count && remaining.length > 0) {
46
+ const total = remaining.reduce((sum, c) => sum + c.weight, 0);
47
+ let index = 0;
48
+ if (total > 0) {
49
+ const threshold = rng() * total;
50
+ let acc = 0;
51
+ for (let i = 0; i < remaining.length; i++) {
52
+ acc += remaining[i].weight;
53
+ if (acc >= threshold) {
54
+ index = i;
55
+ break;
56
+ }
57
+ }
58
+ }
59
+ else {
60
+ // All-zero weights: uniform pick over the remaining pool.
61
+ index = Math.min(remaining.length - 1, Math.floor(rng() * remaining.length));
62
+ }
63
+ picked.push(remaining[index].item);
64
+ remaining.splice(index, 1);
65
+ }
66
+ return picked;
67
+ };
68
+ /**
69
+ * Select 3–5 items for a quiz session (PURE; see file header for the rule).
70
+ *
71
+ * Items are read-only inputs; their difficulty is never mutated (E3). No clock,
72
+ * file, or network access (E5). Output is deterministic for identical inputs
73
+ * (and identical injected {@link Rng} sequence, if any).
74
+ *
75
+ * Behaviour summary:
76
+ * - Builds `target = min(θ + targetOffset, nextBoundary + hysteresisMargin)`.
77
+ * - Pool = candidates within ±`selectWindow` of `target`, excluding
78
+ * `recentItemIds`.
79
+ * - Weights each by `p·(1−p)` (`p = expectedScore(θ, difficulty)`).
80
+ * - Guarantees one anchor item within ±`anchorWindow` of θ (the most
81
+ * informative such item) is included when one exists in the pool.
82
+ * - Returns at most `length` items; fewer if the pool is smaller.
83
+ *
84
+ * @returns The chosen items, length ≤ `length`, anchor-first when an anchor
85
+ * exists, then the remaining picks.
86
+ */
87
+ export const selectItems = (params) => {
88
+ const { ability, candidates, nextBoundary, recentItemIds = [], length = ASSESSMENT_CONFIG.defaultQuizLength, rng, } = params;
89
+ if (length <= 0)
90
+ return [];
91
+ const { targetOffset, hysteresisMargin, selectWindow, anchorWindow } = ASSESSMENT_CONFIG;
92
+ const target = Math.min(ability + targetOffset, nextBoundary + hysteresisMargin);
93
+ const excluded = new Set(recentItemIds);
94
+ // Eligible pool: within ±selectWindow of target, not recently served.
95
+ const pool = candidates
96
+ .filter((item) => !excluded.has(item.id) &&
97
+ Math.abs(item.difficulty - target) <= selectWindow)
98
+ .map((item) => {
99
+ const p = expectedScore(ability, item.difficulty);
100
+ return { item, weight: p * (1 - p) };
101
+ });
102
+ if (pool.length === 0)
103
+ return [];
104
+ // Pick the anchor: most-informative item within ±anchorWindow of θ.
105
+ // Deterministic ordering guarantees a stable choice on ties.
106
+ const anchorPool = pool
107
+ .filter((c) => Math.abs(c.item.difficulty - ability) <= anchorWindow)
108
+ .sort(byWeightThenId);
109
+ const anchor = anchorPool[0]?.item;
110
+ const remainingNeeded = anchor ? length - 1 : length;
111
+ const rest = pool.filter((c) => c.item.id !== anchor?.id);
112
+ const chosenRest = remainingNeeded <= 0
113
+ ? []
114
+ : rng
115
+ ? weightedSampleWithoutReplacement(rest, remainingNeeded, rng)
116
+ : [...rest].sort(byWeightThenId).slice(0, remainingNeeded).map((c) => c.item);
117
+ return anchor ? [anchor, ...chosenRest] : chosenRest;
118
+ };
119
+ //# sourceMappingURL=selection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selection.js","sourceRoot":"","sources":["../../src/engine/selection.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @file PURE deterministic grading (T030, FR-011, SC-004).
3
+ *
4
+ * Grades the two deterministic question types — `multiple_choice` and
5
+ * `short_answer` — entirely in-engine, with NO IO, NO clock, and NO host-agent
6
+ * involvement. Grading is objective and **reproducible**: the same item + same
7
+ * answer always yields the same `score` and `Grade` (SC-004). Free-form grading
8
+ * is the host-agent verdict handshake and lives elsewhere (T048); this module
9
+ * never touches it.
10
+ *
11
+ * Both graders return a continuous `score ∈ {0, 1}` (deterministic items are
12
+ * all-or-nothing — there is no partial credit for MC/short-answer), which the
13
+ * Elo engine consumes, plus enough context for the caller to build the
14
+ * `submit_answer` result (`correctChoiceId` for MC). The binary {@link Grade}
15
+ * projection is derived from the score via {@link toGrade} against the item's
16
+ * pass threshold.
17
+ *
18
+ * Invariants (mirrors engine/elo.ts, engine/selection.ts):
19
+ * - PURE: no `Date`, no `fs`, no `Math.random`, no network.
20
+ * - Item difficulty is read-only input — never mutated here (E3).
21
+ * - Determinism: identical inputs ⇒ identical outputs (SC-004).
22
+ *
23
+ * Source of truth: specs/001-vibe-hero-mvp/contracts/mcp-tools.md
24
+ * (`submit_answer` deterministic path), spec.md FR-011 / SC-004,
25
+ * data-model.md (Grade = binary projection of a continuous score).
26
+ */
27
+ import type { ContentItem } from "../schemas/content.js";
28
+ import type { Grade } from "../schemas/common.js";
29
+ /** Result of grading a `multiple_choice` item. */
30
+ export interface MultipleChoiceGrade {
31
+ /** All-or-nothing score: `1` if the chosen id matches the key, else `0`. */
32
+ readonly score: 0 | 1;
33
+ /** The authored correct choice id (for the `submit_answer` `correctAnswer`). */
34
+ readonly correctChoiceId: string;
35
+ }
36
+ /** Result of grading a `short_answer` item. */
37
+ export interface ShortAnswerGrade {
38
+ /** All-or-nothing score: `1` if the normalized text matches a key, else `0`. */
39
+ readonly score: 0 | 1;
40
+ }
41
+ /**
42
+ * Apply a `short_answer` answer key's `normalize` directive to a string before
43
+ * comparison. Pure and idempotent:
44
+ * - `"lower"` → lowercase only.
45
+ * - `"trim"` → trim leading/trailing whitespace only.
46
+ * - `"both"` → trim then lowercase.
47
+ * - omitted → exact comparison (no normalization).
48
+ *
49
+ * @param value - The raw string (user text or an authored keyword).
50
+ * @param normalize - The key's normalization mode, if any.
51
+ * @returns The normalized string.
52
+ */
53
+ export declare const applyNormalize: (value: string, normalize: "lower" | "trim" | "both" | undefined) => string;
54
+ /**
55
+ * Grade a `multiple_choice` item (PURE, reproducible).
56
+ *
57
+ * Compares the submitted `choiceId` to the item's `answerKey.correctChoiceId`.
58
+ * The score is `1` for an exact id match, `0` otherwise; the correct choice id
59
+ * is returned regardless so the caller can surface it after grading.
60
+ *
61
+ * @param item - The catalog item being graded (must be `multiple_choice` with a
62
+ * `choice` answer key — guaranteed by {@link ContentItem} validation).
63
+ * @param choiceId - The choice id the user selected (may be `undefined` if the
64
+ * user submitted no/blank choice, which scores `0`).
65
+ * @returns The score and the authored correct choice id.
66
+ * @throws {Error} if the item is not a `multiple_choice` item with a `choice`
67
+ * answer key (a programming error — the catalog schema forbids it).
68
+ */
69
+ export declare const gradeMultipleChoice: (item: ContentItem, choiceId: string | undefined) => MultipleChoiceGrade;
70
+ /**
71
+ * Grade a `short_answer` item (PURE, reproducible).
72
+ *
73
+ * Applies the key's `normalize` directive to BOTH the user's text and each
74
+ * accepted keyword, then scores `1` if the normalized user text equals any
75
+ * accepted keyword (`anyOf`), else `0`. Normalizing both sides keeps the match
76
+ * symmetric (the same rule applied to authored keys and user input).
77
+ *
78
+ * @param item - The catalog item being graded (must be `short_answer` with a
79
+ * `keyword` answer key — guaranteed by {@link ContentItem} validation).
80
+ * @param text - The user's free-text answer (may be `undefined` if the user
81
+ * submitted no/blank text, which scores `0`).
82
+ * @returns The all-or-nothing score.
83
+ * @throws {Error} if the item is not a `short_answer` item with a `keyword`
84
+ * answer key (a programming error — the catalog schema forbids it).
85
+ */
86
+ export declare const gradeShortAnswer: (item: ContentItem, text: string | undefined) => ShortAnswerGrade;
87
+ /**
88
+ * Project a continuous `score ∈ [0, 1]` to the binary {@link Grade} persisted in
89
+ * history: `score ≥ passThreshold ⇒ "correct"`, else `"incorrect"`
90
+ * (data-model.md / research.md). The same projection serves deterministic
91
+ * (score ∈ {0, 1}) and free-form (fractional score) grades, keeping one
92
+ * definition of "correct".
93
+ *
94
+ * For deterministic items the natural threshold is any value in `(0, 1]` (e.g.
95
+ * the default `1`), so a `0` is always incorrect and a `1` always correct.
96
+ *
97
+ * @param score - The continuous outcome in `[0, 1]`.
98
+ * @param passThreshold - The fraction at/above which the grade is `"correct"`.
99
+ * @returns The derived binary grade.
100
+ */
101
+ export declare const toGrade: (score: number, passThreshold: number) => Grade;
102
+ //# sourceMappingURL=deterministic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deterministic.d.ts","sourceRoot":"","sources":["../../src/grading/deterministic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAElD,kDAAkD;AAClD,MAAM,WAAW,mBAAmB;IAClC,4EAA4E;IAC5E,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC;IACtB,gFAAgF;IAChF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED,+CAA+C;AAC/C,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC;CACvB;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc,GACzB,OAAO,MAAM,EACb,WAAW,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,KAC/C,MAWF,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,mBAAmB,GAC9B,MAAM,WAAW,EACjB,UAAU,MAAM,GAAG,SAAS,KAC3B,mBAWF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,gBAAgB,GAC3B,MAAM,WAAW,EACjB,MAAM,MAAM,GAAG,SAAS,KACvB,gBAcF,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,OAAO,GAAI,OAAO,MAAM,EAAE,eAAe,MAAM,KAAG,KACb,CAAC"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @file PURE deterministic grading (T030, FR-011, SC-004).
3
+ *
4
+ * Grades the two deterministic question types — `multiple_choice` and
5
+ * `short_answer` — entirely in-engine, with NO IO, NO clock, and NO host-agent
6
+ * involvement. Grading is objective and **reproducible**: the same item + same
7
+ * answer always yields the same `score` and `Grade` (SC-004). Free-form grading
8
+ * is the host-agent verdict handshake and lives elsewhere (T048); this module
9
+ * never touches it.
10
+ *
11
+ * Both graders return a continuous `score ∈ {0, 1}` (deterministic items are
12
+ * all-or-nothing — there is no partial credit for MC/short-answer), which the
13
+ * Elo engine consumes, plus enough context for the caller to build the
14
+ * `submit_answer` result (`correctChoiceId` for MC). The binary {@link Grade}
15
+ * projection is derived from the score via {@link toGrade} against the item's
16
+ * pass threshold.
17
+ *
18
+ * Invariants (mirrors engine/elo.ts, engine/selection.ts):
19
+ * - PURE: no `Date`, no `fs`, no `Math.random`, no network.
20
+ * - Item difficulty is read-only input — never mutated here (E3).
21
+ * - Determinism: identical inputs ⇒ identical outputs (SC-004).
22
+ *
23
+ * Source of truth: specs/001-vibe-hero-mvp/contracts/mcp-tools.md
24
+ * (`submit_answer` deterministic path), spec.md FR-011 / SC-004,
25
+ * data-model.md (Grade = binary projection of a continuous score).
26
+ */
27
+ /**
28
+ * Apply a `short_answer` answer key's `normalize` directive to a string before
29
+ * comparison. Pure and idempotent:
30
+ * - `"lower"` → lowercase only.
31
+ * - `"trim"` → trim leading/trailing whitespace only.
32
+ * - `"both"` → trim then lowercase.
33
+ * - omitted → exact comparison (no normalization).
34
+ *
35
+ * @param value - The raw string (user text or an authored keyword).
36
+ * @param normalize - The key's normalization mode, if any.
37
+ * @returns The normalized string.
38
+ */
39
+ export const applyNormalize = (value, normalize) => {
40
+ switch (normalize) {
41
+ case "lower":
42
+ return value.toLowerCase();
43
+ case "trim":
44
+ return value.trim();
45
+ case "both":
46
+ return value.trim().toLowerCase();
47
+ case undefined:
48
+ return value;
49
+ }
50
+ };
51
+ /**
52
+ * Grade a `multiple_choice` item (PURE, reproducible).
53
+ *
54
+ * Compares the submitted `choiceId` to the item's `answerKey.correctChoiceId`.
55
+ * The score is `1` for an exact id match, `0` otherwise; the correct choice id
56
+ * is returned regardless so the caller can surface it after grading.
57
+ *
58
+ * @param item - The catalog item being graded (must be `multiple_choice` with a
59
+ * `choice` answer key — guaranteed by {@link ContentItem} validation).
60
+ * @param choiceId - The choice id the user selected (may be `undefined` if the
61
+ * user submitted no/blank choice, which scores `0`).
62
+ * @returns The score and the authored correct choice id.
63
+ * @throws {Error} if the item is not a `multiple_choice` item with a `choice`
64
+ * answer key (a programming error — the catalog schema forbids it).
65
+ */
66
+ export const gradeMultipleChoice = (item, choiceId) => {
67
+ if (item.type !== "multiple_choice" || item.answerKey?.kind !== "choice") {
68
+ throw new Error(`gradeMultipleChoice: item ${JSON.stringify(item.id)} is not a multiple_choice item with a choice answer key`);
69
+ }
70
+ const correctChoiceId = item.answerKey.correctChoiceId;
71
+ return {
72
+ score: choiceId === correctChoiceId ? 1 : 0,
73
+ correctChoiceId,
74
+ };
75
+ };
76
+ /**
77
+ * Grade a `short_answer` item (PURE, reproducible).
78
+ *
79
+ * Applies the key's `normalize` directive to BOTH the user's text and each
80
+ * accepted keyword, then scores `1` if the normalized user text equals any
81
+ * accepted keyword (`anyOf`), else `0`. Normalizing both sides keeps the match
82
+ * symmetric (the same rule applied to authored keys and user input).
83
+ *
84
+ * @param item - The catalog item being graded (must be `short_answer` with a
85
+ * `keyword` answer key — guaranteed by {@link ContentItem} validation).
86
+ * @param text - The user's free-text answer (may be `undefined` if the user
87
+ * submitted no/blank text, which scores `0`).
88
+ * @returns The all-or-nothing score.
89
+ * @throws {Error} if the item is not a `short_answer` item with a `keyword`
90
+ * answer key (a programming error — the catalog schema forbids it).
91
+ */
92
+ export const gradeShortAnswer = (item, text) => {
93
+ if (item.type !== "short_answer" || item.answerKey?.kind !== "keyword") {
94
+ throw new Error(`gradeShortAnswer: item ${JSON.stringify(item.id)} is not a short_answer item with a keyword answer key`);
95
+ }
96
+ if (text === undefined)
97
+ return { score: 0 };
98
+ const { anyOf, normalize } = item.answerKey;
99
+ const normalizedText = applyNormalize(text, normalize);
100
+ const matches = anyOf.some((keyword) => applyNormalize(keyword, normalize) === normalizedText);
101
+ return { score: matches ? 1 : 0 };
102
+ };
103
+ /**
104
+ * Project a continuous `score ∈ [0, 1]` to the binary {@link Grade} persisted in
105
+ * history: `score ≥ passThreshold ⇒ "correct"`, else `"incorrect"`
106
+ * (data-model.md / research.md). The same projection serves deterministic
107
+ * (score ∈ {0, 1}) and free-form (fractional score) grades, keeping one
108
+ * definition of "correct".
109
+ *
110
+ * For deterministic items the natural threshold is any value in `(0, 1]` (e.g.
111
+ * the default `1`), so a `0` is always incorrect and a `1` always correct.
112
+ *
113
+ * @param score - The continuous outcome in `[0, 1]`.
114
+ * @param passThreshold - The fraction at/above which the grade is `"correct"`.
115
+ * @returns The derived binary grade.
116
+ */
117
+ export const toGrade = (score, passThreshold) => score >= passThreshold ? "correct" : "incorrect";
118
+ //# sourceMappingURL=deterministic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deterministic.js","sourceRoot":"","sources":["../../src/grading/deterministic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAmBH;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAC5B,KAAa,EACb,SAAgD,EACxC,EAAE;IACV,QAAQ,SAAS,EAAE,CAAC;QAClB,KAAK,OAAO;YACV,OAAO,KAAK,CAAC,WAAW,EAAE,CAAC;QAC7B,KAAK,MAAM;YACT,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;QACtB,KAAK,MAAM;YACT,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACpC,KAAK,SAAS;YACZ,OAAO,KAAK,CAAC;IACjB,CAAC;AACH,CAAC,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,IAAiB,EACjB,QAA4B,EACP,EAAE;IACvB,IAAI,IAAI,CAAC,IAAI,KAAK,iBAAiB,IAAI,IAAI,CAAC,SAAS,EAAE,IAAI,KAAK,QAAQ,EAAE,CAAC;QACzE,MAAM,IAAI,KAAK,CACb,6BAA6B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,yDAAyD,CAC9G,CAAC;IACJ,CAAC;IACD,MAAM,eAAe,GAAG,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC;IACvD,OAAO;QACL,KAAK,EAAE,QAAQ,KAAK,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3C,eAAe;KAChB,CAAC;AACJ,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAC9B,IAAiB,EACjB,IAAwB,EACN,EAAE;IACpB,IAAI,IAAI,CAAC,IAAI,KAAK,cAAc,IAAI,IAAI,CAAC,SAAS,EAAE,IAAI,KAAK,SAAS,EAAE,CAAC;QACvE,MAAM,IAAI,KAAK,CACb,0BAA0B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,uDAAuD,CACzG,CAAC;IACJ,CAAC;IACD,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;IAE5C,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC;IAC5C,MAAM,cAAc,GAAG,cAAc,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACvD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CACxB,CAAC,OAAO,EAAE,EAAE,CAAC,cAAc,CAAC,OAAO,EAAE,SAAS,CAAC,KAAK,cAAc,CACnE,CAAC;IACF,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACpC,CAAC,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,KAAa,EAAE,aAAqB,EAAS,EAAE,CACrE,KAAK,IAAI,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @file PURE free-form grading (T048, FR-012/013, US-4).
3
+ *
4
+ * Scores a free-form item from the host agent's **per-criterion** verdict against
5
+ * the item's MCP-supplied {@link Rubric}. The score is the FRACTION of rubric
6
+ * criteria the agent marked `met`; the binary {@link Grade} is the projection of
7
+ * that score against the rubric's `passThreshold` (default
8
+ * {@link ASSESSMENT_CONFIG.freeFormPassThreshold} = 0.6) via the shared
9
+ * {@link toGrade} (one definition of "correct" across deterministic + free-form).
10
+ *
11
+ * Anti-gaming (critique E2, FR-013): the verdict MUST be a per-criterion array
12
+ * that aligns to the rubric's criteria ids. This module REJECTS a bare boolean,
13
+ * a missing/empty criteria array, an unknown criterion id, or a verdict that
14
+ * does not cover every rubric criterion — a lazy single self-pass is
15
+ * non-conformant and never silently scored. The host agent cannot invent its own
16
+ * criteria: only ids present in the authoritative rubric are accepted.
17
+ *
18
+ * Invariants (mirrors grading/deterministic.ts, engine/elo.ts):
19
+ * - PURE: no `Date`, no `fs`, no `Math.random`, no network.
20
+ * - The rubric is read-only input — never mutated here.
21
+ * - Determinism: identical verdict + rubric ⇒ identical score + grade.
22
+ *
23
+ * Source of truth: specs/001-vibe-hero-mvp/contracts/mcp-tools.md
24
+ * (`submit_answer` free-form path), spec.md FR-012 / FR-013, research.md
25
+ * (OD-002 — free-form IN v1, `freeFormPassThreshold` 0.6), data-model.md
26
+ * (Grade = binary projection of a continuous score).
27
+ */
28
+ import type { Grade } from "../schemas/common.js";
29
+ import type { Rubric } from "../schemas/content.js";
30
+ import type { FreeFormVerdict } from "../schemas/tools.js";
31
+ /** The outcome of scoring a free-form verdict against its rubric. */
32
+ export interface FreeFormGrade {
33
+ /** Fraction of rubric criteria the agent marked `met`, in `[0, 1]`. */
34
+ readonly score: number;
35
+ /** Binary projection of `score` against the rubric's pass threshold. */
36
+ readonly grade: Grade;
37
+ }
38
+ /**
39
+ * Score a free-form item from the host agent's per-criterion verdict (PURE).
40
+ *
41
+ * The score is `metCount / rubric.criteria.length` — the fraction of the
42
+ * authoritative rubric criteria the agent reported as met. The grade derives
43
+ * from that score against the rubric's `passThreshold` (falling back to
44
+ * {@link ASSESSMENT_CONFIG.freeFormPassThreshold} when the rubric omits one).
45
+ *
46
+ * The verdict is validated for SHAPE before scoring (anti-gaming, FR-013):
47
+ * - it MUST carry a non-empty `criteria` array (a bare boolean / missing
48
+ * criteria is rejected — the SDK union also rejects it, this is defence in
49
+ * depth so a direct caller can't bypass the structure);
50
+ * - every verdict criterion id MUST reference a real rubric criterion id (the
51
+ * agent cannot invent criteria);
52
+ * - no rubric criterion id may be duplicated in the verdict (an id is reported
53
+ * once);
54
+ * - EVERY rubric criterion MUST be covered by the verdict (the agent cannot
55
+ * silently skip a criterion to inflate the fraction).
56
+ *
57
+ * @param verdict - The host agent's per-criterion verdict.
58
+ * @param rubric - The item's authoritative MCP-supplied rubric.
59
+ * @returns The continuous score and its derived binary grade.
60
+ * @throws {Error} with a clear message when the verdict shape does not conform
61
+ * to the rubric (per the rules above).
62
+ */
63
+ export declare const scoreVerdict: (verdict: FreeFormVerdict, rubric: Rubric) => FreeFormGrade;
64
+ //# sourceMappingURL=freeform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"freeform.d.ts","sourceRoot":"","sources":["../../src/grading/freeform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D,qEAAqE;AACrE,MAAM,WAAW,aAAa;IAC5B,uEAAuE;IACvE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,wEAAwE;IACxE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,YAAY,GACvB,SAAS,eAAe,EACxB,QAAQ,MAAM,KACb,aA0CF,CAAC"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @file PURE free-form grading (T048, FR-012/013, US-4).
3
+ *
4
+ * Scores a free-form item from the host agent's **per-criterion** verdict against
5
+ * the item's MCP-supplied {@link Rubric}. The score is the FRACTION of rubric
6
+ * criteria the agent marked `met`; the binary {@link Grade} is the projection of
7
+ * that score against the rubric's `passThreshold` (default
8
+ * {@link ASSESSMENT_CONFIG.freeFormPassThreshold} = 0.6) via the shared
9
+ * {@link toGrade} (one definition of "correct" across deterministic + free-form).
10
+ *
11
+ * Anti-gaming (critique E2, FR-013): the verdict MUST be a per-criterion array
12
+ * that aligns to the rubric's criteria ids. This module REJECTS a bare boolean,
13
+ * a missing/empty criteria array, an unknown criterion id, or a verdict that
14
+ * does not cover every rubric criterion — a lazy single self-pass is
15
+ * non-conformant and never silently scored. The host agent cannot invent its own
16
+ * criteria: only ids present in the authoritative rubric are accepted.
17
+ *
18
+ * Invariants (mirrors grading/deterministic.ts, engine/elo.ts):
19
+ * - PURE: no `Date`, no `fs`, no `Math.random`, no network.
20
+ * - The rubric is read-only input — never mutated here.
21
+ * - Determinism: identical verdict + rubric ⇒ identical score + grade.
22
+ *
23
+ * Source of truth: specs/001-vibe-hero-mvp/contracts/mcp-tools.md
24
+ * (`submit_answer` free-form path), spec.md FR-012 / FR-013, research.md
25
+ * (OD-002 — free-form IN v1, `freeFormPassThreshold` 0.6), data-model.md
26
+ * (Grade = binary projection of a continuous score).
27
+ */
28
+ import { ASSESSMENT_CONFIG } from "../config.js";
29
+ import { toGrade } from "./deterministic.js";
30
+ /**
31
+ * Score a free-form item from the host agent's per-criterion verdict (PURE).
32
+ *
33
+ * The score is `metCount / rubric.criteria.length` — the fraction of the
34
+ * authoritative rubric criteria the agent reported as met. The grade derives
35
+ * from that score against the rubric's `passThreshold` (falling back to
36
+ * {@link ASSESSMENT_CONFIG.freeFormPassThreshold} when the rubric omits one).
37
+ *
38
+ * The verdict is validated for SHAPE before scoring (anti-gaming, FR-013):
39
+ * - it MUST carry a non-empty `criteria` array (a bare boolean / missing
40
+ * criteria is rejected — the SDK union also rejects it, this is defence in
41
+ * depth so a direct caller can't bypass the structure);
42
+ * - every verdict criterion id MUST reference a real rubric criterion id (the
43
+ * agent cannot invent criteria);
44
+ * - no rubric criterion id may be duplicated in the verdict (an id is reported
45
+ * once);
46
+ * - EVERY rubric criterion MUST be covered by the verdict (the agent cannot
47
+ * silently skip a criterion to inflate the fraction).
48
+ *
49
+ * @param verdict - The host agent's per-criterion verdict.
50
+ * @param rubric - The item's authoritative MCP-supplied rubric.
51
+ * @returns The continuous score and its derived binary grade.
52
+ * @throws {Error} with a clear message when the verdict shape does not conform
53
+ * to the rubric (per the rules above).
54
+ */
55
+ export const scoreVerdict = (verdict, rubric) => {
56
+ const rubricIds = new Set(rubric.criteria.map((c) => c.id));
57
+ // A bare boolean / missing criteria — defence in depth beyond the SDK union.
58
+ if (!Array.isArray(verdict.criteria) || verdict.criteria.length === 0) {
59
+ throw new Error("scoreVerdict: free-form verdict must be a per-criterion array " +
60
+ "(a bare boolean or empty verdict is non-conformant — FR-013)");
61
+ }
62
+ const seen = new Set();
63
+ for (const c of verdict.criteria) {
64
+ if (!rubricIds.has(c.id)) {
65
+ throw new Error(`scoreVerdict: verdict references unknown criterion id ${JSON.stringify(c.id)}; ` +
66
+ `the host agent may only judge the MCP-supplied rubric criteria ` +
67
+ `${JSON.stringify([...rubricIds])}`);
68
+ }
69
+ if (seen.has(c.id)) {
70
+ throw new Error(`scoreVerdict: verdict reports criterion id ${JSON.stringify(c.id)} more than once`);
71
+ }
72
+ seen.add(c.id);
73
+ }
74
+ // Every rubric criterion must be judged — no silent omissions.
75
+ if (seen.size !== rubricIds.size) {
76
+ const missing = [...rubricIds].filter((id) => !seen.has(id));
77
+ throw new Error(`scoreVerdict: verdict does not cover every rubric criterion; missing ` +
78
+ `${JSON.stringify(missing)} (a partial verdict is non-conformant — FR-013)`);
79
+ }
80
+ const metCount = verdict.criteria.filter((c) => c.met).length;
81
+ const score = metCount / rubric.criteria.length;
82
+ const passThreshold = rubric.passThreshold ?? ASSESSMENT_CONFIG.freeFormPassThreshold;
83
+ return { score, grade: toGrade(score, passThreshold) };
84
+ };
85
+ //# sourceMappingURL=freeform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"freeform.js","sourceRoot":"","sources":["../../src/grading/freeform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAa7C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAC1B,OAAwB,EACxB,MAAc,EACC,EAAE;IACjB,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAE5D,6EAA6E;IAC7E,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtE,MAAM,IAAI,KAAK,CACb,gEAAgE;YAC9D,8DAA8D,CACjE,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;QACjC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CACb,yDAAyD,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI;gBAC/E,iEAAiE;gBACjE,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,EAAE,CACtC,CAAC;QACJ,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CACb,8CAA8C,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,iBAAiB,CACpF,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACjB,CAAC;IAED,+DAA+D;IAC/D,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,IAAI,EAAE,CAAC;QACjC,MAAM,OAAO,GAAG,CAAC,GAAG,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QAC7D,MAAM,IAAI,KAAK,CACb,uEAAuE;YACrE,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,iDAAiD,CAC9E,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9D,MAAM,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;IAChD,MAAM,aAAa,GACjB,MAAM,CAAC,aAAa,IAAI,iBAAiB,CAAC,qBAAqB,CAAC;IAClE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,aAAa,CAAC,EAAE,CAAC;AACzD,CAAC,CAAC"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @file vibe-hero MCP server entry point (T020).
3
+ *
4
+ * Bootstraps a stdio MCP server (`@modelcontextprotocol/sdk`) named `vibe-hero`
5
+ * with a `tools` capability, then registers all 10 tools from
6
+ * {@link TOOL_REGISTRY}. Each tool's handler is wrapped with the first-run setup
7
+ * gate (T021, {@link withSetupGate}) and adapted from its plain-JSON result into
8
+ * the SDK's `CallToolResult` shape ({@link toCallToolResult}).
9
+ *
10
+ * The SDK idiom (sdk 1.29.0): construct {@link McpServer}, then call
11
+ * `registerTool(name, { description, inputSchema }, cb)`. `registerTool` expects
12
+ * `inputSchema` as a *raw Zod shape* (`Record<string, ZodType>`), so we pass the
13
+ * registry's `inputSchema.shape` and the SDK hands the callback the parsed,
14
+ * validated args. Transport is {@link StdioServerTransport}.
15
+ *
16
+ * Importing this module never starts the server; only running it as the process
17
+ * entrypoint does (see the `import.meta.url` guard at the bottom), so tests can
18
+ * import {@link createServer} freely.
19
+ *
20
+ * Source of truth: specs/001-vibe-hero-mvp/contracts/mcp-tools.md, plan.md.
21
+ */
22
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
23
+ import { type AnyToolModule } from "./tools/types.js";
24
+ /** Server name advertised to MCP hosts. Matches the product/skill namespace. */
25
+ export declare const SERVER_NAME = "vibe-hero";
26
+ /** Server version advertised to MCP hosts. */
27
+ export declare const SERVER_VERSION = "0.1.0";
28
+ /**
29
+ * Register one tool module on an {@link McpServer}, gating its handler and
30
+ * adapting the JSON result to a `CallToolResult`. Pulled out so both production
31
+ * and tests register tools the same way; `dirOverride` flows to the gate's
32
+ * profile lookup as a test seam.
33
+ *
34
+ * @param server - The MCP server to register onto.
35
+ * @param tool - The tool module from {@link TOOL_REGISTRY}.
36
+ * @param dirOverride - Profile-directory override (test seam) for the gate.
37
+ */
38
+ export declare const registerToolModule: (server: McpServer, tool: AnyToolModule, dirOverride?: string) => void;
39
+ /**
40
+ * Construct the vibe-hero MCP server with all tools registered (gated). Does not
41
+ * connect a transport — call {@link main} (or wire your own transport) to start.
42
+ *
43
+ * @param dirOverride - Profile-directory override (test seam) for the gate.
44
+ * @returns A configured, not-yet-connected {@link McpServer}.
45
+ */
46
+ export declare const createServer: (dirOverride?: string) => McpServer;
47
+ /**
48
+ * Start the server over stdio. Connects a {@link StdioServerTransport} and
49
+ * resolves once connected; the process then stays alive serving tool calls.
50
+ */
51
+ export declare const main: () => Promise<void>;
52
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAExE,gFAAgF;AAChF,eAAO,MAAM,WAAW,cAAc,CAAC;AAEvC,8CAA8C;AAC9C,eAAO,MAAM,cAAc,UAAU,CAAC;AAEtC;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,SAAS,EACjB,MAAM,aAAa,EACnB,cAAc,MAAM,KACnB,IAWF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,GAAI,cAAc,MAAM,KAAG,SASnD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,IAAI,QAAa,OAAO,CAAC,IAAI,CAIzC,CAAC"}