@zosmaai/pi-llm-wiki 0.11.0 → 0.11.2
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/CHANGELOG.md +5 -0
- package/README.de.md +19 -1
- package/README.es.md +19 -1
- package/README.fr.md +19 -1
- package/README.hi.md +19 -1
- package/README.ja.md +19 -1
- package/README.ko.md +19 -1
- package/README.md +38 -3
- package/README.pt.md +19 -1
- package/README.ru.md +19 -1
- package/README.zh.md +19 -1
- package/dist/extensions/llm-wiki/lib/bootstrap.js +4 -1
- package/dist/extensions/llm-wiki/lib/ingest-worker.js +129 -29
- package/dist/extensions/llm-wiki/lib/metadata.js +37 -31
- package/dist/extensions/llm-wiki/lib/model-command.js +0 -1
- package/dist/extensions/llm-wiki/lib/runtime.js +0 -4
- package/dist/extensions/llm-wiki/lib/source-packet.js +1 -1
- package/dist/extensions/llm-wiki/lib/task-config.js +29 -0
- package/dist/extensions/llm-wiki/lib/tools.js +7 -0
- package/dist/extensions/llm-wiki/lib/utils.js +6 -0
- package/dist/mcp/index.js +2 -1
- package/docs/api.md +5 -2
- package/docs/architecture.md +5 -2
- package/docs/configuration.md +25 -0
- package/docs/superpowers/plans/2026-08-06-authoritative-event-history-phase-1-foundation-hardening.md +937 -0
- package/docs/superpowers/plans/2026-08-07-synthesis-language.md +98 -0
- package/docs/superpowers/specs/2026-08-02-okf-foundation-design.md +17 -2
- package/docs/superpowers/specs/2026-08-02-okf-v0.2-interoperability-design.md +6 -2
- package/docs/superpowers/specs/2026-08-07-synthesis-language-design.md +94 -0
- package/extensions/llm-wiki/lib/bootstrap.ts +4 -1
- package/extensions/llm-wiki/lib/ingest-worker.ts +161 -26
- package/extensions/llm-wiki/lib/knowledge-document.ts +2 -0
- package/extensions/llm-wiki/lib/metadata.ts +38 -31
- package/extensions/llm-wiki/lib/model-command.ts +0 -1
- package/extensions/llm-wiki/lib/runtime.ts +0 -3
- package/extensions/llm-wiki/lib/source-packet.ts +1 -1
- package/extensions/llm-wiki/lib/task-config.ts +36 -0
- package/extensions/llm-wiki/lib/tools.ts +9 -0
- package/extensions/llm-wiki/lib/utils.ts +7 -0
- package/mcp/index.ts +2 -1
- package/package.json +2 -2
- package/skills/llm-wiki/SKILL.md +4 -2
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Configurable Synthesis Language Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use /skill:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Allow vault owners to configure the narrative language used by background ingest synthesis via `.pi/settings.json`.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Add `synthesisLanguage` field to `TaskConfig`, parse it in `readNamespacedConfig`, pass it through `wiki_ingest` → `runIngestSynthesis`, and conditionally append a language instruction to the ingest worker's system prompt.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript (ES2022, ESM), Vitest, Biome
|
|
10
|
+
|
|
11
|
+
**Roadmap:** None
|
|
12
|
+
|
|
13
|
+
**Phase:** Single-plan implementation
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
### Task 1: Add `synthesisLanguage` to TaskConfig
|
|
18
|
+
|
|
19
|
+
**Files:**
|
|
20
|
+
- Modify: `extensions/llm-wiki/lib/task-config.ts`
|
|
21
|
+
|
|
22
|
+
- [ ] Add `synthesisLanguage?: string` field to `TaskConfig` interface (after `trajectories`)
|
|
23
|
+
- [ ] In `readNamespacedConfig`, parse `section.synthesisLanguage` as a non-empty trimmed string:
|
|
24
|
+
```ts
|
|
25
|
+
const lang = section.synthesisLanguage;
|
|
26
|
+
if (typeof lang === "string" && lang.trim()) out.synthesisLanguage = lang.trim();
|
|
27
|
+
```
|
|
28
|
+
- [ ] Run `pnpm typecheck` — confirm no errors
|
|
29
|
+
- [ ] Commit: `feat: add synthesisLanguage config field`
|
|
30
|
+
|
|
31
|
+
### Task 2: Wire `synthesisLanguage` into ingest worker
|
|
32
|
+
|
|
33
|
+
**Files:**
|
|
34
|
+
- Modify: `extensions/llm-wiki/lib/ingest-worker.ts`
|
|
35
|
+
|
|
36
|
+
- [ ] Add `synthesisLanguage?: string` to `RunIngestSynthesisArgs` interface
|
|
37
|
+
- [ ] In `runIngestSynthesis`, after destructuring args, build the system prompt:
|
|
38
|
+
```ts
|
|
39
|
+
const languageInstruction = synthesisLanguage
|
|
40
|
+
? `\n\nWrite all generated narrative content in ${synthesisLanguage}. Preserve product names, repository names, APIs, paths, commands, code, field names, and technical identifiers in their original form.`
|
|
41
|
+
: "";
|
|
42
|
+
const systemPrompt = INGEST_SYSTEM + languageInstruction;
|
|
43
|
+
```
|
|
44
|
+
- [ ] Pass `systemPrompt` (instead of `INGEST_SYSTEM`) to `runSubAgent`
|
|
45
|
+
- [ ] Run `pnpm typecheck` — confirm no errors
|
|
46
|
+
- [ ] Commit: `feat: inject synthesisLanguage into ingest system prompt`
|
|
47
|
+
|
|
48
|
+
### Task 3: Pass `synthesisLanguage` from wiki_ingest tool
|
|
49
|
+
|
|
50
|
+
**Files:**
|
|
51
|
+
- Modify: `extensions/llm-wiki/lib/tools.ts`
|
|
52
|
+
|
|
53
|
+
- [ ] In the `wiki_ingest` tool's background synthesis block (around line 409), pass `synthesisLanguage` to `runIngestSynthesis`:
|
|
54
|
+
```ts
|
|
55
|
+
const committed = await runIngestSynthesis({
|
|
56
|
+
model: resolved.model as Parameters<typeof runIngestSynthesis>[0]["model"],
|
|
57
|
+
apiKey: resolved.apiKey,
|
|
58
|
+
headers: resolved.headers,
|
|
59
|
+
paths,
|
|
60
|
+
sourceId: s.id,
|
|
61
|
+
manifest: s.manifest,
|
|
62
|
+
extracted: s.extracted,
|
|
63
|
+
synthesisLanguage: runtime.config.synthesisLanguage,
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
- [ ] Run `pnpm typecheck` — confirm no errors
|
|
67
|
+
- [ ] Run `pnpm test` — confirm existing tests pass
|
|
68
|
+
- [ ] Commit: `feat: wire synthesisLanguage through wiki_ingest tool`
|
|
69
|
+
|
|
70
|
+
### Task 4: Add unit test for synthesis language injection
|
|
71
|
+
|
|
72
|
+
**Files:**
|
|
73
|
+
- Create: `extensions/llm-wiki/test/ingest-worker-synthesis-language.test.ts`
|
|
74
|
+
|
|
75
|
+
- [ ] Write a focused test: verify that when `synthesisLanguage` is set, the language instruction is appended to the system prompt
|
|
76
|
+
- Mock `runSubAgent` or inspect the constructed prompt
|
|
77
|
+
- Assert the instruction contains the configured language tag and the preservation clause
|
|
78
|
+
- [ ] Run `pnpm test` — confirm test passes
|
|
79
|
+
- [ ] Commit: `test: verify synthesisLanguage injection`
|
|
80
|
+
|
|
81
|
+
### Task 5: Update documentation
|
|
82
|
+
|
|
83
|
+
**Files:**
|
|
84
|
+
- Modify: `docs/configuration.md`
|
|
85
|
+
|
|
86
|
+
- [ ] Add a section for `synthesisLanguage` under the `llm-wiki` config docs:
|
|
87
|
+
- Description: language for background ingest synthesis narrative content
|
|
88
|
+
- Type: BCP 47 language tag (string)
|
|
89
|
+
- Default: undefined (English synthesis)
|
|
90
|
+
- Example JSON snippet
|
|
91
|
+
- [ ] Commit: `docs: document synthesisLanguage config`
|
|
92
|
+
|
|
93
|
+
### Task 6: Final verification
|
|
94
|
+
|
|
95
|
+
- [ ] Run `pnpm lint` — confirm no Biome issues
|
|
96
|
+
- [ ] Run `pnpm test` — confirm all tests pass
|
|
97
|
+
- [ ] Run `pnpm typecheck` — confirm no TypeScript errors
|
|
98
|
+
- [ ] Commit: `ci: verify synthesisLanguage implementation`
|
|
@@ -43,7 +43,7 @@ Those requirements belong to later child specs.
|
|
|
43
43
|
2. **Mode controls bundle behavior, not readability.** Every mode reads both legacy and OKF-shaped pages. Mode controls reserved files and generated projections.
|
|
44
44
|
3. **Missing metadata does not become invented metadata.** Foundation never fabricates authorship, verification, provenance, or timestamps it cannot know.
|
|
45
45
|
4. **Unknown data survives rewrites.** Unknown ordinary YAML fields remain semantically equivalent after parse and serialize.
|
|
46
|
-
5. **Generated files are projections.** Registry, backlinks, indexes, and logs derive from authoritative pages and events.
|
|
46
|
+
5. **Generated files are projections; authoritative extension state is not.** Registry, backlinks, indexes, and logs derive from authoritative pages and events. `meta/events.jsonl` is extension-written state, but it is not generated metadata because no rebuild can reconstruct it.
|
|
47
47
|
6. **Invalid explicit configuration fails closed.** Unknown mode and OKF version values never silently downgrade to legacy behavior.
|
|
48
48
|
|
|
49
49
|
## Vault Mode
|
|
@@ -350,6 +350,12 @@ An index is a projection. Users and importers cannot use it to hide a concept fr
|
|
|
350
350
|
|
|
351
351
|
`meta/events.jsonl` remains the authoritative append-only event source in both modes. `wiki/log.md` is a generated OKF projection only in `okf-0.2` mode. Foundation generates no per-directory logs.
|
|
352
352
|
|
|
353
|
+
`meta/events.jsonl` is durable extension-owned vault state. Users who need activity continuity must preserve it when backing up or synchronizing a complete pi-llm-wiki vault. It is not derivable from canonical pages, raw source packets, `meta/log.md`, or `wiki/log.md`.
|
|
354
|
+
|
|
355
|
+
Foundation does not make the JSONL event source part of the distributable OKF bundle. `wiki/log.md` is a portable snapshot of recorded activity at projection time, not a recovery format and not a promise that an imported bundle can continue the originating vault's event stream. Import, export, and imported-history composition belong to the later Interchange child specification.
|
|
356
|
+
|
|
357
|
+
The event stream records selected extension operations. It is not a complete revision history: manual file edits do not fabricate events, while extension-owned operational actions may emit events. Documentation and UI text must call it an activity history rather than a complete content audit trail.
|
|
358
|
+
|
|
353
359
|
### Authoritative event shape
|
|
354
360
|
|
|
355
361
|
Each valid JSONL line must contain:
|
|
@@ -363,6 +369,8 @@ Each valid JSONL line must contain:
|
|
|
363
369
|
|
|
364
370
|
Additional JSON-compatible fields are allowed. Event production is mode-independent: the same successful capture, creation, update, retro, observation, ingestion, and other tool-owned mutations append the same event in legacy and OKF modes. Event writers append only after the associated authoritative wiki mutation succeeds. A projection rebuild itself does not append an event. Manual file edits do not fabricate events because the extension cannot infer actor or intent safely.
|
|
365
371
|
|
|
372
|
+
Fields projected into `wiki/log.md` must be safe for a distributable bundle. A local file capture event records its stable `source_id` and format but not the caller-supplied `file_path`; the exact path remains in the extension-owned raw source manifest. Manual event details are user-controlled and documentation must warn callers not to include secrets or machine-local paths intended to remain private.
|
|
373
|
+
|
|
366
374
|
### Log template
|
|
367
375
|
|
|
368
376
|
```markdown
|
|
@@ -398,7 +406,14 @@ If any concept has malformed frontmatter, normalized identity collision, or unsu
|
|
|
398
406
|
- preserve the previous registry, backlinks, indexes, and logs
|
|
399
407
|
- do not publish a partial metadata generation
|
|
400
408
|
|
|
401
|
-
Unresolved links and malformed event lines are non-blocking projection diagnostics: valid concepts may still be indexed, and
|
|
409
|
+
Unresolved links and malformed event lines are non-blocking projection diagnostics: valid concepts may still be indexed, and valid event lines may still be projected. A missing or unreadable `meta/events.jsonl` is different from a present empty stream. Rebuild reports `event_source_missing` or `event_source_unreadable`, continues publishing registry, backlink, and index projections, and leaves existing `meta/log.md` and `wiki/log.md` byte-identical. A present zero-byte event file is an explicitly empty authoritative stream and generates the normal empty log projections.
|
|
410
|
+
|
|
411
|
+
Non-blocking diagnostics include:
|
|
412
|
+
|
|
413
|
+
- `link_unresolved`
|
|
414
|
+
- `event_invalid_json`
|
|
415
|
+
- `event_source_missing`
|
|
416
|
+
- `event_source_unreadable`
|
|
402
417
|
|
|
403
418
|
On successful rebuild:
|
|
404
419
|
|
|
@@ -106,12 +106,14 @@ The competitive advantage for pi-llm-wiki is combining OKF interoperability with
|
|
|
106
106
|
├── imports/ # extension-owned import staging and audit records
|
|
107
107
|
│ ├── pending/<import-id>/
|
|
108
108
|
│ └── applied/<import-id>/manifest.json
|
|
109
|
-
├── meta/ # generated
|
|
109
|
+
├── meta/ # durable local events + generated internal projections
|
|
110
110
|
└── outputs/ # reports and explicit exports
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
`.llm-wiki/wiki/` is the distributable OKF bundle. Product-specific raw packets, staging state, and generated search metadata remain outside it.
|
|
114
114
|
|
|
115
|
+
`meta/events.jsonl` is durable local pi-llm-wiki state but is not part of the base OKF bundle. A full-vault backup preserves it; an OKF-only export does not. The exported `wiki/log.md` is therefore a readable history snapshot, not a lossless or resumable event source.
|
|
116
|
+
|
|
115
117
|
Source pages inside the bundle provide stable provenance targets for canonical pages. Raw packet paths may remain pi-llm-wiki extension metadata on source pages, but portable provenance references should resolve to source pages or external resources rather than escaping the bundle.
|
|
116
118
|
|
|
117
119
|
`imports/**` is extension-owned and protected by the same tool-call guardrail model as `raw/**` and `meta/**`. Pending imports never participate in recall or metadata indexing.
|
|
@@ -253,7 +255,7 @@ The metadata rebuild writes:
|
|
|
253
255
|
|
|
254
256
|
Each directory index lists only direct concepts and immediate child directories. This preserves progressive disclosure and avoids loading a recursive catalog into context.
|
|
255
257
|
|
|
256
|
-
Reserved `index.md` and `log.md` files are not concept documents and are excluded from ordinary concept recall. Imported reserved files are validated and recorded in the import manifest, but the live bundle regenerates its own indexes and log. Foreign concept paths and document-level metadata are preserved; arbitrary foreign index prose is not merged into the live generated index.
|
|
258
|
+
Reserved `index.md` and `log.md` files are not concept documents and are excluded from ordinary concept recall. Imported reserved files are validated and recorded in the import manifest, but the live bundle regenerates its own indexes and log. Foreign concept paths and document-level metadata are preserved; arbitrary foreign index prose is not merged into the live generated index. Before Interchange implementation, its normative child spec must define whether an imported `log.md` is archived, retained as a separate historical baseline, or replaced when a new local event stream begins. It must not imply that Markdown prose can reconstruct the originating JSONL stream.
|
|
257
259
|
|
|
258
260
|
## Import Design
|
|
259
261
|
|
|
@@ -334,6 +336,8 @@ live legacy + OKF pages
|
|
|
334
336
|
|
|
335
337
|
Export never mutates the live vault. It preserves safe concept paths, bodies, unknown document fields, standard metadata, and pi-llm-wiki extension fields. Legacy pages are converted in memory. Generated `meta/**`, pending imports, source packet internals, and embeddings are not exported.
|
|
336
338
|
|
|
339
|
+
Because `meta/events.jsonl` is excluded, exported `log.md` is a deterministic snapshot rather than a resumable event ledger. Export must use only bundle-safe projected fields. Portable event continuity or a machine-readable event sidecar requires a separately reviewed Interchange decision and is not implied by Foundation.
|
|
340
|
+
|
|
337
341
|
Raw evidence remains represented through portable source concept pages and their provenance links. A later option may package selected original artifacts under an OKF `references/` convention.
|
|
338
342
|
|
|
339
343
|
## Migration Design
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Configurable Language for Background Ingest Synthesis
|
|
2
|
+
|
|
3
|
+
**Issue:** [#124](https://github.com/zosmaai/pi-llm-wiki/issues/124)
|
|
4
|
+
**Date:** 2026-08-07
|
|
5
|
+
**Status:** Approved
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
Background ingest synthesis (`wiki_ingest(background=true)`) always produces English narrative content, regardless of the vault's authoring language. The background sub-agent does not inherit language instructions from `AGENTS.md`, `APPEND_SYSTEM.md`, `WIKI_SCHEMA.md`, or the main session prompt.
|
|
10
|
+
|
|
11
|
+
Workarounds are inadequate:
|
|
12
|
+
- `background=false` returns extracted content to the main session, consuming context window.
|
|
13
|
+
- Manual translation after ingest is error-prone and defeats the purpose of background synthesis.
|
|
14
|
+
|
|
15
|
+
## Goal
|
|
16
|
+
|
|
17
|
+
Allow vault owners to configure the narrative language used by background ingest synthesis, without copying the main conversation context into the background worker.
|
|
18
|
+
|
|
19
|
+
## Design
|
|
20
|
+
|
|
21
|
+
### Configuration
|
|
22
|
+
|
|
23
|
+
- **Field:** `synthesisLanguage`
|
|
24
|
+
- **Type:** BCP 47 language tag (e.g., `"ru"`, `"fr"`, `"en"`)
|
|
25
|
+
- **Location:** `.pi/settings.json` under the `llm-wiki` namespace
|
|
26
|
+
- **Default:** undefined (no change to current behavior)
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"llm-wiki": {
|
|
32
|
+
"synthesisLanguage": "ru"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This matches the existing pattern used by `taskModel`, `trajectories`, `notices`, etc.
|
|
38
|
+
|
|
39
|
+
### System Prompt Modification
|
|
40
|
+
|
|
41
|
+
When `synthesisLanguage` is configured, the background ingest worker appends a fixed instruction block to its system prompt (`INGEST_SYSTEM` in `ingest-worker.ts`):
|
|
42
|
+
|
|
43
|
+
> Write all generated content in {language}, including titles, headings, summaries, descriptions, and concept/entity names. Only preserve code, API names, file paths, commands, exact technical identifiers, and verbatim quotations in their original form.
|
|
44
|
+
|
|
45
|
+
The BCP 47 tag is validated using `Intl.getCanonicalLocales()` and canonicalized before use. Invalid or suspicious tags (containing newlines, quotes, or instruction-like words) are rejected.
|
|
46
|
+
|
|
47
|
+
Additionally, the page renderer translates fixed headings (Summary, Key Takeaways, etc.) into the configured language for supported languages (Russian, French, German, Japanese). Unsupported languages fall back to English headings.
|
|
48
|
+
|
|
49
|
+
### Scope
|
|
50
|
+
|
|
51
|
+
The language setting applies to LLM-generated narrative fields:
|
|
52
|
+
- Page titles
|
|
53
|
+
- Headings
|
|
54
|
+
- Summaries
|
|
55
|
+
- Descriptions
|
|
56
|
+
- Key takeaways
|
|
57
|
+
- Conclusions
|
|
58
|
+
- Entity and concept descriptions
|
|
59
|
+
- Synthesis and analysis text
|
|
60
|
+
|
|
61
|
+
It does NOT apply to:
|
|
62
|
+
- Raw captured source content (`extracted.md`)
|
|
63
|
+
- Code blocks
|
|
64
|
+
- Technical identifiers (API names, paths, commands, field names)
|
|
65
|
+
- Source quotations
|
|
66
|
+
|
|
67
|
+
### Implementation Changes
|
|
68
|
+
|
|
69
|
+
1. **`lib/task-config.ts`**
|
|
70
|
+
- Add `synthesisLanguage?: string` to `TaskConfig` interface
|
|
71
|
+
- Parse in `readNamespacedConfig` as a non-empty trimmed string
|
|
72
|
+
|
|
73
|
+
2. **`lib/ingest-worker.ts`**
|
|
74
|
+
- Add `synthesisLanguage?: string` to `RunIngestSynthesisArgs`
|
|
75
|
+
- In `runIngestSynthesis`, conditionally append the language instruction to `INGEST_SYSTEM` when `synthesisLanguage` is set
|
|
76
|
+
|
|
77
|
+
3. **`lib/tools.ts`** (wiki_ingest tool)
|
|
78
|
+
- Pass `runtime.config.synthesisLanguage` into `runIngestSynthesis` args
|
|
79
|
+
|
|
80
|
+
### Acceptance Criteria
|
|
81
|
+
|
|
82
|
+
- [ ] Background ingest generates synthesis in the configured language
|
|
83
|
+
- [ ] Configuration works with `wiki_ingest(background=true)`
|
|
84
|
+
- [ ] Main conversation context is NOT copied into the background worker
|
|
85
|
+
- [ ] Existing behavior unchanged when no language is configured
|
|
86
|
+
- [ ] Technical identifiers and source quotations remain in their original language
|
|
87
|
+
- [ ] Generated titles, headings, summaries, and conclusions consistently follow the configured language
|
|
88
|
+
|
|
89
|
+
## Out of Scope
|
|
90
|
+
|
|
91
|
+
- Per-source language override
|
|
92
|
+
- Automatic language detection from source content
|
|
93
|
+
- User-editable prompt template (fixed wording for now)
|
|
94
|
+
- Language setting for other background tasks (embeddings, topic inference) — can be added later if needed
|
|
@@ -14,9 +14,12 @@ export const WIKI_SCHEMA = [
|
|
|
14
14
|
"|------|-------|------|",
|
|
15
15
|
"| raw/** | extension | immutable after capture |",
|
|
16
16
|
"| wiki/** | model + user | editable knowledge pages |",
|
|
17
|
-
"| meta
|
|
17
|
+
"| meta/events.jsonl | extension tools | append-only authoritative state |",
|
|
18
|
+
"| meta/* except events.jsonl | extension | generated projections |",
|
|
18
19
|
"| . | human + explicit request | operating rules |",
|
|
19
20
|
"",
|
|
21
|
+
"Back up `meta/events.jsonl` to preserve activity history. Generated logs cannot reconstruct it.",
|
|
22
|
+
"",
|
|
20
23
|
"## Source Packet Format",
|
|
21
24
|
"",
|
|
22
25
|
"```",
|
|
@@ -92,13 +92,113 @@ export type CommitSynthesisOutcome =
|
|
|
92
92
|
|
|
93
93
|
// ── deterministic persistence (no LLM) ────────────────────
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
/** Localized headings for generated pages (issue #124). */
|
|
96
|
+
const HEADINGS: Record<string, Record<string, string>> = {
|
|
97
|
+
ru: {
|
|
98
|
+
summary: "Резюме",
|
|
99
|
+
keyTakeaways: "Ключевые выводы",
|
|
100
|
+
entitiesMentioned: "Упомянутые сущности",
|
|
101
|
+
conceptsMentioned: "Упомянутые концепции",
|
|
102
|
+
notableQuotes: "Заметные цитаты",
|
|
103
|
+
contradictions: "Противоречия",
|
|
104
|
+
sourcePacket: "Пакет источника",
|
|
105
|
+
overview: "Обзор",
|
|
106
|
+
definition: "Определение",
|
|
107
|
+
noneRecorded: "[Не записано]",
|
|
108
|
+
none: "[Нет]",
|
|
109
|
+
id: "ID",
|
|
110
|
+
extracted: "Извлечено",
|
|
111
|
+
manifest: "Манифест",
|
|
112
|
+
contradiction: "⚠️ **Противоречие**",
|
|
113
|
+
},
|
|
114
|
+
fr: {
|
|
115
|
+
summary: "Résumé",
|
|
116
|
+
keyTakeaways: "Points clés",
|
|
117
|
+
entitiesMentioned: "Entités mentionnées",
|
|
118
|
+
conceptsMentioned: "Concepts mentionnés",
|
|
119
|
+
notableQuotes: "Citations notables",
|
|
120
|
+
contradictions: "Contradictions",
|
|
121
|
+
sourcePacket: "Paquet source",
|
|
122
|
+
overview: "Aperçu",
|
|
123
|
+
definition: "Définition",
|
|
124
|
+
noneRecorded: "[Aucun enregistré]",
|
|
125
|
+
none: "[Aucun]",
|
|
126
|
+
id: "ID",
|
|
127
|
+
extracted: "Extrait",
|
|
128
|
+
manifest: "Manifeste",
|
|
129
|
+
contradiction: "⚠️ **Contradiction**",
|
|
130
|
+
},
|
|
131
|
+
de: {
|
|
132
|
+
summary: "Zusammenfassung",
|
|
133
|
+
keyTakeaways: "Wichtige Erkenntnisse",
|
|
134
|
+
entitiesMentioned: "Erwähnte Entitäten",
|
|
135
|
+
conceptsMentioned: "Erwähnte Konzepte",
|
|
136
|
+
notableQuotes: "Bemerkenswerte Zitate",
|
|
137
|
+
contradictions: "Widersprüche",
|
|
138
|
+
sourcePacket: "Quellenpaket",
|
|
139
|
+
overview: "Übersicht",
|
|
140
|
+
definition: "Definition",
|
|
141
|
+
noneRecorded: "[Keine aufgezeichnet]",
|
|
142
|
+
none: "[Keine]",
|
|
143
|
+
id: "ID",
|
|
144
|
+
extracted: "Extrahiert",
|
|
145
|
+
manifest: "Manifest",
|
|
146
|
+
contradiction: "⚠️ **Widerspruch**",
|
|
147
|
+
},
|
|
148
|
+
ja: {
|
|
149
|
+
summary: "要約",
|
|
150
|
+
keyTakeaways: "主な要点",
|
|
151
|
+
entitiesMentioned: "言及されたエンティティ",
|
|
152
|
+
conceptsMentioned: "言及された概念",
|
|
153
|
+
notableQuotes: "注目すべき引用",
|
|
154
|
+
contradictions: "矛盾",
|
|
155
|
+
sourcePacket: "ソースパケット",
|
|
156
|
+
overview: "概要",
|
|
157
|
+
definition: "定義",
|
|
158
|
+
noneRecorded: "[記録なし]",
|
|
159
|
+
none: "[なし]",
|
|
160
|
+
id: "ID",
|
|
161
|
+
extracted: "抽出済み",
|
|
162
|
+
manifest: "マニフェスト",
|
|
163
|
+
contradiction: "⚠️ **矛盾**",
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
function getHeadings(lang?: string): Record<string, string> {
|
|
168
|
+
if (lang && HEADINGS[lang]) return HEADINGS[lang];
|
|
169
|
+
// English defaults
|
|
170
|
+
return {
|
|
171
|
+
summary: "Summary",
|
|
172
|
+
keyTakeaways: "Key Takeaways",
|
|
173
|
+
entitiesMentioned: "Entities Mentioned",
|
|
174
|
+
conceptsMentioned: "Concepts Mentioned",
|
|
175
|
+
notableQuotes: "Notable Quotes",
|
|
176
|
+
contradictions: "Contradictions",
|
|
177
|
+
sourcePacket: "Source Packet",
|
|
178
|
+
overview: "Overview",
|
|
179
|
+
definition: "Definition",
|
|
180
|
+
noneRecorded: "[None recorded]",
|
|
181
|
+
none: "[None]",
|
|
182
|
+
id: "ID",
|
|
183
|
+
extracted: "Extracted",
|
|
184
|
+
manifest: "Manifest",
|
|
185
|
+
contradiction: "⚠️ **Contradiction**",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function buildEntityPageBody(
|
|
190
|
+
title: string,
|
|
191
|
+
description: string,
|
|
192
|
+
sourceId: string,
|
|
193
|
+
lang?: string,
|
|
194
|
+
): string {
|
|
96
195
|
const desc = description.trim() || "One-line description.";
|
|
196
|
+
const h = getHeadings(lang);
|
|
97
197
|
return `# ${title}
|
|
98
198
|
|
|
99
199
|
${desc}
|
|
100
200
|
|
|
101
|
-
##
|
|
201
|
+
## ${h.overview}
|
|
102
202
|
|
|
103
203
|
[Key facts]
|
|
104
204
|
|
|
@@ -107,13 +207,19 @@ ${desc}
|
|
|
107
207
|
- [${sourceId}](/sources/${sourceId}.md)`;
|
|
108
208
|
}
|
|
109
209
|
|
|
110
|
-
function buildConceptPageBody(
|
|
210
|
+
function buildConceptPageBody(
|
|
211
|
+
title: string,
|
|
212
|
+
definition: string,
|
|
213
|
+
sourceId: string,
|
|
214
|
+
lang?: string,
|
|
215
|
+
): string {
|
|
111
216
|
const def = definition.trim() || "One-line definition.";
|
|
217
|
+
const h = getHeadings(lang);
|
|
112
218
|
return `# ${title}
|
|
113
219
|
|
|
114
220
|
${def}
|
|
115
221
|
|
|
116
|
-
##
|
|
222
|
+
## ${h.definition}
|
|
117
223
|
|
|
118
224
|
[Clear explanation]
|
|
119
225
|
|
|
@@ -127,60 +233,62 @@ export function buildIngestedSourcePageBody(
|
|
|
127
233
|
manifest: Record<string, unknown>,
|
|
128
234
|
data: SynthesisData,
|
|
129
235
|
_date: string,
|
|
236
|
+
lang?: string,
|
|
130
237
|
): string {
|
|
131
238
|
const id = String(manifest.id);
|
|
132
239
|
const title = String(manifest.title || id);
|
|
133
240
|
const url = manifest.url ? `\n> _Original: [${manifest.url}](${manifest.url})_` : "";
|
|
241
|
+
const h = getHeadings(lang);
|
|
134
242
|
|
|
135
243
|
const takeaways =
|
|
136
244
|
data.key_takeaways.length > 0
|
|
137
245
|
? data.key_takeaways.map((t) => `- ${t.trim()}`).join("\n")
|
|
138
|
-
:
|
|
246
|
+
: `- ${h.noneRecorded}`;
|
|
139
247
|
const entities =
|
|
140
248
|
data.entities.length > 0
|
|
141
249
|
? data.entities.map((e) => `- [${e.title}](/entities/${slugify(e.title)}.md)`).join("\n")
|
|
142
|
-
:
|
|
250
|
+
: `- ${h.none}`;
|
|
143
251
|
const concepts =
|
|
144
252
|
data.concepts.length > 0
|
|
145
253
|
? data.concepts.map((c) => `- [${c.title}](/concepts/${slugify(c.title)}.md)`).join("\n")
|
|
146
|
-
:
|
|
254
|
+
: `- ${h.none}`;
|
|
147
255
|
const quotes =
|
|
148
256
|
data.quotes && data.quotes.length > 0
|
|
149
257
|
? data.quotes
|
|
150
258
|
.map((q) => `> ${q.text.trim()}${q.attribution ? ` — ${q.attribution}` : ""}`)
|
|
151
259
|
.join("\n\n")
|
|
152
|
-
:
|
|
260
|
+
: `> ${h.noneRecorded}`;
|
|
153
261
|
const contradictions =
|
|
154
262
|
data.contradictions && data.contradictions.length > 0
|
|
155
|
-
? `\n##
|
|
263
|
+
? `\n## ${h.contradictions}\n\n${data.contradictions.map((c) => `${h.contradiction}: ${c.trim()}`).join("\n")}\n`
|
|
156
264
|
: "";
|
|
157
265
|
|
|
158
266
|
return `# ${title}${url}
|
|
159
267
|
|
|
160
|
-
##
|
|
268
|
+
## ${h.summary}
|
|
161
269
|
|
|
162
270
|
${data.summary.trim()}
|
|
163
271
|
|
|
164
|
-
##
|
|
272
|
+
## ${h.keyTakeaways}
|
|
165
273
|
|
|
166
274
|
${takeaways}
|
|
167
275
|
|
|
168
|
-
##
|
|
276
|
+
## ${h.entitiesMentioned}
|
|
169
277
|
|
|
170
278
|
${entities}
|
|
171
279
|
|
|
172
|
-
##
|
|
280
|
+
## ${h.conceptsMentioned}
|
|
173
281
|
|
|
174
282
|
${concepts}
|
|
175
283
|
|
|
176
|
-
##
|
|
284
|
+
## ${h.notableQuotes}
|
|
177
285
|
|
|
178
286
|
${quotes}
|
|
179
|
-
${contradictions}##
|
|
287
|
+
${contradictions}## ${h.sourcePacket}
|
|
180
288
|
|
|
181
|
-
-
|
|
182
|
-
-
|
|
183
|
-
-
|
|
289
|
+
- **${h.id}:** \`sources/${id}\`
|
|
290
|
+
- **${h.extracted}:** \`raw/sources/${id}/extracted.md\`
|
|
291
|
+
- **${h.manifest}:** \`raw/sources/${id}/manifest.json\`
|
|
184
292
|
`;
|
|
185
293
|
}
|
|
186
294
|
|
|
@@ -189,12 +297,13 @@ export function buildIngestedSourcePage(
|
|
|
189
297
|
manifest: Record<string, unknown>,
|
|
190
298
|
data: SynthesisData,
|
|
191
299
|
date: string,
|
|
300
|
+
lang?: string,
|
|
192
301
|
): string {
|
|
193
302
|
const id = String(manifest.id);
|
|
194
303
|
const title = String(manifest.title || id);
|
|
195
304
|
const format = String(manifest.format || "unknown");
|
|
196
305
|
const captured = String(manifest.captured || date);
|
|
197
|
-
const body = buildIngestedSourcePageBody(manifest, data, date);
|
|
306
|
+
const body = buildIngestedSourcePageBody(manifest, data, date, lang);
|
|
198
307
|
const doc = createKnowledgeDocument(
|
|
199
308
|
`sources/${id}.md`,
|
|
200
309
|
{
|
|
@@ -223,6 +332,7 @@ export function commitSynthesis(
|
|
|
223
332
|
manifest: Record<string, unknown>,
|
|
224
333
|
data: SynthesisData,
|
|
225
334
|
date: string = fmtDate(),
|
|
335
|
+
lang?: string,
|
|
226
336
|
): CommitSynthesisOutcome {
|
|
227
337
|
const result: CommitResult = {
|
|
228
338
|
sourceId,
|
|
@@ -250,7 +360,7 @@ export function commitSynthesis(
|
|
|
250
360
|
if (!parsed.ok) return { ok: false, sourceId, diagnostics: parsed.diagnostics };
|
|
251
361
|
sourceDocument = patchKnowledgeDocument(parsed.document, {
|
|
252
362
|
fields: { status: "ingested", updated: date },
|
|
253
|
-
body: buildIngestedSourcePageBody(manifest, data, date),
|
|
363
|
+
body: buildIngestedSourcePageBody(manifest, data, date, lang),
|
|
254
364
|
});
|
|
255
365
|
} else {
|
|
256
366
|
sourceDocument = createKnowledgeDocument(
|
|
@@ -265,7 +375,7 @@ export function commitSynthesis(
|
|
|
265
375
|
status: "ingested",
|
|
266
376
|
updated: date,
|
|
267
377
|
},
|
|
268
|
-
buildIngestedSourcePageBody(manifest, data, date),
|
|
378
|
+
buildIngestedSourcePageBody(manifest, data, date, lang),
|
|
269
379
|
);
|
|
270
380
|
}
|
|
271
381
|
mkdirSync(join(paths.wiki, "sources"), { recursive: true });
|
|
@@ -289,7 +399,7 @@ export function commitSynthesis(
|
|
|
289
399
|
created: date,
|
|
290
400
|
updated: date,
|
|
291
401
|
},
|
|
292
|
-
buildEntityPageBody(e.title, e.description, sourceId),
|
|
402
|
+
buildEntityPageBody(e.title, e.description, sourceId, lang),
|
|
293
403
|
[{ id: sourceId, resource: `/sources/${sourceId}.md` }],
|
|
294
404
|
);
|
|
295
405
|
writeKnowledgeDocumentFile(pagePath, entityDoc);
|
|
@@ -315,7 +425,7 @@ export function commitSynthesis(
|
|
|
315
425
|
created: date,
|
|
316
426
|
updated: date,
|
|
317
427
|
},
|
|
318
|
-
buildConceptPageBody(c.title, c.definition, sourceId),
|
|
428
|
+
buildConceptPageBody(c.title, c.definition, sourceId, lang),
|
|
319
429
|
[{ id: sourceId, resource: `/sources/${sourceId}.md` }],
|
|
320
430
|
);
|
|
321
431
|
writeKnowledgeDocumentFile(pagePath, conceptDoc);
|
|
@@ -363,6 +473,8 @@ export interface RunIngestSynthesisArgs {
|
|
|
363
473
|
/** Cap on extracted chars fed to the model (avoid huge prompts). Default 24k. */
|
|
364
474
|
maxChars?: number;
|
|
365
475
|
signal?: AbortSignal;
|
|
476
|
+
/** BCP 47 language tag for narrative content (issue #124). */
|
|
477
|
+
synthesisLanguage?: string;
|
|
366
478
|
}
|
|
367
479
|
|
|
368
480
|
/**
|
|
@@ -373,10 +485,26 @@ export interface RunIngestSynthesisArgs {
|
|
|
373
485
|
export async function runIngestSynthesis(
|
|
374
486
|
args: RunIngestSynthesisArgs,
|
|
375
487
|
): Promise<CommitResult | undefined> {
|
|
376
|
-
const {
|
|
488
|
+
const {
|
|
489
|
+
model,
|
|
490
|
+
apiKey,
|
|
491
|
+
headers,
|
|
492
|
+
paths,
|
|
493
|
+
sourceId,
|
|
494
|
+
manifest,
|
|
495
|
+
extracted,
|
|
496
|
+
maxChars,
|
|
497
|
+
signal,
|
|
498
|
+
synthesisLanguage,
|
|
499
|
+
} = args;
|
|
377
500
|
const content = extracted.slice(0, maxChars ?? 24_000);
|
|
378
501
|
if (!content.trim()) return undefined;
|
|
379
502
|
|
|
503
|
+
const languageInstruction = synthesisLanguage
|
|
504
|
+
? `\n\nWrite all generated content in ${synthesisLanguage}, including titles, headings, summaries, descriptions, and concept/entity names. Only preserve code, API names, file paths, commands, exact technical identifiers, and verbatim quotations in their original form.`
|
|
505
|
+
: "";
|
|
506
|
+
const systemPrompt = INGEST_SYSTEM + languageInstruction;
|
|
507
|
+
|
|
380
508
|
let committed: CommitResult | undefined;
|
|
381
509
|
|
|
382
510
|
const commitTool: AgentTool<typeof CommitSynthesisSchema> = {
|
|
@@ -386,7 +514,14 @@ export async function runIngestSynthesis(
|
|
|
386
514
|
"Persist the structured synthesis of this source into wiki pages. Call exactly once.",
|
|
387
515
|
parameters: CommitSynthesisSchema,
|
|
388
516
|
execute: async (_id, params) => {
|
|
389
|
-
const outcome = commitSynthesis(
|
|
517
|
+
const outcome = commitSynthesis(
|
|
518
|
+
paths,
|
|
519
|
+
sourceId,
|
|
520
|
+
manifest,
|
|
521
|
+
params,
|
|
522
|
+
undefined,
|
|
523
|
+
synthesisLanguage,
|
|
524
|
+
);
|
|
390
525
|
if (!outcome.ok) {
|
|
391
526
|
return {
|
|
392
527
|
content: [{ type: "text", text: `Failed: ${outcome.diagnostics[0].message}` }],
|
|
@@ -411,7 +546,7 @@ export async function runIngestSynthesis(
|
|
|
411
546
|
model,
|
|
412
547
|
apiKey,
|
|
413
548
|
headers,
|
|
414
|
-
systemPrompt
|
|
549
|
+
systemPrompt,
|
|
415
550
|
userPrompt,
|
|
416
551
|
tools: [commitTool as AgentTool],
|
|
417
552
|
signal,
|