Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
- [ ] Tests added or updated
- [ ] `make check` passes locally (`ruff` + `mypy`)
- [ ] `CHANGELOG.md` updated under `[Unreleased]`
- [ ] Documentation updated (if the public API changed)
- [ ] Documentation updated (if the public API changed), `docs/agents.md` included

## Related issues

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
- run: uv sync --no-dev --group docs
- run: cp CHANGELOG.md docs/changelog.md
- run: uv run zensical build --clean
- run: uv run python scripts/emit_markdown.py
- uses: actions/upload-pages-artifact@v5
with:
path: site
Expand Down
14 changes: 14 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ Breaking changes: add `!` after the type (`feat!:`) or include a `BREAKING CHANG
4. Run `make check && make test-unit` locally
5. Open a PR against `master`

## The agents page

`docs/agents.md` is the whole library on one page, written for a coding assistant: the
public API, the rules that break code when they are broken, the mistakes models make, and
a map of which page to fetch for the rest. People hand it to an assistant instead of the
site, which is what makes a stale one worse than none — it teaches a model an API that no
longer exists.

It is part of the public API, so it changes in the same pull request the API does: a name
added, renamed or removed, a changed default or signature, a new rule a caller has to obey.
A new docs page means a new row in the documentation map. The review check is mechanical —
if the diff changes the public surface and `docs/agents.md` is untouched, the pull request
is not finished.

## Releasing (maintainers only)

Releases are fully automated via [Release Please](https://github.com/googleapis/release-please).
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@

Only `sqlalchemy[asyncio]` and `pydantic` are required by default — everything else is an opt-in extra.

> [!TIP]
> **Building this with an AI assistant?** Hand it
> **[one page](https://bedrock-python.github.io/sqlalchemy-foundation-kit/agents/)** instead of the
> whole site: the public API surface, the session-lifetime and transaction-ownership rules that
> break code when they are broken, the mistakes models actually make with a Unit of Work, and a
> map of which page to fetch for the rest. Every docs page is also served as raw Markdown at its
> own URL, and a **Copy page** button at the top of each one hands it straight to a chat window.

## Key Features

✅ **Single dependency** — All foundation pieces in one place
Expand Down Expand Up @@ -369,6 +377,8 @@ graph LR

Full documentation is available at [https://bedrock-python.github.io/sqlalchemy-foundation-kit/](https://bedrock-python.github.io/sqlalchemy-foundation-kit/)

- [For AI agents](https://bedrock-python.github.io/sqlalchemy-foundation-kit/agents/) — the whole library on one page, for a coding assistant

## License

This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
525 changes: 525 additions & 0 deletions docs/agents.md

Large diffs are not rendered by default.

158 changes: 158 additions & 0 deletions docs/assets/javascripts/copy-page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/* Behaviour for the "Copy page" control.
*
* Every handler is delegated from `document`, because the theme swaps the
* content in place when instant navigation is on: a listener bound to an
* element of one page would not survive the move to the next.
*/
(function () {
"use strict";

var RESET_AFTER_MS = 2000;

/* Where the Markdown of a page is written, as an absolute URL. Two data
attributes say how far the site root is from here and where this page sits
below it; scripts/emit_markdown.py writes the file to match. The site's own
name is a third, so this file is the same in every project that carries it. */
function markdownUrl(widget) {
var base = (widget.dataset.copyBase || ".").replace(/\/$/, "");
var page = widget.dataset.copyPage || "";
var relative = page === "" ? "index.md" : page.replace(/\/$/, "") + ".md";
return new URL(base + "/" + relative, window.location.href).href;
}

function prompt(widget) {
var title = widget.dataset.copyTitle || document.title;
var site = widget.dataset.copySite || "project";
return (
"Read " +
markdownUrl(widget) +
' -- the "' +
title +
'" page of the ' +
site +
" documentation -- so I can ask questions about it."
);
}

function destination(widget, name) {
var question = encodeURIComponent(prompt(widget));
switch (name) {
case "markdown":
return markdownUrl(widget);
case "chatgpt":
return "https://chatgpt.com/?hints=search&q=" + question;
case "claude":
return "https://claude.ai/new?q=" + question;
case "perplexity":
return "https://www.perplexity.ai/search?q=" + question;
default:
return markdownUrl(widget);
}
}

function write(text) {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text);
}
/* Insecure origins have no clipboard API; the old selection dance still
works there, which keeps a local preview usable. */
return new Promise(function (resolve, reject) {
var area = document.createElement("textarea");
area.value = text;
area.style.position = "fixed";
area.style.opacity = "0";
document.body.appendChild(area);
area.select();
var ok = document.execCommand("copy");
document.body.removeChild(area);
ok ? resolve() : reject(new Error("copy refused"));
});
}

function announce(widget, label, state) {
var slot = widget.querySelector("[data-copy-label]");
if (slot) slot.textContent = label;
if (state) {
widget.dataset.copyState = state;
} else {
delete widget.dataset.copyState;
}
}

function copy(widget) {
var reset = function () {
window.setTimeout(function () {
announce(widget, "Copy page", null);
}, RESET_AFTER_MS);
};
fetch(markdownUrl(widget))
.then(function (response) {
if (!response.ok) throw new Error(String(response.status));
return response.text();
})
.then(write)
.then(function () {
announce(widget, "Copied", "copied");
reset();
})
.catch(function () {
announce(widget, "Copy failed", null);
reset();
});
}

function close(widget) {
var menu = widget.querySelector("[data-copy-menu]");
var toggle = widget.querySelector("[data-copy-toggle]");
if (menu) menu.hidden = true;
if (toggle) toggle.setAttribute("aria-expanded", "false");
}

function closeAll(except) {
var widgets = document.querySelectorAll(".md-copy-page");
for (var i = 0; i < widgets.length; i++) {
if (widgets[i] !== except) close(widgets[i]);
}
}

document.addEventListener("click", function (event) {
var target = event.target;
if (!(target instanceof Element)) return;

var widget = target.closest(".md-copy-page");
if (!widget) {
closeAll(null);
return;
}
closeAll(widget);

if (target.closest("[data-copy-action]")) {
event.preventDefault();
close(widget);
copy(widget);
return;
}

var toggle = target.closest("[data-copy-toggle]");
if (toggle) {
event.preventDefault();
var menu = widget.querySelector("[data-copy-menu]");
if (!menu) return;
/* The destinations are filled in on the way out rather than at load:
the page under the widget may have changed since. */
var links = menu.querySelectorAll("[data-copy-open]");
for (var i = 0; i < links.length; i++) {
links[i].href = destination(widget, links[i].dataset.copyOpen);
}
menu.hidden = !menu.hidden;
toggle.setAttribute("aria-expanded", menu.hidden ? "false" : "true");
return;
}

if (target.closest("[data-copy-open]")) close(widget);
});

document.addEventListener("keydown", function (event) {
if (event.key === "Escape") closeAll(null);
});
})();
Loading