From d0f893e2e996fc62f09f40580ee1b28a069c485a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 09:23:54 +0000 Subject: [PATCH 1/2] docs(create-objectstack): make the starter comments followable by a stranger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two files a newcomer opens first after scaffolding cited four ADR identifiers, a bare issue number and a release-time script path — none of which ship in a scaffolded project. Rewrite them self-contained, keeping what they explain and pointing at public docs pages, and pin both halves: no unfollowable reference, and the rationale still stated. Fixes #10324 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r --- .changeset/starter-comments-self-contained.md | 28 +++ .../starter-comments-self-contained.test.ts | 203 ++++++++++++++++++ .../src/templates/blank/Dockerfile | 2 +- .../src/templates/blank/objectstack.config.ts | 34 +-- .../blank/src/objects/note.object.ts | 8 +- 5 files changed, 257 insertions(+), 18 deletions(-) create mode 100644 .changeset/starter-comments-self-contained.md create mode 100644 packages/create-objectstack/src/starter-comments-self-contained.test.ts diff --git a/.changeset/starter-comments-self-contained.md b/.changeset/starter-comments-self-contained.md new file mode 100644 index 0000000000..32a2bdcb7c --- /dev/null +++ b/.changeset/starter-comments-self-contained.md @@ -0,0 +1,28 @@ +--- +"create-objectstack": patch +--- + +Rewrite the scaffolded project's starter comments so a newcomer can actually +follow them (#10324). `objectstack.config.ts` and `src/objects/note.object.ts` +are the first two files opened after scaffolding, and between them they cited +four ADR identifiers, one bare issue number and the path of a release-time +script in this monorepo — none of which ship in, or are linked from, a +scaffolded project. `// per ADR-0097` read as a reference the reader was +failing to follow rather than as the context it was meant to be. + +The explanations are kept and made self-contained; only the dead ends are +gone. Each now states the fact the identifier stood for — the protocol range +is checked before anything loads and was stamped to match the installed +version rather than hand-tuned; `automation` must stay whenever `plugins:` +lists a connector or the executors have nowhere to register; a declarative +`mcp` stdio transport is denied by default; the org-wide default is required +so the baseline is an authored decision — and points at the public docs page +that covers it in full. The blank `Dockerfile` likewise stops pointing at a +file in this repo and points at the self-hosting guide it already links. + +A pin (`starter-comments-self-contained.test.ts`) keeps it that way from both +sides: no shipped template file may cite an ADR identifier, a bare issue +number or a repo script path, and the facts those references carried must +still be stated — so the comments cannot be "fixed" by deleting them. It also +resolves every canonical-origin docs URL in the shipped tree against +`content/docs`, because a link that 404s is the same defect one level out. diff --git a/packages/create-objectstack/src/starter-comments-self-contained.test.ts b/packages/create-objectstack/src/starter-comments-self-contained.test.ts new file mode 100644 index 0000000000..181f72a14d --- /dev/null +++ b/packages/create-objectstack/src/starter-comments-self-contained.test.ts @@ -0,0 +1,203 @@ +// Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. +// +// Every comment that ships INTO a scaffolded project must be followable by the +// person reading it — someone who has this project and nothing else. +// +// ## The defect +// +// The two files a newcomer opens first after scaffolding, objectstack.config.ts +// and src/objects/note.object.ts, carried six references addressed to a reader +// with this monorepo open: four ADR identifiers, one bare issue number, and the +// path of a release-time script. None of docs/adr, the issue tracker, or that +// script ships in a scaffolded project, so "// per ADR-0097" was a reference the +// reader could not resolve — it read as an instruction they were failing to +// follow rather than as the context it was meant to be. +// +// ## Why this pin has TWO halves, and why the second is the load-bearing one +// +// The cheap way to make the references disappear is to delete the comments. That +// would be a worse project than the one with the dead references: the comments +// explain WHY each setting is the way it is, which is exactly what a newcomer +// deciding whether to change it needs. So a one-way "no ADR identifiers" grep +// would rot in the one direction that matters — it stays green while the +// rationale is deleted out from under it. +// +// Hence: no unfollowable reference (assertion 1) AND the fact each comment +// carries still stated (assertion 2). A future edit can reword freely; it cannot +// quietly strip the explanation, and it cannot re-introduce a dead end. +// +// ## The third half: a public link is only a fix while it resolves +// +// Replacing an internal identifier with a docs URL moves the same defect one +// level out if the URL 404s — a reference that looks authoritative and lands +// nowhere. Assertion 3 resolves every canonical-origin docs URL in the shipped +// tree against content/docs the way Fumadocs routes it: baseUrl /docs over +// content/docs, and a directory that exists but carries no index page is a 404. +// That candidate list is check-docs-redirects' pageCandidates, restated in six +// 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. + +import { describe, it, expect } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +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. + */ +const EXCLUDED = new Map([['blank/README.md', 'still carries an ADR identifier; owned by another card']]); + +/** Text files the scaffolder copies into the user's project. */ +function shippedFiles(): string[] { + const out: string[] = []; + const walk = (dir: string) => { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const abs = path.join(dir, entry.name); + if (entry.isDirectory()) walk(abs); + else out.push(path.relative(templateRoot, abs).split(path.sep).join('/')); + } + }; + walk(templateRoot); + return out.sort(); +} + +/** + * References a reader who has only their own scaffolded project cannot follow. + * Each is spelled to match the identifier, not any particular sentence, so the + * prose around it stays free to change. + */ +const MONOREPO_ONLY = [ + { label: 'an ADR identifier', re: /\bADR-\d{3,4}\b/ }, + { 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 }, +]; + +const read = (rel: string) => fs.readFileSync(path.join(templateRoot, rel), 'utf8'); + +describe('shipped template comments are followable by a stranger', () => { + const files = shippedFiles(); + + it('reads a real template tree (vacuity guard)', () => { + expect(files).toContain('blank/objectstack.config.ts'); + expect(files).toContain('blank/src/objects/note.object.ts'); + expect(files.length).toBeGreaterThan(8); + }); + + // ── assertion 1: nothing unfollowable ──────────────────────────────────── + it.each(shippedFiles().filter((f) => !EXCLUDED.has(f)))( + '%s cites nothing that only exists in this monorepo', + (rel) => { + const text = read(rel); + for (const { label, re } of MONOREPO_ONLY) { + const hit = re.exec(text); + expect( + hit, + `${rel} cites ${label} (${JSON.stringify(hit?.[0])}). A scaffolded project ` + + 'ships no ADRs, no issue tracker and none of this repo\'s scripts, so this ' + + 'reads as a reference the newcomer is failing to follow. State the fact ' + + 'self-contained, or link a public docs page — do not delete the rationale.', + ).toBeNull(); + } + }, + ); + + // ── assertion 2: the rationale survives ────────────────────────────────── + // Each entry is the FACT the removed reference was carrying, matched loosely + // enough that rewording is free and deletion is not. + const RATIONALE: { file: string; facts: { what: string; re: RegExp }[] }[] = [ + { + file: 'blank/objectstack.config.ts', + facts: [ + { what: 'why the protocol range exists (an incompatible runtime refuses the app)', re: /refuses? this app|refuse this package|incompatible runtime/i }, + { what: 'that the protocol range is stamped for you, not hand-tuned', re: /stamped|scaffold(ing|ed)/i }, + { what: 'why `automation` must stay when a connector is listed', re: /nowhere to register|boot fails/i }, + { what: 'that a declarative mcp stdio transport is denied by default', re: /denied by default/i }, + ], + }, + { + file: 'blank/src/objects/note.object.ts', + facts: [ + { what: 'what the org-wide default means', re: /org-wide default|OWD/i }, + { what: 'that declaring it is required rather than optional', re: /required|refuses/i }, + ], + }, + ]; + + for (const { file, facts } of RATIONALE) { + describe(file, () => { + for (const { what, re } of facts) { + it(`still explains ${what}`, () => { + expect( + read(file), + `${file} no longer explains ${what}. These comments were rewritten to drop ` + + 'monorepo-only references while KEEPING what they explain; deleting the ' + + 'explanation is not the same fix.', + ).toMatch(re); + }); + } + }); + } + + // ── assertion 3: canonical docs links resolve ──────────────────────────── + it('every canonical docs URL in the shipped tree resolves to a real page', () => { + // baseUrl '/docs' is mounted over content/docs, so the route path is the + // file path minus the extension; a directory resolves only via an index page. + const candidates = (route: string) => [ + `${route}.mdx`, + `${route}.md`, + `${route}/index.mdx`, + `${route}/index.md`, + ]; + const urls: { rel: string; url: string; route: string }[] = []; + for (const rel of shippedFiles()) { + const text = read(rel); + for (const m of text.matchAll(/https:\/\/objectstack\.ai\/docs\/([\w./-]*[\w-])/g)) { + urls.push({ rel, url: m[0], route: m[1] }); + } + } + // Non-vacuity: the rewritten starter comments put docs links in this tree on + // purpose. Zero matches means the extractor broke, not that the tree is clean. + expect(urls.length, 'no canonical docs URLs found — the extractor is broken').toBeGreaterThan(0); + + for (const { rel, url, route } of urls) { + const found = candidates(route).some((c) => fs.existsSync(path.join(contentDocs, c))); + expect( + found, + `${rel} links ${url}, which content/docs serves from none of ` + + `${candidates(route).join(', ')}. A link that 404s is the same defect one ` + + 'level out — repoint it, or make the comment self-contained instead.', + ).toBe(true); + } + }); + + // ── the exclusion is live, or it is gone ───────────────────────────────── + it.each([...EXCLUDED.keys()])('%s still needs its exclusion', (rel) => { + const text = read(rel); + const hits = MONOREPO_ONLY.filter(({ re }) => re.test(text)); + expect( + hits.length, + `${rel} no longer cites anything monorepo-only — remove it from EXCLUDED in ` + + 'this file so it is scanned like every other shipped file. An exclusion kept ' + + 'past its cause is how a file stops being checked without anyone deciding to ' + + 'stop checking it.', + ).toBeGreaterThan(0); + }); +}); diff --git a/packages/create-objectstack/src/templates/blank/Dockerfile b/packages/create-objectstack/src/templates/blank/Dockerfile index 5b8a3db673..11599be626 100644 --- a/packages/create-objectstack/src/templates/blank/Dockerfile +++ b/packages/create-objectstack/src/templates/blank/Dockerfile @@ -20,7 +20,7 @@ RUN npx os build # → dist/objectstack.json # ── Runtime: the official ObjectStack runtime image ────────────────── # Ships Node + @objectstack/cli with `os start`, a non-root user, the # /api/v1/health HEALTHCHECK, and OS_ARTIFACT_PATH/OS_PORT preset (port 8080) -# — see docker/README.md in the framework repo. +# — see the self-hosting guide linked above. # # Dependencies were not installed while scaffolding, so the tag below could # not be resolved for you. `latest` floats to whatever release is newest, diff --git a/packages/create-objectstack/src/templates/blank/objectstack.config.ts b/packages/create-objectstack/src/templates/blank/objectstack.config.ts index 5e1435a077..fb07e6e17f 100644 --- a/packages/create-objectstack/src/templates/blank/objectstack.config.ts +++ b/packages/create-objectstack/src/templates/blank/objectstack.config.ts @@ -12,27 +12,31 @@ export default defineStack({ type: 'app', name: 'Blank Starter', description: 'Minimal ObjectStack environment — a clean slate for building.', - // Protocol compatibility range (ADR-0087 D1): lets an incompatible runtime - // refuse this package at the boundary with the exact migration command, - // instead of crashing later. Kept in lockstep with releases by - // scripts/sync-template-versions.mjs. + // Protocol compatibility range: the metadata-protocol major this app is + // authored against. The runtime checks it before it loads anything, so a + // runtime outside the range refuses this app at the boundary with the exact + // migration command instead of crashing later. Scaffolding stamped it to + // match the ObjectStack version you installed — change it when you + // deliberately move to a new protocol major, not to silence a mismatch. + // Guide: https://objectstack.ai/docs/upgrading engines: { protocol: '^17' }, }, - // `automation` backs flow execution and, per ADR-0097, materializes any - // declarative `connectors:` entry into a live, dispatchable connector at boot. - // The connector executors below register their provider factories with it — - // without `automation` loaded they have nowhere to register and boot fails, so - // keep this capability whenever `plugins:` lists a connector. + // `automation` backs flow execution and materializes any declarative + // `connectors:` entry into a live, dispatchable connector at boot. The + // connector executors below register their provider factories with it — + // without `automation` loaded they have nowhere to register and boot fails, + // so keep this capability whenever `plugins:` lists a connector. requires: ['automation'], - // Generic connector executors (ADR-0022/0023/0024 + ADR-0097), default-present - // so you can add a `connectors:` entry naming `provider: 'rest' | 'openapi' | - // 'mcp'` and have it materialize with zero host code. Zero-arg = contribute the - // provider factory only. Brand connectors (Slack, …) stay marketplace/opt-in. - // Security (#3055): a declarative `mcp` stdio transport spawns a local process - // from metadata and is denied by default — opt in per host with + // Generic connector executors, default-present so you can add a `connectors:` + // entry naming `provider: 'rest' | 'openapi' | 'mcp'` and have it materialize + // with zero host code. Zero-arg = contribute the provider factory only. Brand + // connectors (Slack, …) stay marketplace/opt-in. + // Security: a declarative `mcp` stdio transport spawns a local process from + // metadata, so it is denied by default — opt in per host with // `new ConnectorMcpPlugin({ declarativeStdio: [''] })`. + // Authoring guide: https://objectstack.ai/docs/automation/connectors plugins: [ new ConnectorRestPlugin(), new ConnectorOpenApiPlugin(), diff --git a/packages/create-objectstack/src/templates/blank/src/objects/note.object.ts b/packages/create-objectstack/src/templates/blank/src/objects/note.object.ts index 9197ac8470..bfcf76e390 100644 --- a/packages/create-objectstack/src/templates/blank/src/objects/note.object.ts +++ b/packages/create-objectstack/src/templates/blank/src/objects/note.object.ts @@ -21,8 +21,12 @@ export const Note = ObjectSchema.create({ }), }, - // Org-wide default (OWD): who can see records they don't own. The security - // posture gate (ADR-0090) requires an explicit, authored decision here. + // Org-wide default (OWD): who can see records they don't own. `private` is + // owner-only until access is widened by a permission grant or a sharing rule. + // Declaring it is required, deliberately: `npm run build` refuses an object + // with no OWD, so the baseline is always an authored decision rather than an + // accident. The other values, and how to widen access safely: + // https://objectstack.ai/docs/permissions/sharing-rules sharingModel: 'private', enable: { From e6a31555b0f5234cf7b2d4de061acfd447a3ebc9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 09:35:03 +0000 Subject: [PATCH 2/2] docs(create-objectstack): name the build command package-manager-neutrally The scaffolded README uses pnpm throughout, so a hardcoded `npm run build` in the starter object contradicted the project's own docs. Name the underlying `objectstack build` instead, which is what the project's build script runs whichever package manager invoked it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r --- .../src/templates/blank/src/objects/note.object.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/create-objectstack/src/templates/blank/src/objects/note.object.ts b/packages/create-objectstack/src/templates/blank/src/objects/note.object.ts index bfcf76e390..f6bad90b87 100644 --- a/packages/create-objectstack/src/templates/blank/src/objects/note.object.ts +++ b/packages/create-objectstack/src/templates/blank/src/objects/note.object.ts @@ -23,9 +23,9 @@ export const Note = ObjectSchema.create({ // Org-wide default (OWD): who can see records they don't own. `private` is // owner-only until access is widened by a permission grant or a sharing rule. - // Declaring it is required, deliberately: `npm run build` refuses an object - // with no OWD, so the baseline is always an authored decision rather than an - // accident. The other values, and how to widen access safely: + // Declaring it is required, deliberately: `objectstack build` refuses an + // object that declares no OWD, so the baseline is always an authored decision + // rather than an accident. The other values, and how to widen access safely: // https://objectstack.ai/docs/permissions/sharing-rules sharingModel: 'private',