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_kind enum extended with entity_profile, aggregate_intelligence, and recall_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 frontmatter doc_id taking precedence over path-derived IDs.
  • New §11.5 defines two CQRS projections: doc_backlink_projection (migration 049) and entity_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, and propose_doc_edit.
  • New §12.5 specifies embedded Vega-Lite chart rendering inside entity profiles.
  • New §13.5 defines the doc.edit_proposed agent 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, and operational_memory.
  • Do not create a runtime documents table.
  • Use StorageProvider for 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 = commodity row: structured golden profile (~900 attributes JSONB). Mutable. Queried by structured filters.
  • knowledge.domain = doc row with doc_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_profile body must contain at least one projected section bound to the source entity row.
  • A recall_trace body must be 100% projected sections. Authored content is rejected at write time.
  • A decision_record body must be 100% authored sections.
  • A proof_package may mix, but every authored claim must include fact_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 lacks fact_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 allowed

4.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.ingested
  • doc.indexed
  • doc.render_requested
  • doc.rendered
  • doc.viewed
  • doc.queried
  • doc.draft_created
  • doc.edit_proposed
  • doc.version_committed
  • doc.projection_generated
  • doc.archived
  • mcp.call_logged
  • mcp.usage_metered
  • portal.viewed
  • tenant.vault_configured
  • tenant.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.ts
  • packages/shared/src/docs/resolve-wikilinks.ts
  • packages/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_hash of 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.

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 frontmatter entity_type_hint; on ambiguity, mark as unresolved_wikilink and emit doc.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:

  1. Frontmatter doc_id field (canonical).
  2. Frontmatter entity_id + entity_type if the doc is an entity profile.
  3. 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_id
  • outbound_links[].resolved_entity_id
  • linked_entities[]
  • the backlink projection (§11.5)

7. GitHub Vault Source Adapter

Target files:

  • packages/mcp-server/src/ingestion/github-vault.ts
  • packages/mcp-server/src/routes/github-vault-webhook.ts
  • packages/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:

  • tenantId
  • actorId
  • actorRole
  • query
  • filters: tags, doc_kind, data_plane, visibility, linked entity, source system
  • topK

Behavior:

  • combine structured knowledge filters with semantic operational_memory search where available
  • enforce visibility before returning results
  • return summaries with citations

8.2 get_doc

Input:

  • tenantId
  • actorId
  • actorRole
  • docId or path

Behavior:

  • fetch doc row
  • enforce visibility
  • return full content or requested rendering format
  • emit doc.queried

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:

  • tenantId
  • actorId
  • actorRole
  • entityType (e.g. commodity, vendor, region)
  • entityId or slug
  • snapshotPeriod? (optional; defaults to current)
  • responseShape? (markdown | structured | both; default both)

Behavior:

  • Resolve to the entity_profile doc row.
  • When responseShape includes structured, 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_private for internal calls, public_anonymized for Tier 0 / Harvest Directory callers).
  • Emit doc.queried with entity_type and snapshot_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:

  • tenantId
  • actorId (privileged role required)
  • lotId (or productId, arrivalId)
  • windowDays (default 30)
  • audience (internal | customer | regulator)

Behavior:

  • Walk the doc graph: lot:Xcommodity:Y → linked arrivals, shipments, customers, holds, qc events, temp exceptions.
  • Compose a recall_trace doc 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_generated and compliance.recall_trace_assembled events.
  • Return the new doc_id plus 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:

  • tenantId
  • actorId (may be human or agent)
  • docId
  • patch (markdown body diff or JSON Patch on frontmatter)
  • rationale
  • evidence (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_proposed event 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:

  • tenantId
  • actorId
  • actorRole
  • docId
  • anchor? (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; default tree)

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.walked event 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_provenance on every claim in a recall_trace before 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 on knowledge JSONB fields, tags, classifications.
  • semantic — vector search over operational_memory chunks scoped to the same tenant and visibility.
  • graph — 1-hop or 2-hop neighborhood traversal using outbound_links + inbound_links.

Default blend for search_docs:

  1. Run structured filter to produce candidate doc set.
  2. Run semantic search within the candidate set (or whole tenant if filter is empty).
  3. Expand top-K results with their 1-hop graph neighborhood for context.
  4. Re-rank using a deterministic scorer (lexical + cosine + freshness + graph centrality).
  5. Return summaries with fact_citations aggregated 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/docs
  • GET /api/v1/docs/:docId
  • GET /api/v1/docs/:docId/graph
  • GET /api/v1/docs/:docId/backlinks
  • POST /api/v1/docs/import
  • POST /api/v1/docs/draft

Do not expose mutating routes to customer/vendor surfaces until policy gates are implemented.

10. Workspace UI

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_proposed event
  • 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 by assemble_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).

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_at

