@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.
- package/LICENSE +190 -0
- package/README.md +151 -0
- package/dist/catalog/bundled/claude-code/.gitkeep +0 -0
- package/dist/catalog/bundled/claude-code/context-management.yaml +302 -0
- package/dist/catalog/bundled/claude-code/planning.yaml +313 -0
- package/dist/catalog/bundled/claude-code/subagents.yaml +357 -0
- package/dist/catalog/bundled/general/.gitkeep +0 -0
- package/dist/catalog/bundled/general/_placeholder.yaml +39 -0
- package/dist/catalog/bundled/general/task-decomposition.yaml +390 -0
- package/dist/catalog/bundled/index.d.ts +39 -0
- package/dist/catalog/bundled/index.d.ts.map +1 -0
- package/dist/catalog/bundled/index.js +41 -0
- package/dist/catalog/bundled/index.js.map +1 -0
- package/dist/catalog/fetcher.d.ts +201 -0
- package/dist/catalog/fetcher.d.ts.map +1 -0
- package/dist/catalog/fetcher.js +452 -0
- package/dist/catalog/fetcher.js.map +1 -0
- package/dist/catalog/loader.d.ts +165 -0
- package/dist/catalog/loader.d.ts.map +1 -0
- package/dist/catalog/loader.js +241 -0
- package/dist/catalog/loader.js.map +1 -0
- package/dist/catalog/resolve.d.ts +85 -0
- package/dist/catalog/resolve.d.ts.map +1 -0
- package/dist/catalog/resolve.js +103 -0
- package/dist/catalog/resolve.js.map +1 -0
- package/dist/cli/getOffer.d.ts +38 -0
- package/dist/cli/getOffer.d.ts.map +1 -0
- package/dist/cli/getOffer.js +150 -0
- package/dist/cli/getOffer.js.map +1 -0
- package/dist/cli/index.d.ts +46 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +88 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config.d.ts +34 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +63 -0
- package/dist/config.js.map +1 -0
- package/dist/engine/elo.d.ts +76 -0
- package/dist/engine/elo.d.ts.map +1 -0
- package/dist/engine/elo.js +79 -0
- package/dist/engine/elo.js.map +1 -0
- package/dist/engine/graduation.d.ts +108 -0
- package/dist/engine/graduation.d.ts.map +1 -0
- package/dist/engine/graduation.js +161 -0
- package/dist/engine/graduation.js.map +1 -0
- package/dist/engine/lapse.d.ts +80 -0
- package/dist/engine/lapse.d.ts.map +1 -0
- package/dist/engine/lapse.js +125 -0
- package/dist/engine/lapse.js.map +1 -0
- package/dist/engine/selection.d.ts +84 -0
- package/dist/engine/selection.d.ts.map +1 -0
- package/dist/engine/selection.js +119 -0
- package/dist/engine/selection.js.map +1 -0
- package/dist/grading/deterministic.d.ts +102 -0
- package/dist/grading/deterministic.d.ts.map +1 -0
- package/dist/grading/deterministic.js +118 -0
- package/dist/grading/deterministic.js.map +1 -0
- package/dist/grading/freeform.d.ts +64 -0
- package/dist/grading/freeform.d.ts.map +1 -0
- package/dist/grading/freeform.js +85 -0
- package/dist/grading/freeform.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/dist/observation/hookEvents.d.ts +113 -0
- package/dist/observation/hookEvents.d.ts.map +1 -0
- package/dist/observation/hookEvents.js +170 -0
- package/dist/observation/hookEvents.js.map +1 -0
- package/dist/observation/offers.d.ts +215 -0
- package/dist/observation/offers.d.ts.map +1 -0
- package/dist/observation/offers.js +327 -0
- package/dist/observation/offers.js.map +1 -0
- package/dist/observation/source.d.ts +133 -0
- package/dist/observation/source.d.ts.map +1 -0
- package/dist/observation/source.js +105 -0
- package/dist/observation/source.js.map +1 -0
- package/dist/profile/migrate.d.ts +122 -0
- package/dist/profile/migrate.d.ts.map +1 -0
- package/dist/profile/migrate.js +147 -0
- package/dist/profile/migrate.js.map +1 -0
- package/dist/profile/store.d.ts +84 -0
- package/dist/profile/store.d.ts.map +1 -0
- package/dist/profile/store.js +267 -0
- package/dist/profile/store.js.map +1 -0
- package/dist/schemas/common.d.ts +95 -0
- package/dist/schemas/common.d.ts.map +1 -0
- package/dist/schemas/common.js +106 -0
- package/dist/schemas/common.js.map +1 -0
- package/dist/schemas/content.d.ts +828 -0
- package/dist/schemas/content.d.ts.map +1 -0
- package/dist/schemas/content.js +219 -0
- package/dist/schemas/content.js.map +1 -0
- package/dist/schemas/profile.d.ts +599 -0
- package/dist/schemas/profile.d.ts.map +1 -0
- package/dist/schemas/profile.js +177 -0
- package/dist/schemas/profile.js.map +1 -0
- package/dist/schemas/tools.d.ts +1581 -0
- package/dist/schemas/tools.d.ts.map +1 -0
- package/dist/schemas/tools.js +286 -0
- package/dist/schemas/tools.js.map +1 -0
- package/dist/tools/config.d.ts +51 -0
- package/dist/tools/config.d.ts.map +1 -0
- package/dist/tools/config.js +104 -0
- package/dist/tools/config.js.map +1 -0
- package/dist/tools/gate.d.ts +50 -0
- package/dist/tools/gate.d.ts.map +1 -0
- package/dist/tools/gate.js +67 -0
- package/dist/tools/gate.js.map +1 -0
- package/dist/tools/guidance.d.ts +36 -0
- package/dist/tools/guidance.d.ts.map +1 -0
- package/dist/tools/guidance.js +117 -0
- package/dist/tools/guidance.js.map +1 -0
- package/dist/tools/listTopics.d.ts +55 -0
- package/dist/tools/listTopics.d.ts.map +1 -0
- package/dist/tools/listTopics.js +78 -0
- package/dist/tools/listTopics.js.map +1 -0
- package/dist/tools/offers.d.ts +60 -0
- package/dist/tools/offers.d.ts.map +1 -0
- package/dist/tools/offers.js +152 -0
- package/dist/tools/offers.js.map +1 -0
- package/dist/tools/placeholders.d.ts +27 -0
- package/dist/tools/placeholders.d.ts.map +1 -0
- package/dist/tools/placeholders.js +49 -0
- package/dist/tools/placeholders.js.map +1 -0
- package/dist/tools/recordObservation.d.ts +52 -0
- package/dist/tools/recordObservation.d.ts.map +1 -0
- package/dist/tools/recordObservation.js +87 -0
- package/dist/tools/recordObservation.js.map +1 -0
- package/dist/tools/startQuiz.d.ts +82 -0
- package/dist/tools/startQuiz.d.ts.map +1 -0
- package/dist/tools/startQuiz.js +180 -0
- package/dist/tools/startQuiz.js.map +1 -0
- package/dist/tools/status.d.ts +59 -0
- package/dist/tools/status.d.ts.map +1 -0
- package/dist/tools/status.js +133 -0
- package/dist/tools/status.js.map +1 -0
- package/dist/tools/submitAnswer.d.ts +156 -0
- package/dist/tools/submitAnswer.d.ts.map +1 -0
- package/dist/tools/submitAnswer.js +402 -0
- package/dist/tools/submitAnswer.js.map +1 -0
- package/dist/tools/types.d.ts +82 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +48 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/us2/standing.d.ts +111 -0
- package/dist/tools/us2/standing.d.ts.map +1 -0
- package/dist/tools/us2/standing.js +143 -0
- package/dist/tools/us2/standing.js.map +1 -0
- package/package.json +62 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file vibe-hero MCP server entry point (T020).
|
|
3
|
+
*
|
|
4
|
+
* Bootstraps a stdio MCP server (`@modelcontextprotocol/sdk`) named `vibe-hero`
|
|
5
|
+
* with a `tools` capability, then registers all 10 tools from
|
|
6
|
+
* {@link TOOL_REGISTRY}. Each tool's handler is wrapped with the first-run setup
|
|
7
|
+
* gate (T021, {@link withSetupGate}) and adapted from its plain-JSON result into
|
|
8
|
+
* the SDK's `CallToolResult` shape ({@link toCallToolResult}).
|
|
9
|
+
*
|
|
10
|
+
* The SDK idiom (sdk 1.29.0): construct {@link McpServer}, then call
|
|
11
|
+
* `registerTool(name, { description, inputSchema }, cb)`. `registerTool` expects
|
|
12
|
+
* `inputSchema` as a *raw Zod shape* (`Record<string, ZodType>`), so we pass the
|
|
13
|
+
* registry's `inputSchema.shape` and the SDK hands the callback the parsed,
|
|
14
|
+
* validated args. Transport is {@link StdioServerTransport}.
|
|
15
|
+
*
|
|
16
|
+
* Importing this module never starts the server; only running it as the process
|
|
17
|
+
* entrypoint does (see the `import.meta.url` guard at the bottom), so tests can
|
|
18
|
+
* import {@link createServer} freely.
|
|
19
|
+
*
|
|
20
|
+
* Source of truth: specs/001-vibe-hero-mvp/contracts/mcp-tools.md, plan.md.
|
|
21
|
+
*/
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { argv } from "node:process";
|
|
24
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
25
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
26
|
+
import { TOOL_REGISTRY } from "./tools/placeholders.js";
|
|
27
|
+
import { withSetupGate } from "./tools/gate.js";
|
|
28
|
+
import { toCallToolResult } from "./tools/types.js";
|
|
29
|
+
/** Server name advertised to MCP hosts. Matches the product/skill namespace. */
|
|
30
|
+
export const SERVER_NAME = "vibe-hero";
|
|
31
|
+
/** Server version advertised to MCP hosts. */
|
|
32
|
+
export const SERVER_VERSION = "0.1.0";
|
|
33
|
+
/**
|
|
34
|
+
* Register one tool module on an {@link McpServer}, gating its handler and
|
|
35
|
+
* adapting the JSON result to a `CallToolResult`. Pulled out so both production
|
|
36
|
+
* and tests register tools the same way; `dirOverride` flows to the gate's
|
|
37
|
+
* profile lookup as a test seam.
|
|
38
|
+
*
|
|
39
|
+
* @param server - The MCP server to register onto.
|
|
40
|
+
* @param tool - The tool module from {@link TOOL_REGISTRY}.
|
|
41
|
+
* @param dirOverride - Profile-directory override (test seam) for the gate.
|
|
42
|
+
*/
|
|
43
|
+
export const registerToolModule = (server, tool, dirOverride) => {
|
|
44
|
+
const gated = withSetupGate(tool.name, tool.handler, dirOverride);
|
|
45
|
+
server.registerTool(tool.name, {
|
|
46
|
+
description: tool.description,
|
|
47
|
+
// `registerTool` wants a raw Zod shape, not a wrapped object schema.
|
|
48
|
+
inputSchema: tool.inputSchema.shape,
|
|
49
|
+
}, async (args) => toCallToolResult(await gated(args)));
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Construct the vibe-hero MCP server with all tools registered (gated). Does not
|
|
53
|
+
* connect a transport — call {@link main} (or wire your own transport) to start.
|
|
54
|
+
*
|
|
55
|
+
* @param dirOverride - Profile-directory override (test seam) for the gate.
|
|
56
|
+
* @returns A configured, not-yet-connected {@link McpServer}.
|
|
57
|
+
*/
|
|
58
|
+
export const createServer = (dirOverride) => {
|
|
59
|
+
const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {} } });
|
|
60
|
+
for (const tool of TOOL_REGISTRY) {
|
|
61
|
+
registerToolModule(server, tool, dirOverride);
|
|
62
|
+
}
|
|
63
|
+
return server;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Start the server over stdio. Connects a {@link StdioServerTransport} and
|
|
67
|
+
* resolves once connected; the process then stays alive serving tool calls.
|
|
68
|
+
*/
|
|
69
|
+
export const main = async () => {
|
|
70
|
+
const server = createServer();
|
|
71
|
+
const transport = new StdioServerTransport();
|
|
72
|
+
await server.connect(transport);
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Entrypoint guard: only auto-start when this module is the process entrypoint
|
|
76
|
+
* (`node .../index.js`), not when imported by tests. Compares the resolved
|
|
77
|
+
* module path to `argv[1]`.
|
|
78
|
+
*/
|
|
79
|
+
const isEntrypoint = () => {
|
|
80
|
+
const entry = argv[1];
|
|
81
|
+
if (entry === undefined)
|
|
82
|
+
return false;
|
|
83
|
+
return fileURLToPath(import.meta.url) === entry;
|
|
84
|
+
};
|
|
85
|
+
if (isEntrypoint()) {
|
|
86
|
+
main().catch((err) => {
|
|
87
|
+
process.stderr.write(`vibe-hero: fatal error starting MCP server: ${String(err)}\n`);
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEpC,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAsB,MAAM,kBAAkB,CAAC;AAExE,gFAAgF;AAChF,MAAM,CAAC,MAAM,WAAW,GAAG,WAAW,CAAC;AAEvC,8CAA8C;AAC9C,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC;AAEtC;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAChC,MAAiB,EACjB,IAAmB,EACnB,WAAoB,EACd,EAAE;IACR,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;IAClE,MAAM,CAAC,YAAY,CACjB,IAAI,CAAC,IAAI,EACT;QACE,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,qEAAqE;QACrE,WAAW,EAAE,IAAI,CAAC,WAAW,CAAC,KAAK;KACpC,EACD,KAAK,EAAE,IAAa,EAAE,EAAE,CAAC,gBAAgB,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,CAC7D,CAAC;AACJ,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,WAAoB,EAAa,EAAE;IAC9D,MAAM,MAAM,GAAG,IAAI,SAAS,CAC1B,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,EAAE,EAC9C,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAChC,CAAC;IACF,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;QACjC,kBAAkB,CAAC,MAAM,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,KAAK,IAAmB,EAAE;IAC5C,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,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,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QAC5B,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,+CAA+C,MAAM,CAAC,GAAG,CAAC,IAAI,CAC/D,CAAC;QACF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IACvB,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Hook-payload → derived-signal extraction (FR-018, SC-008).
|
|
3
|
+
*
|
|
4
|
+
* Privacy boundary. A Claude Code `PostToolUse` hook payload carries
|
|
5
|
+
* `session_id`, `transcript_path`, `cwd`, `tool_name`, `tool_input`,
|
|
6
|
+
* `tool_output` (a.k.a. `tool_response`), and `tool_use_id` (research.md
|
|
7
|
+
* § Observation). The user's raw prompts, command lines, file contents, and
|
|
8
|
+
* tool outputs live inside `tool_input` / `tool_output`.
|
|
9
|
+
*
|
|
10
|
+
* {@link extractSignals} reads ONLY the privacy-safe fields — `tool_name`,
|
|
11
|
+
* derived `success`, a `timestamp`, and `tool_use_id` — and MUST NEVER copy,
|
|
12
|
+
* return, or persist `tool_input` or `tool_output` (or any nested raw content).
|
|
13
|
+
* `success` is *derived* from the shape of the output (exit code / error
|
|
14
|
+
* presence) WITHOUT retaining the output itself. The payload is untrusted
|
|
15
|
+
* `unknown` and is validated defensively.
|
|
16
|
+
*
|
|
17
|
+
* This module does NOT do topic matching: a {@link DerivedSignal} carries no
|
|
18
|
+
* `topicKeys`. Mapping signals → topics against `TriggerSignal` declarations
|
|
19
|
+
* happens later in `record_observation`. Keeping extraction topic-free makes
|
|
20
|
+
* the privacy contract — "no raw field ever leaves this function" — auditable
|
|
21
|
+
* in one place (see test/unit/privacy.test.ts).
|
|
22
|
+
*
|
|
23
|
+
* Source of truth: specs/001-vibe-hero-mvp/spec.md (FR-015..018, E4),
|
|
24
|
+
* specs/001-vibe-hero-mvp/research.md (§ Observation & hook correlation).
|
|
25
|
+
*/
|
|
26
|
+
import { type ObservationEvent } from "../schemas/profile.js";
|
|
27
|
+
import { type ToolId } from "../schemas/common.js";
|
|
28
|
+
import { type ObservationSource } from "./source.js";
|
|
29
|
+
/**
|
|
30
|
+
* The privacy-safe projection of a single hook event. Deliberately NARROW:
|
|
31
|
+
* exactly the four fields the offer engine needs, and nothing derived from the
|
|
32
|
+
* raw input/output content.
|
|
33
|
+
*
|
|
34
|
+
* Note there is no `topicKeys` here — topic attribution is a later step
|
|
35
|
+
* (`record_observation`) and is intentionally out of scope for extraction.
|
|
36
|
+
*/
|
|
37
|
+
export interface DerivedSignal {
|
|
38
|
+
/** The host tool name reported by the hook (e.g. `"Bash"`, `"Edit"`). */
|
|
39
|
+
readonly toolName: string;
|
|
40
|
+
/**
|
|
41
|
+
* Whether the tool call succeeded, *derived* from the output shape (exit
|
|
42
|
+
* code / error presence) without retaining the output. Trigger metadata
|
|
43
|
+
* only — never a scoring signal (FR-005).
|
|
44
|
+
*/
|
|
45
|
+
readonly success: boolean;
|
|
46
|
+
/** ISO-8601 extraction timestamp. */
|
|
47
|
+
readonly timestamp: string;
|
|
48
|
+
/**
|
|
49
|
+
* The hook's `tool_use_id`, the shared id that aligns a hook event with the
|
|
50
|
+
* transcript's `tool_use`/`tool_result` blocks for deterministic correlation
|
|
51
|
+
* (FR-017). `undefined` when the payload omits it.
|
|
52
|
+
*/
|
|
53
|
+
readonly toolUseId: string | undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Extract privacy-safe {@link DerivedSignal}s from an untrusted hook payload.
|
|
57
|
+
*
|
|
58
|
+
* Reads ONLY `tool_name`, a derived `success`, a `timestamp`, and
|
|
59
|
+
* `tool_use_id`. NEVER reads, copies, returns, or persists `tool_input` or
|
|
60
|
+
* `tool_output`/`tool_response` (FR-018, SC-008). A payload missing `tool_name`
|
|
61
|
+
* yields no signal (nothing actionable to attribute).
|
|
62
|
+
*
|
|
63
|
+
* @param hookPayload - an untrusted `PostToolUse` hook payload.
|
|
64
|
+
* @param now - clock for the extraction timestamp, injectable for tests.
|
|
65
|
+
* Defaults to `Date.now`-backed {@link Date}.
|
|
66
|
+
* @returns zero or one derived signal (array for forward-compatibility with
|
|
67
|
+
* batched payloads).
|
|
68
|
+
*/
|
|
69
|
+
export declare const extractSignals: (hookPayload: unknown, now?: () => Date) => DerivedSignal[];
|
|
70
|
+
/**
|
|
71
|
+
* {@link ObservationSource} wrapper over the Claude Code hook (FR-016).
|
|
72
|
+
*
|
|
73
|
+
* Buffers derived events as hook payloads arrive (via {@link record}) and
|
|
74
|
+
* drains them on {@link poll}. Topic attribution is NOT done here: because a
|
|
75
|
+
* {@link DerivedSignal} has no topics, this wrapper produces a *minimal*
|
|
76
|
+
* {@link ObservationEvent} with an empty `topicKeys` array — the downstream
|
|
77
|
+
* `record_observation` step matches signals to topics against `TriggerSignal`
|
|
78
|
+
* declarations and fills in the real keys. Keeping that out of the privacy
|
|
79
|
+
* boundary preserves the single-responsibility extraction contract.
|
|
80
|
+
*
|
|
81
|
+
* The buffer holds only already-derived, privacy-safe events; no raw payload is
|
|
82
|
+
* ever retained.
|
|
83
|
+
*/
|
|
84
|
+
export declare class HookSource implements ObservationSource {
|
|
85
|
+
private readonly tool;
|
|
86
|
+
private readonly now;
|
|
87
|
+
readonly kind = "claude-code-hook";
|
|
88
|
+
private readonly buffer;
|
|
89
|
+
/**
|
|
90
|
+
* @param tool - the host tool these hook events belong to (Claude Code in
|
|
91
|
+
* v1). Stamped onto each derived {@link ObservationEvent}.
|
|
92
|
+
* @param now - clock for timestamps, injectable for deterministic tests.
|
|
93
|
+
*/
|
|
94
|
+
constructor(tool?: ToolId, now?: () => Date);
|
|
95
|
+
/** Drain and clear the buffered derived events. */
|
|
96
|
+
poll(): Promise<readonly ObservationEvent[]>;
|
|
97
|
+
/**
|
|
98
|
+
* Derive privacy-safe events from one hook payload, buffer them, and return
|
|
99
|
+
* them. The returned events carry an empty `topicKeys` (filled downstream).
|
|
100
|
+
*
|
|
101
|
+
* @param raw - an untrusted `PostToolUse` hook payload.
|
|
102
|
+
*/
|
|
103
|
+
record(raw: unknown): readonly ObservationEvent[];
|
|
104
|
+
/**
|
|
105
|
+
* Project a {@link DerivedSignal} into a minimal {@link ObservationEvent}.
|
|
106
|
+
* `topicKeys` is intentionally empty here (attribution is downstream). The
|
|
107
|
+
* `tool_use_id` becomes the correlation id when present, else a synthetic
|
|
108
|
+
* `hook:<timestamp>` token (FR-017 correlation needs the real id; absence is
|
|
109
|
+
* tolerated). Re-validated against the schema, which has no raw-content field.
|
|
110
|
+
*/
|
|
111
|
+
private toEvent;
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=hookEvents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hookEvents.d.ts","sourceRoot":"","sources":["../../src/observation/hookEvents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EACL,KAAK,gBAAgB,EAEtB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAmB,KAAK,MAAM,EAAE,MAAM,sBAAsB,CAAC;AACpE,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,qCAAqC;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC;AA2DD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,GACzB,aAAa,OAAO,EACpB,MAAK,MAAM,IAAuB,KACjC,aAAa,EAyBf,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,qBAAa,UAAW,YAAW,iBAAiB;IAWhD,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAXtB,SAAgB,IAAI,sBAAsB;IAE1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;IAEjD;;;;OAIG;gBAEgB,IAAI,GAAE,MAAsB,EAC5B,GAAG,GAAE,MAAM,IAAuB;IAGrD,mDAAmD;IAC5C,IAAI,IAAI,OAAO,CAAC,SAAS,gBAAgB,EAAE,CAAC;IAKnD;;;;;OAKG;IACI,MAAM,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,gBAAgB,EAAE;IAQxD;;;;;;OAMG;IACH,OAAO,CAAC,OAAO;CAWhB"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Hook-payload → derived-signal extraction (FR-018, SC-008).
|
|
3
|
+
*
|
|
4
|
+
* Privacy boundary. A Claude Code `PostToolUse` hook payload carries
|
|
5
|
+
* `session_id`, `transcript_path`, `cwd`, `tool_name`, `tool_input`,
|
|
6
|
+
* `tool_output` (a.k.a. `tool_response`), and `tool_use_id` (research.md
|
|
7
|
+
* § Observation). The user's raw prompts, command lines, file contents, and
|
|
8
|
+
* tool outputs live inside `tool_input` / `tool_output`.
|
|
9
|
+
*
|
|
10
|
+
* {@link extractSignals} reads ONLY the privacy-safe fields — `tool_name`,
|
|
11
|
+
* derived `success`, a `timestamp`, and `tool_use_id` — and MUST NEVER copy,
|
|
12
|
+
* return, or persist `tool_input` or `tool_output` (or any nested raw content).
|
|
13
|
+
* `success` is *derived* from the shape of the output (exit code / error
|
|
14
|
+
* presence) WITHOUT retaining the output itself. The payload is untrusted
|
|
15
|
+
* `unknown` and is validated defensively.
|
|
16
|
+
*
|
|
17
|
+
* This module does NOT do topic matching: a {@link DerivedSignal} carries no
|
|
18
|
+
* `topicKeys`. Mapping signals → topics against `TriggerSignal` declarations
|
|
19
|
+
* happens later in `record_observation`. Keeping extraction topic-free makes
|
|
20
|
+
* the privacy contract — "no raw field ever leaves this function" — auditable
|
|
21
|
+
* in one place (see test/unit/privacy.test.ts).
|
|
22
|
+
*
|
|
23
|
+
* Source of truth: specs/001-vibe-hero-mvp/spec.md (FR-015..018, E4),
|
|
24
|
+
* specs/001-vibe-hero-mvp/research.md (§ Observation & hook correlation).
|
|
25
|
+
*/
|
|
26
|
+
import { ObservationEventSchema, } from "../schemas/profile.js";
|
|
27
|
+
/** Narrow `value` to a plain object record, else `undefined`. */
|
|
28
|
+
const asRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
|
|
29
|
+
? value
|
|
30
|
+
: undefined;
|
|
31
|
+
/** Read a string field if present and non-empty, else `undefined`. */
|
|
32
|
+
const readString = (obj, key) => {
|
|
33
|
+
const v = obj[key];
|
|
34
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Derive success from the tool output WITHOUT copying it.
|
|
38
|
+
*
|
|
39
|
+
* Heuristics, in order (any matched branch decides):
|
|
40
|
+
* 1. an explicit boolean `success` flag,
|
|
41
|
+
* 2. a numeric `exit_code` / `exitCode` (`0` ⇒ success),
|
|
42
|
+
* 3. presence of an `error` / `is_error` / `isError` marker (⇒ failure),
|
|
43
|
+
* 4. otherwise assume success (absence of an error signal).
|
|
44
|
+
*
|
|
45
|
+
* Only a boolean is ever returned; no field of `rawOutput` is retained.
|
|
46
|
+
*
|
|
47
|
+
* @param rawOutput - the untrusted `tool_output` / `tool_response` value.
|
|
48
|
+
*/
|
|
49
|
+
const deriveSuccess = (rawOutput) => {
|
|
50
|
+
const out = asRecord(rawOutput);
|
|
51
|
+
if (out === undefined) {
|
|
52
|
+
// No structured output (or a bare scalar/array): nothing signals failure.
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (typeof out["success"] === "boolean") {
|
|
56
|
+
return out["success"];
|
|
57
|
+
}
|
|
58
|
+
const exit = out["exit_code"] ?? out["exitCode"];
|
|
59
|
+
if (typeof exit === "number") {
|
|
60
|
+
return exit === 0;
|
|
61
|
+
}
|
|
62
|
+
const isError = out["is_error"] ?? out["isError"];
|
|
63
|
+
if (typeof isError === "boolean") {
|
|
64
|
+
return !isError;
|
|
65
|
+
}
|
|
66
|
+
const err = out["error"];
|
|
67
|
+
if (err !== undefined && err !== null && err !== false && err !== "") {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Extract privacy-safe {@link DerivedSignal}s from an untrusted hook payload.
|
|
74
|
+
*
|
|
75
|
+
* Reads ONLY `tool_name`, a derived `success`, a `timestamp`, and
|
|
76
|
+
* `tool_use_id`. NEVER reads, copies, returns, or persists `tool_input` or
|
|
77
|
+
* `tool_output`/`tool_response` (FR-018, SC-008). A payload missing `tool_name`
|
|
78
|
+
* yields no signal (nothing actionable to attribute).
|
|
79
|
+
*
|
|
80
|
+
* @param hookPayload - an untrusted `PostToolUse` hook payload.
|
|
81
|
+
* @param now - clock for the extraction timestamp, injectable for tests.
|
|
82
|
+
* Defaults to `Date.now`-backed {@link Date}.
|
|
83
|
+
* @returns zero or one derived signal (array for forward-compatibility with
|
|
84
|
+
* batched payloads).
|
|
85
|
+
*/
|
|
86
|
+
export const extractSignals = (hookPayload, now = () => new Date()) => {
|
|
87
|
+
const payload = asRecord(hookPayload);
|
|
88
|
+
if (payload === undefined) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const toolName = readString(payload, "tool_name");
|
|
92
|
+
if (toolName === undefined) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
// `tool_output` is the documented field; some hook versions / docs use
|
|
96
|
+
// `tool_response`. We read it ONLY to derive a boolean and immediately drop
|
|
97
|
+
// the reference — its content is never copied into the result.
|
|
98
|
+
const success = deriveSuccess(payload["tool_output"] ?? payload["tool_response"]);
|
|
99
|
+
const signal = {
|
|
100
|
+
toolName,
|
|
101
|
+
success,
|
|
102
|
+
timestamp: now().toISOString(),
|
|
103
|
+
toolUseId: readString(payload, "tool_use_id"),
|
|
104
|
+
};
|
|
105
|
+
return [signal];
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* {@link ObservationSource} wrapper over the Claude Code hook (FR-016).
|
|
109
|
+
*
|
|
110
|
+
* Buffers derived events as hook payloads arrive (via {@link record}) and
|
|
111
|
+
* drains them on {@link poll}. Topic attribution is NOT done here: because a
|
|
112
|
+
* {@link DerivedSignal} has no topics, this wrapper produces a *minimal*
|
|
113
|
+
* {@link ObservationEvent} with an empty `topicKeys` array — the downstream
|
|
114
|
+
* `record_observation` step matches signals to topics against `TriggerSignal`
|
|
115
|
+
* declarations and fills in the real keys. Keeping that out of the privacy
|
|
116
|
+
* boundary preserves the single-responsibility extraction contract.
|
|
117
|
+
*
|
|
118
|
+
* The buffer holds only already-derived, privacy-safe events; no raw payload is
|
|
119
|
+
* ever retained.
|
|
120
|
+
*/
|
|
121
|
+
export class HookSource {
|
|
122
|
+
tool;
|
|
123
|
+
now;
|
|
124
|
+
kind = "claude-code-hook";
|
|
125
|
+
buffer = [];
|
|
126
|
+
/**
|
|
127
|
+
* @param tool - the host tool these hook events belong to (Claude Code in
|
|
128
|
+
* v1). Stamped onto each derived {@link ObservationEvent}.
|
|
129
|
+
* @param now - clock for timestamps, injectable for deterministic tests.
|
|
130
|
+
*/
|
|
131
|
+
constructor(tool = "claude-code", now = () => new Date()) {
|
|
132
|
+
this.tool = tool;
|
|
133
|
+
this.now = now;
|
|
134
|
+
}
|
|
135
|
+
/** Drain and clear the buffered derived events. */
|
|
136
|
+
poll() {
|
|
137
|
+
const drained = this.buffer.splice(0, this.buffer.length);
|
|
138
|
+
return Promise.resolve(drained);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Derive privacy-safe events from one hook payload, buffer them, and return
|
|
142
|
+
* them. The returned events carry an empty `topicKeys` (filled downstream).
|
|
143
|
+
*
|
|
144
|
+
* @param raw - an untrusted `PostToolUse` hook payload.
|
|
145
|
+
*/
|
|
146
|
+
record(raw) {
|
|
147
|
+
const events = extractSignals(raw, this.now).map((signal) => this.toEvent(signal));
|
|
148
|
+
this.buffer.push(...events);
|
|
149
|
+
return events;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Project a {@link DerivedSignal} into a minimal {@link ObservationEvent}.
|
|
153
|
+
* `topicKeys` is intentionally empty here (attribution is downstream). The
|
|
154
|
+
* `tool_use_id` becomes the correlation id when present, else a synthetic
|
|
155
|
+
* `hook:<timestamp>` token (FR-017 correlation needs the real id; absence is
|
|
156
|
+
* tolerated). Re-validated against the schema, which has no raw-content field.
|
|
157
|
+
*/
|
|
158
|
+
toEvent(signal) {
|
|
159
|
+
const topicKeys = [];
|
|
160
|
+
const event = {
|
|
161
|
+
tool: this.tool,
|
|
162
|
+
topicKeys,
|
|
163
|
+
success: signal.success,
|
|
164
|
+
timestamp: signal.timestamp,
|
|
165
|
+
correlationId: signal.toolUseId ?? `hook:${signal.timestamp}`,
|
|
166
|
+
};
|
|
167
|
+
return ObservationEventSchema.parse(event);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=hookEvents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hookEvents.js","sourceRoot":"","sources":["../../src/observation/hookEvents.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Offer engine (T035) — cadence + anti-fatigue decision logic.
|
|
3
|
+
*
|
|
4
|
+
* This is the brain behind the observation → offer pipeline. It does two
|
|
5
|
+
* trigger-only jobs (it NEVER scores — FR-005 / SC-003):
|
|
6
|
+
*
|
|
7
|
+
* 1. **Match** derived activity signals to candidate topic keys by scanning the
|
|
8
|
+
* loaded topics' {@link TriggerSignal}s ({@link matchCandidates}).
|
|
9
|
+
* 2. **Decide** whether an offer may surface at an end-of-work breakpoint, and
|
|
10
|
+
* for which key, honoring the configured cadence and the full anti-fatigue
|
|
11
|
+
* stack ({@link resolveOffer}) — within-session decline suppression
|
|
12
|
+
* (FR-020), configurable cadence off / per_session / per_topic (FR-020a),
|
|
13
|
+
* and cross-session decline backoff + global mute (FR-020b).
|
|
14
|
+
*
|
|
15
|
+
* Design: the decision core is **pure**. {@link resolveOffer},
|
|
16
|
+
* {@link matchCandidates}, {@link applyDecline}, and {@link applyAccept} take
|
|
17
|
+
* plain state + a `now` timestamp and return plain results — no IO, no clock, no
|
|
18
|
+
* randomness — so they are trivially testable and deterministic. The tool layer
|
|
19
|
+
* (`tools/offers.ts`, `tools/recordObservation.ts`) is the thin wrapper that
|
|
20
|
+
* reads the clock, loads the catalog, and persists via `updateProfile`.
|
|
21
|
+
*
|
|
22
|
+
* Source of truth: specs/001-vibe-hero-mvp/spec.md (FR-005, FR-015..017,
|
|
23
|
+
* FR-019/020/020a/020b, SC-003), specs/001-vibe-hero-mvp/data-model.md
|
|
24
|
+
* (§ OfferLedger), specs/001-vibe-hero-mvp/contracts/mcp-tools.md
|
|
25
|
+
* (`record_observation` / `get_offer` / `record_offer_response`),
|
|
26
|
+
* src/config.ts (ASSESSMENT_CONFIG.declineMuteThreshold / backoff*).
|
|
27
|
+
*/
|
|
28
|
+
import { type AbilityKey } from "../schemas/common.js";
|
|
29
|
+
import type { Config, OfferBackoff, OfferLedger } from "../schemas/profile.js";
|
|
30
|
+
import type { Topic } from "../schemas/content.js";
|
|
31
|
+
import type { OfferCandidate } from "../schemas/tools.js";
|
|
32
|
+
/**
|
|
33
|
+
* A single derived activity signal as accepted by `record_observation`. Mirrors
|
|
34
|
+
* the privacy-safe {@link DerivedSignal} projection — `toolName` (host tool name
|
|
35
|
+
* e.g. `"Bash"`), an optional `mcpTool` (an MCP tool name e.g.
|
|
36
|
+
* `"mcp__github__create_pr"`), an optional derived `success`, and an optional
|
|
37
|
+
* `toolUseId` for correlation. Trigger-only: success/ids never affect scoring.
|
|
38
|
+
*/
|
|
39
|
+
export interface ObservedSignal {
|
|
40
|
+
readonly toolName?: string;
|
|
41
|
+
readonly mcpTool?: string;
|
|
42
|
+
readonly success?: boolean;
|
|
43
|
+
readonly toolUseId?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Match derived signals against the loaded topics' trigger declarations and
|
|
47
|
+
* return the distinct candidate topics (deduped by {@link AbilityKey}, in
|
|
48
|
+
* topic-iteration order). Trigger-only (FR-015): this selects *which topic to
|
|
49
|
+
* offer* and never scores.
|
|
50
|
+
*
|
|
51
|
+
* Only triggers whose `tool` equals the observation's `tool` are considered, so
|
|
52
|
+
* a Claude Code Bash signal never trips a Codex topic. A topic with no matching
|
|
53
|
+
* trigger is omitted.
|
|
54
|
+
*
|
|
55
|
+
* @param topics - The loaded catalog topics (each carrying `triggerSignals`).
|
|
56
|
+
* @param tool - The host tool the activity belongs to.
|
|
57
|
+
* @param signals - The derived signals observed this turn.
|
|
58
|
+
* @returns The distinct offer candidates `{ key, title, reason }`.
|
|
59
|
+
*/
|
|
60
|
+
export declare const matchCandidates: (topics: readonly Topic[], tool: Config["toolsLearning"][number], signals: readonly ObservedSignal[]) => OfferCandidate[];
|
|
61
|
+
/**
|
|
62
|
+
* Why an offer was suppressed, mirroring the `get_offer` contract's
|
|
63
|
+
* `suppressed` enum. `cadence` covers the per_session / per_topic exhaustion and
|
|
64
|
+
* the cross-session backoff/mute cases.
|
|
65
|
+
*/
|
|
66
|
+
export type SuppressionReason = "offers_off" | "declined" | "cadence" | "no_candidate";
|
|
67
|
+
/** A resolved offer decision: either a chosen key, or a suppression reason. */
|
|
68
|
+
export type OfferDecision = {
|
|
69
|
+
readonly kind: "offer";
|
|
70
|
+
readonly key: AbilityKey;
|
|
71
|
+
} | {
|
|
72
|
+
readonly kind: "suppressed";
|
|
73
|
+
readonly reason: SuppressionReason;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* The full state the pure {@link resolveOffer} decision needs. Bundles the
|
|
77
|
+
* relevant config flags, the current per-session ledger, the cross-session
|
|
78
|
+
* backoff, and the ordered candidate keys (most-relevant first) for the session.
|
|
79
|
+
*/
|
|
80
|
+
export interface OfferState {
|
|
81
|
+
/** Master proactive-offers switch (FR-031); `false` ⇒ never offer. */
|
|
82
|
+
readonly proactiveOffers: boolean;
|
|
83
|
+
/** Configured cadence (FR-020a). */
|
|
84
|
+
readonly offerCadence: Config["offerCadence"];
|
|
85
|
+
/** The current per-session offer ledger (anti-fatigue, FR-020/020a). */
|
|
86
|
+
readonly ledger: OfferLedger;
|
|
87
|
+
/** Cross-session decline backoff + global mute (FR-020b). */
|
|
88
|
+
readonly backoff: OfferBackoff;
|
|
89
|
+
/**
|
|
90
|
+
* Candidate keys eligible this turn, most-relevant first. Typically the
|
|
91
|
+
* `offeredTopicKeys` accumulated by `record_observation`, or freshly matched
|
|
92
|
+
* candidates. `resolveOffer` picks the first key that survives every gate.
|
|
93
|
+
*/
|
|
94
|
+
readonly candidates: readonly AbilityKey[];
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Decide whether an end-of-work offer may surface, and for which key (FR-019
|
|
98
|
+
* non-interrupting timing is the caller's concern; this is the *whether/which*).
|
|
99
|
+
*
|
|
100
|
+
* Gates, in order (first failure wins):
|
|
101
|
+
* 1. `offerCadence === "off"` ⇒ `offers_off` (FR-020a).
|
|
102
|
+
* 2. `proactiveOffers === false` ⇒ `offers_off` (master switch, FR-031).
|
|
103
|
+
* 3. `mutedUntil` in the future ⇒ `cadence` (global mute, FR-020b).
|
|
104
|
+
* 4. `declinedThisSession` ⇒ `declined` (within-session suppression, FR-020).
|
|
105
|
+
* 5. `per_session` and an offer already surfaced this session ⇒ `cadence`
|
|
106
|
+
* (≤1 offer/session, FR-020a).
|
|
107
|
+
* 6. No candidate keys at all ⇒ `no_candidate`.
|
|
108
|
+
* 7. Otherwise pick the first candidate that is BOTH not already offered this
|
|
109
|
+
* session under `per_topic` (≤1 per distinct key/session, FR-020a) AND not
|
|
110
|
+
* within its cross-session `perTopicNextEligibleAt` backoff window
|
|
111
|
+
* (FR-020b). If none survives ⇒ `cadence`.
|
|
112
|
+
*
|
|
113
|
+
* Pure: no clock, no IO. `now` is supplied by the caller.
|
|
114
|
+
*
|
|
115
|
+
* @param state - The bundled offer state (config flags, ledger, backoff, candidates).
|
|
116
|
+
* @param now - The current instant, for muted/backoff comparisons.
|
|
117
|
+
* @returns An {@link OfferDecision}.
|
|
118
|
+
*/
|
|
119
|
+
export declare const resolveOffer: (state: OfferState, now: Date) => OfferDecision;
|
|
120
|
+
/**
|
|
121
|
+
* The per-session ledger reset to a fresh `sessionId`. The cross-session backoff
|
|
122
|
+
* is intentionally NOT reset here — it persists across sessions until an accept
|
|
123
|
+
* resets it (FR-020b). Used by the tool layer when a new session begins.
|
|
124
|
+
*/
|
|
125
|
+
export declare const freshLedger: (sessionId: string) => OfferLedger;
|
|
126
|
+
/**
|
|
127
|
+
* Reconcile the persisted ledger with the session being observed/queried. If the
|
|
128
|
+
* ledger's `sessionId` differs (or is the empty-string sentinel), the previous
|
|
129
|
+
* session's per-session accounting is stale, so return a {@link freshLedger}.
|
|
130
|
+
* Otherwise return the ledger unchanged. Pure.
|
|
131
|
+
*
|
|
132
|
+
* @param ledger - The persisted per-session ledger.
|
|
133
|
+
* @param sessionId - The session id of the current request.
|
|
134
|
+
*/
|
|
135
|
+
export declare const ledgerForSession: (ledger: OfferLedger, sessionId: string) => OfferLedger;
|
|
136
|
+
/**
|
|
137
|
+
* Record that an offer for `key` surfaced this session: bump the per-session
|
|
138
|
+
* count and add the key to `offeredTopicKeys` (deduped). Pure; the tool layer
|
|
139
|
+
* persists the result. Called when `get_offer` actually returns an offer so the
|
|
140
|
+
* cadence caps are enforced on the *next* `get_offer`.
|
|
141
|
+
*/
|
|
142
|
+
export declare const markOffered: (ledger: OfferLedger, key: AbilityKey) => OfferLedger;
|
|
143
|
+
/**
|
|
144
|
+
* Merge freshly-matched candidate keys into the per-session ledger's candidate
|
|
145
|
+
* pool (`candidateKeys`) without counting them as offered. `record_observation`
|
|
146
|
+
* uses this to accumulate candidates across the session as signals arrive;
|
|
147
|
+
* `get_offer` (which receives no signals) later resolves from this pool. New
|
|
148
|
+
* candidates are appended in match order, deduped, after the existing pool so
|
|
149
|
+
* the most-relevant-first ordering of a given turn is preserved. Does NOT touch
|
|
150
|
+
* `offeredTopicKeys` or `offersThisSession` — a candidate is not an offer until
|
|
151
|
+
* {@link markOffered}. Pure.
|
|
152
|
+
*
|
|
153
|
+
* @param ledger - The per-session ledger (already reconciled to this session).
|
|
154
|
+
* @param keys - The candidate keys matched this turn (most-relevant first).
|
|
155
|
+
*/
|
|
156
|
+
export declare const noteCandidates: (ledger: OfferLedger, keys: readonly AbilityKey[]) => OfferLedger;
|
|
157
|
+
/**
|
|
158
|
+
* The result of applying a decline to the cross-session backoff (FR-020b): the
|
|
159
|
+
* updated per-session ledger (decline flag set, suppressing the rest of the
|
|
160
|
+
* session per FR-020) AND the updated cross-session backoff (consecutive count
|
|
161
|
+
* bumped, this topic's next-eligible time pushed out by exponential backoff,
|
|
162
|
+
* and a global mute set once `declineMuteThreshold` is reached).
|
|
163
|
+
*/
|
|
164
|
+
export interface DeclineResult {
|
|
165
|
+
readonly ledger: OfferLedger;
|
|
166
|
+
readonly backoff: OfferBackoff;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Apply a decline for `key` (FR-020 within-session + FR-020b cross-session).
|
|
170
|
+
*
|
|
171
|
+
* Within-session (FR-020): set `declinedThisSession = true` so no further offer
|
|
172
|
+
* surfaces for the rest of the session.
|
|
173
|
+
*
|
|
174
|
+
* Cross-session (FR-020b):
|
|
175
|
+
* - increment `consecutiveDeclines`;
|
|
176
|
+
* - push `perTopicNextEligibleAt[key]` out by an exponential backoff:
|
|
177
|
+
* `backoffBaseHours * backoffFactor^(consecutiveDeclines - 1)` hours from
|
|
178
|
+
* `now`, so each successive decline lengthens the re-offer interval;
|
|
179
|
+
* - once `consecutiveDeclines >= declineMuteThreshold`, set a global
|
|
180
|
+
* `mutedUntil` far in the future (offers globally muted until the user
|
|
181
|
+
* re-enables — modeled as a long horizon derived from the backoff).
|
|
182
|
+
*
|
|
183
|
+
* Pure: `now` and config are inputs; the tool layer persists the result.
|
|
184
|
+
*
|
|
185
|
+
* @param ledger - The current per-session ledger (for this session).
|
|
186
|
+
* @param backoff - The current cross-session backoff.
|
|
187
|
+
* @param key - The declined topic key.
|
|
188
|
+
* @param now - The decline instant.
|
|
189
|
+
* @param config - Tunables (decline mute threshold + backoff base/factor).
|
|
190
|
+
* @returns The updated ledger + backoff.
|
|
191
|
+
*/
|
|
192
|
+
export declare const applyDecline: (ledger: OfferLedger, backoff: OfferBackoff, key: AbilityKey, now: Date, config?: {
|
|
193
|
+
declineMuteThreshold: number;
|
|
194
|
+
backoffBaseHours: number;
|
|
195
|
+
backoffFactor: number;
|
|
196
|
+
}) => DeclineResult;
|
|
197
|
+
/**
|
|
198
|
+
* Apply an accept (FR-020b): reset `consecutiveDeclines` to 0 and clear the
|
|
199
|
+
* global `mutedUntil`. Per-topic backoff entries are left as-is (an accept on
|
|
200
|
+
* one topic does not retroactively un-back-off unrelated topics, but the global
|
|
201
|
+
* counter/mute reset means the user is engaged again). The per-session ledger is
|
|
202
|
+
* unchanged by an accept. Pure.
|
|
203
|
+
*
|
|
204
|
+
* @param backoff - The current cross-session backoff.
|
|
205
|
+
* @returns The reset backoff.
|
|
206
|
+
*/
|
|
207
|
+
export declare const applyAccept: (backoff: OfferBackoff) => OfferBackoff;
|
|
208
|
+
/**
|
|
209
|
+
* Apply a defer (FR-020): a defer is treated as "ask me later" — it does NOT
|
|
210
|
+
* count as a decline (no backoff increment, no within-session decline flag) and
|
|
211
|
+
* does NOT reset the consecutive-decline counter. The state is returned
|
|
212
|
+
* unchanged so the next end-of-work breakpoint may re-offer normally. Pure.
|
|
213
|
+
*/
|
|
214
|
+
export declare const applyDefer: (ledger: OfferLedger, backoff: OfferBackoff) => DeclineResult;
|
|
215
|
+
//# sourceMappingURL=offers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offers.d.ts","sourceRoot":"","sources":["../../src/observation/offers.ts"],"names":[],"mappings":"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"}
|