OKF-runden lukket ved trinn F (konsensus). Vår trinn E-svarfil bodde i den interne postkassen (~/repos/_okf-interim/), som nå rives. Løfter den varige substansen som kun fantes der inn i planen (ny §10): vår §7-registerrad (status `planned`, ingen dør-A-stempling, uforfalskbarhet utenfor fila), D1-posisjon (ratifisert D1 matcher vår innvending), D3 `partial`-posisjon (ratifisert def = samme dør; vi er `planned`, ikke `partial`), samt tre fremadrettede tekniske krav som ellers gikk tapt: tomt-vokabular-ikke-snap (+ okrs egen RECOMMENDED-konstant som støtte), index preserve-unowned-grensen, og lag-1-som-sett (delte golden-fixtures over fire resolve_link-akser). Alt verifisert mot kode 2026-07-20. Ingen kodeendring i pluginen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S7SQpXJpBSNvpWaTNq1kWZ
341 lines
26 KiB
Markdown
341 lines
26 KiB
Markdown
# OKF-ingestion plan — mapping this repo against `llm-ingestion-okf` (phase 4)
|
||
|
||
> **Status: `planned` (2026-07-20).** Survey + requirements only — nothing wired, nothing built. The
|
||
> library's Node half does not exist yet (its phase 4), so there is nothing to adopt today. This document
|
||
> is the repo's durable answer to *what door A or door B must support before this plugin can adopt*, and
|
||
> *what must never move into the shared library*.
|
||
>
|
||
> **Library:** `llm-ingestion-okf` v0.3.1 — Python 3.10+, zero runtime deps, **door A only**
|
||
> (manifest → connector → deterministic materialization of `ingest-{id}.md` → index generation, no model
|
||
> calls). Doors B (inbox) and C (external bundle import) are phase 2; the Node half is phase 4.
|
||
> **Spec:** `ingest-spec.md`, owned by `portfolio-optimiser-commons` — changes go via commons, never locally.
|
||
> **Security boundary:** `llm-ingestion-guard` (repo `llm-ingestion-pipeline-security`) owns all security.
|
||
> Door A is **ungated** — it calls no guard function before writing to disk (verified: no guard import or
|
||
> call site anywhere in the library's `src/`; `dependencies = []`). "Security is delegated" does **not**
|
||
> mean "safe by default"; gating is the caller's responsibility.
|
||
>
|
||
> **Separate, do not merge:** `docs/ingestion-guard/plan.md` (`planned`) is this repo's *security* wiring
|
||
> plan. It shares persist points with this document by necessity, but is a distinct decision with a
|
||
> distinct dependency. §7 states the boundary.
|
||
|
||
## 1. Executive answer
|
||
|
||
**Nothing here is a clean door-A fit today, and the reason is not the one the library expects.**
|
||
|
||
The library's open finding F1 is *"door A has no free-text connector"*. That is real for us, but it is not
|
||
our blocker. Our blocker is one level deeper:
|
||
|
||
> **Door A reaches sources with deterministic code. This plugin does not fetch anything with code.**
|
||
|
||
Every external input in this repo arrives through a *model turn* — the `trend-spotter` agent's
|
||
WebSearch/WebFetch/MCP calls, `/linkedin:react`'s WebFetch of an operator-supplied URL. There is no raw
|
||
HTTP fetch anywhere in plugin code (verified across `scripts/`). Door A's three connectors (`file`, `sql`,
|
||
`http`) all assume code can go get the bytes. For our largest and least-trusted surface, code cannot.
|
||
|
||
So the requirement we contribute to F1 is not *"add a free-text connector"* — it is **"add a door that
|
||
accepts an already-fetched, model-produced payload and materializes it deterministically."** That door
|
||
would immediately fit `trends capture`, which is already exactly this shape (stdin JSON envelope →
|
||
validate → deterministic store write). Details in §4.
|
||
|
||
Adoption verdict: **`planned`, conditional.** Adopting door A as it stands would mean converting typed,
|
||
incrementally-accreting stores into rewrite-on-run markdown documents — a downgrade. We adopt when §4's
|
||
four requirements land, not before.
|
||
|
||
## 2. Our sources — tabular vs free-text
|
||
|
||
| Source | On disk | Shape | Trust boundary | Door-A fit today |
|
||
|---|---|---|---|---|
|
||
| **Analytics CSV export** → batches | `exports/*.csv` → `posts/<date>-<id>.json` | **Tabular** (header row + rows) | First-party (operator's own LinkedIn export), but an unvalidated parse surface | **Closest fit.** Satisfies `read_csv`'s header requirement. But see §3 — the output shape is wrong. |
|
||
| **Trend capture** | `trends/trends.json` (schema v4) | JSON envelope; `title`/`url`/`summary` are **verbatim free text** | **External**, model-mediated (web search/fetch via agent) | **No connector exists.** Not `http` — the fetch already happened, in a model turn. |
|
||
| **Morning brief** | `trends/morning-brief/<date>.md` | Structured md, carries external strings forward | Machine-rendered from store; re-injected by SessionStart hook | Destination, not a source. |
|
||
| **Brain inbox → published** | `ingest/inbox/*.md` → `ingest/published/<id>.md` | **Free text**, byte-exact body | Operator-dropped today; drop-zone has no origin control | **Excluded by design** — see §5. |
|
||
| **Brain profile** | `brain/profile.md` | Line grammar (6 constrained tokens + one-line value) | Machine-folded, operator-gated (`--apply --confirm`) | Destination. Already an OKF-conformant bundle (Stage 1, 2026-06-26). |
|
||
| **Specifics-bank** | `specifics-bank/specifics-bank.json` | JSON + verbatim free-text `content` | Operator-written by invariant (never AI-generated lived experience) | Destination. |
|
||
| **Contract-gate** | — writes nothing | Reads local operator files only | Local | Not applicable. |
|
||
|
||
**Summary:** one genuinely tabular *source* (analytics CSV). One free-text external source that is not
|
||
reachable by any connector shape the library has (trend capture). One free-text local source that is
|
||
excluded by a fixed decision (published posts). Everything else is a destination store, not an ingestion
|
||
input.
|
||
|
||
## 3. Where door A's contract collides with ours
|
||
|
||
Four structural mismatches, each verified against the library's code. These are not preferences — each one
|
||
would break an invariant we hold today.
|
||
|
||
**(1) Fixed 7-key frontmatter, no extension keys.** `materialize.py` emits exactly `type`, `title`,
|
||
`source_system`, `source_query`, `ingested_at`, `ingest_manifest`, `generated` — in fixed order, all values
|
||
collapsed to single lines. Our records carry domain fields that must survive: `provenance`, `status`,
|
||
`first_seen`, `last_seen`, `evidence_count` (brain), `score`, `pillar`, `topics`, `surfaced` (trends).
|
||
Note the tension with OKF the *format*, which explicitly requires consumers to preserve unknown keys — the
|
||
library's materializer is stricter than the spec it implements. Our Stage-1 OKF conformance work
|
||
(`docs/okf-convergence-brief.md`) depends on rich fields riding along as extension keys; door A has no
|
||
mechanism for that.
|
||
|
||
> **Addition, 2026-07-20 (trinn E).** Two runs of `okr/scripts/okf-check.mjs` against a fresh
|
||
> `brain init` scaffold sharpen this into a concrete argument, and correct one of our own claims:
|
||
>
|
||
> - The reference checker's own constant is named `RECOMMENDED`, not `REQUIRED` (`okf-check.mjs:21`).
|
||
> A bundle missing `resource`/`timestamp` gets **warnings and exit 0**. The de-facto conformance
|
||
> tool already treats every §5 key beyond `type` as advisory — so a mandatory ordered prefix would
|
||
> turn a green bundle red without a byte changing. Argument fed to commons' D1.
|
||
> - **Correction:** trinn C claimed our `type` vocabulary is five values. It is three —
|
||
> `Profile` (`scripts/brain/src/profile.ts:48`), `Operations` (`scaffold.ts:56`),
|
||
> `PendingDiff` (`cli.ts:252`). `JournalEntry`/`TributarySummary` exist only as examples in
|
||
> `docs/okf-convergence-brief.md:101`, never in code.
|
||
> - **Correction:** we assented to a closed `type` vocabulary without checking its contents.
|
||
> `okf-check.mjs --strict-ingest` **exits 1** on our conformant bundle — `okr/lib/okf-vocab.mjs:11-21`
|
||
> is nine okr-domain values, and the safe default (`Dokument`) collapses all three of our types into
|
||
> one, erasing the distinction `okf-conform.test.ts` rests on. Our assent now carries a condition:
|
||
> an absent vocabulary must mean *do not snap*, and the vocabulary is per-bundle, never spec-global.
|
||
|
||
**(2) Rewrite-on-run ownership vs incremental accretion.** Door A owns files via `generated: true` +
|
||
`ingest_manifest`, then **deletes every stamped file and rewrites the set** each run. Our stores accrete:
|
||
dedupe by content id, topic-union on re-capture, last-wins score, collision-suffix on body divergence,
|
||
compare-then-skip on scaffold. A door-A run against our data would be destructive by design.
|
||
|
||
**(3) Operator-chosen slug ids vs content-addressed ids.** Library id = the manifest's `extraction.id`
|
||
(a hand-written slug, `[a-z0-9][a-z0-9-]*`), with provenance carried by a manifest stamp
|
||
(`{stem}@{sha256(manifest bytes)[:16]}`). Ours = `sha256(content)[:12]` — the id *is* the dedupe key, which
|
||
is why re-capturing the same trend or re-ingesting the same post is idempotent for free. A slug-keyed
|
||
model cannot express "same content, seen twice."
|
||
|
||
**(4) Table rendering destroys free text.** `render_table` escapes `\` then `|`, then collapses CRLF/CR/LF
|
||
**to a single space**. Any multi-line body loses its line structure. This is fatal for post bodies (§5) and
|
||
lossy for trend summaries.
|
||
|
||
## 4. What door A / door B must support before we can adopt — the concrete asks
|
||
|
||
Ordered by how much they unblock. (1) is the one that decides F1 for us.
|
||
|
||
**R1 — A model-mediated door ("already-fetched payload").** Accept a caller-supplied, schema-validated
|
||
payload (stdin JSON envelope or in-process record array) instead of a connector-fetched one, and
|
||
materialize it with the same determinism, same stamping, same idempotence as door A. The manifest would
|
||
declare shape and destination but not a fetch. This is the door that fits how an agentic plugin actually
|
||
ingests, and we have a working precedent to donate: `trends capture` is this contract already
|
||
(`normalizeItem` → "the one schema downstream never branches on" → deterministic store write).
|
||
**Why this over a free-text connector:** a `read_text`/`read_markdown` connector would serve repos whose
|
||
free text sits in a folder. Ours sits in a model's tool result. Both are needed; they are not the same ask,
|
||
and solving only the folder case leaves us exactly where we are.
|
||
*Security note:* this door is precisely where untrusted, model-touched bytes cross into a persisted store,
|
||
so it is where a guard call belongs. That wiring is `docs/ingestion-guard/plan.md`'s decision, not this
|
||
document's — but the door must at minimum expose a seam for it rather than writing straight through.
|
||
|
||
**R2 — Extension keys preserved through materialization.** Let a record carry arbitrary additional
|
||
frontmatter keys, emitted after the 7 reserved ones, order-stable, never coerced. Without this, every
|
||
domain field we have is lost on the way through the library, and OKF's own "preserve unknown keys" rule is
|
||
violated by the tool that writes OKF.
|
||
|
||
**R3 — Incremental/upsert materialization mode.** A mode where a run merges into an existing bundle keyed
|
||
by record id — add new, update changed, leave untouched what this run did not see — instead of
|
||
delete-all-stamped-then-rewrite. Door A's index maintenance already does exactly this kind of careful
|
||
merge for `index.md` (managed lines refreshed, curated lines preserved byte-verbatim); the ask is to extend
|
||
that discipline from the index to the records.
|
||
|
||
**R4 — Content-addressed record ids as a first-class option.** Allow `id = hash(content)` rather than a
|
||
manifest slug, so dedupe and idempotence come from the data. Our three stores (brain, trends,
|
||
specifics-bank) all independently converged on `sha256(...)[:12]`; the pattern is not LinkedIn-specific.
|
||
|
||
**R5 (phase 4, Node half) — contract parity details.** Stable string error codes on the error object
|
||
(the Python half's `exc.code` discipline, mirrored so Node consumers assert on `err.code`, never on message
|
||
text); zero runtime dependencies; ESM with `node:` prefixes; and the golden-fixture set shared across both
|
||
halves so byte-identity is the test, not the promise.
|
||
|
||
> **Correction, 2026-07-20 (trinn D-review).** An earlier revision of this section claimed we "already meet
|
||
> the zero-dep/ESM/LF conventions". Verified against the code, two parts of that were false:
|
||
>
|
||
> - **We have no stable error codes.** None of the four CLIs set a machine-readable code; every failure is a
|
||
> free-text `Error` plus a numeric `process.exit` (`scripts/brain/src/ingest.ts:99,:113`,
|
||
> `scripts/trends/src/cli.ts:125,:255`). `err.code` is an ask *of* the library, not a discipline we mirror.
|
||
> - **"Exactly one trailing newline" is not asserted here either.** Nothing tests it, and
|
||
> `serializePublishedRecord` deliberately omits it (`scripts/brain/src/ingest.ts:71`) because the record
|
||
> must end on the verbatim body or the byte-exact round-trip (SC2, §5) breaks.
|
||
>
|
||
> **Second correction, same day, narrowing the one above.** The trinn D-review first read this as a
|
||
> conflict with `claude-playlist-corpus`, who asked the library to own frame normalization (LF-only, one
|
||
> blank line, trailing newline). It is not a conflict, and the check should have come first: our
|
||
> newline-free serializer governs `ingest/published/`, which is **excluded from the OKF bundle by design**
|
||
> (§5, `scripts/brain/tests/okf-conform.test.ts:22-27`). The files `write_concept` would actually write for
|
||
> us — the `brain/` bundle — get their trailing newline from our own serializer
|
||
> (`scripts/brain/src/profile.ts:65`). On the concept-writing path we agree with them. What survives is
|
||
> narrower: `write_concept` is also being asked to serve as a *general* verbatim writer callable outside a
|
||
> bundle (`ms-ai-architect`, `po-claude`), so frame normalization should be a documented parameter rather
|
||
> than an unconditional guarantee. The fixture ask applies to that general writer, not to the concept corpus.
|
||
>
|
||
> Zero-dep/ESM does hold (sole exception: `scripts/analytics` depends on `csv-parse`).
|
||
|
||
## 5. What must never move here
|
||
|
||
**`ingest/published/` provenance grammar stays plugin-local — fixed decision, and it has a technical
|
||
floor.** This is not merely a scope preference:
|
||
|
||
- The store holds a **byte-exact round-trip invariant** (`parse ∘ serialize = identity`, SC2) on the
|
||
verbatim post body. Door A's table renderer collapses newlines to spaces (§3.4); its frontmatter
|
||
collapses whitespace runs. Either would break the invariant on contact.
|
||
- The record id is `mintContentId(verbatim body)` — deliberately **un-normalized**, so two
|
||
structurally-different posts never collide and the write path never silently drops a differing body.
|
||
- The grammar is deliberately YAML-free (a fixed 5-line header + `\n---\n` sentinel), which is also why our
|
||
own Stage-1 OKF conformance work scoped the concept-bundle to `brain/` and **excluded the `ingest/`
|
||
tributary** — verified 2026-06-26, `okf-check.mjs` exit 0 on `brain/`.
|
||
- `provenance=published` carries **model-collapse-guard semantics**: the voice/profile learning surface
|
||
learns from human-published content only, never from `ai-draft`. The *form* is generic; the *guarantee*
|
||
is domain policy and belongs where the policy is enforced.
|
||
|
||
We are happy to describe the interface (this section is that description). We are not planning to hand it
|
||
over.
|
||
|
||
**Also staying — LinkedIn domain logic:** trend scoring weights and mode SSOT
|
||
(`references/trend-scoring-modes.md`); brief ranking, pillar logic and Norwegian rendering; the
|
||
analytics↔post join heuristic (title-prefix matching with the 110-char hook rule behind `PREFIX_FLOOR`);
|
||
CSV column fuzzing against LinkedIn's export column names, the `engagementRate` formula and `saves`
|
||
semantics; the specifics taxonomy and its verification rules; contract-gate's writing-contract rules.
|
||
|
||
**Candidates for sharing (bundle mechanics, if the Node half wants them):** content-addressed id minting;
|
||
dedupe + tag-union upsert; migrate-on-load `schemaVersion` stamping; collision-safe idempotent writes
|
||
(compare-then-skip, collision-suffix); path-traversal-safe filename resolution (our `storage.ts` twin of
|
||
the library's fail-closed `safe_resolve`); managed-line index maintenance. Several of these modules already
|
||
declare themselves generic by architecture. Note we carry the data-root resolver in **five** inlined copies
|
||
(four `scripts/*` packages + a zero-dep hooks twin, synchrony guarded by test) — deliberate, since the
|
||
*path convention* is plugin-local even where the *idiom* is not; a shared library should take the idiom and
|
||
leave the path.
|
||
|
||
## 6. Bundle inventory and placement
|
||
|
||
Operator decision, 2026-07-20: **user-owned OKF bundles live outside the plugin/repo tree and are only
|
||
referenced from it.** Plugin-generated bundles that are part of the plugin's own delivery stay.
|
||
|
||
**We already comply — the migration happened in M0 (v0.6.0), before the decision existed.** Every
|
||
user-owned artifact resolves through `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/`
|
||
(`references/data-path-convention.md`), with an idempotent session-start migration. Nothing user-owned has
|
||
lived in the repo tree since.
|
||
|
||
| Artifact | Path | OKF bundle? | Class |
|
||
|---|---|---|---|
|
||
| **Brain** | `<data-dir>/brain/` | **Yes** — the repo's only OKF bundle (`type` + per-level `index.md` + root `okf_version: 0.1`; `okf-check.mjs` exit 0, 2026-06-26) | **User-owned** — see boundary case below |
|
||
| Published-post tributary | `<data-dir>/ingest/{inbox,published}/` | No — deliberately excluded (§5) | User-owned |
|
||
| Trend store | `<data-dir>/trends/trends.json` | No — a single JSON file, not a markdown bundle | User-owned |
|
||
| Morning briefs | `<data-dir>/trends/morning-brief/<date>.md` | No — dated briefs with frontmatter, no `index.md`, not concept files | User-owned |
|
||
| Specifics-bank | `<data-dir>/specifics-bank/specifics-bank.json` | No | User-owned |
|
||
| Analytics batches | `<analytics-root>/posts/*.json` | No | User-owned |
|
||
| Reference docs (28) | `references/*.md` **in repo** | No — no `type`, no `index.md` | **Plugin-owned**, ships with the plugin, correctly in-tree |
|
||
|
||
**Nothing breaks on migration, because there is nothing left to migrate.** The one caveat is historical:
|
||
pre-M0 installs kept data in-tree, and the session-start migration already handles that path. A second
|
||
caveat worth naming: `assets/drafts/queue.json` and `assets/analytics/` remain in-tree as gitignored
|
||
scratch — they are not bundles, but they are the last in-tree paths that hold user bytes, and they should
|
||
be revisited if the decision is ever tightened to "no user bytes in-tree at all."
|
||
|
||
### The boundary case — and it says the taxonomy's axis is wrong
|
||
|
||
The decision's table discriminates on **who writes**: plugin writes → plugin-owned → lives in the repo.
|
||
Our brain is **written by the plugin** (`consolidate`/fold, operator-gated at `--apply --confirm`) and
|
||
never hand-authored. By that table it is plugin-owned and belongs in the repo. **That conclusion is
|
||
exactly wrong** — it is the single most personal artifact we hold, it must survive plugin upgrade and
|
||
uninstall, and the repo is publicly distributed.
|
||
|
||
The discriminator that gives the right answer everywhere in our inventory is **whose lifecycle the content
|
||
follows**, not whose hand writes the bytes. Our brain is machine-written and user-owned, and that
|
||
combination is not a rare corner — it is what any learning system produces. We suggest the axis be
|
||
lifecycle/ownership, with authorship as a non-determinative attribute. Otherwise every plugin that
|
||
*derives* user knowledge lands on the wrong side of a table it read correctly.
|
||
|
||
## 7. Boundary against the guard plan
|
||
|
||
`docs/ingestion-guard/plan.md` identifies four persist gates: trends `capture`, brain
|
||
`writePublished`/`scanInbox`, newsletter→`ekstern` bindings, analytics `saveBatch`. Two of those (trends
|
||
capture, analytics import) reappear here as adoption candidates — unavoidably, since they are the same
|
||
boundary viewed from two sides.
|
||
|
||
The split: **the guard plan decides whether bytes are safe to persist; this plan decides what writes them
|
||
and in what shape.** They share a dependency in one place only — R1's seam is where a guard call would
|
||
land — and that is noted as a seam requirement, not a security decision. Neither plan is a prerequisite for
|
||
the other's approval. The guard plan's own interop blocker (Python↔Node) is tracked there.
|
||
|
||
## 8. Open questions for the library owners
|
||
|
||
1. Does R1 (model-mediated payload door) belong to door B, or is it a fourth door? It is neither an inbox
|
||
scan nor an external bundle import.
|
||
2. Is R2 (extension keys) a spec change via commons, or a materializer relaxation within the current spec?
|
||
OKF v0.1 already mandates preserving unknown keys, which suggests the latter.
|
||
3. ~~For phase 4: is the Node half expected to reach parity with door A only, or with whatever doors exist
|
||
when it starts?~~ **Answered 2026-07-20 (trinn E, felles §6): phase 4 delivers the layer-1 primitives,
|
||
not `materialize_bundle` parity** — no Node repo in the set runs door A (verified here: the only Python
|
||
in this repo is `hooks/scripts/compile-hooks.py`, a build-time script never on a data path). Our register
|
||
status therefore stays **`planned`**, not `blocked on phase 4 scope`. One follow-up outstanding: layer 1
|
||
grew after the question was asked (`read_concept`, `navigate_bundle`, `routeLevel`), so we asked whether
|
||
phase 4 tracks the set as it stands or as it evolves — the reader is decision-relevant for us, since our
|
||
dependency is `serialize ∘ parse = identity`, not the writer alone.
|
||
|
||
## 9. References
|
||
|
||
- Library: `https://git.fromaitochitta.com/open/llm-ingestion-okf`
|
||
- Guard: `https://git.fromaitochitta.com/open/llm-ingestion-pipeline-security`
|
||
- Spec owner: `portfolio-optimiser-commons/ingest-spec.md`
|
||
- This repo: `docs/okf-convergence-brief.md` (Stage-1 OKF conformance, `brain/` bundle),
|
||
`docs/ingestion-guard/plan.md` (separate, `planned`), `docs/second-brain/architecture.md`
|
||
|
||
## 10. Trinn E — consolidated positions (preserved before postbox teardown, 2026-07-21)
|
||
|
||
The OKF adoption round closed at trinn F (consensus). Our trinn E answer lived in the interim
|
||
postbox (`~/repos/_okf-interim/svar/linkedin-studio.md`), which is now torn down. Its durable
|
||
substance is preserved here; everything below was verified against code on 2026-07-20. The
|
||
technical requirements (R1–R5) already live in §4; this section captures the round-positional and
|
||
follow-up material that existed only in the postbox.
|
||
|
||
**Our §7 register row (status + position).**
|
||
- Status stays **`planned`**, not `blocked on phase 4 scope` — verified: the only Python in this
|
||
repo is `hooks/scripts/compile-hooks.py`, a build-time script never on a data path; we do not
|
||
run door A and would gain nothing from parity.
|
||
- We do **not** need door-A stamping (`generated` / `ingest_manifest`). The negative requirement
|
||
holds: inject nothing, normalize nothing.
|
||
- Unforgeability must live **outside the file** — our content-address `mintContentId` (byte-exact
|
||
sha256, `scripts/brain/src/id.ts:53-55`) is unforgeable without the writer forming an opinion
|
||
about the frontmatter. If the writer inspects the string to enforce a stamp, passthrough (R2) is
|
||
gone in the same move.
|
||
- §5 must tolerate extension keys (R2, unchanged); the mandatory reserved-key prefix is limited to
|
||
**stamped, extracted** concepts, not concepts as such.
|
||
|
||
**D1 (frontmatter/stamp) — we are affected, not owner.** The ratified D1 **matches our ask**:
|
||
enforcement moved to `check_bundle` (per-file outcome); the door-C writer rejects the *complete*
|
||
ownership stamp (`generated:true` **and** `ingest_manifest` together), not the five names, so
|
||
round-trip for legitimate door C is preserved; the mandatory prefix binds stamped (door-A) files
|
||
alone. We accept commons' point that passthrough makes §5's key contract unenforceable at the
|
||
writer by construction — that is the right outcome, since the writer cannot make the
|
||
stamped-vs-curated distinction without reading what it promised not to read.
|
||
|
||
**D3 (`partial`) — accepted with one caveat, now resolved.** We accepted the definition and flagged
|
||
that "comparable path" was undefined (it decided our value outright: `brain/` writing through the
|
||
library while trend-store / specifics-bank stay standalone JSON — `scripts/trends/src/store.ts:126-141`).
|
||
The ratified definition (trinn F) is: comparable path = would have gone through the same door
|
||
(A/B/C). Under that, our JSON/CSV/domain-store paths are not comparable and do not force us
|
||
`partial`. **Our status today is `planned`, not `partial`** — zero production paths go through the
|
||
library; we do not emit `partial` (now a transitory value carrying owner + next step) until it is
|
||
ratified.
|
||
|
||
**Durable technical additions (would otherwise be lost with the postbox).**
|
||
- **Empty vocabulary must mean "don't snap", not "snap to default".** Verified:
|
||
`node okr/scripts/okf-check.mjs <brain> --strict-ingest` collapses our three real types
|
||
(`Profile` / `Operations` / `PendingDiff`) to `DEFAULT_TYPE='Dokument'`
|
||
(`okr/lib/okf-vocab.mjs:26`), erasing the distinction our conformance test rests on
|
||
(`scripts/brain/tests/okf-conform.test.ts:15-20`). Vocabulary is per-bundle, never spec-global.
|
||
Ask: absence of an injected vocab list is a legal state (= "don't snap"), not "snap all to default".
|
||
- **Supporting evidence, and it is not ours:** `okf-check.mjs:21` names the constant `RECOMMENDED`,
|
||
not `REQUIRED` — the de-facto checker all nine repos read already treats §5 keys beyond `type` as
|
||
warnings, not requirements. Making the prefix mandatory would turn a green bundle red with zero
|
||
byte change, contradicting the tool that defined conformance in practice during the round.
|
||
- **`type` vocabulary is three values, not five** (corrects trinn C §5): only `Profile`
|
||
(`scripts/brain/src/profile.ts:48-50`), `Operations` (`scaffold.ts:56-58`), `PendingDiff`
|
||
(`cli.ts:252`) are emitted by code; `JournalEntry` / `TributarySummary` exist only as "e.g." in a
|
||
design note (`docs/okf-convergence-brief.md:101`), never in code.
|
||
- **Index `preserve-unowned` boundary.** We have a fourth index mode — create-if-absent, no managed
|
||
lines, whole file hand-editable (`scripts/brain/src/scaffold.ts:33-36,126-141`). Concrete
|
||
requirement for eventual adoption: `write_index(mode="preserve-unowned")` against an existing file
|
||
with no managed lines must leave it **byte-untouched**, not normalize the frame "while it is there
|
||
anyway" — else we lose a file the operator hand-writes.
|
||
- **Layer 1 = the evolving set, not a frozen snapshot** (also §8 item 3). Since §5 added
|
||
`read_concept`, `navigate_bundle`, and `routeLevel` after Q3 was asked, "the layer-1 primitives"
|
||
must read as the set as it stands, not a snapshot: our invariant is `serialize ∘ parse = identity`
|
||
(`scripts/brain/tests/ingest.test.ts:102`), and a Node half with a writer but no reader gives us
|
||
half the invariant. Derived (not a new ask): the shared golden fixtures for `resolve_link` must
|
||
cover all four orthogonal axes across runtime — our R5 stated the fixture requirement for the
|
||
writer's frame alone, which is now too narrow.
|