Skip to content

Ingest uploaded documents and hosted-doc links as documentable sources #32

Description

@baonguyenNava

Issue readiness state

  • Reviewed by human engineer
  • Issue refinement complete
  • All open questions answered
  • Post-refinement changes reviewed by human
  • Ready for work

Goal

Today the engine can only document git repositories. Real Strata knowledge also lives outside code: design decisions, runbooks, and hand-written guides that sit in local documents or in Google Docs and Confluence pages. We want a person to be able to point the engine at one of those documents, or at a link to one, and have it documented, indexed, and cross-linked into the graph the same way a source repository is.

Decisions (from review)

  • Acquired source content is written under .sources/<id>/ and is gitignored, never committed; generated docs live under docs/sources/<id>/. .sources/ stays runtime-only per existing convention.
  • Each input family gets its own manifest type value and its own profile under references/profiles/: gdoc, confluence, local-doc. This is not a single shared document type.
  • A shared internal acquire(source) -> (canonical_text, upstream_id) seam sits in the skill layer with per-family adapters for Google Drive MCP, Atlassian Rovo MCP, and local filesystem. Normalization, hashing, and the failure signal live once in the seam. This seam is an implementation detail and does not collapse the per-family types or profiles.
  • Acquisition uses MCP servers, Google Drive and Atlassian Rovo, under the operator's local auth. MCP and an SDK adapter behind the same seam are both sanctioned mechanisms; the SDK adapter is the fallback for a family where no MCP server fits, for example local-fs.
  • Acquisition and hashing live in the skill layer; the deterministic Python scripts stay network- and auth-free. compute_delta (source_delta) and lint_docs require no change for the happy path. The content hash rides the existing {id: sha} map passed via --shas, and lint_docs already tolerates a non-git source_ref.repo since it only checks the field is non-empty. Regression tests prove this.
  • lint_manifest's git-URL and non-empty-ref checks become conditional on source type. Git-type sources still hard-fail on an empty ref or a non-git repo. Non-git types (gdoc, confluence, local-doc) skip the git-URL check and instead require a non-empty repo slot, a stable id or path, and a ref that is a 40-char hex hash.
  • No new mid-table manifest columns. parse_manifest zips cells positionally against the fixed columns [id, type, repo, ref, subpaths, notes], so an inserted column would silently shift every existing row. The git-URL and git-ref semantics are dropped for non-git types and the existing slots are reused instead: the repo slot holds the stable upstream id, a Google fileId or Confluence pageId, or a local path; the ref slot holds a 40-char hex content hash; the human-facing URL goes in notes. Any genuinely new field is appended only after notes. No script renders source_ref.repo as a link, since INDEX and graph.json use the doc path, so repurposing the slot is safe; the doc-frontmatter schema wording widens repo: <git url> to allow an id or path per type.
  • Source identity and drift: source_ref.ref holds a SHA-1 digest, exactly 40 lowercase hex chars, matched by source_delta's _SHA_RE.fullmatch, of the acquired document's canonical extracted body text, not raw export bytes. SHA-256 is disallowed since 64 chars would never compare equal and would classify the source "changed" forever. A changed hash re-documents; an unchanged hash classifies unchanged.
  • The canonical extracted-text recipe, documented by the acquisition child: extract to a single text representation, normalize to LF newlines, strip per-line trailing whitespace, collapse trailing blank lines, and remove a documented deny-list of volatile nodes, comments, suggestions, and dynamic timestamp or macro content, before hashing. This deliberately narrows the original "any small change is picked up by a hash change": edits to excluded volatile nodes will not trip drift, and dynamic-macro content is accepted residual noise. This tradeoff needs ratification, carried into Open Questions below.
  • No CI execution and no shared-secret or service-account auth in this epic; explicitly deferred. Runs use the invoking operator's own local Google or Confluence credentials only. A consequence: a hosted-doc-link source can only be acquired and re-documented by an operator with access, so runs are operator-dependent by design.