Writers:

  • doc.indexed consumer parses the doc’s outbound_links[] and upserts one row per (target, source).
  • doc.archived consumer 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 stale rows, runs the deterministic entity renderer, upserts the profile doc, writes chunks, and marks current.

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() wraps INSERT/UPDATE knowledge, chunk writes, and projection upserts in BEGIN ... COMMIT.
  • Failures roll back all four operations.
  • The lifecycle event is emitted from inside the transaction with a follow-up waitUntil only for fan-out work (search reindex, etc.).
  • A nightly reconciler verifies (a) every doc.indexed event has a corresponding knowledge row, (b) every doc row has the expected chunks, (c) every entity_doc_projection row’s source_row_version matches the current entity row, and emits doc.coherence_drift_detected events 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_period field set (e.g. 2026-W19).
  • lifecycle_status = archived from 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

ConcernLocation
Workerfin-central-renderer (Cloudflare)
Renderer codepackages/mcp-server/src/ingestion/renderers/entity-doc-renderer-worker.ts
Projection tableentity_doc_projection (migration 050, see §11.5.2)
Output pathfin-central-intranet:docs/entity_profiles/<domain>/<key>.md
Output frontmatterdoc_kind: entity_profile (§3.3), entity_domain, entity_key, renderer_version
Triggerentity_doc_projection row with status='stale'
CadenceCron + 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

