FIN Intranet Human-Agent Projection Engineering Spec
Date: 2026-05-13 (v0.2 amended 2026-05-15)
Status: Draft v0.2
Related audit: docs/audits/FIN_INTRA_GTM_PLANNING_CODEBASE_GAP_AUDIT_2026-05-13.md
Related design: docs/design/FIN_INTRANET_HUMAN_AGENT_PROJECTION_LAYER_DESIGN_2026-05-13.md
0. v0.2 Revision Summary
Amended 2026-05-15 after architectural review confirmed the markdown projection layer should also serve as the agent-readable rendering of Taproot entities and Harvest Directory public intelligence. Changes from v0.1:
doc_kindenum extended withentity_profile,aggregate_intelligence, andrecall_trace.- New §3.3 specifies entity-profile additional fields and the one-to-one rule: every Taproot entity row has exactly one current profile doc plus optional weekly/monthly snapshots.
- §6 specifies typed wikilinks
[[type:id|display]]as the canonical form, with frontmatterdoc_idtaking precedence over path-derived IDs. - New §11.5 defines two CQRS projections:
doc_backlink_projection(migration 049) andentity_doc_projection(migration 050). Originally drafted as 045/046; renumbered per ADR-005 to avoid slot collisions. - New §11.6 defines the time-snapshot pattern for entity profiles.
- §8 adds a hybrid retrieval contract (structured + semantic + graph blend) and
three new MCP tools:
assemble_recall_trace,get_entity_profile, andpropose_doc_edit. - New §12.5 specifies embedded Vega-Lite chart rendering inside entity profiles.
- New §13.5 defines the
doc.edit_proposedagent enrichment loop. - §3.1 visibility model is preserved for authored docs; entity profiles use a
simplified two-state visibility (
tenant_private/public_anonymized) defined in §3.3. - §14, §15, §17 updated to add tests, proof gates, and stop conditions for the above.
Amended 2026-05-17 (P1-W5-05) after WS-17 (the wave-5 audit) flagged the taproot sync mechanism as the deepest single under-spec’d component of the remediation program. Additional change:
- New §11.7 defines the 3-rung Taproot architecture (central renderer +
push-based GitHub transport via
repository_dispatch+ tenant overlay renderer). The prior §11.7 (“Incremental Embedding Rule”) is now §11.8. Wave 4 push is the chosen transport; Wave 1 poll is documented as a reserved fallback. Source workflows already reference “Engineering Spec §11.7” in their headers; this amendment makes that reference resolvable.
The v0.1 architectural laws (no fifth runtime table, events as truth, knowledge as projection, operational_memory for chunks, StorageProvider for bytes) are unchanged.
1. Scope
This spec defines the first implementable version of the FIN intranet and
human-agent projection layer. It reconciles the FIN Intra_GTM plan with the
active four-table architecture.
The implementation must be pure, portable, tenant-scoped, and testable. It must
not add a documents table.
2. Architecture Constraints
- Use
events,knowledge,actors, andoperational_memory. - Do not create a runtime
documentstable. - Use
StorageProviderfor bytes. - Use Hono for HTTP routes.
- MCP tools are pure functions with dependency injection.
- All writes include
tenant_id. - All queries run under tenant scope.
- Agent-facing outputs include source attribution/citations.
- Customer-facing outputs enforce customer information boundaries.
3. New Shared Schemas
Target file:
packages/shared/src/schemas/contracts/doc.ts
3.1 Doc Content Schema
type DocContent = {
schema_version: 1;
doc_id: string;
doc_kind:
| "authored"
| "system_projected"
| "decision_record"
| "runbook"
| "sop"
| "proof_package"
| "event_digest"
| "portal_page"
| "entity_profile" // v0.2: Taproot entity rendered as markdown
| "aggregate_intelligence" // v0.2: cross-tenant anonymized projection (Harvest Directory)
| "recall_trace"; // v0.2: deterministic FSMA-style traceability artifact
entity_type?: // v0.2: required when doc_kind === "entity_profile"
| "commodity"
| "variety"
| "vendor"
| "region"
| "customer"
| "lot"
| "grower"
| "facility"
| "certification"
| "season_window";
renderer_version?: string; // v0.2: required for system_projected/entity_profile/aggregate_intelligence/recall_trace
snapshot_period?: string; // v0.2: e.g. "2026-W19", "2026-M05"; null for current/live
content_hash?: string; // v0.2: SHA-256 of body for incremental embedding
title: string;
slug: string;
source_system:
| "github_vault"
| "workspace_editor"
| "event_projection"
| "upload"
| "knowledge_forge"
| "manual_import";
source_uri?: string;
github?: {
owner: string;
repo: string;
branch: string;
path: string;
commit_sha: string;
};
frontmatter: Record<string, unknown>;
body_preview: string;
headings: Array<{ level: number; text: string; anchor: string }>;
outbound_links: Array<{ raw: string; resolved_doc_id?: string; resolved_path?: string }>;
inbound_links: Array<{ doc_id: string; path?: string; title?: string }>;
tags: string[];
linked_entities: Array<{ entity_type: string; entity_id: string; label?: string }>;
generated_from_event_ids: string[];
linked_artifact_ids: string[];
chunk_refs: Array<{ memory_id: string; chunk_index: number }>;
visibility: "personal" | "company" | "customer_visible" | "restricted" | "confidential";
data_plane: "private_workspace" | "relationship_data" | "universal_intelligence";
lifecycle_status: "draft" | "active" | "archived";
source_attribution: string[];
created_at: string;
updated_at: string;
};3.2 Doc Collection Schema
Target storage:
knowledge.domain = doc_collection
Use for curated intranet sections, saved graph neighborhoods, portal bundles, and proof packages.
3.3 Entity Profile Additional Fields (v0.2)
When doc_kind === "entity_profile", the doc is the markdown rendering of a
Taproot entity row. The entity row remains the structured source of truth in
its existing knowledge.domain (e.g. commodity, vendor, region). The
profile doc is a derived projection. Both rows coexist:
knowledge.domain = commodityrow: structured golden profile (~900 attributes JSONB). Mutable. Queried by structured filters.knowledge.domain = docrow withdoc_kind = entity_profile: rendered markdown projection. Rebuildable from the entity row + linked events. Queried by graph traversal and semantic retrieval.
Required additional fields on entity-profile DocContent:
type EntityProfileContent = DocContent & {
doc_kind: "entity_profile";
entity_type: string; // see §3.1 enum
source_entity: {
domain: string; // source knowledge domain (e.g. "commodity")
key: string; // source knowledge key
last_observed_version: string;
};
visibility:
| "tenant_private" // v0.2 simplified for entity profiles
| "public_anonymized"; // Harvest Directory mirror
anonymization_policy_id?: string; // required when visibility = public_anonymized
fact_citations: Array<{ // every non-trivial claim must cite
claim_anchor: string; // doc-local anchor (e.g. "#post-harvest-temp")
event_ids?: string[];
knowledge_keys?: string[];
external_source_uri?: string;
confidence: "observed" | "inferred" | "external_authoritative";
}>;
embedded_charts: Array<{ // see §12.5
anchor: string;
spec_kind: "vega-lite";
spec: Record<string, unknown>;
generated_from_event_ids: string[];
}>;
related_entities: Array<{ // outbound typed wikilinks resolved
entity_type: string;
entity_id: string;
relation: "variety_of" | "supplied_by" | "grown_in" | "substitute_for" |
"co_occurs_with" | "shipped_to" | "regulated_under" | string;
weight?: number;
}>;
};Projection rule: exactly one current profile doc per entity row. Updates
to the entity row enqueue a renderer job. Optional weekly/monthly snapshots
(§11.6) carry snapshot_period and are immutable once written.
Public-anonymized profiles for Harvest Directory are written by a separate
adapter that reads tenant entity rows, applies the anonymization policy, and
projects into a tenant-neutral harvest_directory row scope. They are not the
same row as the tenant profile.
3.4 Authored vs Projected Sections (v0.2 amendment 2026-05-15)
A doc body is composed of typed sections. Each section is either human-authored or system-projected. They are NEVER mixed within a single paragraph. The frontmatter declares the section map.
Section type schema:
type DocSection = {
section_id: string;
anchor: string;
kind: "authored" | "projected";
// Authored sections:
author_actor_id?: string;
author_commit_sha?: string;
// Projected sections:
projection_source?: "events" | "knowledge_aggregate" | "external_source" | "agent_generated";
generated_from_event_ids?: string[];
generated_from_knowledge_keys?: string[];
generator_module?: string;
renderer_version?: string;
// Both:
fact_citations: FactCitation[];
confidence: "observed" | "inferred" | "external_authoritative" | "authored";
};Frontmatter body_section_map[] lists sections in document order. The
renderer assembles the body from sections, applying confidence-stratified
visual treatment per §3.5.
Constraints:
- An
entity_profilebody must contain at least oneprojectedsection bound to the source entity row. - A
recall_tracebody must be 100%projectedsections. Authored content is rejected at write time. - A
decision_recordbody must be 100%authoredsections. - A
proof_packagemay mix, but every authored claim must includefact_citations[]. - Agent-generated sections (kind:
projected, projection_source:agent_generated) are visually distinguished and require the per-claim attribution return from §13.6.
The section model is what makes the doc reader able to show “this paragraph came from a temperature sensor reading, this paragraph was typed by Jake, this paragraph was synthesized by an agent from three event citations” — all in the same document, all individually provable.
3.5 Confidence-Stratified Rendering (v0.2 amendment 2026-05-15)
The doc reader and the public Harvest Directory share a single rendering contract for confidence visibility:
observed: rendered plain. The fact is from raw event data or authoritative external publication.inferred: rendered with a soft marker (typographic, not loud). The fact was synthesized from multiple sources or generated by an agent against cited evidence. Hover reveals the inference chain.external_authoritative: rendered with inline source attribution. The fact is cited to a versioned external source (USDA publication, FDA rule, GAP certification body).authored: rendered plain when sourced; rendered with a “no citation” warning marker when the authored section lacksfact_citations[]. For legal-grade doc kinds, missing citations are a CI failure (§15).
This is the single UX rule that converts “AI-generated content” into “auditable content.”
4. Knowledge Storage Contract
4.1 Doc Row
table: knowledge
tenant_id: caller tenant
domain: doc
category: DocContent.doc_kind
key: doc:<doc_id>
title: DocContent.title
content: DocContent JSONB
source: mcp.doc_catalog | github.vault | event.projection
tags: DocContent.tags plus kind/source/status
surfaces: workspace, agent, intranet, fin-central, portal as allowed4.2 Chunk Rows
Extracted text chunks should be written to operational_memory with metadata:
{
content_node_type: "doc" | "artifact",
doc_id?: string,
artifact_id?: string,
source_path?: string,
commit_sha?: string,
version_id?: string,
heading_path?: string[],
chunk_index: number,
visibility: string,
data_plane: string
}5. Event Types
Add event payload schemas for:
doc.ingesteddoc.indexeddoc.render_requesteddoc.rendereddoc.vieweddoc.querieddoc.draft_createddoc.edit_proposeddoc.version_committeddoc.projection_generateddoc.archivedmcp.call_loggedmcp.usage_meteredportal.viewedtenant.vault_configuredtenant.portal_entitlement_updated
Events must include:
- tenant ID
- actor ID when available
- surface
- doc/content node ID
- source commit SHA or source event IDs when available
- classification and visibility
- request/correlation ID
6. Parser Package
Target files:
packages/shared/src/docs/parse-markdown-doc.tspackages/shared/src/docs/resolve-wikilinks.tspackages/shared/src/docs/chunk-doc-content.ts- tests co-located
Responsibilities:
- parse YAML frontmatter
- preserve body text
- extract headings and anchors
- extract wikilinks
- extract markdown links
- extract tags from frontmatter and body
- create deterministic doc IDs from tenant/repo/path or explicit frontmatter ID
- create deterministic chunk IDs/indices
- compute SHA-256
content_hashof the body for incremental embedding (§11.8)
Do not parse markdown with regex-only logic when a structured parser is available in the workspace. If adding a parser dependency, keep it small and runtime-compatible with Workers.
6.1 Typed Wikilink Rules (v0.2)
Wikilinks are not free-text references. They resolve to typed entity IDs so the doc graph carries semantic relations.
Canonical form:
[[type:id]]
[[type:id|display text]]
Examples:
[[commodity:watermelon]]
[[vendor:yuma-melon-co|Yuma Melon Co.]]
[[lot:LX2026-1142]]
[[event:signal.temperature_exception:01HZ...]]
Authoring shorthand is permitted in source markdown. The resolver normalizes
all wikilinks during doc.ingested processing:
[[watermelon]]→ search canonical names within the current tenant scope with type ambiguity resolved by frontmatterentity_type_hint; on ambiguity, mark asunresolved_wikilinkand emitdoc.link_unresolved.[[Yuma Melon Co.]]→ resolve to[[vendor:yuma-melon-co]]if a single vendor row matches; otherwise unresolved.- Already-typed wikilinks pass through unchanged.
Resolver precedence for doc IDs:
- Frontmatter
doc_idfield (canonical). - Frontmatter
entity_id+entity_typeif the doc is an entity profile. - Deterministic hash of
(tenant_id, source_repo, source_path)(legacy).
Renames in the GitHub vault MUST NOT change doc IDs. Frontmatter IDs are the
durable handle. Path-derived IDs are migrated to frontmatter on first observed
rename and the legacy ID is recorded in aliases[].
Output of the resolver feeds:
outbound_links[].resolved_doc_idoutbound_links[].resolved_entity_idlinked_entities[]- the backlink projection (§11.5)
7. GitHub Vault Source Adapter
Target files:
packages/mcp-server/src/ingestion/github-vault.tspackages/mcp-server/src/routes/github-vault-webhook.tspackages/mcp-server/src/tools/docs.ts
Responsibilities:
- verify GitHub webhook signature
- map repo to tenant/vault config
- fetch changed markdown files
- call parser package
- upsert
knowledge.domain = doc - write chunks to
operational_memory - update backlinks for affected docs
- emit lifecycle events
- enqueue render request
Configuration should live in environment variables or tenant config knowledge rows, not hardcoded URLs.
8. MCP Tools
Target file:
packages/mcp-server/src/tools/docs.ts
Register in:
packages/mcp-server/src/tools/index.ts- capability registry and permission policies as appropriate
8.1 search_docs
Input:
tenantIdactorIdactorRolequery- filters: tags, doc_kind, data_plane, visibility, linked entity, source system
topK
Behavior:
- combine structured
knowledgefilters with semanticoperational_memorysearch where available - enforce visibility before returning results
- return summaries with citations
8.2 get_doc
Input:
tenantIdactorIdactorRoledocIdorpath
Behavior:
- fetch doc row
- enforce visibility
- return full content or requested rendering format
- emit
doc.queried
8.3 get_backlinks
Return incoming links for a doc.
8.4 get_doc_graph
Return graph neighborhood up to bounded depth.
8.5 list_by_tag
Return docs/artifacts matching a tag.
8.6 get_decisions
Return decision records filtered by topic, status, date, or linked entity.
8.7 list_recent_changes
Return recently indexed/committed docs from event and knowledge metadata.
8.8 get_entity_profile (v0.2)
Input:
tenantIdactorIdactorRoleentityType(e.g.commodity,vendor,region)entityIdorslugsnapshotPeriod?(optional; defaults to current)responseShape?(markdown|structured|both; defaultboth)
Behavior:
- Resolve to the
entity_profiledoc row. - When
responseShapeincludesstructured, also fetch the source entity knowledge row and return its golden-profile JSONB alongside the rendered markdown. - Always include
fact_citations[]so callers can verify any claim. - Enforce visibility (
tenant_privatefor internal calls,public_anonymizedfor Tier 0 / Harvest Directory callers). - Emit
doc.queriedwithentity_typeandsnapshot_period.
This is the agent-friendly single-call entrypoint that replaces the two-step “query knowledge row + render narrative” pattern used today.
8.9 assemble_recall_trace (v0.2)
Input:
tenantIdactorId(privileged role required)lotId(orproductId,arrivalId)windowDays(default 30)audience(internal|customer|regulator)
Behavior:
- Walk the doc graph:
lot:X→commodity:Y→ linked arrivals, shipments, customers, holds, qc events, temp exceptions. - Compose a
recall_tracedoc with deterministic structure: scope, affected lots, customer reach, root cause candidates, mitigation events, citations. - Persist the resulting doc as
knowledge.domain = doc,doc_kind = recall_trace,lifecycle_status = active. - Emit
doc.projection_generatedandcompliance.recall_trace_assembledevents. - Return the new
doc_idplus a summary digest for the agent caller.
Audience flag controls which fields are projected:
internal: full vendor, cost, margin, all linked actors.customer: customer-safe scope only.regulator: timestamps, lot identifiers, temperature curve, corrective actions, recordkeeping evidence; no commercial fields.
This tool is the headline federal demo. It must produce identical output
given identical inputs (deterministic renderer) and must record
renderer_version for audit.
8.10 propose_doc_edit (v0.2)
Input:
tenantIdactorId(may be human or agent)docIdpatch(markdown body diff or JSON Patch on frontmatter)rationaleevidence(event_ids, knowledge_keys, or external citations supporting the change)
Behavior:
- Validate that the edit does not violate visibility/data-plane rules.
- Persist as a
doc.edit_proposedevent with the patch and evidence. - Surface in the doc reader as a pending suggestion (§13.5).
- No mutation of the canonical doc row until a privileged actor approves.
This implements the AI-assisted enrichment loop without breaking the “GitHub is the authored ledger” invariant. Approval commits to the vault branch and re-runs ingest; rejection is logged.
8.12 walk_provenance (v0.2 amendment 2026-05-15)
Input:
tenantIdactorIdactorRoledocIdanchor?(specific claim anchor; if omitted, walks every anchor in the doc)depth?(default 5; how far back to walk the chain)responseShape?(tree|flat|mermaid; defaulttree)
Behavior:
- For each
fact_citations[]entry under the anchor, walk the chain: citation → event/knowledge/external source → upstream events that produced it → source artifacts referenced. - For events, return: event_id, event_type, occurred_at, originating_actor, upstream correlation_ids, payload digest.
- For knowledge rows, return: domain, key, last_observed_version, source_event_ids that produced the current version.
- For external sources, return: URI, observed_content_hash, observed_at, drift status (current | drifted | unreachable).
- For source artifacts, return: artifact_id, content_hash, uploader, upload_event_id, mime_type, source_channel.
- Return as a tree (or flattened, or as a mermaid graph for the doc reader).
- Emit
provenance.walkedevent with the requested anchor and a digest of the result (so frequent walks are observable in FIN Central).
Use cases:
- A federal reviewer clicks a claim and walks the citation chain to raw events.
- A customer asks “how do you know X” and the agent returns the walk result.
- A CI gate runs
walk_provenanceon every claim in arecall_tracebefore allowing the doc to commit.
Privileged variant: walk_provenance_admin includes private metadata
(actor identities, IP, device) for FIN operator audit. Standard
walk_provenance strips those fields per the audience filter.
8.13 Hybrid Retrieval Contract (v0.2)
Every doc-reading MCP tool (search_docs, get_doc, get_entity_profile,
get_doc_graph) MUST support a three-mode retrieval blend:
structured— filter onknowledgeJSONB fields, tags, classifications.semantic— vector search overoperational_memorychunks scoped to the same tenant and visibility.graph— 1-hop or 2-hop neighborhood traversal usingoutbound_links+inbound_links.
Default blend for search_docs:
- Run structured filter to produce candidate doc set.
- Run semantic search within the candidate set (or whole tenant if filter is empty).
- Expand top-K results with their 1-hop graph neighborhood for context.
- Re-rank using a deterministic scorer (lexical + cosine + freshness + graph centrality).
- Return summaries with
fact_citationsaggregated from all three substrates.
Callers may override the blend by setting retrieval_modes: ["structured"], etc. The default is the blend.
9. HTTP Routes
Target route group:
/api/v1/docs
Initial endpoints:
GET /api/v1/docsGET /api/v1/docs/:docIdGET /api/v1/docs/:docId/graphGET /api/v1/docs/:docId/backlinksPOST /api/v1/docs/importPOST /api/v1/docs/draft
Do not expose mutating routes to customer/vendor surfaces until policy gates are implemented.
10. Workspace UI
10.1 Deep-Link Fix
Add route:
/files/:artifactId
Behavior:
- mount authenticated workspace shell or a focused reader route
- fetch artifact by ID
- enforce visibility
- show preview/download/source attribution
10.2 Content Explorer
Extend current file explorer into a content explorer with modes:
- Files
- Docs
- Decisions
- Proof Packages
- Recent Changes
Candidate files:
packages/workspace/src/components/files/NativeFileExplorer.tsx- new
packages/workspace/src/components/docs/DocExplorer.tsx - new
packages/workspace/src/components/docs/DocReader.tsx - new
packages/workspace/src/mobile/MobileDocsPanel.tsx
10.3 Doc Reader
Doc reader must show:
- title
- status/classification
- source path/commit
- source attribution
- linked entities
- backlinks
- related artifacts
- rendered markdown body
- event provenance
- agent ask/summarize affordance
10.4 Doc Editor
First version should be draft/review only:
- open markdown content
- ask agent to draft edits
- show provenance
- save as proposed edit
- create GitHub branch/commit or
doc.edit_proposedevent - require review before commit to main
Automatic two-way reverse projection is out of scope for v1.
10.5 Doc Lifecycle Events (v0.2 extension)
Extend the §5 event list with v0.2 additions:
doc.link_unresolved— wikilink could not be resolved to an entity/doc.doc.entity_profile_rebuild_requested— triggered when source entity row changes; consumed by entity-doc projection (§11.5.2).doc.snapshot_committed— a periodic snapshot (§11.6) was written.doc.anonymization_applied— a public-anonymized projection was generated for Harvest Directory.compliance.recall_trace_assembled— emitted byassemble_recall_trace.
11. FIN Central Additions
New panels:
- Vault Status
- Doc Ingest Status
- Render Status
- MCP Key Status
- Tenant Readiness
- Channel Health
- Nathel Proof Ledger
- Federal Readiness
- Tier Capability Matrix
- Orgo Exec Agent Status
Each panel should be backed by events/knowledge projections and have tests for empty/error/loading states.
11.5 CQRS Projections For Docs (v0.2)
Two new CQRS projections support the doc layer. Both follow the established pattern from migrations 041-044 but with stricter coherence rules than today’s fire-and-forget projection writes (see Stop Conditions §17).
11.5.1 doc_backlink_projection (migration 049)1
Purpose: precompute inbound links so get_backlinks and get_doc_graph do
not traverse outbound_links arrays at read time.
Shape (illustrative):
tenant_id, target_doc_id, source_doc_id, source_path, source_anchor,
relation_kind, observed_commit_sha, last_seen_atWriters:
doc.indexedconsumer parses the doc’soutbound_links[]and upserts one row per (target, source).doc.archivedconsumer deletes the source’s rows.
Reads: get_backlinks(docId) reads rows where target_doc_id = docId.
get_doc_graph(docId, depth) performs a bounded BFS using the projection.
11.5.2 entity_doc_projection (migration 050)2
Purpose: maintain the one-to-one mapping between Taproot entity rows and their
entity_profile docs, plus the rebuild queue.
Shape (illustrative):
tenant_id, entity_domain, entity_key, profile_doc_id,
renderer_version, last_rendered_at, source_row_version,
status (current | stale | rendering | failed)Writers:
- Entity-row update consumer marks the row
stale. - Renderer worker picks
stalerows, runs the deterministic entity renderer, upserts the profile doc, writes chunks, and markscurrent.
Reads: get_entity_profile() looks up by (entity_domain, entity_key) and
returns the current profile.
11.5.3 Transactional Write Discipline
Doc-related projection writes MUST run inside a single transaction with the
canonical knowledge row write. This is stricter than the existing CQRS
projections which use waitUntil + Promise.allSettled (data-plane audit
P0, 2026-04-30). Rationale: doc drift produces silently wrong agent answers,
not visibly stale UI cells. Implementation:
upsertDoc()wrapsINSERT/UPDATE knowledge, chunk writes, and projection upserts inBEGIN ... COMMIT.- Failures roll back all four operations.
- The lifecycle event is emitted from inside the transaction with a
follow-up
waitUntilonly for fan-out work (search reindex, etc.). - A nightly reconciler verifies (a) every
doc.indexedevent has a corresponding knowledge row, (b) every doc row has the expected chunks, (c) everyentity_doc_projectionrow’ssource_row_versionmatches the current entity row, and emitsdoc.coherence_drift_detectedevents on mismatch.
This discipline should be extended back to the lot_status / actor_pulse / kb_health / system_health projections in a follow-up patch tracked in the data-plane audit ticket.
11.6 Time Snapshot Pattern (v0.2)
Entity profiles and aggregate intelligence docs maintain immutable periodic snapshots in addition to the current live doc.
Path convention:
taproot/commodities/watermelon.md # current
taproot/commodities/watermelon/2026-W19.md # weekly snapshot
taproot/commodities/watermelon/2026-M05.md # monthly snapshot
harvest-directory/commodities/watermelon.md # public current
harvest-directory/commodities/watermelon/2026-W19.md # public snapshot
Doc rows for snapshots:
snapshot_periodfield set (e.g.2026-W19).lifecycle_status = archivedfrom the moment of write.- Immutable: any later attempt to upsert with the same snapshot key is
rejected with
409 SnapshotImmutable. - The current doc’s
aliases[]does NOT include snapshot IDs; snapshots are separate citation targets so “what we knew in W19” stays addressable.
Snapshots run via scheduled projection jobs:
- Weekly: Mondays 06:00 tenant-local, freezes the prior week.
- Monthly: 1st of each month 06:00 tenant-local, freezes the prior month.
Federal-grade evidence packs SHOULD cite snapshots, not current docs, so claims remain pinned to a specific point in time.
11.7 3-Rung Taproot Architecture (Wave 4 — Active, v0.2 amendment 2026-05-17)
The taproot — the centrally-authored canon of entity profiles (commodities, varieties, vendors, customers, regions, seasons, growers, certifications) — reaches tenant Neon through three distinct rungs. Each rung has its own renderer, its own write target, and its own failure mode. The middle rung (transport) is the load-bearing one and the one that was unspec’d until this amendment.
WS-17 (the wave-5 audit) flagged “Wave 1 + Wave 4 of taproot sync
design are NOT IMPLEMENTED” as the deepest single gap of the
remediation program. P0-W5-05 shipped Wave 4. This section
promotes the architectural description out of
FIN_CENTRAL_TENANT_DEMARCATION_PLAN_2026-05-15.md §16 (where it
landed first, as a non-canonical doc) into the engineering spec
as the canonical reference. The publish/receive workflows
themselves already cite “Engineering Spec §11.7” in their headers.
11.7.1 The Three Rungs
Rung 1 — Central rendering
┌──────────────────────────────────────────────────────────┐
│ fin-central-renderer (Cloudflare Worker) │
│ reads: knowledge.commodity/vendor/etc. (Central Neon) │
│ writes: entity_doc_projection row (migration 050) │
│ + markdown file at docs/entity_profiles/<...>.md│
│ + git commit to fin-central-intranet:main │
└──────────────────────┬───────────────────────────────────┘
│
▼ push to main
Rung 2 — Central → Tenant transport
┌──────────────────────────────────────────────────────────┐
│ fin-central-intranet/.github/workflows/taproot-publish │
│ trigger: push to main touching docs/entity_profiles/** │
│ action: repository_dispatch event=taproot_sync │
│ client_payload={paths, source_sha, ...} │
│ fan out to FIN_TAPROOT_TENANT_REPOS │
└──────────────────────┬───────────────────────────────────┘
│
▼ GitHub Actions REST API
┌──────────────────────────────────────────────────────────┐
│ nathel-intranet/.github/workflows/taproot-receive.yml │
│ trigger: repository_dispatch taproot_sync │
│ action: fetch each path from source @ source_sha, │
│ write to _taproot_mirror/<same-path>, │
│ commit + push to nathel-intranet:main │
└──────────────────────┬───────────────────────────────────┘
│
▼ GitHub webhook (P0-W5-03)
┌──────────────────────────────────────────────────────────┐
│ staging Worker /webhooks/github-vault │
│ verify HMAC, fetch file body, parseDocMarkdown, │
│ upsertDoc into tenant Neon knowledge │
└──────────────────────┬───────────────────────────────────┘
│
▼ knowledge upsert + event
Rung 3 — Tenant overlay rendering
┌──────────────────────────────────────────────────────────┐
│ tenant projector workers (e.g. daily-arrivals) │
│ reads: events (lot.received, inbound.load_expected, │
│ ...) + the mirrored taproot knowledge │
│ writes: operational profile doc back to │
│ nathel-intranet:docs/operational/<...>.md │
│ emits: taproot.mirror_updated (Rung 2 ack) + │
│ doc.indexed (Rung 3 write) │
└──────────────────────────────────────────────────────────┘
The split is deliberate: Rung 1 is pure central knowledge (no
tenant data ever flows into docs/entity_profiles/**), Rung 2 is
push-based transport with no state of its own, and Rung 3 is
where tenant operational facts overlay the centrally-authored
canon. A tenant never edits the mirror directly — overlays land
on knowledge.domain = taproot_overlay_* rows and docs/operational/
docs, leaving _taproot_mirror/ byte-identical to the source.
11.7.2 Rung 1 — Central MD Renderer
| Concern | Location |
|---|---|
| Worker | fin-central-renderer (Cloudflare) |
| Renderer code | packages/mcp-server/src/ingestion/renderers/entity-doc-renderer-worker.ts |
| Projection table | entity_doc_projection (migration 050, see §11.5.2) |
| Output path | fin-central-intranet:docs/entity_profiles/<domain>/<key>.md |
| Output frontmatter | doc_kind: entity_profile (§3.3), entity_domain, entity_key, renderer_version |
| Trigger | entity_doc_projection row with status='stale' |
| Cadence | Cron + on-demand via worker queue |
The renderer is deterministic: given the same Central Neon
state and the same renderer_version, the output markdown is
byte-identical. This is a hard constraint — drift here would
silently break the diff-detection in Rung 2. Renderer version
bumps require the explicit backfill discipline of §11.8.
The current entity domain coverage as of this amendment:
commodity, variety, vendor, customer, location,
lot_profile. P1-09 shipped the four non-commodity renderers.
11.7.3 Rung 2 — Central → Tenant Transport
| Concern | Location |
|---|---|
| Publish workflow | fin-platform/fin-central-intranet/.github/workflows/taproot-publish.yml |
| Receive workflow | fin-platform/nathel-intranet/.github/workflows/taproot-receive.yml |
| Mirror directory | nathel-intranet:_taproot_mirror/docs/entity_profiles/** |
| Receiver-side README | nathel-intranet:_taproot_mirror/README.md |
| Author-side README | fin-central-intranet:docs/entity_profiles/README.md |
| Activation runbook | fin-agentic:.artifacts/remediation/P0-W5-05/activation-runbook.md |
Why push (Wave 4) over poll (Wave 1):
| Concern | Wave 1 poll | Wave 4 push (chosen) |
|---|---|---|
| Latency | Up to 5 min (cron interval) | < 30 s (event-driven) |
| Cost | Cron-runs whether or not anything changed | Zero traffic when Central is idle |
| State to maintain | ”Last sync time” cursor in KV or knowledge row | None — every dispatch carries its own SHA |
| Failure recovery | Next cron picks up the diff | Manual workflow_dispatch resync |
| Code surface | New Cloudflare Worker + KV + GitHub Commits API client | Two YAML files using built-in GH Actions |
| Operational complexity | Worker deploy, Worker logs, KV inspection | Two Actions tabs, standard run logs |
| Coverage | Reliable if cron always fires | Loses the dispatch if receive workflow is briefly down |
Wave 4 wins on latency, cost, simplicity, and state. The single
weakness (briefly-down receiver loses a dispatch) is mitigated by
the workflow_dispatch full-resync escape hatch on the receiver.
Wave 1 remains a documented fallback (not currently built) for
the edge case where Wave 4 dispatches are systematically lost.
Activation prerequisites:
- Repo secret
TAPROOT_DISPATCH_TOKENonfin-platform/fin-central-intranet— PAT withrepoplusactions:writescope on every tenant intranet repo. - Repo secret
TAPROOT_FETCH_TOKENon each tenant intranet — PAT withContents: readonfin-central-intranet. Required because the source repo is private; falls back toGITHUB_TOKENfor public sources. - Optional repo variable
FIN_TAPROOT_TENANT_REPOSonfin-central-intranet— newline- or comma-separated<owner>/<repo>list. Defaultfin-platform/nathel-intranet.
The two workflows ship with permissions: contents: write (receive)
/ contents: read (publish) and use only minimal-scope tokens.
Every dynamic value is passed via env: to avoid script injection;
the mirror step refuses any path outside docs/entity_profiles/.
End-to-end latency target: < 60 s from fin-central-intranet
merge to nathel-intranet:main commit; < 5 min to events row
in tenant Neon.
The implementation landed in two paired commits:
fin-central-intranet@0020bad7 (publish workflow + author-side
README) and nathel-intranet@99b2a1b0 (receive workflow + mirror
README flipped to ACTIVE).
11.7.4 Rung 3 — Tenant Overlay Renderer
Where Rung 1 writes centrally-authored canon and Rung 2 mirrors
that canon byte-identically into the tenant intranet repo,
Rung 3 is where tenant operational truth gets composed on top
of the canon. The output of Rung 3 lives in the tenant
intranet repo at docs/operational/<...>.md, never under
_taproot_mirror/, preserving the read-only invariant of Rung 2.
| Concern | Location |
|---|---|
| Worker | tenant projector workers in packages/mcp-server/src/workers/tenant-projector/ |
| First exemplar | daily-arrivals.ts (P0-W5-06) |
| Trigger | event stream (e.g. inbound.load_expected, lot.received) + scheduled rollups |
| Read inputs | tenant events, tenant knowledge (including mirrored taproot rows), tenant actors.memory |
| Write target | nathel-intranet:docs/operational/<...>.md via the same vault webhook path used by Rung 2 |
| Output frontmatter | doc_kind: aggregate_intelligence or doc_kind: operational_brief, data_plane: operational_data, visibility: tenant_private |
| Events emitted | taproot.mirror_updated (acks Rung 2 land), doc.projection_generated, doc.indexed |
Rung 3 is intentionally pluggable: each tenant operational
projection is its own worker. The daily-arrivals 6 AM buyer
briefing is the first concrete instance; future rungs include
vendor scorecards, lot-receipt summaries, and FSMA recall traces.
A Rung 3 projector MUST:
- Read tenant data via
getDb()withSET LOCAL app.tenant_id. - NEVER write to
_taproot_mirror/**(the file watcher refuses such paths anyway; this is a defense-in-depth rule). - Cite both the canonical taproot doc (Rung 2 mirror, by
doc_id) and the underlying events (byevent_id) insource_attribution[]of every output chunk. - Be idempotent: re-running on the same window must produce the same output bytes (so the same content hash flows through §11.8 and no re-embedding is triggered).
11.7.5 Multi-Tenant Generalization
Adding a second tenant <X> is two steps:
- Provision
<X>-intranetrepo, copy_taproot_mirror/README.mdand the receive workflow. - Append
fin-platform/<X>-intranetto theFIN_TAPROOT_TENANT_REPOSrepo variable onfin-central-intranet.
The fan-out loop in taproot-publish.yml already handles N
targets. Rung 1 is single-instance per Central deployment; Rung 3
is per-tenant by construction.
11.7.6 Failure Modes and Their Owners
| Failure | Detected by | Owner |
|---|---|---|
| Rung 1 renderer crash | entity_doc_projection.status='failed' row | Platform — re-run worker |
| Rung 1 byte drift between runs | nightly reconciler (§11.5.3 (c)) emits doc.coherence_drift_detected | Platform — bump renderer_version + backfill plan |
| Rung 2 dispatch token expired | publish workflow run fails red on gh api .../dispatches | Platform DevOps — rotate TAPROOT_DISPATCH_TOKEN |
| Rung 2 receiver down at dispatch time | dispatch is lost (no retry built into GH Actions) | Orchestrator — manual workflow_dispatch full resync |
Rung 2 mirror write outside docs/entity_profiles/ | receive workflow’s path guard emits ::warning:: and increments skipped | Platform — investigate source diff |
| Rung 3 projector failure | tenant worker logs + Sentry | Tenant DevOps |
Rung 3 writes to _taproot_mirror/** | publish workflow path filter does NOT fire (good); vault webhook records as authored doc | Code review (should never reach prod) |
11.7.7 Tracker and Provenance
- P0-W5-05 in
docs/programs/PROJECTION_REMEDIATION_TRACKER_2026-05-17.md(fin-agentic) closed Rung 2. - P0-W5-06 closed the first Rung 3 projector (daily_arrivals).
- P1-09 closed the four non-commodity Rung 1 renderers (vendor, customer, location, lot_profile).
- T-W4-A first drafted this architecture into
docs/architecture/FIN_CENTRAL_TENANT_DEMARCATION_PLAN_2026-05-15.md§16 of fin-agentic; P1-W5-05 (this amendment) promotes it into the canonical engineering spec.
Wave 1 (poll) remains documented in the demarcation plan as a reserved fallback but is intentionally not built. See §11.7.3.
11.8 Incremental Embedding Rule (v0.2)
Entity-profile projection at full scale is ~17.5K entity docs (Taproot current size). Naïve re-embedding on every commit is prohibitive.
Rules:
- Every doc row carries
content_hash(§3.1). - Every chunk in
operational_memorycarries thecontent_hashof its parent doc body at the time it was embedded. - On
doc.indexed, compare newcontent_hashto chunk-sidecontent_hash. Only re-embed chunks whose body changed. - Frontmatter-only changes (tag edits, status changes) do NOT trigger re-embedding.
- Renderer version bumps DO trigger re-embedding, and require an explicit
backfill plan recorded in
docs/migrations/renderer-version-bumps.mdbefore merge.
Embedding cost budget alert: if a single doc-indexed batch would re-embed
more than 2,500 docs, the worker must page the request and emit
doc.embedding_batch_paged.
12. Artifact Route Enhancements
Current allowed upload content types should be extended in a separate patch with policy review:
text/markdowntext/plaintext/html- DOCX MIME
- XLSX MIME
Each added type needs parser, preview, classification, and security review.
12.5 Embedded Vega-Lite Chart Rendering (v0.2)
Entity profiles and proof packages MAY embed deterministic charts projected
from events. Charts live inside the doc body as fenced blocks and inside the
DocContent embedded_charts[] field for programmatic access.
Authoring syntax:
```vega-lite
{
"title": "Watermelon arrivals — last 12 weeks",
"data": {"name": "weekly_arrivals"},
"mark": "line",
"encoding": { ... }
}
Renderer responsibilities:
- Each chart `spec` MUST be a deterministic transform of
`generated_from_event_ids[]`. No live queries at render time. Data is
pre-aggregated by the entity-doc projection (§11.5.2) into the chart's
embedded `data.values`.
- Charts are NOT re-rendered on view. They are baked at projection time.
- The doc reader and the public Harvest Directory share the same renderer
(Vega-Lite to inline SVG) so charts work for both humans and agent
responses that include rendered image links.
- Anonymized profiles strip series labels that could re-identify customers
or vendors; aggregate ranges replace specific values.
Allowed chart kinds (v0.2): line, bar, area, scatter, heatmap. No
interactive controls in v0.2. Time-series sparklines for KPI strips are
permitted as a constrained sub-form.
## 13. Tenant Provisioning Contract
Add a tenant readiness projection that tracks:
- tenant record exists
- DB branch/schema ready
- Cloudflare route ready
- R2/storage prefix ready
- email inbound configured
- outbound email configured
- Telnyx number and 10DLC status
- GitHub vault configured
- Quartz/render target configured
- MCP keys provisioned
- portal entitlements configured
- smoke tests passed
## 13.5 Agent Enrichment Loop (v0.2)
The `propose_doc_edit` MCP tool (§8.10) and the `doc.edit_proposed` event
enable an explicit human-agent collaboration cycle on entity profiles and
authored docs.
Loop:
1. An agent (operator agent, ops agent, exec agent) is answering a question
and discovers a gap — e.g. `commodity:papaya` profile is missing
post-harvest physiology data the agent needed to cite.
2. Agent calls `propose_doc_edit` with the patch and evidence (the events,
knowledge rows, or external sources that support the claim).
3. `doc.edit_proposed` event persists with full provenance.
4. Doc reader (§10.3) renders pending edits as inline suggestions with the
evidence linked.
5. A privileged human reviewer accepts, edits, or rejects. Acceptance opens
a GitHub branch with the patch, runs CI, and merges to the vault. The
normal ingest path re-projects the doc.
6. Rejection logs `doc.edit_rejected` with rationale; that rationale becomes
part of the doc's enrichment history and prevents repeated near-duplicate
suggestions.
Guardrails:
- Agents may NOT propose edits to `decision_record` docs.
- Agents may NOT propose edits that change `visibility` or `data_plane`.
- Agents may NOT propose deletion. Only `lifecycle_status` change to
`archived` is permitted, and only with privileged human approval.
- All proposals MUST include at least one citation in `evidence`. Empty
evidence is rejected at the MCP boundary.
Metrics to track in FIN Central:
- proposals per agent per week
- acceptance rate per agent
- average time-to-review
- doc enrichment rate (lines added per week across entity profiles)
- agent reasoning quality lift after enrichment (measurable via gradeboard)
## 13.6 Cite-Then-Claim Agent Generation Contract (v0.2 amendment 2026-05-15)
Any MCP tool that generates doc prose (entity-profile bodies, event-digest
narratives, proof-package summaries, suggested edits via `propose_doc_edit`)
MUST conform to the cite-then-claim contract.
The tool signature:
```ts
type GenerateDocSectionInput = {
tenantId: string;
actorId: string;
targetDocId: string;
targetSectionAnchor: string;
topic: string;
evidence_chunks: Array<{ // required: at least one chunk
chunk_id: string;
source_kind: "event" | "knowledge_row" | "operational_memory_chunk" | "external_uri";
source_ref: string;
excerpt: string;
confidence: "observed" | "inferred" | "external_authoritative";
}>;
style_hints?: string;
max_tokens?: number;
};
type GenerateDocSectionOutput = {
generated_claims: Array<{
text: string; // a single sentence or claim
cited_chunk_ids: string[]; // required: at least one
confidence: "observed" | "inferred" | "external_authoritative";
inference_notes?: string; // required if confidence = "inferred"
}>;
unresolved_topics: string[]; // topics the agent could not ground
rejected_reason?: string;
};
Contract rules:
- Empty
evidence_chunksinput is REJECTED with400 NoEvidenceProvided. - Output is a structured array of attributed claims, never free prose. The renderer assembles the prose from the structured output.
- Each claim MUST have
cited_chunk_idswith at least one entry. Claims without citations are dropped and listed inunresolved_topics. - Confidence is the MINIMUM across cited chunks (never higher than the least-confident source).
inference_notesMUST explain the reasoning when confidence isinferred. The renderer surfaces this on hover.- If the agent cannot ground a topic against the provided evidence, the
topic appears in
unresolved_topics. The reviewer sees the gap. The doc does NOT include unresolved content. - The tool emits a
doc.section_generatedevent with the input topic, output claim count, dropped claim count, and model/version used.
The contract prevents the dominant failure mode of LLM authoring: hallucination plausibly mixed with grounded content. By forcing structure at the tool boundary, ungrounded content is structurally impossible to emit through this path.
13.7 External Source Drift Monitoring (v0.2 amendment 2026-05-15)
External authoritative sources cited in docs (USDA publications, FDA rules, GAP certification documents, university extension pages) mutate over time. The system MUST detect drift and flag affected docs.
Mechanism:
- Every
external_source_uricitation recordsobserved_content_hashandobserved_atat citation time. - A scheduled job (
external_source_drift_monitor) fetches each unique external URI weekly, computescurrent_content_hash, and compares. - On hash mismatch, emit
external_source_drift_detectedwith the URI, prior and current hashes, and the list of citing docs. - Affected docs transition to
lifecycle_status = stale_external_citationand surface a banner in the doc reader. - A reviewer either confirms the new content is still consistent (records
the new hash + an
external_citation_revalidatedevent) or proposes an edit viapropose_doc_editto update the citation.
Constraints:
- Some external sources are paywalled, rate-limited, or anti-scraping. The
monitor MUST respect robots.txt and rate limits. Failures emit
external_source_unreachablenotexternal_source_drift_detected. - For high-value sources (USDA, FDA, GS1), prefer subscribing to their change-feed where available, not polling.
- Sources may be intentionally archived (a paper that’s been superseded).
The reviewer can mark a citation
lifecycle = archived_sourceto suppress drift alerts.
Drift events feed the FIN Central tenant readiness projection and the public Harvest Directory page that lists “knowledge base freshness.”
14. Tests
14.1 Unit Tests
- markdown parser
- wikilink resolver (typed and shorthand forms, ambiguity, rename stability)
- doc schema validation including entity_profile required fields
- permission filters for both five-state authored docs and two-state entity profiles
- event payload schemas (including v0.2 lifecycle events)
- doc MCP tool contracts
- content_hash determinism
- vega-lite spec validation
- recall-trace renderer determinism (identical inputs → identical output)
- anonymization policy gate (public_anonymized projections strip required fields)
14.2 Integration Tests
- GitHub webhook ingest to knowledge doc row
- doc chunks to operational memory
- backlinks recompute via
doc_backlink_projection - entity row update →
entity_doc_projectionrebuild → fresh chunks - transactional doc write rollback on chunk failure
- snapshot immutability (second write to same period returns 409)
- incremental embedding skips unchanged chunks
- hybrid retrieval blend returns merged citations
assemble_recall_traceproduces deterministic outputpropose_doc_editrejects empty-evidence proposalsdoc.coherence_drift_detectedfires on injected driftsearch_docsstructured filterget_docvisibility enforcement- file deep link route
- FIN Central vault status empty/error/success states
- Harvest Directory adapter projects without leaking tenant fields
14.3 Browser Tests
- desktop content explorer
- mobile content explorer
- doc reader with backlinks
- doc reader renders embedded vega-lite charts
- doc reader surfaces pending agent-proposed edits with evidence links
- restricted doc hidden from unauthorized actor
- customer portal excludes internal docs
- file share deep link loads permitted artifact
- entity profile shows live doc + snapshot picker
- Harvest Directory public page renders without tenant-private fields
15. Proof Gates
Wave 1 is not complete until:
- no new runtime table is introduced
- parser tests pass
- typed wikilink resolver tests pass
- doc schema tests pass
- doc MCP contract tests pass
- artifact/file deep-link test passes
- tenant scope is proven in docs tools
- unauthorized doc access is rejected
- sample FIN Intra doc is indexed into
knowledge.domain = doc - sample search returns citations
- FIN Central shows vault/doc ingest status
- doc writes are transactional (rollback test passes)
- nightly coherence reconciler runs and emits
doc.coherence_drift_detectedon synthetic drift fixtures - content_hash + incremental embedding test passes (no re-embed on frontmatter-only change)
Wave 2 (entity profile + Taproot projection) is not complete until:
- one Taproot entity type (commodities) projects end-to-end for the pilot tenant
- entity row update → projection rebuild observed within target latency
get_entity_profilereturns markdown + structured + citations- snapshot immutability proven
- renderer_version recorded on every projection
- gradeboard A/B test shows measurable lift on commodity-grounded questions vs. structured-only retrieval
Wave 3 (recall trace + Harvest Directory) is not complete until:
assemble_recall_traceproduces identical output for identical inputs across three runs- regulator-audience trace excludes all commercial fields (negative test)
- Harvest Directory public page renders one full commodity with zero tenant-private leakage
- anonymization policy is recorded as a versioned, reviewable artifact
16. Migration Of FIN Intra_GTM
Treat FIN Intra_GTM as the pilot content fixture.
Required steps:
- Decide Markdown is canonical, or explicitly record exceptions.
- Compare DOCX companions to Markdown and mark DOCX as generated/export unless content exists only in DOCX.
- Add frontmatter IDs where missing.
- Add doc classifications.
- Add tags for planning lanes.
- Run parser fixture tests.
- Index into pilot tenant.
- Render in pilot intranet.
- Make MCP search and
get_docwork over the set.
17. Implementation Stop Conditions
Stop implementation and return to architecture review if:
- a fifth runtime table is required
- doc permissions cannot be enforced before render/search
- external MCP exposes tenant-private data
- GitHub source cannot be authenticated and tenant-mapped
- reverse projection would allow unreviewed edits into operational truth
- live Nathel proof work is blocked by speculative platform buildout
- doc projection writes cannot be made transactional (silent drift risk)
- entity-row → entity-profile divergence cannot be detected by the reconciler
- the renderer cannot be made deterministic for recall traces
- the anonymization gate for Harvest Directory cannot be made auditable and reviewable
- typed wikilinks cannot be resolved deterministically inside Workers runtime limits
19. Editing Surfaces Appendix (v0.2 amendment 2026-05-15)
19.1 Editing Modes
Three editing modes coexist, all terminating in a GitHub commit:
| Mode | Audience | Tools | Path to canon |
|---|---|---|---|
| External authoring | Power users (FIN team, decision authors, SOP maintainers, internal subject experts) | Obsidian, VS Code, Cursor, local git, GitHub web UI | Local edit → git commit/push → vault webhook → ingest |
| In-product lightweight | Operators on phones, customer portal users, vendor portal users, employees in PWA | Workspace doc editor, mobile editor, portal forms | Save in app → API → GitHub commit via service account → vault webhook → ingest |
| Agent-proposed | Operator agent, ops agent, exec agent | propose_doc_edit MCP tool | Agent draft → review queue → human approval → GitHub commit → vault webhook → ingest |
The architectural invariant: NO editing surface may directly mutate a
knowledge.domain = doc row. All edits flow through GitHub.
19.2 In-Product Editor Constraints
The in-product editor is intentionally simple. It MUST:
- support markdown body editing
- preserve unknown frontmatter fields verbatim
- preserve unknown body sections verbatim (do not “clean up” projected sections it does not recognize)
- show a preview rendered identically to the canonical doc reader
- emit a
doc.draft_createdevent on first save anddoc.version_committedon publish-approve - enforce per-doc visibility rules at the editor boundary (don’t allow raising visibility above the actor’s role)
- require non-empty
fact_citations[]for legal-grade doc kinds before publish
The in-product editor MUST NOT:
- offer features beyond markdown body editing in v1 (no AI rewrite buttons
in v1 — those go through
propose_doc_edit) - bypass GitHub for the canonical commit
- modify projected sections of a doc that contains both authored and
projected sections (it edits only
authoredsections)
19.3 External Editor (Obsidian-Class) Integration
Each tenant has a private GitHub repo (the vault). Authors clone locally. Local edits push to GitHub. The vault webhook fires. Ingest re-projects.
Conventions enforced by repo CI:
- frontmatter
doc_idrequired for all docs intaproot/,decisions/,runbooks/,sops/, andproof/ - system-projected folders (
event-digests/,entity-profiles-generated/) are read-only for authors; the CI rejects commits that touch them from human authors - agent-generated commits are made by the platform service account; they bypass the read-only rule for projected folders
- wikilinks resolve at CI time; unresolved links produce
doc.link_unresolvedwarnings (not commit blocks, unless count exceeds threshold) obsidian-vault-config.mdat the vault root declares conventions for local Obsidian users
Multi-author workflows use standard Git: branches, PRs, review comments. Optional: Obsidian Sync, Obsidian Git plugin, or terminal Git.
19.4 File Upload Flow
Every file upload terminates in three persistent shapes: an event, an artifact row, and (optionally) a doc projection.
- File enters via Workspace upload | email attachment | mobile camera | GitHub commit | agent-generated artifact.
- Upload handler:
- computes
content_hash(SHA-256 of bytes) - persists bytes to StorageProvider keyed by
content_hash(dedup automatic across tenants) - emits
artifact.uploadedevent with full provenance metadata - upserts
knowledge.domain = artifactrow
- computes
- Classifier examines MIME and content:
- markdown / DOCX / HTML / text: parses and projects to
knowledge.domain = doc - XLSX: extracts tables, optionally projects each sheet as a doc
- PDF: OCRs (if needed), chunks, projects as a doc with page-anchored chunks
- image: extracts EXIF, runs vision model for content tagging, no doc projection by default
- audio: transcribes (if context warrants), creates doc projection of transcript
- markdown / DOCX / HTML / text: parses and projects to
- If a doc projection is created, the doc carries
linked_artifact_ids: [artifact_id]and the artifact carriesderived_doc_id. Bidirectional. - If the file matches an existing
content_hash, no new bytes are stored; a new artifact row points to the existing blob with a freshartifact.uploadedevent recording the new upload context. - Mobile uploads additionally capture
geo_hint,device_id, and the active PWA context (current task, current lot, current actor) so the upload is operationally grounded.
19.5 Workflow Is Event-Sourced
Doc workflow is encoded entirely in lifecycle events:
doc.draft_created
→ doc.review_requested
→ doc.review_assigned
→ doc.review_approved | doc.review_rejected
→ doc.version_committed (on approval)
Configuration per doc_kind lives in knowledge.domain = workflow_config
rows (one config per doc_kind per tenant). No workflow table.
Capabilities:
- per-doc_kind reviewer requirements (decision records: 2 reviewers; recall_trace: privileged role only; entity_profile: subject expert)
- auto-routing on linked entities (papaya commodity edit routes to actor
with
expertise:papayainactors.memory) - SLA tracking via
doc.review_sla_exceededevents with configurable thresholds - multi-step gates for federal-audience proof_packages (compliance review
- legal review)
19.6 Synchronous Collaboration Is Out Of Scope For v1
Real-time CRDT editing (Yjs, Liveblocks, Notion-style) is explicitly out of scope for v1 and likely v2. Async collab patterns are sufficient for target audiences. If sync collab becomes a requirement, it lives as a CRDT layer on top of draft state, never replacing the GitHub-as-canon invariant.
19.7 Sandbox = Draft State
A draft is a first-class doc row with lifecycle_status = draft and
draft_of linking to a parent doc (or null for new docs). Drafts have
their own doc_id. Agents and humans both produce drafts identically.
The doc reader has a “preview” mode that renders the draft as if
committed.
A separate notion of agent action sandbox applies to MCP tools that
take real-world actions (modify customer pricing, send customer email,
commit doc edits across visibility planes). Those actions run in
proposed mode by default, generate an artifact, route through review,
and execute on approval. This is the OpenClaw / NemoClaw mediation
pattern.
18. Out-Of-Scope For v0.2 Spec (Tracked Separately)
- Two-way Obsidian reverse projection without review (deferred).
- Full Quartz multi-tenant deployment (Phase 0 pilot only).
- Interactive chart controls (sliders, filters) inside docs.
- Cross-tenant aggregate analytics dashboards (separate doc).
- Federal certification workflow automation (federal track owns this).
- Agent-initiated structural edits to ontology (new entity types, new
relation kinds). Ontology changes remain a human-only decision recorded as
an authored
decision_record.
Footnotes
-
This spec originally numbered the projection migration as 045, but the actual 045 slot was filled by an unrelated index migration (
events_causation_index) before the doc-CQRS projections were authored. The projection landed at the next available slot, 049. See ADR-005 for the full rationale. ↩ -
This spec originally numbered the projection migration as 046, but the 046 slot was filled by an unrelated index migration (
events_tenant_actor_timestamp_index). The projection landed at the next available slot, 050. See ADR-005. ↩