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
34 changes: 34 additions & 0 deletions .changeset/scaffold-docs-canonical-host-and-dead-refs.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
---
"create-objectstack": patch
---

fix(create-objectstack): converge scaffold docs on the canonical host and drop the last two dead monorepo references (#10990, #11022)

A freshly scaffolded project shipped a handful of text lines a reader with
only their own project — no monorepo, no `docs/adr/`, no issue tracker —
could not follow:

- `templates/AGENTS.md` linked `https://objectstack.com/docs`, a domain that
is not this project's docs site at all (not even a redirecting alias).
- `templates/blank/Dockerfile` and `templates/blank/docker-compose.yml` both
linked `https://docs.objectstack.ai/...`, an accepted-but-unratified alias.
All three now point at the ruled canonical origin, `https://objectstack.ai`
(maintainer ruling, 2026-08-21).
- `templates/blank/README.md` cited `ADR-0097` and named "the ObjectStack
framework repo" as the home of `skills/` — both rewritten self-contained,
keeping the fact each was carrying: the connector-materialization line now
links the public [Automation → Connectors](https://objectstack.ai/docs/automation/connectors)
page, and the skills line now names the followable
`npx skills add objectstack-ai/objectstack/skills` install the scaffolder's
own closing output already uses.

`packages/create-objectstack/src/starter-comments-self-contained.test.ts`
(#10324) gains two pin obligations these two fixes call for: a host-convergence
assertion driven by the same `shippedFiles()` walker that already enumerates
everything a scaffold ships (no other repo gate's population reaches these
template files — `check:published-readme-links` reads publishable packages'
published markdown only), and a fifth `MONOREPO_ONLY` pattern that catches a
prose-shaped reference to this repo ("the ObjectStack framework repo") the
first four, syntax-shaped patterns could not. The self-retiring `EXCLUDED`
entry for `blank/README.md` is removed now that the file cites nothing
monorepo-only.
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,10 +37,28 @@
// lines rather than imported, because importing a root script into this package
// would widen this suite's declared cross-package read radius to buy nothing.
//
// Host CONVERGENCE is deliberately not asserted here — the tree still carries
// two non-canonical docs hostnames and they are another card's (#10990). This
// pin only judges URLs already on the canonical origin, so the two cards cannot
// collide.
// Assertion 3 only judges URLs already on the canonical origin; host
// CONVERGENCE — is the origin the RULED one at all — is assertion 4 (#10990).
// It could not have lived anywhere else: the published-readme-links gate
// prescribes the same canonical origin, but its population is publishable
// packages' PUBLISHED markdown, which never reaches this package's templates
// — `templates/AGENTS.md` is a template file under `src/`, not a package
// README, and `Dockerfile` / `docker-compose.yml` are not markdown at all.
// `shippedFiles()` below is the one walker in the repo that already
// enumerates exactly what a scaffold ships, so the host pin belongs here.
//
// ## A fifth `MONOREPO_ONLY` pattern, found while fixing #11022
//
// `blank/README.md` named "the ObjectStack framework repo" as the home of
// `skills/` — a monorepo-only reference in PROSE rather than in one of the
// four syntactic shapes (ADR id / issue number / script path / package path)
// the first four patterns matched. Nothing in this file's original four
// patterns could have caught it, which is why removing the ADR-0097 reference
// this same README carried and deleting the (now self-retired) `EXCLUDED`
// entry for it would otherwise have let assertion 1 pass with that second,
// still-unfollowable line untouched. See the fifth `MONOREPO_ONLY` entry
// below for how it is scoped to avoid the reader's own project also being
// called "a monorepo" (`blank/pnpm-workspace.yaml`, correctly).

import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
Expand All@@ -52,17 +70,15 @@ const templateRoot = path.resolve(HERE, 'templates');
const contentDocs = path.resolve(HERE, '..', '..', '..', 'content', 'docs');

/**
* The blank template's README is scanned by nothing here yet: it still carries
* an ADR identifier of its own, and it is owned by other cards in the same
* family (a scaffolding-guidance fix was in flight over it while this landed).
*
* The exclusion is SELF-RETIRING rather than permanent — the last assertion
* fails the moment the README stops needing it, so whoever cleans that file is
* told, in their own run, to delete this entry and let the file be scanned.
* A silent exemption over the most-read file in the tree is the failure this
* shape exists to avoid.
* Nothing is excluded — every shipped file is scanned. `blank/README.md` used
* to carry a self-retiring entry here ("still carries an ADR identifier; owned
* by another card") while its ADR-0097 reference and its unlinked "ObjectStack
* framework repo" reference were another card's (#11022); both are gone now,
* so the retirement fired as designed and this map goes back to empty rather
* than staying around as a silent exemption over the most-read file in the
* tree.
*/
const EXCLUDED = new Map([['blank/README.md', 'still carries an ADR identifier; owned by another card']]);
const EXCLUDED = new Map<string, string>();

/** Text files the scaffolder copies into the user's project. */
function shippedFiles(): string[] {
Expand All@@ -88,6 +104,17 @@ const MONOREPO_ONLY = [
{ label: 'a bare issue number', re: /(^|[^\w/])#\d{3,6}\b/ },
{ label: 'a repo build-script path', re: /\bscripts\/[\w.-]+\.(?:mjs|mts|cjs|ts|js)\b/ },
{ label: 'a monorepo package path', re: /\bpackages\/[a-z0-9][\w-]*\//i },
// #11022: `blank/README.md` named "the ObjectStack framework repo" as the
// home of `skills/`, unlinked — a reader with only their own scaffolded
// project has no way to reach it. The first four patterns are syntactic
// identifiers (an ADR id, an issue number, a repo-relative path); this one
// is the same class of defect in prose form, so it is spelled to the
// FRAMEWORK'S OWN NAME next to a "repo" word, not to that one sentence —
// it survives a reword. Deliberately narrower than a bare "repo" or
// "monorepo" match: this template tree also calls the READER'S OWN project
// "a monorepo root" (`blank/pnpm-workspace.yaml`), which is a correct,
// self-contained, followable statement about a directory they do have.
{ label: 'a reference to the ObjectStack repo as an unlinked location', re: /\bObjectStack (?:framework )?(?:mono)?repo\b/i },
];

const read = (rel: string) => fs.readFileSync(path.join(templateRoot, rel), 'utf8');
Expand DownExpand Up@@ -139,6 +166,19 @@ describe('shipped template comments are followable by a stranger', () => {
{ what: 'that declaring it is required rather than optional', re: /required|refuses/i },
],
},
{
// #11022's two rewrites (ADR-0097 -> a public docs link; "the ObjectStack
// framework repo" -> the followable install form) are RATIONALE entries
// too, for the same reason blank/objectstack.config.ts and
// note.object.ts already are: assertions 1/3/4 only ever check that
// something UNFOLLOWABLE is absent or that a present URL resolves --
// none of them notice a fact quietly disappearing along the way.
file: 'blank/README.md',
facts: [
{ what: 'that a provider-bound connector is materialized into a live, dispatchable connector at boot (not written by hand)', re: /materializ\w*[^.]*?\bat\s+boot\b/i },
{ what: 'the followable skills install command (`npx skills add objectstack-ai/objectstack/skills`)', re: /npx skills add objectstack-ai\/objectstack\/skills/i },
],
},
];

for (const { file, facts } of RATIONALE) {
Expand DownExpand Up@@ -188,6 +228,40 @@ describe('shipped template comments are followable by a stranger', () => {
}
});

// ── assertion 4: no non-canonical docs host ships into a project ────────
// #10990: three shipped lines cited `objectstack.com` (not even a
// redirecting alias — a different, wrong domain) or `docs.objectstack.ai`
// (an accepted-but-unratified alias per the published-readme-links gate's
// DOCS_HOSTS) instead of the ruled canonical origin (maintainer ruling,
// 2026-08-21). Full reasoning on why this population needed its own pin is
// in the file header above.
it.each(shippedFiles())('%s cites no non-canonical docs host', (rel) => {
// Restated, not imported, for the same cross-package-read-radius reason
// assertion 3's candidate list gives above. This is every host the
// published-readme-links gate's DOCS_HOSTS classifies as this docs site
// (canonical + redirecting aliases) MINUS the canonical origin itself,
// plus `objectstack.com` — a different, wrong domain that is not in that
// set at all, so it is not a "host" in that gate's sense, just a mistake
// this population has actually shipped.
const NON_CANONICAL_DOCS_HOSTS = [
'docs.objectstack.ai',
'www.objectstack.ai',
'www.docs.objectstack.ai',
'protocol.objectstack.ai',
'www.protocol.objectstack.ai',
'objectstack.com',
];
const re = new RegExp(
`https?://(?:${NON_CANONICAL_DOCS_HOSTS.map((h) => h.replace(/\./g, '\\.')).join('|')})\\b`,
);
const hit = re.exec(read(rel));
expect(
hit,
`${rel} cites ${JSON.stringify(hit?.[0])}, a non-canonical docs host. Converge ` +
'on https://objectstack.ai (maintainer ruling, 2026-08-21).',
).toBeNull();
});

// ── the exclusion is live, or it is gone ─────────────────────────────────
it.each([...EXCLUDED.keys()])('%s still needs its exclusion', (rel) => {
const text = read(rel);
Expand Down
2 changes: 1 addition & 1 deletion packages/create-objectstack/src/templates/AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,6 +96,6 @@ Skills are triggered automatically based on task context:

## Learn More

- [ObjectStack Documentation](https://objectstack.com/docs)
- [ObjectStack Documentation](https://objectstack.ai/docs)
- [GitHub: objectstack-ai/objectstack](https://github.com/objectstack-ai/objectstack)
- [Skills CLI](https://skills.sh/) — Manage AI skills across agents
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@
# my-app
#
# Or run the full app + Postgres stack: see docker-compose.yml.
# Docs: https://docs.objectstack.ai/docs/deployment/self-hosting
# Docs: https://objectstack.ai/docs/deployment/self-hosting

# ── Build stage: compile TypeScript metadata to the artifact ─────────
FROM node:22-slim AS build
Expand Down
12 changes: 7 additions & 5 deletions packages/create-objectstack/src/templates/blank/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -79,9 +79,11 @@ can call an external system from a flow as pure metadata — no host code:

Add a `connectors:` entry that names one of these `provider`s and the
`automation` capability materializes it into a live, dispatchable connector at
boot (ADR-0097); a flow's `connector_action` node then calls it. To add a brand
connector (e.g. Slack), install its package and add `new ConnectorSlackPlugin()`
to `plugins:`; to drop a provider, remove its plugin.
boot — see [Automation → Connectors](https://objectstack.ai/docs/automation/connectors)
for how that materialization works; a flow's `connector_action` node then
calls it. To add a brand connector (e.g. Slack), install its package and add
`new ConnectorSlackPlugin()` to `plugins:`; to drop a provider, remove its
plugin.

> **Security — declarative MCP over stdio.** An `mcp` connector whose transport
> spawns a local process (`stdio`) is denied by default, because the command
Expand DownExpand Up@@ -136,5 +138,5 @@ covered in [Self-Hosted Deployment](https://objectstack.ai/docs/deployment/self-
- Add a flow or automation: see `objectstack-automation`.
- Add an AI agent: see `objectstack-ai`.

Skills live in `skills/` in the ObjectStack framework repo and in the in-IDE
assistant catalog.
Skills are installed with `npx skills add objectstack-ai/objectstack/skills`
(see `AGENTS.md`) and also show up in the in-IDE assistant catalog.
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@
# POSTGRES_PASSWORD / OS_AUTH_SECRET / OS_SECRET_KEY (generate secrets with
# `openssl rand -hex 32`), then `docker compose up -d`.
#
# Docs: https://docs.objectstack.ai/docs/deployment/self-hosting
# Docs: https://objectstack.ai/docs/deployment/self-hosting

services:
app:
Expand Down
Loading