@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,156 @@
1
+ /**
2
+ * @file Real `submit_answer` tool module — DETERMINISTIC + FREE-FORM paths
3
+ * (T032 deterministic, US-1; T048 free-form, US-4).
4
+ *
5
+ * The core scoring entry point. For a deterministic item (`multiple_choice` /
6
+ * `short_answer`) it: finds the live {@link QuizRecord} + catalog item by
7
+ * `quizId`/`itemId`, grades it in-engine via {@link gradeMultipleChoice} /
8
+ * {@link gradeShortAnswer} (FR-011, instant + reproducible — SC-004), updates the
9
+ * learner's Elo ability against the item's FIXED authored difficulty
10
+ * ({@link updateAbility}; item difficulty never self-updates — E3), and persists
11
+ * the result.
12
+ *
13
+ * Privacy (FR-018): the persisted {@link AnsweredItem} carries ONLY derived
14
+ * fields — `itemId`, `tier`, `difficulty`, `grade`, `score`,
15
+ * `gradedBy: "engine"`, `answeredAt`. The raw answer text / chosen id is used to
16
+ * grade and then DISCARDED; it never reaches disk. The continuous `score` drives
17
+ * the Elo update; the binary {@link Grade} is the projection persisted in
18
+ * history.
19
+ *
20
+ * Persistence (atomic, FR-023a): a single {@link updateProfile} read-modify-write
21
+ * appends the {@link AnsweredItem} to the matching {@link QuizRecord} and updates
22
+ * the {@link AbilityEstimate} (`value`, `itemsSeen++`, `lastAssessedAt`, push
23
+ * `itemId` to `lastItemIds`). Re-running on the same answer is naturally
24
+ * idempotent in grade (SC-004) though it appends another graded item — quiz
25
+ * sessions are append-only event logs.
26
+ *
27
+ * Graduation (T046, US-3): after the ability update, the PURE
28
+ * {@link evaluateGraduation} engine decides whether this graded item promotes
29
+ * the learner to the next tier (hysteresis band + `dwell` consecutive
30
+ * qualifying items — SC-014) or demotes/flags below the lower band (FR-008/009).
31
+ * The decision updates `profile.graduations[key]` and the dwell counter on the
32
+ * AbilityEstimate; on a promotion a proactive spaced {@link ReviewEntry} is
33
+ * enqueued (FR-010) so a one-time streak is later re-verified, and the
34
+ * `graduation` field is surfaced on the result so the host informs the user.
35
+ *
36
+ * Free-form path (T048, US-4): when the item is `free_form` and the input carries
37
+ * a per-criterion `verdict`, the host agent's verdict is scored by the PURE
38
+ * {@link scoreVerdict} against the item's MCP-supplied rubric — the MCP computes
39
+ * the score (fraction of criteria met) and derives the grade; the agent never
40
+ * returns a bare score/boolean (anti-gaming, FR-012/013). The graded result then
41
+ * flows through the SAME ability-update / persistence / graduation pipeline as a
42
+ * deterministic grade, recorded as `gradedBy: "host_agent"` with NO raw answer
43
+ * text (FR-018). The two paths cross-guard: a deterministic answer on a free-form
44
+ * item, or a free-form verdict on a deterministic item, is rejected with a clear
45
+ * error.
46
+ *
47
+ * Gated (FR-032): NOT exempt — the gate runs before this handler.
48
+ *
49
+ * Exposed as a `dirOverride`-closing factory mirroring `config.ts` / `status.ts`.
50
+ *
51
+ * Source of truth: specs/001-vibe-hero-mvp/contracts/mcp-tools.md
52
+ * (`submit_answer` deterministic + free-form paths), spec.md FR-005 / FR-011 /
53
+ * FR-012 / FR-013 / FR-018 / SC-004, data-model.md (AnsweredItem /
54
+ * AbilityEstimate), research.md (OD-002 / OD-005).
55
+ */
56
+ import type { CatalogLoadResult } from "../catalog/loader.js";
57
+ import { z } from "zod";
58
+ import { type AnyToolModule } from "./types.js";
59
+ /**
60
+ * Permissive OBJECT schema registered with the SDK. `submit_answer`'s contract
61
+ * input is a *union* (deterministic answer XOR free-form verdict) which has no
62
+ * single `.shape` for `registerTool`, so the wire schema is a superset:
63
+ * `quizId` + `itemId` required, `answer`/`verdict` optional. The handler
64
+ * re-validates against the authoritative {@link SubmitAnswerInputSchema} union
65
+ * (discriminated structurally) before grading — a malformed payload is rejected
66
+ * there, not silently mis-graded.
67
+ */
68
+ export declare const SubmitAnswerToolInputSchema: z.ZodObject<{
69
+ quizId: z.ZodString;
70
+ itemId: z.ZodString;
71
+ answer: z.ZodOptional<z.ZodObject<{
72
+ choiceId: z.ZodOptional<z.ZodString>;
73
+ text: z.ZodOptional<z.ZodString>;
74
+ }, "strip", z.ZodTypeAny, {
75
+ text?: string | undefined;
76
+ choiceId?: string | undefined;
77
+ }, {
78
+ text?: string | undefined;
79
+ choiceId?: string | undefined;
80
+ }>>;
81
+ verdict: z.ZodOptional<z.ZodObject<{
82
+ criteria: z.ZodArray<z.ZodObject<{
83
+ id: z.ZodString;
84
+ met: z.ZodBoolean;
85
+ justification: z.ZodString;
86
+ }, "strip", z.ZodTypeAny, {
87
+ id: string;
88
+ met: boolean;
89
+ justification: string;
90
+ }, {
91
+ id: string;
92
+ met: boolean;
93
+ justification: string;
94
+ }>, "many">;
95
+ }, "strip", z.ZodTypeAny, {
96
+ criteria: {
97
+ id: string;
98
+ met: boolean;
99
+ justification: string;
100
+ }[];
101
+ }, {
102
+ criteria: {
103
+ id: string;
104
+ met: boolean;
105
+ justification: string;
106
+ }[];
107
+ }>>;
108
+ }, "strip", z.ZodTypeAny, {
109
+ itemId: string;
110
+ quizId: string;
111
+ answer?: {
112
+ text?: string | undefined;
113
+ choiceId?: string | undefined;
114
+ } | undefined;
115
+ verdict?: {
116
+ criteria: {
117
+ id: string;
118
+ met: boolean;
119
+ justification: string;
120
+ }[];
121
+ } | undefined;
122
+ }, {
123
+ itemId: string;
124
+ quizId: string;
125
+ answer?: {
126
+ text?: string | undefined;
127
+ choiceId?: string | undefined;
128
+ } | undefined;
129
+ verdict?: {
130
+ criteria: {
131
+ id: string;
132
+ met: boolean;
133
+ justification: string;
134
+ }[];
135
+ } | undefined;
136
+ }>;
137
+ /** Inferred wire-input type for `submit_answer` (permissive superset). */
138
+ export type SubmitAnswerToolInput = z.infer<typeof SubmitAnswerToolInputSchema>;
139
+ /**
140
+ * A catalog source: returns the loaded topics (+ any per-file errors). Defaults
141
+ * to {@link loadBundledCatalog}; tests inject a fixture-dir loader so grading can
142
+ * resolve a topic with ≥5 deterministic items without disturbing the shared
143
+ * bundled snapshot.
144
+ */
145
+ export type CatalogLoader = () => CatalogLoadResult;
146
+ /**
147
+ * Build the `submit_answer` tool module (US-1 deterministic + US-4 free-form).
148
+ *
149
+ * @param dirOverride - Profile-directory override (test seam); see `profileDir`.
150
+ * @param catalogLoader - Catalog source override (test seam); defaults to the
151
+ * bundled snapshot {@link loadBundledCatalog}.
152
+ */
153
+ export declare const makeSubmitAnswerTool: (dirOverride?: string, catalogLoader?: CatalogLoader) => AnyToolModule;
154
+ /** Default `submit_answer` module (env / `~/.vibe-hero`), used by the registry. */
155
+ export declare const submitAnswerTool: AnyToolModule;
156
+ //# sourceMappingURL=submitAnswer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submitAnswer.d.ts","sourceRoot":"","sources":["../../src/tools/submitAnswer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAwB9D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,OAAO,EAAc,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5D;;;;;;;;GAQG;AACH,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoBtC,CAAC;AACH,0EAA0E;AAC1E,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AA0ShF;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,iBAAiB,CAAC;AAiDpD;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC/B,cAAc,MAAM,EACpB,gBAAe,aAAkC,KAChD,aAwEC,CAAC;AAEL,mFAAmF;AACnF,eAAO,MAAM,gBAAgB,EAAE,aAAsC,CAAC"}
@@ -0,0 +1,402 @@
1
+ /**
2
+ * @file Real `submit_answer` tool module — DETERMINISTIC + FREE-FORM paths
3
+ * (T032 deterministic, US-1; T048 free-form, US-4).
4
+ *
5
+ * The core scoring entry point. For a deterministic item (`multiple_choice` /
6
+ * `short_answer`) it: finds the live {@link QuizRecord} + catalog item by
7
+ * `quizId`/`itemId`, grades it in-engine via {@link gradeMultipleChoice} /
8
+ * {@link gradeShortAnswer} (FR-011, instant + reproducible — SC-004), updates the
9
+ * learner's Elo ability against the item's FIXED authored difficulty
10
+ * ({@link updateAbility}; item difficulty never self-updates — E3), and persists
11
+ * the result.
12
+ *
13
+ * Privacy (FR-018): the persisted {@link AnsweredItem} carries ONLY derived
14
+ * fields — `itemId`, `tier`, `difficulty`, `grade`, `score`,
15
+ * `gradedBy: "engine"`, `answeredAt`. The raw answer text / chosen id is used to
16
+ * grade and then DISCARDED; it never reaches disk. The continuous `score` drives
17
+ * the Elo update; the binary {@link Grade} is the projection persisted in
18
+ * history.
19
+ *
20
+ * Persistence (atomic, FR-023a): a single {@link updateProfile} read-modify-write
21
+ * appends the {@link AnsweredItem} to the matching {@link QuizRecord} and updates
22
+ * the {@link AbilityEstimate} (`value`, `itemsSeen++`, `lastAssessedAt`, push
23
+ * `itemId` to `lastItemIds`). Re-running on the same answer is naturally
24
+ * idempotent in grade (SC-004) though it appends another graded item — quiz
25
+ * sessions are append-only event logs.
26
+ *
27
+ * Graduation (T046, US-3): after the ability update, the PURE
28
+ * {@link evaluateGraduation} engine decides whether this graded item promotes
29
+ * the learner to the next tier (hysteresis band + `dwell` consecutive
30
+ * qualifying items — SC-014) or demotes/flags below the lower band (FR-008/009).
31
+ * The decision updates `profile.graduations[key]` and the dwell counter on the
32
+ * AbilityEstimate; on a promotion a proactive spaced {@link ReviewEntry} is
33
+ * enqueued (FR-010) so a one-time streak is later re-verified, and the
34
+ * `graduation` field is surfaced on the result so the host informs the user.
35
+ *
36
+ * Free-form path (T048, US-4): when the item is `free_form` and the input carries
37
+ * a per-criterion `verdict`, the host agent's verdict is scored by the PURE
38
+ * {@link scoreVerdict} against the item's MCP-supplied rubric — the MCP computes
39
+ * the score (fraction of criteria met) and derives the grade; the agent never
40
+ * returns a bare score/boolean (anti-gaming, FR-012/013). The graded result then
41
+ * flows through the SAME ability-update / persistence / graduation pipeline as a
42
+ * deterministic grade, recorded as `gradedBy: "host_agent"` with NO raw answer
43
+ * text (FR-018). The two paths cross-guard: a deterministic answer on a free-form
44
+ * item, or a free-form verdict on a deterministic item, is rejected with a clear
45
+ * error.
46
+ *
47
+ * Gated (FR-032): NOT exempt — the gate runs before this handler.
48
+ *
49
+ * Exposed as a `dirOverride`-closing factory mirroring `config.ts` / `status.ts`.
50
+ *
51
+ * Source of truth: specs/001-vibe-hero-mvp/contracts/mcp-tools.md
52
+ * (`submit_answer` deterministic + free-form paths), spec.md FR-005 / FR-011 /
53
+ * FR-012 / FR-013 / FR-018 / SC-004, data-model.md (AnsweredItem /
54
+ * AbilityEstimate), research.md (OD-002 / OD-005).
55
+ */
56
+ import { ASSESSMENT_CONFIG } from "../config.js";
57
+ import { loadBundledCatalog } from "../catalog/bundled/index.js";
58
+ import { updateAbility } from "../engine/elo.js";
59
+ import { evaluateGraduation, } from "../engine/graduation.js";
60
+ import { gradeMultipleChoice, gradeShortAnswer, toGrade, } from "../grading/deterministic.js";
61
+ import { scoreVerdict } from "../grading/freeform.js";
62
+ import { loadProfile, updateProfile } from "../profile/store.js";
63
+ import { abilityKey } from "../schemas/common.js";
64
+ import { z } from "zod";
65
+ import { SubmitAnswerInputSchema, } from "../schemas/tools.js";
66
+ import { defineTool } from "./types.js";
67
+ /**
68
+ * Permissive OBJECT schema registered with the SDK. `submit_answer`'s contract
69
+ * input is a *union* (deterministic answer XOR free-form verdict) which has no
70
+ * single `.shape` for `registerTool`, so the wire schema is a superset:
71
+ * `quizId` + `itemId` required, `answer`/`verdict` optional. The handler
72
+ * re-validates against the authoritative {@link SubmitAnswerInputSchema} union
73
+ * (discriminated structurally) before grading — a malformed payload is rejected
74
+ * there, not silently mis-graded.
75
+ */
76
+ export const SubmitAnswerToolInputSchema = z.object({
77
+ quizId: z.string(),
78
+ itemId: z.string(),
79
+ answer: z
80
+ .object({
81
+ choiceId: z.string().optional(),
82
+ text: z.string().optional(),
83
+ })
84
+ .optional(),
85
+ verdict: z
86
+ .object({
87
+ criteria: z.array(z.object({
88
+ id: z.string(),
89
+ met: z.boolean(),
90
+ justification: z.string(),
91
+ })),
92
+ })
93
+ .optional(),
94
+ });
95
+ /**
96
+ * A type guard for the deterministic input variant (carries `answer`, not
97
+ * `verdict`). Free-form verdicts route to the T048 path; until then they are
98
+ * rejected so a free-form answer never silently mis-grades.
99
+ */
100
+ const isDeterministic = (input) => "answer" in input;
101
+ /** Find the catalog item with `itemId` across all topics; returns its topic too. */
102
+ const findItem = (topics, itemId) => {
103
+ for (const topic of topics) {
104
+ const item = topic.items.find((i) => i.id === itemId);
105
+ if (item !== undefined)
106
+ return { topic, item };
107
+ }
108
+ return undefined;
109
+ };
110
+ /**
111
+ * Grade a deterministic item, returning the continuous score plus (for MC) the
112
+ * authored correct choice id so the caller can surface `correctAnswer`.
113
+ *
114
+ * @throws {Error} if `item` is `free_form` — a deterministic answer was submitted
115
+ * for a free-form item; the caller must send a per-criterion `verdict` instead
116
+ * (cross-guard, T048).
117
+ */
118
+ const gradeDeterministic = (item, answer) => {
119
+ switch (item.type) {
120
+ case "multiple_choice": {
121
+ const { score, correctChoiceId } = gradeMultipleChoice(item, answer.choiceId);
122
+ return { score, correctAnswer: correctChoiceId };
123
+ }
124
+ case "short_answer": {
125
+ const { score } = gradeShortAnswer(item, answer.text);
126
+ return { score };
127
+ }
128
+ case "free_form":
129
+ throw new Error(`submit_answer: item ${JSON.stringify(item.id)} is free_form; submit a per-criterion \`verdict\`, not a deterministic \`answer\` (T048)`);
130
+ }
131
+ };
132
+ /**
133
+ * Build the AnsweredItem persisted in history. Derived fields ONLY — never the
134
+ * raw answer text / chosen id / verdict justifications (FR-018). The item's
135
+ * authored `tier` + fixed `difficulty` are recorded so history is
136
+ * self-describing without re-joining the catalog. `gradedBy` records WHO graded:
137
+ * the engine (deterministic) or the host agent (free-form verdict).
138
+ */
139
+ const buildAnsweredItem = (item, score, grade, gradedBy, answeredAt) => ({
140
+ itemId: item.id,
141
+ tier: item.tier,
142
+ difficulty: item.difficulty,
143
+ grade,
144
+ score,
145
+ gradedBy,
146
+ answeredAt,
147
+ });
148
+ /**
149
+ * Advance an ability estimate by one graded item: apply the Elo update against
150
+ * the item's fixed difficulty, stamp `lastAssessedAt`, and push the item id onto
151
+ * `lastItemIds` (so the next `start_quiz` avoids re-serving it). A cold estimate
152
+ * (no prior entry) starts from {@link ASSESSMENT_CONFIG.startingAbility} with
153
+ * zero items seen.
154
+ */
155
+ const advanceEstimate = (prior, item, score, answeredAt) => {
156
+ const before = prior?.value ?? ASSESSMENT_CONFIG.startingAbility;
157
+ const itemsSeen = prior?.itemsSeen ?? 0;
158
+ const update = updateAbility(before, itemsSeen, item.difficulty, score);
159
+ return {
160
+ before,
161
+ estimate: {
162
+ value: update.value,
163
+ itemsSeen: update.itemsSeen,
164
+ lastAssessedAt: answeredAt,
165
+ lastItemIds: [...(prior?.lastItemIds ?? []), item.id],
166
+ // Carry the prior dwell forward unchanged here; the graduation step
167
+ // (applyGraduation) reads it, then overwrites with the engine's next
168
+ // dwell so the consecutive-streak counter advances/resets per item.
169
+ dwell: prior?.dwell ?? 0,
170
+ },
171
+ };
172
+ };
173
+ /**
174
+ * Apply the PURE graduation decision (T046, US-3) to one ability key inside the
175
+ * store transaction. Reads the just-updated ability + the prior dwell counter,
176
+ * asks {@link evaluateGraduation} for the decision, then returns the next
177
+ * `graduations[key]`, the dwell to persist on the estimate, and (on a
178
+ * promotion) a proactive spaced {@link ReviewEntry} to enqueue (FR-010).
179
+ *
180
+ * - On `"graduated"`: write a `current` graduation at the new tier and enqueue a
181
+ * `reason: "spaced"` review one staleness window out (so a one-time streak is
182
+ * later re-verified — durable-knowledge semantics).
183
+ * - On `"demoted"`: step the tier down and flag `due_for_review` (FR-009). No
184
+ * spaced entry — the lapse path (status/standing, T046) owns lapsed entries.
185
+ * - On no change: leave the existing graduation untouched, but still persist the
186
+ * engine's dwell so the consecutive streak carries across items.
187
+ *
188
+ * @returns The decision plus the derived graduation/dwell/review side-outputs.
189
+ */
190
+ const applyGraduation = (key, newAbility, priorDwell, existing, answeredAt) => {
191
+ const currentTier = existing?.currentTier ?? 0;
192
+ const decision = evaluateGraduation({
193
+ ability: newAbility,
194
+ currentTier,
195
+ dwell: priorDwell,
196
+ });
197
+ if (decision.reason === "graduated") {
198
+ const dueAt = new Date(Date.parse(answeredAt) +
199
+ ASSESSMENT_CONFIG.stalenessWindowDays * 24 * 60 * 60 * 1000).toISOString();
200
+ return {
201
+ decision,
202
+ dwell: decision.dwell,
203
+ graduation: {
204
+ currentTier: decision.tier,
205
+ status: "current",
206
+ graduatedAt: answeredAt,
207
+ lastChangeReason: "graduated",
208
+ },
209
+ spacedReview: { key, dueAt, reason: "spaced" },
210
+ };
211
+ }
212
+ if (decision.reason === "demoted") {
213
+ return {
214
+ decision,
215
+ dwell: decision.dwell,
216
+ graduation: {
217
+ currentTier: decision.tier,
218
+ status: "due_for_review",
219
+ // Preserve the original graduation timestamp if we have one; this is an
220
+ // audit field, and a demotion is a status change, not a re-graduation.
221
+ graduatedAt: existing?.graduatedAt ?? answeredAt,
222
+ lastChangeReason: "demoted",
223
+ },
224
+ };
225
+ }
226
+ // No change: keep the existing graduation row but persist the (possibly
227
+ // advanced) dwell streak.
228
+ return { decision, dwell: decision.dwell, graduation: existing };
229
+ };
230
+ /**
231
+ * Project a {@link GraduationDecision} into the `submit_answer` result's
232
+ * `graduation` field `{ changed, tier?, status?, reason? }`. On a change the
233
+ * `status` reflects the resulting graduation state (`current` on promotion,
234
+ * `due_for_review` on demotion); `tier` is included only when the resulting tier
235
+ * is a real tier (100–500) — a demotion all the way to `0` (un-graduated) omits
236
+ * it, matching the result schema (`TierSchema.optional()`). A no-change decision
237
+ * reports `changed: false` and nothing else.
238
+ */
239
+ const toGraduationResult = (decision) => {
240
+ if (!decision.changed)
241
+ return { changed: false };
242
+ const status = decision.reason === "graduated" ? "current" : "due_for_review";
243
+ const base = {
244
+ changed: true,
245
+ status,
246
+ ...(decision.reason !== null ? { reason: decision.reason } : {}),
247
+ };
248
+ return decision.tier === 0 ? base : { ...base, tier: decision.tier };
249
+ };
250
+ /**
251
+ * Apply a graded answer to the profile inside the store's atomic
252
+ * read-modify-write: append the {@link AnsweredItem} to the matching live
253
+ * {@link QuizRecord} (and roll its `abilityAfter`), and replace the
254
+ * {@link AbilityEstimate} for the quiz's key. Returns the new profile plus the
255
+ * before/after abilities for the tool result.
256
+ *
257
+ * @throws {Error} if no live (un-completed) quiz with `quizId` exists.
258
+ */
259
+ const persistGrade = async (quizId, item, score, grade, gradedBy, answeredAt, dirOverride) => {
260
+ let outcome;
261
+ await updateProfile((current) => {
262
+ const recordIndex = current.quizHistory.findIndex((q) => q.id === quizId);
263
+ const record = current.quizHistory[recordIndex];
264
+ if (record === undefined) {
265
+ throw new Error(`submit_answer: no quiz session found for quizId ${JSON.stringify(quizId)}`);
266
+ }
267
+ if (record.completedAt !== undefined) {
268
+ throw new Error(`submit_answer: quiz ${JSON.stringify(quizId)} is already completed`);
269
+ }
270
+ const key = record.key;
271
+ const prior = current.abilities[key];
272
+ const { before, estimate } = advanceEstimate(prior, item, score, answeredAt);
273
+ // Graduation (T046): evaluate hysteresis/dwell against the JUST-updated
274
+ // ability, threading the prior dwell counter through the pure engine.
275
+ const grad = applyGraduation(key, estimate.value, prior?.dwell ?? 0, current.graduations[key], answeredAt);
276
+ // Persist the engine's next dwell on the estimate so the consecutive streak
277
+ // carries across submit_answer calls (advanceEstimate seeded it from prior).
278
+ const estimateWithDwell = { ...estimate, dwell: grad.dwell };
279
+ outcome = { before, after: estimate.value, decision: grad.decision };
280
+ const answered = buildAnsweredItem(item, score, grade, gradedBy, answeredAt);
281
+ const updatedRecord = {
282
+ ...record,
283
+ items: [...record.items, answered],
284
+ abilityAfter: estimate.value,
285
+ };
286
+ const quizHistory = [...current.quizHistory];
287
+ quizHistory[recordIndex] = updatedRecord;
288
+ const graduations = grad.graduation === undefined
289
+ ? current.graduations
290
+ : { ...current.graduations, [key]: grad.graduation };
291
+ // On promotion, enqueue a proactive spaced review (FR-010), de-duped by key
292
+ // so repeated promotions don't pile up duplicate spaced entries.
293
+ const reviewSchedule = grad.spacedReview === undefined
294
+ ? current.reviewSchedule
295
+ : [
296
+ ...current.reviewSchedule.filter((e) => !(e.key === key && e.reason === "spaced")),
297
+ grad.spacedReview,
298
+ ];
299
+ return {
300
+ ...current,
301
+ abilities: { ...current.abilities, [key]: estimateWithDwell },
302
+ graduations,
303
+ reviewSchedule,
304
+ quizHistory,
305
+ };
306
+ }, dirOverride);
307
+ if (outcome === undefined) {
308
+ // Unreachable: the transform either set `outcome` or threw above.
309
+ throw new Error("submit_answer: ability update did not run");
310
+ }
311
+ return outcome;
312
+ };
313
+ /**
314
+ * Grade one item, dispatching on the input variant AND cross-guarding against
315
+ * the item type (so a deterministic answer on a free-form item, or a free-form
316
+ * verdict on a deterministic item, is rejected — never silently mis-graded):
317
+ *
318
+ * - DETERMINISTIC input (`answer`) → {@link gradeDeterministic}; all-or-nothing
319
+ * score graded `"engine"`. The `free_form` branch there throws the cross-guard.
320
+ * - FREE-FORM input (`verdict`) → the item MUST be `free_form` with a rubric;
321
+ * the PURE {@link scoreVerdict} computes the fraction-met score + grade against
322
+ * the MCP-supplied rubric (FR-012/013), graded `"host_agent"`. A bare/partial
323
+ * verdict is rejected inside `scoreVerdict` (anti-gaming, E2).
324
+ */
325
+ const gradeItem = (input, item) => {
326
+ if (isDeterministic(input)) {
327
+ const { score, correctAnswer } = gradeDeterministic(item, input.answer);
328
+ // Deterministic items are all-or-nothing: any threshold in (0,1] suffices.
329
+ // Use the free-form pass threshold so there is one "correct" definition.
330
+ const grade = toGrade(score, ASSESSMENT_CONFIG.freeFormPassThreshold);
331
+ return correctAnswer !== undefined
332
+ ? { score, grade, gradedBy: "engine", correctAnswer }
333
+ : { score, grade, gradedBy: "engine" };
334
+ }
335
+ // Free-form verdict path (T048): the item must be free_form with a rubric.
336
+ if (item.type !== "free_form" || item.rubric === undefined) {
337
+ throw new Error(`submit_answer: item ${JSON.stringify(item.id)} is not free_form; submit a deterministic \`answer\`, not a \`verdict\``);
338
+ }
339
+ // The MCP computes the score from the per-criterion verdict against its own
340
+ // rubric — the agent never returns a bare score/boolean (FR-012/013).
341
+ const { score, grade } = scoreVerdict(input.verdict, item.rubric);
342
+ return { score, grade, gradedBy: "host_agent" };
343
+ };
344
+ /**
345
+ * Build the `submit_answer` tool module (US-1 deterministic + US-4 free-form).
346
+ *
347
+ * @param dirOverride - Profile-directory override (test seam); see `profileDir`.
348
+ * @param catalogLoader - Catalog source override (test seam); defaults to the
349
+ * bundled snapshot {@link loadBundledCatalog}.
350
+ */
351
+ export const makeSubmitAnswerTool = (dirOverride, catalogLoader = loadBundledCatalog) => defineTool({
352
+ name: "submit_answer",
353
+ description: "Grade one quiz item (deterministic answer or free-form host verdict) and update ability.",
354
+ // The SDK needs an object schema; the handler re-validates against the real
355
+ // discriminated union below (the authoritative parse).
356
+ inputSchema: SubmitAnswerToolInputSchema,
357
+ handler: async (raw) => {
358
+ // Authoritative validation: deterministic answer XOR free-form verdict.
359
+ // A bare-boolean / shapeless verdict fails the union here (anti-gaming,
360
+ // E2) before it ever reaches grading.
361
+ const input = SubmitAnswerInputSchema.parse(raw);
362
+ // Resolve the quiz's topic key so we can find the item in catalog scope.
363
+ const profile = await loadProfile(dirOverride);
364
+ const record = profile.quizHistory.find((q) => q.id === input.quizId);
365
+ if (record === undefined) {
366
+ throw new Error(`submit_answer: no quiz session found for quizId ${JSON.stringify(input.quizId)}`);
367
+ }
368
+ const { topics } = catalogLoader();
369
+ const found = findItem(topics, input.itemId);
370
+ if (found === undefined) {
371
+ throw new Error(`submit_answer: no catalog item matches itemId ${JSON.stringify(input.itemId)}`);
372
+ }
373
+ const { topic, item } = found;
374
+ // Defensive: the item must belong to the quiz's topic.
375
+ if (abilityKey(topic.class, topic.id) !== record.key) {
376
+ throw new Error(`submit_answer: item ${JSON.stringify(input.itemId)} does not belong to quiz ${JSON.stringify(input.quizId)}`);
377
+ }
378
+ // Grade via the path matching the input + item type (deterministic engine
379
+ // or free-form host verdict); both yield a continuous score + binary grade.
380
+ const { score, grade, gradedBy, correctAnswer } = gradeItem(input, item);
381
+ const answeredAt = new Date().toISOString();
382
+ const { before, after, decision } = await persistGrade(input.quizId, item, score, grade, gradedBy, answeredAt, dirOverride);
383
+ const result = {
384
+ grade,
385
+ score,
386
+ guidance: item.guidance,
387
+ ability: { before, after },
388
+ // Surface the graduation outcome so the host can congratulate (on a
389
+ // promotion) or flag for review (on a demotion). When nothing changed we
390
+ // still report `changed: false` with the holding tier (US-3).
391
+ graduation: toGraduationResult(decision),
392
+ };
393
+ // Only surface the correct answer for MC (where `correctAnswer` is set);
394
+ // free-form items have no single key to reveal.
395
+ return correctAnswer !== undefined
396
+ ? { ...result, correctAnswer }
397
+ : result;
398
+ },
399
+ });
400
+ /** Default `submit_answer` module (env / `~/.vibe-hero`), used by the registry. */
401
+ export const submitAnswerTool = makeSubmitAnswerTool();
402
+ //# sourceMappingURL=submitAnswer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submitAnswer.js","sourceRoot":"","sources":["../../src/tools/submitAnswer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @file Tool-registry contract shared by every MCP tool module (T020).
3
+ *
4
+ * Each tool lives in its own module exporting a {@link ToolModule}: a name, a
5
+ * human description, a Zod `inputSchema` (an *object* schema — `index.ts` passes
6
+ * its `.shape` to the SDK's `registerTool`), and a `handler`. The handler is the
7
+ * pure tool logic; `index.ts` wraps it with the setup gate and adapts its plain
8
+ * JSON result into the SDK's `CallToolResult` shape.
9
+ *
10
+ * Keeping the handler return type a plain JSON object (not a `CallToolResult`)
11
+ * means tool logic stays decoupled from the transport: tests call handlers
12
+ * directly and assert on the JSON, and later tasks can replace placeholder
13
+ * handlers without touching transport wiring.
14
+ *
15
+ * Source of truth: specs/001-vibe-hero-mvp/contracts/mcp-tools.md.
16
+ */
17
+ import type { z } from "zod";
18
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
19
+ /**
20
+ * A tool's JSON result: any serializable object. Concrete tools narrow this to
21
+ * their contract's output schema (e.g. `GetConfigResult`); the registry keeps it
22
+ * open so all 10 modules share one shape.
23
+ */
24
+ export type ToolResult = Record<string, unknown>;
25
+ /** A tool handler: parsed+validated input in, JSON result out. */
26
+ export type ToolHandler<Args> = (args: Args) => Promise<ToolResult>;
27
+ /**
28
+ * One MCP tool definition. `inputSchema` is a Zod object schema; its `.shape`
29
+ * (a raw Zod shape, i.e. `Record<string, ZodType>`) is what the SDK's
30
+ * `registerTool` expects as `inputSchema`. The `Schema` generic ties the
31
+ * handler's argument to the schema's inferred type, so per-tool modules are
32
+ * fully typed at their definition site.
33
+ */
34
+ export interface ToolModule<Schema extends z.ZodObject<z.ZodRawShape> = z.ZodObject<z.ZodRawShape>> {
35
+ /** The tool's wire name (e.g. `"get_status"`). */
36
+ readonly name: string;
37
+ /** One-line description surfaced to the host agent. */
38
+ readonly description: string;
39
+ /** Zod object schema for the tool's input. */
40
+ readonly inputSchema: Schema;
41
+ /** Pure tool logic, operating on parsed input. */
42
+ readonly handler: ToolHandler<z.infer<Schema>>;
43
+ }
44
+ /**
45
+ * Schema-erased registry entry. The per-tool `Schema` generic makes precise
46
+ * {@link ToolModule} types mutually unassignable (the handler arg is
47
+ * contravariant), so the registry collection stores this widened form:
48
+ * `inputSchema` is a generic object schema and `handler` accepts `unknown`. The
49
+ * SDK validates input against the shape before our handler runs, so the handler
50
+ * may parse/narrow as needed; `index.ts` passes `unknown` straight through the
51
+ * gate to it.
52
+ */
53
+ export interface AnyToolModule {
54
+ readonly name: string;
55
+ readonly description: string;
56
+ readonly inputSchema: z.ZodObject<z.ZodRawShape>;
57
+ readonly handler: ToolHandler<unknown>;
58
+ }
59
+ /**
60
+ * Adapt a typed {@link ToolModule} into a schema-erased {@link AnyToolModule}
61
+ * for the registry. The returned handler parses raw input through the module's
62
+ * `inputSchema` (so the typed handler always receives validated, narrowed args)
63
+ * and delegates. Placeholder handlers that ignore their input pass through
64
+ * unchanged.
65
+ *
66
+ * @param tool - A fully-typed tool module.
67
+ * @returns The erased registry entry.
68
+ */
69
+ export declare const defineTool: <Schema extends z.ZodObject<z.ZodRawShape>>(tool: ToolModule<Schema>) => AnyToolModule;
70
+ /**
71
+ * Adapt a plain JSON {@link ToolResult} into the SDK's `CallToolResult`.
72
+ *
73
+ * We populate both `content` (a JSON text block, the universally-readable form
74
+ * for hosts/tests with no output schema) and `structuredContent` (the same
75
+ * object, machine-readable). This keeps results inspectable without registering
76
+ * an output schema for every tool yet.
77
+ *
78
+ * @param result - The tool's JSON result.
79
+ * @returns A well-formed `CallToolResult`.
80
+ */
81
+ export declare const toCallToolResult: (result: ToolResult) => CallToolResult;
82
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/tools/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEzE;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEjD,kEAAkE;AAClE,MAAM,MAAM,WAAW,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;AAEpE;;;;;;GAMG;AACH,MAAM,WAAW,UAAU,CACzB,MAAM,SAAS,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC;IAEtE,kDAAkD;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uDAAuD;IACvD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,8CAA8C;IAC9C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,kDAAkD;IAClD,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;CAChD;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IACjD,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;CACxC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,UAAU,GAAI,MAAM,SAAS,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,EAClE,MAAM,UAAU,CAAC,MAAM,CAAC,KACvB,aAMD,CAAC;AAEH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,GAAI,QAAQ,UAAU,KAAG,cAGpD,CAAC"}