Success looks like

  • Register a local document (markdown, docx, txt) or a Google Doc / Confluence link as a source in sources.md, without it being a git repository.
  • The engine acquires the content into .sources/<id>/ (gitignored) and produces valid docs under docs/sources/<id>/ that pass lint_docs.
  • Cross-linking: a prose doc earns graph edges only through reference fields (demonstrates, integrates_with, manages, related) resolved against the existing registries; it must not declare ownership fields (feature_keys, component_keys), which stay owned by the git-sourced docs, since build_graph is first-writer-wins and a prose doc declaring them would hijack ownership. Referenced keys must pre-exist in the registries or lint_docs hard-fails. There is no minimum edge count; a legitimately standalone doc may connect to nothing.
  • Zero-edge visibility: build_graph gains an isolated-node report so any doc node with no inbound or outbound edge is surfaced in the run output, analogous to the existing feature_key_gaps and component_key_gaps reports, so a disconnected ingested doc is never silently indexed as a dead end. This is new code.
  • Source identity and drift behave as decided above: a 40-hex SHA-1 of canonical text, a changed hash re-documents, and an unchanged hash classifies unchanged.
  • Acquisition-failure signal: a failed acquisition, whether link unreachable, auth missing or expired, permission revoked, or source deleted, emits a signal distinct from changed. The skill omits the failed id from the --shas map, writes an ACQUISITION_FAILED <id> <reason> line to the run log, leaves the prior doc intact, does not re-document on that path, and the run exits non-zero. A failure is never classifiable as unchanged nor silently absorbed into ordinary changed re-documentation.
  • Regression guardrail: existing git-repo sources are unaffected. The current sources.md still passes lint_manifest, lint_docs, and build_graph, their drift classification is identical to today, the manifest and any source_delta changes are backward-compatible, and the type-conditional lint_manifest change still hard-fails a git-type source with an empty ref or non-git repo.
  • First dogfooding source: the epic names one concrete first source, a specific Google Doc registered as type gdoc (the exact doc or fileId to be named by the author), and success is demonstrated end-to-end: it acquires to .sources/<id>/, produces docs/sources/<id>/, appears in docs/INDEX.md and docs/graph.json, and the isolated-node report either shows a real edge or visibly lists it as zero-edge; a re-run with no content change classifies it unchanged.

Out of scope

  • Arbitrary web-page URLs and PDF ingestion. This epic covers local files and Google Doc / Confluence links only.
  • A web upload UI or endpoint. Ingestion is a sources.md row, consistent with how every existing source is registered.
  • Rewriting or consolidating the existing Confluence content itself (tracked separately by Consolidate Strata documentation in Confluence #8).
  • Browser-observed UI and design-pattern documentation (tracked separately by Spike browser-observed UI and design-pattern documentation #14).
  • Rewriting, summarizing, or normalizing the acquired source prose. This epic acquires and documents content as-is; rewrites are tracked by Consolidate Strata documentation in Confluence #8.
  • CI execution of hosted-source acquisition and any shared-secret or service-account auth. Deferred; local operator credentials only.
  • Confluence support is single page per manifest row. Page trees are out of scope: a tree would fan one manifest row to N docs and break the one-id / one-ref / one-hash model, since it would need per-page rows or a child-doc scheme. Deferred to a follow-up.

Open questions

  • Ratify the hash-scope narrowing: confirm that excluding volatile nodes, comments, suggestions, and dynamic timestamp or macro content, from the drift hash is acceptable, accepting that edits to those nodes will not trip re-documentation. Needs sign-off from @baonguyenNava.
  • Local-file stable identity: local files have no fileId or pageId. Is the manifest id the author-assigned anchor, recommended, with the repo slot holding the path, or is identity derived from the path, where a move would then silently change identity?
  • Per-family canonical-text normalization: does each profile own its own normalization recipe, or is there one shared extraction contract the acquire() seam enforces?
  • Google Docs export format for hashing: canonicalize from a markdown export, which has more structure and more volatile syntax, or plain text?
  • Confluence ADF node ordering: does the Atlassian Rovo MCP guarantee stable child-node order across fetches, or must the seam sort to keep the hash stable?
  • Should acquisition-failed become a first-class source_delta classification for report visibility, or stay log-only plus non-zero exit?
  • Name the concrete first dogfooding source, the specific Google Doc or fileId, for the acceptance demo.

Child issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    epicLarge initiative framed around an outcome; decompose into stories and tasks

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions