diff --git a/.changeset/scaffold-docs-canonical-host-and-dead-refs.md b/.changeset/scaffold-docs-canonical-host-and-dead-refs.md new file mode 100644 index 0000000000..a4b7554258 --- /dev/null +++ b/.changeset/scaffold-docs-canonical-host-and-dead-refs.md @@ -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. diff --git a/packages/create-objectstack/src/starter-comments-self-contained.test.ts b/packages/create-objectstack/src/starter-comments-self-contained.test.ts index 181f72a14d..e781717bb6 100644 --- a/packages/create-objectstack/src/starter-comments-self-contained.test.ts +++ b/packages/create-objectstack/src/starter-comments-self-contained.test.ts @@ -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'; @@ -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(); /** Text files the scaffolder copies into the user's project. */ function shippedFiles(): string[] { @@ -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'); @@ -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) { @@ -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); diff --git a/packages/create-objectstack/src/templates/AGENTS.md b/packages/create-objectstack/src/templates/AGENTS.md index 36aa94c4b6..c4b5ed0aa9 100644 --- a/packages/create-objectstack/src/templates/AGENTS.md +++ b/packages/create-objectstack/src/templates/AGENTS.md @@ -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 diff --git a/packages/create-objectstack/src/templates/blank/Dockerfile b/packages/create-objectstack/src/templates/blank/Dockerfile index 11599be626..0431670690 100644 --- a/packages/create-objectstack/src/templates/blank/Dockerfile +++ b/packages/create-objectstack/src/templates/blank/Dockerfile @@ -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 diff --git a/packages/create-objectstack/src/templates/blank/README.md b/packages/create-objectstack/src/templates/blank/README.md index 36f8d3fb1d..ab302ca65b 100644 --- a/packages/create-objectstack/src/templates/blank/README.md +++ b/packages/create-objectstack/src/templates/blank/README.md @@ -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 @@ -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. diff --git a/packages/create-objectstack/src/templates/blank/docker-compose.yml b/packages/create-objectstack/src/templates/blank/docker-compose.yml index d57511597f..6118dcc8ed 100644 --- a/packages/create-objectstack/src/templates/blank/docker-compose.yml +++ b/packages/create-objectstack/src/templates/blank/docker-compose.yml @@ -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: