@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,38 @@
1
+ /**
2
+ * @file CLI entrypoint for the Stop-hook offer resolver (T037).
3
+ *
4
+ * Usage:
5
+ * node dist/cli/getOffer.js get-offer --session <sessionId> --tool <toolId>
6
+ *
7
+ * Prints a single JSON line to stdout and exits 0. Callers treat any non-zero
8
+ * exit or non-JSON stdout as "no offer / suppress silently" — this binary must
9
+ * NEVER produce a non-zero exit that would surface to the user as a hook error.
10
+ *
11
+ * Output shapes (always valid JSON on stdout, then newline):
12
+ *
13
+ * {"offer":{"key":"…","title":"…","prompt":"…"}}
14
+ * {"suppressed":"cadence"} (or "declined" | "offers_off" | "no_candidate")
15
+ * {"suppressed":"no_candidate"} (SETUP_REQUIRED or any unexpected result)
16
+ *
17
+ * This module is intentionally thin: it validates argv, delegates to the
18
+ * existing `get_offer` handler (reusing all offer-engine + cadence logic),
19
+ * and serializes the result. It does NOT re-implement any logic.
20
+ *
21
+ * Guard: if VIBE_HERO_STOP_HOOK_ACTIVE=1 is set we are already inside a Stop
22
+ * hook invocation (the Claude Code host is running the hook again after the
23
+ * hook itself triggered an action). In that case we exit 0 immediately with a
24
+ * suppressed result to prevent infinite loops.
25
+ *
26
+ * Source of truth: specs/001-vibe-hero-mvp/contracts/mcp-tools.md (`get_offer`),
27
+ * hooks/claude-code/stop-offer.sh, research.md (§ Observation & hook correlation).
28
+ */
29
+ /**
30
+ * Entry point. Called when this module is the process entrypoint (`node dist/cli/getOffer.js`).
31
+ *
32
+ * Resolves an offer for `sessionId` / `tool` by delegating to the existing
33
+ * `get_offer` tool handler, then writes the result as JSON and exits 0.
34
+ * Any error path writes to stderr and falls back to a suppressed result —
35
+ * the hook must NEVER propagate errors back to the user's session.
36
+ */
37
+ export declare const main: () => Promise<void>;
38
+ //# sourceMappingURL=getOffer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getOffer.d.ts","sourceRoot":"","sources":["../../src/cli/getOffer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AA4FH;;;;;;;GAOG;AACH,eAAO,MAAM,IAAI,QAAa,OAAO,CAAC,IAAI,CAwCzC,CAAC"}
@@ -0,0 +1,150 @@
1
+ /**
2
+ * @file CLI entrypoint for the Stop-hook offer resolver (T037).
3
+ *
4
+ * Usage:
5
+ * node dist/cli/getOffer.js get-offer --session <sessionId> --tool <toolId>
6
+ *
7
+ * Prints a single JSON line to stdout and exits 0. Callers treat any non-zero
8
+ * exit or non-JSON stdout as "no offer / suppress silently" — this binary must
9
+ * NEVER produce a non-zero exit that would surface to the user as a hook error.
10
+ *
11
+ * Output shapes (always valid JSON on stdout, then newline):
12
+ *
13
+ * {"offer":{"key":"…","title":"…","prompt":"…"}}
14
+ * {"suppressed":"cadence"} (or "declined" | "offers_off" | "no_candidate")
15
+ * {"suppressed":"no_candidate"} (SETUP_REQUIRED or any unexpected result)
16
+ *
17
+ * This module is intentionally thin: it validates argv, delegates to the
18
+ * existing `get_offer` handler (reusing all offer-engine + cadence logic),
19
+ * and serializes the result. It does NOT re-implement any logic.
20
+ *
21
+ * Guard: if VIBE_HERO_STOP_HOOK_ACTIVE=1 is set we are already inside a Stop
22
+ * hook invocation (the Claude Code host is running the hook again after the
23
+ * hook itself triggered an action). In that case we exit 0 immediately with a
24
+ * suppressed result to prevent infinite loops.
25
+ *
26
+ * Source of truth: specs/001-vibe-hero-mvp/contracts/mcp-tools.md (`get_offer`),
27
+ * hooks/claude-code/stop-offer.sh, research.md (§ Observation & hook correlation).
28
+ */
29
+ import { argv, env, stdout, stderr } from "node:process";
30
+ import { makeGetOfferTool } from "../tools/offers.js";
31
+ import { ToolIdSchema } from "../schemas/common.js";
32
+ // ---------------------------------------------------------------------------
33
+ // Infinite-loop guard
34
+ // ---------------------------------------------------------------------------
35
+ /**
36
+ * If the Stop hook itself triggers Claude Code activity that fires the Stop
37
+ * hook again, `stop_hook_active` would be `true` in the outer payload. We
38
+ * communicate that via this env var (set by the shell hook before calling us)
39
+ * so the CLI can exit immediately rather than re-entering the offer path.
40
+ */
41
+ const isReentrant = () => env["VIBE_HERO_STOP_HOOK_ACTIVE"] === "1";
42
+ /**
43
+ * Parse `argv[2..]` into a {@link CliArgs}. Returns `undefined` (and writes a
44
+ * diagnostic to stderr) when required args are missing or invalid.
45
+ */
46
+ const parseArgs = (args) => {
47
+ const [subcommand, ...rest] = args;
48
+ if (subcommand !== "get-offer") {
49
+ stderr.write(`vibe-hero cli: unknown subcommand ${JSON.stringify(subcommand ?? "(none)")}; expected "get-offer"\n`);
50
+ return undefined;
51
+ }
52
+ // Simple --key value flag parser (no external deps).
53
+ const flags = {};
54
+ for (let i = 0; i < rest.length - 1; i++) {
55
+ const flag = rest[i];
56
+ const value = rest[i + 1];
57
+ if (flag !== undefined && flag.startsWith("--") && value !== undefined && !value.startsWith("--")) {
58
+ flags[flag.slice(2)] = value;
59
+ i++; // skip consumed value
60
+ }
61
+ }
62
+ const sessionId = flags["session"];
63
+ if (!sessionId) {
64
+ stderr.write("vibe-hero cli: --session <sessionId> is required\n");
65
+ return undefined;
66
+ }
67
+ const rawTool = flags["tool"];
68
+ if (!rawTool) {
69
+ stderr.write("vibe-hero cli: --tool <toolId> is required\n");
70
+ return undefined;
71
+ }
72
+ const toolParse = ToolIdSchema.safeParse(rawTool);
73
+ if (!toolParse.success) {
74
+ stderr.write(`vibe-hero cli: unknown tool ${JSON.stringify(rawTool)}; valid values: claude-code, codex, kiro-cli, kiro-ide\n`);
75
+ return undefined;
76
+ }
77
+ return { subcommand, sessionId, tool: toolParse.data };
78
+ };
79
+ // ---------------------------------------------------------------------------
80
+ // Suppress result helper
81
+ // ---------------------------------------------------------------------------
82
+ /** Emit a suppressed result and exit 0 (used on all non-offer paths). */
83
+ const suppress = (reason = "no_candidate") => {
84
+ stdout.write(JSON.stringify({ suppressed: reason }) + "\n");
85
+ // process.exit is called by the caller after this returns; we don't call it
86
+ // here so the function remains unit-testable without killing the test process.
87
+ };
88
+ // ---------------------------------------------------------------------------
89
+ // Main
90
+ // ---------------------------------------------------------------------------
91
+ /**
92
+ * Entry point. Called when this module is the process entrypoint (`node dist/cli/getOffer.js`).
93
+ *
94
+ * Resolves an offer for `sessionId` / `tool` by delegating to the existing
95
+ * `get_offer` tool handler, then writes the result as JSON and exits 0.
96
+ * Any error path writes to stderr and falls back to a suppressed result —
97
+ * the hook must NEVER propagate errors back to the user's session.
98
+ */
99
+ export const main = async () => {
100
+ // Infinite-loop guard (FR-019 / research Stop hook).
101
+ if (isReentrant()) {
102
+ suppress("no_candidate");
103
+ return;
104
+ }
105
+ const args = parseArgs(argv.slice(2));
106
+ if (args === undefined) {
107
+ suppress("no_candidate");
108
+ return;
109
+ }
110
+ // Delegate entirely to the real get_offer handler — no logic duplication.
111
+ const tool = makeGetOfferTool();
112
+ let result;
113
+ try {
114
+ result = await tool.handler({ sessionId: args.sessionId, tool: args.tool });
115
+ }
116
+ catch (err) {
117
+ stderr.write(`vibe-hero cli: get_offer handler threw unexpectedly: ${String(err)}\n`);
118
+ suppress("no_candidate");
119
+ return;
120
+ }
121
+ // The handler may return a SETUP_REQUIRED gate sentinel (profile not
122
+ // configured yet) or a GetOfferResult. In either case we serialize safely.
123
+ if (typeof result === "object" &&
124
+ result !== null &&
125
+ "status" in result &&
126
+ result.status === "SETUP_REQUIRED") {
127
+ // Setup not done yet — suppress silently; the hook must not break the session.
128
+ suppress("no_candidate");
129
+ return;
130
+ }
131
+ stdout.write(JSON.stringify(result) + "\n");
132
+ };
133
+ // ---------------------------------------------------------------------------
134
+ // Entrypoint guard (same pattern as index.ts)
135
+ // ---------------------------------------------------------------------------
136
+ import { fileURLToPath } from "node:url";
137
+ const isEntrypoint = () => {
138
+ const entry = argv[1];
139
+ if (entry === undefined)
140
+ return false;
141
+ return fileURLToPath(import.meta.url) === entry;
142
+ };
143
+ if (isEntrypoint()) {
144
+ main().catch((err) => {
145
+ stderr.write(`vibe-hero cli: fatal: ${String(err)}\n`);
146
+ // Exit 0 — never propagate errors to the Stop hook caller.
147
+ process.exitCode = 0;
148
+ });
149
+ }
150
+ //# sourceMappingURL=getOffer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getOffer.js","sourceRoot":"","sources":["../../src/cli/getOffer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file vibe-hero `bin` dispatcher (T004) — the published CLI entrypoint.
4
+ *
5
+ * This is the single `bin` target declared in package.json
6
+ * (`bin: { "vibe-hero": "dist/cli/index.js" }`). It is a THIN router over the
7
+ * two existing entry points; it contains NO business logic of its own:
8
+ *
9
+ * - no subcommand, or `mcp` → start the stdio MCP server ({@link serverMain}
10
+ * from `../index.js`). This is the primary
11
+ * invocation (`npx -y @vibe-hero/server`) used by
12
+ * the plugin's `.mcp.json` (FR-002, FR-008).
13
+ * - `get-offer …` → run the OPTIONAL Stop-hook offer utility
14
+ * ({@link getOfferMain} from `./getOffer.js`),
15
+ * passing the remaining flags through. The Claude
16
+ * Code Stop hook is agent-mediated and does NOT
17
+ * call this (FR-011); it is retained only for
18
+ * non-Claude-Code hosts and debugging.
19
+ * - any other subcommand → a usage line to stderr, nonzero exit.
20
+ *
21
+ * Routing keys off `process.argv[2]` (the first user arg). For `get-offer` the
22
+ * remaining args (`--session`, `--tool`, …) are already on `process.argv`, and
23
+ * {@link getOfferMain} reads them via `argv.slice(2)` itself — so the dispatcher
24
+ * does not re-parse or re-pass them.
25
+ *
26
+ * Import safety (no double-run): `../index.js` and `./getOffer.js` each auto-run
27
+ * their own `main` ONLY when THEY are the process entrypoint, via a
28
+ * `fileURLToPath(import.meta.url) === argv[1]` guard. When this dispatcher is the
29
+ * entrypoint, `argv[1]` is `dist/cli/index.js`, which never equals either of
30
+ * their module URLs — so importing their `main` here triggers nothing. This file
31
+ * carries the SAME guard, so importing IT (e.g. from tests) is likewise
32
+ * side-effect-free.
33
+ *
34
+ * Source of truth: specs/002-distribution/contracts/cli-and-plugin.md,
35
+ * spec.md FR-002 / FR-008 / FR-011, quickstart.md V2.
36
+ */
37
+ /**
38
+ * Route `process.argv` to the correct existing `main`. Returns the selected
39
+ * `main`'s promise so the caller can await it (and surface a nonzero exit on an
40
+ * unknown subcommand).
41
+ *
42
+ * @param args - The full `process.argv` (the dispatcher reads `args[2]`).
43
+ * @returns A promise that resolves when the routed entrypoint completes.
44
+ */
45
+ export declare const dispatch: (args: readonly string[]) => Promise<void>;
46
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAcH;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,GAAU,MAAM,SAAS,MAAM,EAAE,KAAG,OAAO,CAAC,IAAI,CAsBpE,CAAC"}
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file vibe-hero `bin` dispatcher (T004) — the published CLI entrypoint.
4
+ *
5
+ * This is the single `bin` target declared in package.json
6
+ * (`bin: { "vibe-hero": "dist/cli/index.js" }`). It is a THIN router over the
7
+ * two existing entry points; it contains NO business logic of its own:
8
+ *
9
+ * - no subcommand, or `mcp` → start the stdio MCP server ({@link serverMain}
10
+ * from `../index.js`). This is the primary
11
+ * invocation (`npx -y @vibe-hero/server`) used by
12
+ * the plugin's `.mcp.json` (FR-002, FR-008).
13
+ * - `get-offer …` → run the OPTIONAL Stop-hook offer utility
14
+ * ({@link getOfferMain} from `./getOffer.js`),
15
+ * passing the remaining flags through. The Claude
16
+ * Code Stop hook is agent-mediated and does NOT
17
+ * call this (FR-011); it is retained only for
18
+ * non-Claude-Code hosts and debugging.
19
+ * - any other subcommand → a usage line to stderr, nonzero exit.
20
+ *
21
+ * Routing keys off `process.argv[2]` (the first user arg). For `get-offer` the
22
+ * remaining args (`--session`, `--tool`, …) are already on `process.argv`, and
23
+ * {@link getOfferMain} reads them via `argv.slice(2)` itself — so the dispatcher
24
+ * does not re-parse or re-pass them.
25
+ *
26
+ * Import safety (no double-run): `../index.js` and `./getOffer.js` each auto-run
27
+ * their own `main` ONLY when THEY are the process entrypoint, via a
28
+ * `fileURLToPath(import.meta.url) === argv[1]` guard. When this dispatcher is the
29
+ * entrypoint, `argv[1]` is `dist/cli/index.js`, which never equals either of
30
+ * their module URLs — so importing their `main` here triggers nothing. This file
31
+ * carries the SAME guard, so importing IT (e.g. from tests) is likewise
32
+ * side-effect-free.
33
+ *
34
+ * Source of truth: specs/002-distribution/contracts/cli-and-plugin.md,
35
+ * spec.md FR-002 / FR-008 / FR-011, quickstart.md V2.
36
+ */
37
+ import { fileURLToPath } from "node:url";
38
+ import { argv, stderr } from "node:process";
39
+ import { main as serverMain } from "../index.js";
40
+ import { main as getOfferMain } from "./getOffer.js";
41
+ /** The bin name, used in the usage line. Matches package.json `bin`. */
42
+ const BIN_NAME = "vibe-hero";
43
+ /** One-line usage shown on stderr for an unknown subcommand. */
44
+ const USAGE = `usage: ${BIN_NAME} [mcp | get-offer --session <id> --tool <toolId>]`;
45
+ /**
46
+ * Route `process.argv` to the correct existing `main`. Returns the selected
47
+ * `main`'s promise so the caller can await it (and surface a nonzero exit on an
48
+ * unknown subcommand).
49
+ *
50
+ * @param args - The full `process.argv` (the dispatcher reads `args[2]`).
51
+ * @returns A promise that resolves when the routed entrypoint completes.
52
+ */
53
+ export const dispatch = async (args) => {
54
+ // args[0] = node, args[1] = this script, args[2] = first user subcommand.
55
+ const subcommand = args[2];
56
+ // Default (no subcommand) or explicit `mcp` → the stdio MCP server.
57
+ if (subcommand === undefined || subcommand === "mcp") {
58
+ await serverMain();
59
+ return;
60
+ }
61
+ // `get-offer` → the optional offer utility. It reads its own flags from
62
+ // `argv.slice(2)`, so the remaining args pass through untouched.
63
+ if (subcommand === "get-offer") {
64
+ await getOfferMain();
65
+ return;
66
+ }
67
+ // Anything else is an error: usage line to stderr, nonzero exit.
68
+ stderr.write(`${BIN_NAME}: unknown subcommand ${JSON.stringify(subcommand)}\n${USAGE}\n`);
69
+ process.exitCode = 2;
70
+ };
71
+ /**
72
+ * Entrypoint guard: only auto-dispatch when this module is the process
73
+ * entrypoint (`node .../cli/index.js`), not when imported by tests. Mirrors the
74
+ * `import.meta.url === argv[1]` pattern in `../index.js` and `./getOffer.js`.
75
+ */
76
+ const isEntrypoint = () => {
77
+ const entry = argv[1];
78
+ if (entry === undefined)
79
+ return false;
80
+ return fileURLToPath(import.meta.url) === entry;
81
+ };
82
+ if (isEntrypoint()) {
83
+ dispatch(argv).catch((err) => {
84
+ stderr.write(`${BIN_NAME}: fatal: ${String(err)}\n`);
85
+ process.exitCode = 1;
86
+ });
87
+ }
88
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE5C,OAAO,EAAE,IAAI,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,IAAI,IAAI,YAAY,EAAE,MAAM,eAAe,CAAC;AAErD,wEAAwE;AACxE,MAAM,QAAQ,GAAG,WAAW,CAAC;AAE7B,gEAAgE;AAChE,MAAM,KAAK,GAAG,UAAU,QAAQ,mDAAmD,CAAC;AAEpF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,EAAE,IAAuB,EAAiB,EAAE;IACvE,0EAA0E;IAC1E,MAAM,UAAU,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IAE3B,oEAAoE;IACpE,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,KAAK,EAAE,CAAC;QACrD,MAAM,UAAU,EAAE,CAAC;QACnB,OAAO;IACT,CAAC;IAED,wEAAwE;IACxE,iEAAiE;IACjE,IAAI,UAAU,KAAK,WAAW,EAAE,CAAC;QAC/B,MAAM,YAAY,EAAE,CAAC;QACrB,OAAO;IACT,CAAC;IAED,iEAAiE;IACjE,MAAM,CAAC,KAAK,CACV,GAAG,QAAQ,wBAAwB,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,KAAK,KAAK,IAAI,CAC5E,CAAC;IACF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,YAAY,GAAG,GAAY,EAAE;IACjC,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IACtB,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtC,OAAO,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC;AAClD,CAAC,CAAC;AAEF,IAAI,YAAY,EAAE,EAAE,CAAC;IACnB,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACpC,MAAM,CAAC,KAAK,CAAC,GAAG,QAAQ,YAAY,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACrD,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IACvB,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Tunable assessment configuration (OD-005, research.md).
3
+ * Pure data — no logic, no imports. All engine modules read from here.
4
+ * Change values here; nothing is hard-coded across the codebase.
5
+ */
6
+ export declare const ASSESSMENT_CONFIG: {
7
+ readonly scale: 400;
8
+ readonly startingAbility: 300;
9
+ readonly difficultySeeds: {
10
+ readonly easy: 200;
11
+ readonly medium: 300;
12
+ readonly hard: 400;
13
+ };
14
+ readonly kProvisional: 64;
15
+ readonly kSettled: 24;
16
+ readonly settleAfterItems: 15;
17
+ readonly tierCenters: readonly [100, 200, 300, 400, 500];
18
+ readonly tierBoundaries: readonly [150, 250, 350, 450];
19
+ readonly hysteresisMargin: 30;
20
+ readonly dwell: 2;
21
+ readonly decayHalfLifeDays: 60;
22
+ readonly stalenessWindowDays: 30;
23
+ readonly targetOffset: 50;
24
+ readonly selectWindow: 60;
25
+ readonly anchorWindow: 20;
26
+ readonly declineMuteThreshold: 3;
27
+ readonly backoffBaseHours: 24;
28
+ readonly backoffFactor: 2;
29
+ readonly freeFormPassThreshold: 0.6;
30
+ readonly defaultQuizLength: 4;
31
+ };
32
+ /** Inferred type of the assessment configuration object. */
33
+ export type AssessmentConfig = typeof ASSESSMENT_CONFIG;
34
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;CAkEpB,CAAC;AAEX,4DAA4D;AAC5D,MAAM,MAAM,gBAAgB,GAAG,OAAO,iBAAiB,CAAC"}
package/dist/config.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Tunable assessment configuration (OD-005, research.md).
3
+ * Pure data — no logic, no imports. All engine modules read from here.
4
+ * Change values here; nothing is hard-coded across the codebase.
5
+ */
6
+ export const ASSESSMENT_CONFIG = {
7
+ // --- Elo logistic scale -------------------------------------------------
8
+ // S=400 matches standard chess Elo: a 400-point gap → ~91% win expectation.
9
+ scale: 400,
10
+ // Starting ability for a fresh (topic × tool) estimate — mid-scale.
11
+ startingAbility: 300,
12
+ // Item difficulty seeds by tag; authored at content creation, NEVER updated
13
+ // at runtime (two-way Elo would corrupt the scale with a single learner).
14
+ difficultySeeds: {
15
+ easy: 200,
16
+ medium: 300,
17
+ hard: 400,
18
+ },
19
+ // --- K-factor (learning rate) -------------------------------------------
20
+ // High K while provisional to converge quickly from a cold start.
21
+ kProvisional: 64,
22
+ // Smaller K once settled — reduces noise from individual questions.
23
+ kSettled: 24,
24
+ // Number of graded items after which the estimate is considered settled.
25
+ settleAfterItems: 15,
26
+ // --- Tier centers and boundaries ----------------------------------------
27
+ // Five tiers spread across the 0–600 ability range.
28
+ tierCenters: [100, 200, 300, 400, 500],
29
+ // Boundaries sit halfway between adjacent centers.
30
+ tierBoundaries: [150, 250, 350, 450],
31
+ // --- Hysteresis + dwell (anti-flip-flop, FR-008 / SC-014) ---------------
32
+ // Promote at boundary+30; demote/review only below boundary-30.
33
+ hysteresisMargin: 30,
34
+ // Crossing must hold for this many consecutive graded items before acting.
35
+ dwell: 2,
36
+ // --- Spaced-review / lapse model (OD-003) --------------------------------
37
+ // Exponential ability-decay half-life in days (tier-tunable; e.g. 90 for
38
+ // tier 500 where knowledge is harder-won and decays more slowly).
39
+ decayHalfLifeDays: 60,
40
+ // Topic is due for review when days_since_last >= this threshold.
41
+ stalenessWindowDays: 30,
42
+ // --- Item selection (OD-005 table) --------------------------------------
43
+ // Target difficulty = min(θ + targetOffset, nextBoundary + hysteresisMargin).
44
+ targetOffset: 50,
45
+ // ± window around the target difficulty in which items are eligible.
46
+ selectWindow: 60,
47
+ // One anchor item must fall within ±anchorWindow of the current ability θ.
48
+ anchorWindow: 20,
49
+ // --- Offer / decline suppression (FR-020b) ------------------------------
50
+ // After this many consecutive declines across sessions, mute offers globally.
51
+ declineMuteThreshold: 3,
52
+ // Cross-session backoff base delay (hours) after the first decline.
53
+ backoffBaseHours: 24,
54
+ // Exponential factor applied per additional consecutive decline.
55
+ backoffFactor: 2,
56
+ // --- Free-form judging (OD-002) -----------------------------------------
57
+ // Minimum fraction of criteria that must be met for a free-form pass.
58
+ freeFormPassThreshold: 0.6,
59
+ // --- Quiz length (FR-022) -----------------------------------------------
60
+ // Default number of items per quiz session; configurable 3–5 by the user.
61
+ defaultQuizLength: 4,
62
+ };
63
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,2EAA2E;IAC3E,4EAA4E;IAC5E,KAAK,EAAE,GAAG;IAEV,oEAAoE;IACpE,eAAe,EAAE,GAAG;IAEpB,4EAA4E;IAC5E,0EAA0E;IAC1E,eAAe,EAAE;QACf,IAAI,EAAE,GAAG;QACT,MAAM,EAAE,GAAG;QACX,IAAI,EAAE,GAAG;KACV;IAED,2EAA2E;IAC3E,kEAAkE;IAClE,YAAY,EAAE,EAAE;IAChB,oEAAoE;IACpE,QAAQ,EAAE,EAAE;IACZ,yEAAyE;IACzE,gBAAgB,EAAE,EAAE;IAEpB,2EAA2E;IAC3E,oDAAoD;IACpD,WAAW,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAU;IAC/C,mDAAmD;IACnD,cAAc,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAU;IAE7C,2EAA2E;IAC3E,gEAAgE;IAChE,gBAAgB,EAAE,EAAE;IACpB,2EAA2E;IAC3E,KAAK,EAAE,CAAC;IAER,4EAA4E;IAC5E,yEAAyE;IACzE,kEAAkE;IAClE,iBAAiB,EAAE,EAAE;IACrB,kEAAkE;IAClE,mBAAmB,EAAE,EAAE;IAEvB,2EAA2E;IAC3E,8EAA8E;IAC9E,YAAY,EAAE,EAAE;IAChB,qEAAqE;IACrE,YAAY,EAAE,EAAE;IAChB,2EAA2E;IAC3E,YAAY,EAAE,EAAE;IAEhB,2EAA2E;IAC3E,8EAA8E;IAC9E,oBAAoB,EAAE,CAAC;IACvB,oEAAoE;IACpE,gBAAgB,EAAE,EAAE;IACpB,iEAAiE;IACjE,aAAa,EAAE,CAAC;IAEhB,2EAA2E;IAC3E,sEAAsE;IACtE,qBAAqB,EAAE,GAAG;IAE1B,2EAA2E;IAC3E,0EAA0E;IAC1E,iBAAiB,EAAE,CAAC;CACZ,CAAC"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @file PURE Elo ability-estimation engine (T010, OD-005 / research.md).
3
+ *
4
+ * Single-learner Elo: a learner's continuous `ability` (θ) is updated online
5
+ * against FIXED, authored item difficulties (d). Item difficulty is INPUT DATA
6
+ * and is never mutated here — only the learner's ability moves (invariant E3).
7
+ *
8
+ * This module is IO-FREE and time-free (invariant E5): it never reads the
9
+ * clock, filesystem, or network. Any time-dependent behaviour (decay,
10
+ * staleness) lives elsewhere and passes time in explicitly.
11
+ *
12
+ * Formulas (OD-005):
13
+ * E = 1 / (1 + 10^((d − θ) / S)) // expected score
14
+ * K = provisional (64) while itemsSeen < settleAfterItems (15), else 24
15
+ * θ' = θ + K · (score − E) // score ∈ [0,1] partial credit
16
+ *
17
+ * Source of truth: specs/001-vibe-hero-mvp/research.md (OD-005);
18
+ * constants in ../config.ts (ASSESSMENT_CONFIG).
19
+ */
20
+ /**
21
+ * Expected score (win probability) of a learner with ability `ability` (θ)
22
+ * facing an item of difficulty `itemDifficulty` (d), under the logistic Elo
23
+ * model: `E = 1 / (1 + 10^((d − θ) / scale))`.
24
+ *
25
+ * Monotonically increasing in `(θ − d)`: a higher relative ability yields a
26
+ * higher expected score. When `θ === d` the expectation is exactly `0.5`.
27
+ *
28
+ * @param ability - The learner's current ability estimate (θ).
29
+ * @param itemDifficulty - The item's FIXED authored difficulty (d). Read-only.
30
+ * @param scale - Logistic scale `S` (default {@link ASSESSMENT_CONFIG.scale}).
31
+ * @returns The expected score in the open interval (0, 1).
32
+ */
33
+ export declare const expectedScore: (ability: number, itemDifficulty: number, scale?: number) => number;
34
+ /**
35
+ * The learning-rate `K` for the next update. High while the estimate is
36
+ * provisional (cold start) so it converges quickly, then smaller once settled
37
+ * to damp single-question noise.
38
+ *
39
+ * @param itemsSeen - Count of graded items already incorporated for this
40
+ * (topic × tool) estimate.
41
+ * @returns {@link ASSESSMENT_CONFIG.kProvisional} when
42
+ * `itemsSeen < ASSESSMENT_CONFIG.settleAfterItems`, otherwise
43
+ * {@link ASSESSMENT_CONFIG.kSettled}.
44
+ */
45
+ export declare const kFactor: (itemsSeen: number) => number;
46
+ /** The result of an ability update: the new ability and incremented count. */
47
+ export interface AbilityUpdate {
48
+ /** The updated ability estimate θ'. */
49
+ readonly value: number;
50
+ /** `itemsSeen` after incorporating this graded item (input + 1). */
51
+ readonly itemsSeen: number;
52
+ }
53
+ /**
54
+ * Apply one graded item to a learner's ability estimate (PURE).
55
+ *
56
+ * Computes `θ' = θ + K · (score − E)` where `E = expectedScore(θ, d)` and
57
+ * `K = kFactor(itemsSeen)`, then increments `itemsSeen`. The `score` is partial
58
+ * credit in `[0, 1]` (e.g. 1 = fully correct, 0 = fully wrong, intermediate for
59
+ * a partially-met free-form rubric).
60
+ *
61
+ * The fixed-difficulty invariant (E3) holds: `itemDifficulty` is read only and
62
+ * never returned or mutated — this function moves ONLY the learner's ability.
63
+ *
64
+ * A correct answer (`score > E`) raises θ; a wrong one (`score < E`) lowers it.
65
+ * Because the update is proportional to `(score − E)`, a correct answer on a
66
+ * HARDER item (lower E) raises θ more than the same correct answer on an EASIER
67
+ * item (higher E).
68
+ *
69
+ * @param ability - The learner's current ability estimate (θ).
70
+ * @param itemsSeen - Graded items already incorporated (≥ 0).
71
+ * @param itemDifficulty - The item's FIXED authored difficulty (d). Read-only.
72
+ * @param score - Partial-credit outcome in `[0, 1]`.
73
+ * @returns The new ability and `itemsSeen + 1`.
74
+ */
75
+ export declare const updateAbility: (ability: number, itemsSeen: number, itemDifficulty: number, score: number) => AbilityUpdate;
76
+ //# sourceMappingURL=elo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elo.d.ts","sourceRoot":"","sources":["../../src/engine/elo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa,GACxB,SAAS,MAAM,EACf,gBAAgB,MAAM,EACtB,QAAO,MAAgC,KACtC,MAA8D,CAAC;AAElE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,OAAO,GAAI,WAAW,MAAM,KAAG,MAGZ,CAAC;AAEjC,8EAA8E;AAC9E,MAAM,WAAW,aAAa;IAC5B,uCAAuC;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oEAAoE;IACpE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,aAAa,GACxB,SAAS,MAAM,EACf,WAAW,MAAM,EACjB,gBAAgB,MAAM,EACtB,OAAO,MAAM,KACZ,aAOF,CAAC"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @file PURE Elo ability-estimation engine (T010, OD-005 / research.md).
3
+ *
4
+ * Single-learner Elo: a learner's continuous `ability` (θ) is updated online
5
+ * against FIXED, authored item difficulties (d). Item difficulty is INPUT DATA
6
+ * and is never mutated here — only the learner's ability moves (invariant E3).
7
+ *
8
+ * This module is IO-FREE and time-free (invariant E5): it never reads the
9
+ * clock, filesystem, or network. Any time-dependent behaviour (decay,
10
+ * staleness) lives elsewhere and passes time in explicitly.
11
+ *
12
+ * Formulas (OD-005):
13
+ * E = 1 / (1 + 10^((d − θ) / S)) // expected score
14
+ * K = provisional (64) while itemsSeen < settleAfterItems (15), else 24
15
+ * θ' = θ + K · (score − E) // score ∈ [0,1] partial credit
16
+ *
17
+ * Source of truth: specs/001-vibe-hero-mvp/research.md (OD-005);
18
+ * constants in ../config.ts (ASSESSMENT_CONFIG).
19
+ */
20
+ import { ASSESSMENT_CONFIG } from "../config.js";
21
+ /**
22
+ * Expected score (win probability) of a learner with ability `ability` (θ)
23
+ * facing an item of difficulty `itemDifficulty` (d), under the logistic Elo
24
+ * model: `E = 1 / (1 + 10^((d − θ) / scale))`.
25
+ *
26
+ * Monotonically increasing in `(θ − d)`: a higher relative ability yields a
27
+ * higher expected score. When `θ === d` the expectation is exactly `0.5`.
28
+ *
29
+ * @param ability - The learner's current ability estimate (θ).
30
+ * @param itemDifficulty - The item's FIXED authored difficulty (d). Read-only.
31
+ * @param scale - Logistic scale `S` (default {@link ASSESSMENT_CONFIG.scale}).
32
+ * @returns The expected score in the open interval (0, 1).
33
+ */
34
+ export const expectedScore = (ability, itemDifficulty, scale = ASSESSMENT_CONFIG.scale) => 1 / (1 + 10 ** ((itemDifficulty - ability) / scale));
35
+ /**
36
+ * The learning-rate `K` for the next update. High while the estimate is
37
+ * provisional (cold start) so it converges quickly, then smaller once settled
38
+ * to damp single-question noise.
39
+ *
40
+ * @param itemsSeen - Count of graded items already incorporated for this
41
+ * (topic × tool) estimate.
42
+ * @returns {@link ASSESSMENT_CONFIG.kProvisional} when
43
+ * `itemsSeen < ASSESSMENT_CONFIG.settleAfterItems`, otherwise
44
+ * {@link ASSESSMENT_CONFIG.kSettled}.
45
+ */
46
+ export const kFactor = (itemsSeen) => itemsSeen < ASSESSMENT_CONFIG.settleAfterItems
47
+ ? ASSESSMENT_CONFIG.kProvisional
48
+ : ASSESSMENT_CONFIG.kSettled;
49
+ /**
50
+ * Apply one graded item to a learner's ability estimate (PURE).
51
+ *
52
+ * Computes `θ' = θ + K · (score − E)` where `E = expectedScore(θ, d)` and
53
+ * `K = kFactor(itemsSeen)`, then increments `itemsSeen`. The `score` is partial
54
+ * credit in `[0, 1]` (e.g. 1 = fully correct, 0 = fully wrong, intermediate for
55
+ * a partially-met free-form rubric).
56
+ *
57
+ * The fixed-difficulty invariant (E3) holds: `itemDifficulty` is read only and
58
+ * never returned or mutated — this function moves ONLY the learner's ability.
59
+ *
60
+ * A correct answer (`score > E`) raises θ; a wrong one (`score < E`) lowers it.
61
+ * Because the update is proportional to `(score − E)`, a correct answer on a
62
+ * HARDER item (lower E) raises θ more than the same correct answer on an EASIER
63
+ * item (higher E).
64
+ *
65
+ * @param ability - The learner's current ability estimate (θ).
66
+ * @param itemsSeen - Graded items already incorporated (≥ 0).
67
+ * @param itemDifficulty - The item's FIXED authored difficulty (d). Read-only.
68
+ * @param score - Partial-credit outcome in `[0, 1]`.
69
+ * @returns The new ability and `itemsSeen + 1`.
70
+ */
71
+ export const updateAbility = (ability, itemsSeen, itemDifficulty, score) => {
72
+ const expected = expectedScore(ability, itemDifficulty);
73
+ const k = kFactor(itemsSeen);
74
+ return {
75
+ value: ability + k * (score - expected),
76
+ itemsSeen: itemsSeen + 1,
77
+ };
78
+ };
79
+ //# sourceMappingURL=elo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elo.js","sourceRoot":"","sources":["../../src/engine/elo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEjD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,OAAe,EACf,cAAsB,EACtB,QAAgB,iBAAiB,CAAC,KAAK,EAC/B,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,cAAc,GAAG,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC;AAElE;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,SAAiB,EAAU,EAAE,CACnD,SAAS,GAAG,iBAAiB,CAAC,gBAAgB;IAC5C,CAAC,CAAC,iBAAiB,CAAC,YAAY;IAChC,CAAC,CAAC,iBAAiB,CAAC,QAAQ,CAAC;AAUjC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,OAAe,EACf,SAAiB,EACjB,cAAsB,EACtB,KAAa,EACE,EAAE;IACjB,MAAM,QAAQ,GAAG,aAAa,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;IACxD,MAAM,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IAC7B,OAAO;QACL,KAAK,EAAE,OAAO,GAAG,CAAC,GAAG,CAAC,KAAK,GAAG,QAAQ,CAAC;QACvC,SAAS,EAAE,SAAS,GAAG,CAAC;KACzB,CAAC;AACJ,CAAC,CAAC"}