@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,48 @@
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
+ /**
18
+ * Adapt a typed {@link ToolModule} into a schema-erased {@link AnyToolModule}
19
+ * for the registry. The returned handler parses raw input through the module's
20
+ * `inputSchema` (so the typed handler always receives validated, narrowed args)
21
+ * and delegates. Placeholder handlers that ignore their input pass through
22
+ * unchanged.
23
+ *
24
+ * @param tool - A fully-typed tool module.
25
+ * @returns The erased registry entry.
26
+ */
27
+ export const defineTool = (tool) => ({
28
+ name: tool.name,
29
+ description: tool.description,
30
+ inputSchema: tool.inputSchema,
31
+ handler: async (args) => tool.handler(tool.inputSchema.parse(args)),
32
+ });
33
+ /**
34
+ * Adapt a plain JSON {@link ToolResult} into the SDK's `CallToolResult`.
35
+ *
36
+ * We populate both `content` (a JSON text block, the universally-readable form
37
+ * for hosts/tests with no output schema) and `structuredContent` (the same
38
+ * object, machine-readable). This keeps results inspectable without registering
39
+ * an output schema for every tool yet.
40
+ *
41
+ * @param result - The tool's JSON result.
42
+ * @returns A well-formed `CallToolResult`.
43
+ */
44
+ export const toCallToolResult = (result) => ({
45
+ content: [{ type: "text", text: JSON.stringify(result) }],
46
+ structuredContent: result,
47
+ });
48
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/tools/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAmDH;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CACxB,IAAwB,EACT,EAAE,CAAC,CAAC;IACnB,IAAI,EAAE,IAAI,CAAC,IAAI;IACf,WAAW,EAAE,IAAI,CAAC,WAAW;IAC7B,WAAW,EAAE,IAAI,CAAC,WAAW;IAC7B,OAAO,EAAE,KAAK,EAAE,IAAa,EAAuB,EAAE,CACpD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,CAAoB,CAAC;CAChE,CAAC,CAAC;AAEH;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,MAAkB,EAAkB,EAAE,CAAC,CAAC;IACvE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;IACzD,iBAAiB,EAAE,MAAM;CAC1B,CAAC,CAAC"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * @file Shared standing/ranking helpers for the US-2 read tools (T025–T027).
3
+ *
4
+ * `get_status`, `get_guidance`, and (indirectly) `list_topics` all need the same
5
+ * derivation: given the loaded catalog topics + the learner profile, compute the
6
+ * per-(topic × class) standing — its {@link AbilityKey}, displayed tier, status,
7
+ * and current ability — and rank topics by weakness so the weakest/stale one can
8
+ * be suggested or auto-selected. Centralizing it here keeps the three tools DRY
9
+ * and guarantees they agree on what "weak", "not_started", and "due_for_review"
10
+ * mean.
11
+ *
12
+ * Pure + telemetry-free (SC-011): every value is derived from the catalog
13
+ * (bundled or fetched) plus the profile's `abilities`/`graduations` — NO
14
+ * observation/offer state is consulted, so the whole pull path works with zero
15
+ * telemetry.
16
+ *
17
+ * Source of truth: specs/001-vibe-hero-mvp/contracts/mcp-tools.md
18
+ * (`get_status` / `get_guidance`), spec.md US-2 / FR-021 / SC-011.
19
+ */
20
+ import { type AbilityKey, type ToolId } from "../../schemas/common.js";
21
+ import type { Topic } from "../../schemas/content.js";
22
+ import type { Profile } from "../../schemas/profile.js";
23
+ import type { StatusTopic } from "../../schemas/tools.js";
24
+ /**
25
+ * A topic's computed standing, plus the source {@link Topic} so callers can read
26
+ * its items/summary (e.g. for guidance text) without re-joining.
27
+ */
28
+ export interface TopicStanding {
29
+ /** The serialized `(class, topicId)` key this standing is keyed by. */
30
+ readonly key: AbilityKey;
31
+ /** The source catalog topic. */
32
+ readonly topic: Topic;
33
+ /** The per-topic status row, contract-shaped for `get_status`. */
34
+ readonly row: StatusTopic;
35
+ }
36
+ /**
37
+ * Whether `topic` is in scope for `tool`. A `general` topic is always in scope
38
+ * (it applies to every tool); a tool-scoped topic is in scope only when its tool
39
+ * matches. With no `tool` filter, every topic is in scope.
40
+ */
41
+ export declare const topicInScope: (topic: Topic, tool?: ToolId) => boolean;
42
+ /**
43
+ * Derive the standing for one topic from the profile (telemetry-free).
44
+ *
45
+ * - `tier`/`status` come from `graduations[key]`: a learner with no graduation
46
+ * entry has never graduated, so tier is `0` and status is `not_started`.
47
+ * Otherwise the displayed tier is `currentTier` and the status mirrors the
48
+ * graduation's `current` / `due_for_review`.
49
+ * - `ability` comes from `abilities[key].value`, defaulting to the cold-start
50
+ * {@link ASSESSMENT_CONFIG.startingAbility} when the learner has no estimate
51
+ * yet (so a never-touched topic reports a sensible baseline, not `0`).
52
+ */
53
+ export declare const standingFor: (topic: Topic, profile: Profile) => TopicStanding;
54
+ /**
55
+ * Compute every in-scope topic's standing, in catalog order.
56
+ *
57
+ * @param topics - The loaded catalog topics (bundled or fetched).
58
+ * @param profile - The learner profile (only `abilities`/`graduations` are read).
59
+ * @param tool - Optional tool filter; see {@link topicInScope}.
60
+ */
61
+ export declare const computeStandings: (topics: readonly Topic[], profile: Profile, tool?: ToolId) => TopicStanding[];
62
+ /**
63
+ * Detect knowledge lapse (FR-009 / OD-003) across a set of standings, given the
64
+ * caller-injected `now` (the engine math is PURE; status.ts reads the clock and
65
+ * passes it in — E5). For each currently-graduated topic that has gone stale and
66
+ * whose decayed effective ability has fallen near/under its lower band
67
+ * ({@link isDueForReview}), the topic is surfaced for review:
68
+ *
69
+ * - its status row is overlaid to `due_for_review` (so `get_status` reports the
70
+ * stale topic correctly even before the profile is rewritten), and
71
+ * - its key is collected as `newlyLapsed` so the caller can enqueue a
72
+ * `ReviewEntry{reason:"lapsed"}` and persist the `due_for_review` status.
73
+ *
74
+ * Topics already flagged `due_for_review` are skipped (idempotent — they are
75
+ * already surfaced). Ungraduated / `not_started` topics never lapse.
76
+ *
77
+ * Pure (clock injected). Returns NEW standing objects for the overlaid rows; the
78
+ * input array is not mutated.
79
+ *
80
+ * @param standings - In-scope standings (from {@link computeStandings}).
81
+ * @param profile - The learner profile (reads `abilities`/`graduations`).
82
+ * @param now - Reference ISO datetime, injected by the caller (E5).
83
+ * @returns The (possibly overlaid) standings plus the keys newly found lapsed.
84
+ */
85
+ export declare const detectLapses: (standings: readonly TopicStanding[], profile: Profile, now: string) => {
86
+ standings: TopicStanding[];
87
+ newlyLapsed: AbilityKey[];
88
+ };
89
+ /**
90
+ * Weakness ordering for suggestions / weakest-pick (lower ⇒ weaker ⇒ surfaced
91
+ * first):
92
+ * 1. `not_started` topics rank ahead of any started topic (they are the most
93
+ * valuable next step and need no telemetry to identify).
94
+ * 2. within each group, lower `ability` ranks first (the weakest area).
95
+ * 3. ties break on `key` for a stable, deterministic total order.
96
+ *
97
+ * @returns A negative/zero/positive comparator result for `Array#sort`.
98
+ */
99
+ export declare const byWeakness: (a: TopicStanding, b: TopicStanding) => number;
100
+ /**
101
+ * Rank in-scope standings weakest-first (stable, deterministic).
102
+ * A copy is returned; the input is never mutated.
103
+ */
104
+ export declare const rankByWeakness: (standings: readonly TopicStanding[]) => TopicStanding[];
105
+ /**
106
+ * A short, human reason for why a topic is suggested as a next step.
107
+ * `not_started` topics are framed as "not started yet"; everything else as a
108
+ * relative-weakness nudge.
109
+ */
110
+ export declare const suggestionReason: (standing: TopicStanding) => string;
111
+ //# sourceMappingURL=standing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"standing.d.ts","sourceRoot":"","sources":["../../../src/tools/us2/standing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,MAAM,EACZ,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAE1D;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;IACzB,gCAAgC;IAChC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,kEAAkE;IAClE,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC;CAC3B;AAED;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,KAAK,EAAE,OAAO,MAAM,KAAG,OAG1D,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,GAAI,OAAO,KAAK,EAAE,SAAS,OAAO,KAAG,aAiB5D,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAC3B,QAAQ,SAAS,KAAK,EAAE,EACxB,SAAS,OAAO,EAChB,OAAO,MAAM,KACZ,aAAa,EAGgC,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,YAAY,GACvB,WAAW,SAAS,aAAa,EAAE,EACnC,SAAS,OAAO,EAChB,KAAK,MAAM,KACV;IAAE,SAAS,EAAE,aAAa,EAAE,CAAC;IAAC,WAAW,EAAE,UAAU,EAAE,CAAA;CAezD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,UAAU,GAAI,GAAG,aAAa,EAAE,GAAG,aAAa,KAAG,MAM/D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,GACzB,WAAW,SAAS,aAAa,EAAE,KAClC,aAAa,EAAqC,CAAC;AAEtD;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAAI,UAAU,aAAa,KAAG,MAKmB,CAAC"}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * @file Shared standing/ranking helpers for the US-2 read tools (T025–T027).
3
+ *
4
+ * `get_status`, `get_guidance`, and (indirectly) `list_topics` all need the same
5
+ * derivation: given the loaded catalog topics + the learner profile, compute the
6
+ * per-(topic × class) standing — its {@link AbilityKey}, displayed tier, status,
7
+ * and current ability — and rank topics by weakness so the weakest/stale one can
8
+ * be suggested or auto-selected. Centralizing it here keeps the three tools DRY
9
+ * and guarantees they agree on what "weak", "not_started", and "due_for_review"
10
+ * mean.
11
+ *
12
+ * Pure + telemetry-free (SC-011): every value is derived from the catalog
13
+ * (bundled or fetched) plus the profile's `abilities`/`graduations` — NO
14
+ * observation/offer state is consulted, so the whole pull path works with zero
15
+ * telemetry.
16
+ *
17
+ * Source of truth: specs/001-vibe-hero-mvp/contracts/mcp-tools.md
18
+ * (`get_status` / `get_guidance`), spec.md US-2 / FR-021 / SC-011.
19
+ */
20
+ import { ASSESSMENT_CONFIG } from "../../config.js";
21
+ import { isDueForReview } from "../../engine/lapse.js";
22
+ import { abilityKey, } from "../../schemas/common.js";
23
+ /**
24
+ * Whether `topic` is in scope for `tool`. A `general` topic is always in scope
25
+ * (it applies to every tool); a tool-scoped topic is in scope only when its tool
26
+ * matches. With no `tool` filter, every topic is in scope.
27
+ */
28
+ export const topicInScope = (topic, tool) => {
29
+ if (tool === undefined)
30
+ return true;
31
+ return topic.class.kind === "general" || topic.class.tool === tool;
32
+ };
33
+ /**
34
+ * Derive the standing for one topic from the profile (telemetry-free).
35
+ *
36
+ * - `tier`/`status` come from `graduations[key]`: a learner with no graduation
37
+ * entry has never graduated, so tier is `0` and status is `not_started`.
38
+ * Otherwise the displayed tier is `currentTier` and the status mirrors the
39
+ * graduation's `current` / `due_for_review`.
40
+ * - `ability` comes from `abilities[key].value`, defaulting to the cold-start
41
+ * {@link ASSESSMENT_CONFIG.startingAbility} when the learner has no estimate
42
+ * yet (so a never-touched topic reports a sensible baseline, not `0`).
43
+ */
44
+ export const standingFor = (topic, profile) => {
45
+ const key = abilityKey(topic.class, topic.id);
46
+ const graduation = profile.graduations[key];
47
+ const ability = profile.abilities[key]?.value ?? ASSESSMENT_CONFIG.startingAbility;
48
+ const row = graduation === undefined
49
+ ? { key, title: topic.title, tier: 0, status: "not_started", ability }
50
+ : {
51
+ key,
52
+ title: topic.title,
53
+ tier: graduation.currentTier,
54
+ status: graduation.status,
55
+ ability,
56
+ };
57
+ return { key, topic, row };
58
+ };
59
+ /**
60
+ * Compute every in-scope topic's standing, in catalog order.
61
+ *
62
+ * @param topics - The loaded catalog topics (bundled or fetched).
63
+ * @param profile - The learner profile (only `abilities`/`graduations` are read).
64
+ * @param tool - Optional tool filter; see {@link topicInScope}.
65
+ */
66
+ export const computeStandings = (topics, profile, tool) => topics
67
+ .filter((topic) => topicInScope(topic, tool))
68
+ .map((topic) => standingFor(topic, profile));
69
+ /**
70
+ * Detect knowledge lapse (FR-009 / OD-003) across a set of standings, given the
71
+ * caller-injected `now` (the engine math is PURE; status.ts reads the clock and
72
+ * passes it in — E5). For each currently-graduated topic that has gone stale and
73
+ * whose decayed effective ability has fallen near/under its lower band
74
+ * ({@link isDueForReview}), the topic is surfaced for review:
75
+ *
76
+ * - its status row is overlaid to `due_for_review` (so `get_status` reports the
77
+ * stale topic correctly even before the profile is rewritten), and
78
+ * - its key is collected as `newlyLapsed` so the caller can enqueue a
79
+ * `ReviewEntry{reason:"lapsed"}` and persist the `due_for_review` status.
80
+ *
81
+ * Topics already flagged `due_for_review` are skipped (idempotent — they are
82
+ * already surfaced). Ungraduated / `not_started` topics never lapse.
83
+ *
84
+ * Pure (clock injected). Returns NEW standing objects for the overlaid rows; the
85
+ * input array is not mutated.
86
+ *
87
+ * @param standings - In-scope standings (from {@link computeStandings}).
88
+ * @param profile - The learner profile (reads `abilities`/`graduations`).
89
+ * @param now - Reference ISO datetime, injected by the caller (E5).
90
+ * @returns The (possibly overlaid) standings plus the keys newly found lapsed.
91
+ */
92
+ export const detectLapses = (standings, profile, now) => {
93
+ const newlyLapsed = [];
94
+ const overlaid = standings.map((standing) => {
95
+ const graduation = profile.graduations[standing.key];
96
+ const ability = profile.abilities[standing.key];
97
+ if (graduation === undefined || ability === undefined)
98
+ return standing;
99
+ if (!isDueForReview(graduation, ability, now))
100
+ return standing;
101
+ newlyLapsed.push(standing.key);
102
+ return {
103
+ ...standing,
104
+ row: { ...standing.row, status: "due_for_review" },
105
+ };
106
+ });
107
+ return { standings: overlaid, newlyLapsed };
108
+ };
109
+ /**
110
+ * Weakness ordering for suggestions / weakest-pick (lower ⇒ weaker ⇒ surfaced
111
+ * first):
112
+ * 1. `not_started` topics rank ahead of any started topic (they are the most
113
+ * valuable next step and need no telemetry to identify).
114
+ * 2. within each group, lower `ability` ranks first (the weakest area).
115
+ * 3. ties break on `key` for a stable, deterministic total order.
116
+ *
117
+ * @returns A negative/zero/positive comparator result for `Array#sort`.
118
+ */
119
+ export const byWeakness = (a, b) => {
120
+ const aNotStarted = a.row.status === "not_started" ? 0 : 1;
121
+ const bNotStarted = b.row.status === "not_started" ? 0 : 1;
122
+ if (aNotStarted !== bNotStarted)
123
+ return aNotStarted - bNotStarted;
124
+ if (a.row.ability !== b.row.ability)
125
+ return a.row.ability - b.row.ability;
126
+ return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
127
+ };
128
+ /**
129
+ * Rank in-scope standings weakest-first (stable, deterministic).
130
+ * A copy is returned; the input is never mutated.
131
+ */
132
+ export const rankByWeakness = (standings) => [...standings].sort(byWeakness);
133
+ /**
134
+ * A short, human reason for why a topic is suggested as a next step.
135
+ * `not_started` topics are framed as "not started yet"; everything else as a
136
+ * relative-weakness nudge.
137
+ */
138
+ export const suggestionReason = (standing) => standing.row.status === "not_started"
139
+ ? "Not started yet — a good place to begin."
140
+ : standing.row.status === "due_for_review"
141
+ ? "Due for review — knowledge may be going stale."
142
+ : `Weakest area so far (ability ≈ ${Math.round(standing.row.ability)}).`;
143
+ //# sourceMappingURL=standing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"standing.js","sourceRoot":"","sources":["../../../src/tools/us2/standing.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@vibe-hero/server",
3
+ "version": "0.1.0",
4
+ "description": "MCP server engine for the vibe-hero adaptive-learning plugin — not used standalone; install the vibe-hero Claude Code plugin, which launches this and ships the skills + hook.",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "claude-code",
9
+ "codex",
10
+ "learning",
11
+ "quiz",
12
+ "adaptive-learning",
13
+ "agentic-coding",
14
+ "education"
15
+ ],
16
+ "author": "Sjors Robroek",
17
+ "license": "Apache-2.0",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/srobroek/vibe-hero.git",
21
+ "directory": "packages/server"
22
+ },
23
+ "homepage": "https://github.com/srobroek/vibe-hero#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/srobroek/vibe-hero/issues"
26
+ },
27
+ "private": false,
28
+ "type": "module",
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "bin": {
33
+ "vibe-hero": "dist/cli/index.js"
34
+ },
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "dependencies": {
42
+ "@modelcontextprotocol/sdk": "^1.29.0",
43
+ "js-yaml": "^4.1.0",
44
+ "proper-lockfile": "^4.1.2",
45
+ "zod": "^3.24.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/js-yaml": "^4.0.9",
49
+ "@types/node": "^22.0.0",
50
+ "@types/proper-lockfile": "^4.1.4",
51
+ "typescript": "^5.7.0",
52
+ "vitest": "^3.0.0"
53
+ },
54
+ "scripts": {
55
+ "build": "tsc -p tsconfig.json && pnpm run copy-assets",
56
+ "copy-assets": "node -e \"const{cpSync,existsSync,mkdirSync}=require('node:fs');const path=require('node:path');function cp(s,d){if(existsSync(s)){mkdirSync(d,{recursive:true});cpSync(s,d,{recursive:true,filter:p=>!p.endsWith('.ts')});}}cp('src/catalog/bundled','dist/catalog/bundled');const root=path.resolve('../..');['claude-code','general'].forEach(cls=>{const s=path.join(root,'content',cls);const d=path.join('dist','catalog','bundled',cls);if(existsSync(s)){mkdirSync(d,{recursive:true});cpSync(s,d,{recursive:true});}});\"",
57
+ "test": "vitest run",
58
+ "test:watch": "vitest",
59
+ "start": "node dist/index.js",
60
+ "typecheck": "tsc --noEmit"
61
+ }
62
+ }