ConcernLocation
Publish workflowfin-platform/fin-central-intranet/.github/workflows/taproot-publish.yml
Receive workflowfin-platform/nathel-intranet/.github/workflows/taproot-receive.yml
Mirror directorynathel-intranet:_taproot_mirror/docs/entity_profiles/**
Receiver-side READMEnathel-intranet:_taproot_mirror/README.md
Author-side READMEfin-central-intranet:docs/entity_profiles/README.md
Activation runbookfin-agentic:.artifacts/remediation/P0-W5-05/activation-runbook.md

Why push (Wave 4) over poll (Wave 1):

ConcernWave 1 pollWave 4 push (chosen)
LatencyUp to 5 min (cron interval)< 30 s (event-driven)
CostCron-runs whether or not anything changedZero traffic when Central is idle
State to maintain”Last sync time” cursor in KV or knowledge rowNone — every dispatch carries its own SHA
Failure recoveryNext cron picks up the diffManual workflow_dispatch resync
Code surfaceNew Cloudflare Worker + KV + GitHub Commits API clientTwo YAML files using built-in GH Actions
Operational complexityWorker deploy, Worker logs, KV inspectionTwo Actions tabs, standard run logs
CoverageReliable if cron always firesLoses 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:

  1. Repo secret TAPROOT_DISPATCH_TOKEN on fin-platform/fin-central-intranet — PAT with repo plus actions:write scope on every tenant intranet repo.
  2. Repo secret TAPROOT_FETCH_TOKEN on each tenant intranet — PAT with Contents: read on fin-central-intranet. Required because the source repo is private; falls back to GITHUB_TOKEN for public sources.
  3. Optional repo variable FIN_TAPROOT_TENANT_REPOS on fin-central-intranet — newline- or comma-separated <owner>/<repo> list. Default fin-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.

ConcernLocation
Workertenant projector workers in packages/mcp-server/src/workers/tenant-projector/
First exemplardaily-arrivals.ts (P0-W5-06)
Triggerevent stream (e.g. inbound.load_expected, lot.received) + scheduled rollups
Read inputstenant events, tenant knowledge (including mirrored taproot rows), tenant actors.memory
Write targetnathel-intranet:docs/operational/<...>.md via the same vault webhook path used by Rung 2
Output frontmatterdoc_kind: aggregate_intelligence or doc_kind: operational_brief, data_plane: operational_data, visibility: tenant_private
Events emittedtaproot.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:

  1. Read tenant data via getDb() with SET LOCAL app.tenant_id.
  2. NEVER write to _taproot_mirror/** (the file watcher refuses such paths anyway; this is a defense-in-depth rule).
  3. Cite both the canonical taproot doc (Rung 2 mirror, by doc_id) and the underlying events (by event_id) in source_attribution[] of every output chunk.
  4. 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:

  1. Provision <X>-intranet repo, copy _taproot_mirror/README.md and the receive workflow.
  2. Append fin-platform/<X>-intranet to the FIN_TAPROOT_TENANT_REPOS repo variable on fin-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

FailureDetected byOwner
Rung 1 renderer crashentity_doc_projection.status='failed' rowPlatform — re-run worker
Rung 1 byte drift between runsnightly reconciler (§11.5.3 (c)) emits doc.coherence_drift_detectedPlatform — bump renderer_version + backfill plan
Rung 2 dispatch token expiredpublish workflow run fails red on gh api .../dispatchesPlatform DevOps — rotate TAPROOT_DISPATCH_TOKEN
Rung 2 receiver down at dispatch timedispatch 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 skippedPlatform — investigate source diff
Rung 3 projector failuretenant worker logs + SentryTenant DevOps
Rung 3 writes to _taproot_mirror/**publish workflow path filter does NOT fire (good); vault webhook records as authored docCode 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_memory carries the content_hash of its parent doc body at the time it was embedded.
  • On doc.indexed, compare new content_hash to chunk-side content_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.md before 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/markdown
  • text/plain
  • text/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_chunks input is REJECTED with 400 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_ids with at least one entry. Claims without citations are dropped and listed in unresolved_topics.
  • Confidence is the MINIMUM across cited chunks (never higher than the least-confident source).
  • inference_notes MUST explain the reasoning when confidence is inferred. 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_generated event 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_uri citation records observed_content_hash and observed_at at citation time.
  • A scheduled job (external_source_drift_monitor) fetches each unique external URI weekly, computes current_content_hash, and compares.
  • On hash mismatch, emit external_source_drift_detected with the URI, prior and current hashes, and the list of citing docs.
  • Affected docs transition to lifecycle_status = stale_external_citation and surface a banner in the doc reader.
  • A reviewer either confirms the new content is still consistent (records the new hash + an external_citation_revalidated event) or proposes an edit via propose_doc_edit to 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_unreachable not external_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_source to 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_projection rebuild → 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_trace produces deterministic output
  • propose_doc_edit rejects empty-evidence proposals
  • doc.coherence_drift_detected fires on injected drift
  • search_docs structured filter
  • get_doc visibility 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_detected on 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_profile returns 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_trace produces 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:

  1. Decide Markdown is canonical, or explicitly record exceptions.
  2. Compare DOCX companions to Markdown and mark DOCX as generated/export unless content exists only in DOCX.
  3. Add frontmatter IDs where missing.
  4. Add doc classifications.
  5. Add tags for planning lanes.
  6. Run parser fixture tests.
  7. Index into pilot tenant.
  8. Render in pilot intranet.
  9. Make MCP search and get_doc work 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:

ModeAudienceToolsPath to canon
External authoringPower users (FIN team, decision authors, SOP maintainers, internal subject experts)Obsidian, VS Code, Cursor, local git, GitHub web UILocal edit → git commit/push → vault webhook → ingest
In-product lightweightOperators on phones, customer portal users, vendor portal users, employees in PWAWorkspace doc editor, mobile editor, portal formsSave in app → API → GitHub commit via service account → vault webhook → ingest
Agent-proposedOperator agent, ops agent, exec agentpropose_doc_edit MCP toolAgent 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_created event on first save and doc.version_committed on 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 authored sections)

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_id required for all docs in taproot/, decisions/, runbooks/, sops/, and proof/
  • 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_unresolved warnings (not commit blocks, unless count exceeds threshold)
  • obsidian-vault-config.md at 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.

  1. File enters via Workspace upload | email attachment | mobile camera | GitHub commit | agent-generated artifact.
  2. Upload handler:
    • computes content_hash (SHA-256 of bytes)
    • persists bytes to StorageProvider keyed by content_hash (dedup automatic across tenants)
    • emits artifact.uploaded event with full provenance metadata
    • upserts knowledge.domain = artifact row
  3. 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
  4. If a doc projection is created, the doc carries linked_artifact_ids: [artifact_id] and the artifact carries derived_doc_id. Bidirectional.
  5. If the file matches an existing content_hash, no new bytes are stored; a new artifact row points to the existing blob with a fresh artifact.uploaded event recording the new upload context.
  6. 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:papaya in actors.memory)
  • SLA tracking via doc.review_sla_exceeded events 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

  1. 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.

  2. 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.