@agfpd/iapeer-memory-core 0.1.1

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.
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Role-doctrine renderer — templates → peerDoctrine (ADR-009/010, нюанс 4).
3
+ *
4
+ * Port of the surviving core of the reference
5
+ * `scripts/mergemind-render-agent-files.py` (parity against
6
+ * `tests/python/test_render_agent_files.py`, 7 fixtures). What survives and
7
+ * what changed:
8
+ *
9
+ * - the ONLY render target is the peer's doctrine
10
+ * `<peerCwd>/.iapeer/IAPEER.md` — the `.claude/agents/*` targets are
11
+ * claude-специфика and are DROPPED (план адаптации / ADR-009); the
12
+ * per-role gate generalises from "index only" to "any role peer";
13
+ * - the template's leading YAML frontmatter is STRIPPED (the launch layer
14
+ * glues its own identity block — the reference IAPEER.md branch did the
15
+ * same);
16
+ * - a VERSION MARKER comment is prepended (ADR-010): `verify --repair`
17
+ * compares the rendered version against the package templates and
18
+ * re-renders on mismatch; roles pick the new doctrine up on their next
19
+ * cold-wake (ADR-007), no restarts;
20
+ * - idempotent: bytes-compare before writing (no mtime churn); atomic
21
+ * temp + rename; a missing template is reported, never thrown.
22
+ */
23
+
24
+ import fs from "node:fs";
25
+ import path from "node:path";
26
+ import crypto from "node:crypto";
27
+
28
+ /** `<!-- iapeer-memory doctrine v<version> -->` — machine-checkable. */
29
+ export function versionMarker(version: string): string {
30
+ return `<!-- iapeer-memory doctrine v${version} -->`;
31
+ }
32
+
33
+ /** Extract the rendered version from a doctrine file's marker, or null. */
34
+ export function renderedVersion(content: string): string | null {
35
+ const m = /^<!-- iapeer-memory doctrine v(.+?) -->$/m.exec(content);
36
+ return m ? m[1] : null;
37
+ }
38
+
39
+ /** Strip a leading `---\n…\n---\n` template frontmatter block, if any. */
40
+ export function stripTemplateFrontmatter(text: string): string {
41
+ const m = /^---[^\S\n]*\n[\s\S]*?\n---[^\S\n]*(?:\n|$)/.exec(text);
42
+ return m ? text.slice(m[0].length).replace(/^\n+/, "") : text;
43
+ }
44
+
45
+ export type RenderOutcome = {
46
+ action: "written" | "identical" | "missing-template";
47
+ target: string;
48
+ };
49
+
50
+ /**
51
+ * Render one role template into a peer's doctrine. Returns the outcome —
52
+ * the caller (package CLI / verify) aggregates and reports.
53
+ */
54
+ export function renderDoctrine(opts: {
55
+ templatePath: string;
56
+ peerCwd: string;
57
+ version: string;
58
+ }): RenderOutcome {
59
+ const target = path.join(opts.peerCwd, ".iapeer", "IAPEER.md");
60
+
61
+ let template: string;
62
+ try {
63
+ template = fs.readFileSync(opts.templatePath, "utf-8");
64
+ } catch {
65
+ return { action: "missing-template", target };
66
+ }
67
+
68
+ const body = stripTemplateFrontmatter(template);
69
+ const rendered = `${versionMarker(opts.version)}\n${body.startsWith("\n") ? body.slice(1) : body}`;
70
+
71
+ let existing: string | null = null;
72
+ try {
73
+ existing = fs.readFileSync(target, "utf-8");
74
+ } catch {
75
+ existing = null;
76
+ }
77
+ if (existing === rendered) {
78
+ return { action: "identical", target };
79
+ }
80
+
81
+ fs.mkdirSync(path.dirname(target), { recursive: true });
82
+ const tmp = path.join(
83
+ path.dirname(target),
84
+ `.IAPEER.md.${crypto.randomBytes(6).toString("hex")}.tmp`,
85
+ );
86
+ try {
87
+ fs.writeFileSync(tmp, rendered, "utf-8");
88
+ fs.renameSync(tmp, target);
89
+ } catch (err) {
90
+ try {
91
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
92
+ } catch {
93
+ // best effort
94
+ }
95
+ throw err;
96
+ }
97
+ return { action: "written", target };
98
+ }
99
+
100
+ /**
101
+ * Render a set of role doctrines (role → template path / peer cwd).
102
+ * Missing templates are reported per-role; other roles still render
103
+ * (parity with the reference's per-entry resilience).
104
+ */
105
+ export function renderRoleDoctrines(opts: {
106
+ roles: Array<{ role: string; templatePath: string; peerCwd: string }>;
107
+ version: string;
108
+ }): Array<{ role: string } & RenderOutcome> {
109
+ return opts.roles.map(({ role, templatePath, peerCwd }) => ({
110
+ role,
111
+ ...renderDoctrine({ templatePath, peerCwd, version: opts.version }),
112
+ }));
113
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Reranker provider adapters (ADR-013) on top of the shared HTTP client.
3
+ *
4
+ * Providers (`RerankerProvider`) — every adapter maps to a uniform
5
+ * `RerankItem[] = [{index, score}]`, sorted by descending score:
6
+ *
7
+ * - `tei` — native TEI `/rerank` (DEFAULT, inherited behaviour):
8
+ * request { query, texts }
9
+ * response [ { index, score }, ... ]
10
+ * - `jina` — Jina reranker API and compatibles; covers the key LOCAL case:
11
+ * llama.cpp server `/v1/rerank` speaks this format (fact-checked
12
+ * 09.06.2026 against tools/server/README.md — request is
13
+ * {query, documents}, "similar to jina", NOT TEI-compatible):
14
+ * request { model, query, documents: [...texts], top_n }
15
+ * response { results: [{ index, relevance_score }, ...] }
16
+ * - `cohere` — Cohere v2 `/v2/rerank` (same wire shape as jina; kept as a
17
+ * separate provider so endpoint defaults/docs stay honest):
18
+ * request { model, query, documents: [...texts], top_n }
19
+ * response { results: [{ index, relevance_score }, ...] }
20
+ * - `nvidia` — NeMo Retriever ranking NIM `/v1/ranking`:
21
+ * request { model, query: {text}, passages: [{text}, ...], truncate: "END" }
22
+ * response { rankings: [{ index, logit }, ...] } (logit = score)
23
+ *
24
+ * `none` is a config-level value (reranker disabled), not an adapter —
25
+ * configFromEnv resolves it to a null reranker block.
26
+ *
27
+ * Graceful degradation: `{items: null, status}` on any failure — the search
28
+ * pipeline keeps its pre-rerank ranking. Timeout & circuit breaker live in
29
+ * the shared client; this module keeps its OWN breaker instance (reranker
30
+ * failures must not block embeddings and vice versa).
31
+ */
32
+
33
+ import { makeCircuitBreaker, postJson } from "./http-client.js";
34
+ import type { ProviderCallStatus } from "./http-client.js";
35
+
36
+ export type RerankerProvider = "tei" | "cohere" | "nvidia" | "jina";
37
+
38
+ export const RERANKER_PROVIDERS: readonly RerankerProvider[] = [
39
+ "tei",
40
+ "cohere",
41
+ "nvidia",
42
+ "jina",
43
+ ];
44
+
45
+ export function isRerankerProvider(value: string): value is RerankerProvider {
46
+ return (RERANKER_PROVIDERS as readonly string[]).includes(value);
47
+ }
48
+
49
+ export type RerankerConfig = {
50
+ endpoint: string;
51
+ model: string;
52
+ topK: number;
53
+ // Bearer token when the endpoint sits behind auth. Null for direct
54
+ // local access.
55
+ apiKey: string | null;
56
+ /** Wire format (ADR-013). Default "tei" — the inherited behaviour. */
57
+ provider?: RerankerProvider;
58
+ };
59
+
60
+ export type RerankItem = {
61
+ index: number;
62
+ score: number;
63
+ };
64
+
65
+ export type RerankerStatus = ProviderCallStatus;
66
+
67
+ export type RerankResult = {
68
+ items: RerankItem[] | null;
69
+ status: RerankerStatus;
70
+ };
71
+
72
+ const breaker = makeCircuitBreaker();
73
+
74
+ /**
75
+ * Test-only helper to reset the circuit breaker between test cases.
76
+ */
77
+ export function _resetRerankerCircuitForTests(): void {
78
+ breaker._resetForTests();
79
+ }
80
+
81
+ function buildRequestBody(
82
+ query: string,
83
+ texts: string[],
84
+ config: RerankerConfig,
85
+ ): unknown {
86
+ const provider = config.provider ?? "tei";
87
+ switch (provider) {
88
+ case "tei":
89
+ return { query, texts };
90
+ case "jina":
91
+ case "cohere":
92
+ return { model: config.model, query, documents: texts, top_n: config.topK };
93
+ case "nvidia":
94
+ return {
95
+ model: config.model,
96
+ query: { text: query },
97
+ passages: texts.map((text) => ({ text })),
98
+ truncate: "END",
99
+ };
100
+ }
101
+ }
102
+
103
+ /** Parse one response into RerankItem[], or null when the shape is wrong. */
104
+ function parseResponse(json: unknown, config: RerankerConfig): RerankItem[] | null {
105
+ const provider = config.provider ?? "tei";
106
+ try {
107
+ if (provider === "tei") {
108
+ const items = json as RerankItem[];
109
+ if (!Array.isArray(items)) return null;
110
+ return items.map((i) => ({ index: i.index, score: i.score }));
111
+ }
112
+ if (provider === "nvidia") {
113
+ const rankings = (json as { rankings: Array<{ index: number; logit: number }> })
114
+ .rankings;
115
+ if (!Array.isArray(rankings)) return null;
116
+ return rankings.map((r) => ({ index: r.index, score: r.logit }));
117
+ }
118
+ // jina / cohere
119
+ const results = (
120
+ json as { results: Array<{ index: number; relevance_score: number }> }
121
+ ).results;
122
+ if (!Array.isArray(results)) return null;
123
+ return results.map((r) => ({ index: r.index, score: r.relevance_score }));
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Rerank documents against a query. Returns `{items, status}` where `items`
131
+ * are sorted by descending score, or `null` on any non-ok status.
132
+ */
133
+ export async function rerank(
134
+ query: string,
135
+ texts: string[],
136
+ config: RerankerConfig,
137
+ signal?: AbortSignal,
138
+ ): Promise<RerankResult> {
139
+ if (!texts.length) return { items: [], status: "ok" };
140
+
141
+ const result = await postJson({
142
+ endpoint: config.endpoint,
143
+ body: buildRequestBody(query, texts, config),
144
+ apiKey: config.apiKey,
145
+ signal,
146
+ breaker,
147
+ });
148
+ if (result.status !== "ok") {
149
+ return { items: null, status: result.status };
150
+ }
151
+
152
+ const items = parseResponse(result.json, config);
153
+ if (items === null) {
154
+ breaker.recordFailure();
155
+ return { items: null, status: "error" };
156
+ }
157
+
158
+ return {
159
+ items: items.sort((a, b) => b.score - a.score),
160
+ status: "ok",
161
+ };
162
+ }