docs/design/l0-l1-okf-sidecars-rfc.md
Status: Draft
OpenViking today stores built-in context-layer summaries as directory-level Markdown sidecars:
.abstract.md for L0.overview.md for L1This RFC proposes keeping those filenames and the visible Markdown body, while upgrading their internal document format to minimal OKF-style Markdown: YAML frontmatter metadata followed by the summary body.
The change makes L0/L1 sidecars more self-describing without changing the public context-layer model described in docs/zh/concepts/03-context-layers.md.
OpenViking uses a three-layer context model:
| Layer | Current file | Role |
|---|---|---|
| L0 | .abstract.md | short summary for fast filtering and vector retrieval |
| L1 | .overview.md | directory overview for navigation and rerank |
| L2 | source files and subdirectories | full content loaded on demand |
The current implementation treats .abstract.md and .overview.md as generated hidden sidecars. SemanticProcessor / SemanticDagExecutor generate them bottom-up, and the vector pipeline maps those two filenames to ContextLevel.ABSTRACT and ContextLevel.OVERVIEW.
find, ls, tree, and agent previews should show only the visible summary body unless explicitly reading the file.read / get access to the sidecar files.write.L0 and L1 remain UTF-8 Markdown files with .md suffixes. Each generated file starts with YAML frontmatter:
Use one directory as the running example:
viking://resources/images_2/
The stored .abstract.md keeps metadata and body together:
---
directory: viking://resources/images_2/
source:
kind: http
uri: http://demo.com/demo.pdf
generated_by:
component: SemanticProcessor
trigger: resource_ingest
freshness:
total_entries: 161
sampled_entries: 32
unsampled_entries: 129
pending_child_changes: 0
---
This directory contains a collection of visual assets including screenshots, diagrams, logos, and QR codes related to a context database for AI agents, AI coding assistant tools, and cloud-based AI model management.
abstract(), ls output=agent, tree agent output, and find previews use the body only:
This directory contains a collection of visual assets including screenshots, diagrams, logos, and QR codes related to a context database for AI agents, AI coding assistant tools, and cloud-based AI model management.
Normal ls still hides .abstract.md unless hidden files are explicitly requested.
read() / get() of the sidecar file return the raw stored document, including frontmatter and body. This is the path for callers that intentionally need metadata.
Embedding uses the body plus whitelisted metadata. The initial whitelist contains directory, so the text sent to the embedder is equivalent to:
---
directory: viking://resources/images_2/
---
This directory contains a collection of visual assets including screenshots, diagrams, logos, and QR codes related to a context database for AI agents, AI coding assistant tools, and cloud-based AI model management.
source, generated_by, and freshness are intentionally absent from this embedding text.
When a parent summary is regenerated from this L0, the semantic pipeline uses the body only:
This directory contains a collection of visual assets including screenshots, diagrams, logos, and QR codes related to a context database for AI agents, AI coding assistant tools, and cloud-based AI model management.
Metadata is not prompt input for re-summarization. freshness may still be updated out of band to indicate that the stored body is stale relative to child changes.
L1 uses the same storage and access rules. Its body is longer and navigation-oriented, but metadata handling is identical.
The metadata block is intentionally narrower than general OKF wiki pages. L0/L1 are generated context abstractions, so metadata should only carry machine-facing fields that are not already available from file attributes such as stat, and not repeat the visible summary itself.
The initial metadata should stay small and deterministic. Timestamps are intentionally omitted at first to avoid rewriting sidecars when semantic content has not changed.
Existing L0/L1 content length limits apply to the Markdown body only. Metadata is outside those limits and should not be truncated by summary-size enforcement.
The initial metadata set should stay small:
| Field | Purpose | Notes |
|---|---|---|
directory | Directory represented by this L0/L1 sidecar | Initial embedding whitelist field |
source | Optional original source, when known | Top-level import root only unless a nested source boundary exists |
generated_by | Component and coarse trigger that created or updated the sidecar | Operational metadata, not content provenance |
freshness | Child counts, sampling coverage, and known pending child changes | A freshness signal; not a replacement for semantic refresh |
freshness counts direct child entries, not the total recursive subtree size. Files and child directories share the same counters because both contribute one direct input to the parent summary. For large directories, it should distinguish total input size from the subset actually read for summary generation. A directory with many entries may record total_entries: 161, sampled_entries: 32, and unsampled_entries: 129. freshness.pending_child_changes > 0 means the body is still readable, but known to lag behind lower-level changes.
Sampling should be deterministic for a stable tree so repeated refreshes do not rewrite sidecars unnecessarily. The first policy can be simple: summarize all direct children up to a threshold, and for larger directories use a bounded sample that preserves useful ordering and diversity. Freshness metadata should make that choice visible without forcing the body to enumerate unsampled files.
Metadata should not duplicate information already available through stat, such as file name, file size, mode, modified time, or lock state.
The embedding input for generated L0/L1 sidecars should be the visible Markdown body plus explicitly whitelisted metadata fields.
The initial whitelist includes directory, because the directory URI helps retrieval connect a summary to its location in the context tree. Operational and provenance fields remain excluded by default, including source, generated_by, and freshness.
Conceptually:
raw sidecar bytes
-> parse OKF frontmatter
-> body_markdown
-> append whitelisted metadata fields (`directory` initially)
-> embedding text
This keeps the semantic vector space focused on user-facing summaries and stable location context, while avoiding accidental ranking changes from source URLs, generator details, or freshness counters.
Direct file reads of sidecar files remain raw: callers receive frontmatter plus body.
Semantic accessors return body-only content. Preview surfaces also use body-only content:
find result summariesls output=agentNormal ls behavior remains unchanged: hidden sidecars are not listed unless the caller explicitly asks for hidden entries.
Existing sidecars without frontmatter remain valid. Readers should treat them as legacy Markdown and return the full content as body.
Writers should emit OKF-formatted sidecars after the feature lands. There is no need for an eager migration job. Existing directories migrate naturally when semantic refresh rewrites their L0/L1 files.
The current L0 extraction rule should continue to operate on the L1 body, not the raw OKF document. This preserves the current convention that L0 is derived from the brief paragraph before the first ## section in L1.
Introduce a small semantic-sidecar document helper near the existing sidecar write/read code:
render_semantic_sidecar(level, dir_uri, body, metadata) -> strparse_semantic_sidecar(raw) -> {metadata, body}body_for_preview(raw) -> strbody_for_embedding(raw, whitelist=()) -> strUse it in these paths:
| Area | Required change |
|---|---|
| Sidecar writeback | Render OKF frontmatter before writing .abstract.md / .overview.md |
| Source propagation | Carry optional source metadata to the import root sidecars when available |
| Generation metadata | Record the component and trigger that created or updated each sidecar |
| Freshness accounting | Write direct child input counters during semantic generation |
| Large directory sampling | Record total direct children, sampled children, and unsampled count when generation uses a bounded sample |
| Deferred changes | Increment pending child-change counters when filesystem changes do not immediately refresh the parent summary |
abstract() / overview() | Strip OKF frontmatter before returning semantic accessor content |
| Incremental summary reuse | Parse .overview.md body before extracting file summaries |
| L0 derivation | Extract abstract from L1 body |
| Vectorization | Enqueue the body plus explicitly whitelisted metadata for L0/L1 embeddings |
| Preview formatting | Keep using semantic accessor/body-only content |
The parser should be tolerant: valid YAML object frontmatter is metadata; missing frontmatter means legacy body; malformed frontmatter should not silently enter embeddings. For generated sidecars, malformed frontmatter should be treated as a processing error.
Size-limit enforcement should parse the sidecar and apply existing L0/L1 limits only to the body before rendering metadata back around it. Metadata size should remain bounded by field policy rather than by the summary body limit.
Format and compatibility:
Read and preview behavior:
abstract() and overview() return body-only content.read_file() returns raw frontmatter plus body.ls output=agent, tree agent output, and find previews do not include metadata fields.ls still hides .abstract.md / .overview.md unless hidden files are requested.Metadata generation:
source metadata is preserved on the generated import root and not repeated in nested directories unless a nested source boundary exists.generated_by records the component and trigger for generated sidecars.Freshness and sampling:
pending_child_changes without changing the visible summary body.pending_child_changes and updates freshness coverage.Semantic reuse and embedding:
.overview.md..abstract.md and .overview.md includes whitelisted directory metadata.source, generated_by, and freshness.read_metadata() API in the first version. Direct read() / get() of the sidecar file is enough for callers that intentionally need metadata.directory is included initially; source, generated_by, and freshness are excluded.The current implementation schedules a parent refresh after every successful resource/skill semantic task, even when the newly generated child summary is unchanged. It marks the parent pending_child_changes before enqueue so freshness remains accurate, but unconditional bubbling is not the intended final scheduling policy.
Future work should use freshness state to control bubbling frequency. Candidate inputs include pending_child_changes, sampling coverage, direct-child change volume, whether the child L0 body actually changed, and recent parent-refresh state. The scheduler may coalesce changes, apply thresholds, or use a time window to reduce repeated refreshes and upward write amplification in hot directory trees while preserving eventual consistency.