Skip to content

Add the two packages: TypeScript reader, Python pull-request writer - #1

Merged
dmccoystephenson merged 1 commit into
mainfrom
add-library
Sep 1, 2026
Merged

Add the two packages: TypeScript reader, Python pull-request writer#1
dmccoystephenson merged 1 commit into
mainfrom
add-library

Conversation

@dmccoystephenson

Copy link
Copy Markdown
Contributor

Adds the actual library: one repo, two packages, because the read half and the write half were built in different languages.

Package Language Does
Read @kingdom-community/github-docs (js/) TypeScript, strict Fetches markdown from a GitHub repo for rendering
Write github-docs (python/) Python 3.9+, stdlib only Lands documentation edits as pull requests

Both at 0.1.0, MIT, unpublished. Two release workflows, both workflow_dispatch-only, neither fired.

Read side (TypeScript)

Where it came fromkingdom-community/kingdomfirstera-dot-com:

Source Became
services/githubContent.ts js/src/client.ts
utils/markdownUrl.ts js/src/markdownUrl.ts (near-verbatim)
utils/guides.ts js/src/urls.ts + part of js/src/catalogue.ts
utils/lore.ts (allowlist parsing only) js/src/catalogue.ts
__tests__/markdownUrl.test.ts, __tests__/guides.test.ts js/test/*

Plus repo-slug parsing folded in from dansplugins-dot-com/utils/github.ts (MIT, same owner), reimplemented rather than copied and widened to accept a bare slug, a .git suffix, a /tree/main/... URL and the whitespace/slashes a human leaves in an environment variable.

The two rules that had to survive extraction, and did:

  1. Failure is a value, not an exception. fetchMarkdown never rejects. Every way a fetch can go wrong — 404, rate limit, upstream 500, timeout, DNS failure, oversized body, a body that fails mid-stream — returns {status: 'unavailable'}. not-configured and not-listed are kept separate because they are deployment states rather than outages.
  2. Nothing upstream is ever quoted back. A failing response's body is never even read. There is an explicit test that serialises every own property of the returned value and asserts the token, the body text, the status and the request URL are all absent — including through an upstream that echoes the credential back in its error body.

What changed during generalisation:

  • DEFAULT_GUIDE_REPO / DEFAULT_LORE_REPO and the KFE_* env reads are gone. The package reads no environment at all; createDocsClient({repo, token, documents, ...}) takes everything as arguments, so it can be configured twice in one process and tested without stubbing process.env.
  • The utils/lore.ts allowlist coupling is dropped. The idea survives as an optional, generic catalogue: documents accepts a list or the comma-separated string an env var comes in, and is deny-by-default when set. Unset means the whole repository is fetchable; an empty list means nothing is — an operator who has not filled it in publishes nothing rather than everything.
  • The fixed three-document guide catalogue is gone; it is now whatever the caller lists, with title/summary/slug overridable per entry.
  • The two fetch paths (public raw host, private Contents API) became one transport option that defaults sensibly: api when a token is configured, raw when it is not. The raw path still sends no credentials at all, which is now asserted in a test.
  • resolveGuideLink grew a fromPath, so links resolve against the containing document's directory rather than assuming every doc sits at the repo root; a link that climbs out of the repository is left as written rather than turned into a URL that is confidently wrong.
  • Comments citing ARCHITECTURE.md / MVP.md / sibling services were rewritten to state the principle instead of pointing at a document the reader cannot open. The reasoning itself — why the URL check runs on the resolved value, why whitespace is stripped before validating, why the probe exists — is preserved.

Write side (Python)

Where it came from~/kfe-staff-webapp (read-only, nothing cloned):

Source Became
github_docs.py python/src/github_docs/client.py
test_github_docs.py python/tests/test_client.py

The flow is unchanged: look up default branch and tip SHA → ensure a per-file branch → PUT content via the Contents API → reuse the open PR for that branch or open one. Never a direct push.

What changed during generalisation:

  • Module-level REPO / TOKEN / TRACKED_FOLDERS constants read from KFE_* env vars became a frozen GitHubDocsConfig dataclass and a GitHubDocsClient instance. The package reads no environment of its own.
  • repo, token, allowed_roots, extensions, branch_prefix, api_base, timeout, user_agent and the commit-message / PR-title / PR-body templates are all configuration.
  • allowed_roots=None means the whole repository; an empty tuple means nothing. The old code's implicit "empty tuple denies everything" is now deliberate and documented.
  • Added a path-safety check the original did not have: traversal, absolute paths, backslashes and empty segments are refused even under a permitted root, and the gate provably runs before any request (test asserts urlopen is never called).
  • Added token redaction on the way into GitHubDocsError. The write half deliberately does surface GitHub's message — a human is reading it and needs to know permissions vs conflict — so it is worth being explicit that the credential is the one thing that never travels with it.
  • Dicts became frozen dataclasses (Document, DocumentSummary, SaveResult) with type hints throughout.
  • branch_prefix default is now docs-edit/ rather than staff-edit/.
  • Error messages no longer name a specific env var or bot account.

Tests

$ cd js && npx vitest run
 ✓ test/catalogue.test.ts  (12 tests)
 ✓ test/markdownUrl.test.ts  (10 tests)
 ✓ test/urls.test.ts  (19 tests)
 ✓ test/client.test.ts  (24 tests)
 Test Files  4 passed (4)
      Tests  65 passed (65)

$ cd python && python3 -m unittest discover -s tests
Ran 31 tests in 0.104s
OK

Also verified locally: tsc --noEmit clean, tsc -p tsconfig.build.json emits dist/ with declarations, the built ESM imports and runs under Node 24, python -m build produces an sdist and wheel, and the 31 Python tests pass against the installed wheel rather than the source tree (which is what CI does).

Deliberately left out

  • The lore allowlist's link-resolution half (LoreLinkTarget, resolveLoreLink and the "unpublished link renders as plain text" rule). It is closely tied to a site that serves lore at /lore/<slug> and knows which slugs are published; the general half — resolving a relative path against the containing document, refusing one that escapes the repo root — is here as resolveRepoPath / resolveDocLink.
  • Release-tag fetching from dansplugins-dot-com/utils/github.ts (getLatestRelease, getLatestReleasesWithRateLimit). It is about plugin downloads, not documentation, and it console.errors upstream text — which is exactly the rule this package exists to hold.
  • Rendering. No React, no markdown parser, no components. The package hands back a string and the safety helpers to render it with; the JSX in the README is an example, not a dependency.
  • Nothing was published to npm or PyPI, and neither release workflow has been run. Both names are currently unregistered.

js/ ships @kingdom-community/github-docs, which fetches markdown out of a
GitHub repository for rendering. Failure is a value rather than an exception,
and nothing upstream -- body, header, URL, token -- is ever quoted back.

python/ ships github-docs, which lands documentation edits as pull requests
against a per-file branch, reusing an open PR for that branch when one exists.
stdlib urllib only.

Both are configured by the caller: repository, paths and token are arguments,
not constants.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBrW2fYYTzvhgK7WMFv92r
@dmccoystephenson
dmccoystephenson merged commit a5c775b into main Sep 1, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant