You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
Issue readiness state
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)
.sources/<id>/and is gitignored, never committed; generated docs live underdocs/sources/<id>/..sources/stays runtime-only per existing convention.typevalue and its own profile underreferences/profiles/:gdoc,confluence,local-doc. This is not a single shareddocumenttype.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.compute_delta(source_delta) andlint_docsrequire no change for the happy path. The content hash rides the existing{id: sha}map passed via--shas, andlint_docsalready tolerates a non-gitsource_ref.reposince 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 emptyrefor a non-gitrepo. 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 arefthat is a 40-char hex hash.parse_manifestzips 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: thereposlot holds the stable upstream id, a GooglefileIdor ConfluencepageId, or a local path; therefslot holds a 40-char hex content hash; the human-facing URL goes innotes. Any genuinely new field is appended only afternotes. No script renderssource_ref.repoas a link, since INDEX and graph.json use the docpath, so repurposing the slot is safe; the doc-frontmatter schema wording widensrepo: <git url>to allow an id or path per type.source_ref.refholds 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 classifiesunchanged.Success looks like
sources.md, without it being a git repository..sources/<id>/(gitignored) and produces valid docs underdocs/sources/<id>/that passlint_docs.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 orlint_docshard-fails. There is no minimum edge count; a legitimately standalone doc may connect to nothing.build_graphgains an isolated-node report so any doc node with no inbound or outbound edge is surfaced in the run output, analogous to the existingfeature_key_gapsandcomponent_key_gapsreports, so a disconnected ingested doc is never silently indexed as a dead end. This is new code.unchanged.changed. The skill omits the failed id from the--shasmap, writes anACQUISITION_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 asunchangednor silently absorbed into ordinarychangedre-documentation.sources.mdstill passeslint_manifest,lint_docs, andbuild_graph, their drift classification is identical to today, the manifest and any source_delta changes are backward-compatible, and the type-conditionallint_manifestchange still hard-fails a git-type source with an empty ref or non-git repo.gdoc(the exact doc or fileId to be named by the author), and success is demonstrated end-to-end: it acquires to.sources/<id>/, producesdocs/sources/<id>/, appears indocs/INDEX.mdanddocs/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 itunchanged.Out of scope
sources.mdrow, consistent with how every existing source is registered.Open questions
fileIdorpageId. Is the manifestidthe author-assigned anchor, recommended, with thereposlot holding the path, or is identity derived from the path, where a move would then silently change identity?acquire()seam enforces?acquisition-failedbecome a first-classsource_deltaclassification for report visibility, or stay log-only plus non-zero exit?Child issues