Skip to content

Commit ecd06f6

Browse files
Elon Muskclaude
andauthored
Make the scaffolded starter comments followable by a stranger (#11024)
* docs(create-objectstack): make the starter comments followable by a stranger 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r * 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8d21f7a commit ecd06f6

5 files changed

Lines changed: 257 additions & 18 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
"create-objectstack": patch
3+
---
4+
5+
Rewrite the scaffolded project's starter comments so a newcomer can actually
6+
follow them (#10324). `objectstack.config.ts` and `src/objects/note.object.ts`
7+
are the first two files opened after scaffolding, and between them they cited
8+
four ADR identifiers, one bare issue number and the path of a release-time
9+
script in this monorepo — none of which ship in, or are linked from, a
10+
scaffolded project. `// per ADR-0097` read as a reference the reader was
11+
failing to follow rather than as the context it was meant to be.
12+
13+
The explanations are kept and made self-contained; only the dead ends are
14+
gone. Each now states the fact the identifier stood for — the protocol range
15+
is checked before anything loads and was stamped to match the installed
16+
version rather than hand-tuned; `automation` must stay whenever `plugins:`
17+
lists a connector or the executors have nowhere to register; a declarative
18+
`mcp` stdio transport is denied by default; the org-wide default is required
19+
so the baseline is an authored decision — and points at the public docs page
20+
that covers it in full. The blank `Dockerfile` likewise stops pointing at a
21+
file in this repo and points at the self-hosting guide it already links.
22+
23+
A pin (`starter-comments-self-contained.test.ts`) keeps it that way from both
24+
sides: no shipped template file may cite an ADR identifier, a bare issue
25+
number or a repo script path, and the facts those references carried must
26+
still be stated — so the comments cannot be "fixed" by deleting them. It also
27+
resolves every canonical-origin docs URL in the shipped tree against
28+
`content/docs`, because a link that 404s is the same defect one level out.
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license.
2+
//
3+
// Every comment that ships INTO a scaffolded project must be followable by the
4+
// person reading it — someone who has this project and nothing else.
5+
//
6+
// ## The defect
7+
//
8+
// The two files a newcomer opens first after scaffolding, objectstack.config.ts
9+
// and src/objects/note.object.ts, carried six references addressed to a reader
10+
// with this monorepo open: four ADR identifiers, one bare issue number, and the
11+
// path of a release-time script. None of docs/adr, the issue tracker, or that
12+
// script ships in a scaffolded project, so "// per ADR-0097" was a reference the
13+
// reader could not resolve — it read as an instruction they were failing to
14+
// follow rather than as the context it was meant to be.
15+
//
16+
// ## Why this pin has TWO halves, and why the second is the load-bearing one
17+
//
18+
// The cheap way to make the references disappear is to delete the comments. That
19+
// would be a worse project than the one with the dead references: the comments
20+
// explain WHY each setting is the way it is, which is exactly what a newcomer
21+
// deciding whether to change it needs. So a one-way "no ADR identifiers" grep
22+
// would rot in the one direction that matters — it stays green while the
23+
// rationale is deleted out from under it.
24+
//
25+
// Hence: no unfollowable reference (assertion 1) AND the fact each comment
26+
// carries still stated (assertion 2). A future edit can reword freely; it cannot
27+
// quietly strip the explanation, and it cannot re-introduce a dead end.
28+
//
29+
// ## The third half: a public link is only a fix while it resolves
30+
//
31+
// Replacing an internal identifier with a docs URL moves the same defect one
32+
// level out if the URL 404s — a reference that looks authoritative and lands
33+
// nowhere. Assertion 3 resolves every canonical-origin docs URL in the shipped
34+
// tree against content/docs the way Fumadocs routes it: baseUrl /docs over
35+
// content/docs, and a directory that exists but carries no index page is a 404.
36+
// That candidate list is check-docs-redirects' pageCandidates, restated in six
37+
// lines rather than imported, because importing a root script into this package
38+
// would widen this suite's declared cross-package read radius to buy nothing.
39+
//
40+
// Host CONVERGENCE is deliberately not asserted here — the tree still carries
41+
// two non-canonical docs hostnames and they are another card's (#10990). This
42+
// pin only judges URLs already on the canonical origin, so the two cards cannot
43+
// collide.
44+
45+
import { describe, it, expect } from 'vitest';
46+
import fs from 'node:fs';
47+
import path from 'node:path';
48+
import { fileURLToPath } from 'node:url';
49+
50+
const HERE = path.dirname(fileURLToPath(import.meta.url));
51+
const templateRoot = path.resolve(HERE, 'templates');
52+
const contentDocs = path.resolve(HERE, '..', '..', '..', 'content', 'docs');
53+
54+
/**
55+
* The blank template's README is scanned by nothing here yet: it still carries
56+
* an ADR identifier of its own, and it is owned by other cards in the same
57+
* family (a scaffolding-guidance fix was in flight over it while this landed).
58+
*
59+
* The exclusion is SELF-RETIRING rather than permanent — the last assertion
60+
* fails the moment the README stops needing it, so whoever cleans that file is
61+
* told, in their own run, to delete this entry and let the file be scanned.
62+
* A silent exemption over the most-read file in the tree is the failure this
63+
* shape exists to avoid.
64+
*/
65+
const EXCLUDED = new Map([['blank/README.md', 'still carries an ADR identifier; owned by another card']]);
66+
67+
/** Text files the scaffolder copies into the user's project. */
68+
function shippedFiles(): string[] {
69+
const out: string[] = [];
70+
const walk = (dir: string) => {
71+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
72+
const abs = path.join(dir, entry.name);
73+
if (entry.isDirectory()) walk(abs);
74+
else out.push(path.relative(templateRoot, abs).split(path.sep).join('/'));
75+
}
76+
};
77+
walk(templateRoot);
78+
return out.sort();
79+
}
80+
81+
/**
82+
* References a reader who has only their own scaffolded project cannot follow.
83+
* Each is spelled to match the identifier, not any particular sentence, so the
84+
* prose around it stays free to change.
85+
*/
86+
const MONOREPO_ONLY = [
87+
{ label: 'an ADR identifier', re: /\bADR-\d{3,4}\b/ },
88+
{ label: 'a bare issue number', re: /(^|[^\w/])#\d{3,6}\b/ },
89+
{ label: 'a repo build-script path', re: /\bscripts\/[\w.-]+\.(?:mjs|mts|cjs|ts|js)\b/ },
90+
{ label: 'a monorepo package path', re: /\bpackages\/[a-z0-9][\w-]*\//i },
91+
];
92+
93+
const read = (rel: string) => fs.readFileSync(path.join(templateRoot, rel), 'utf8');
94+
95+
describe('shipped template comments are followable by a stranger', () => {
96+
const files = shippedFiles();
97+
98+
it('reads a real template tree (vacuity guard)', () => {
99+
expect(files).toContain('blank/objectstack.config.ts');
100+
expect(files).toContain('blank/src/objects/note.object.ts');
101+
expect(files.length).toBeGreaterThan(8);
102+
});
103+
104+
// ── assertion 1: nothing unfollowable ────────────────────────────────────
105+
it.each(shippedFiles().filter((f) => !EXCLUDED.has(f)))(
106+
'%s cites nothing that only exists in this monorepo',
107+
(rel) => {
108+
const text = read(rel);
109+
for (const { label, re } of MONOREPO_ONLY) {
110+
const hit = re.exec(text);
111+
expect(
112+
hit,
113+
`${rel} cites ${label} (${JSON.stringify(hit?.[0])}). A scaffolded project ` +
114+
'ships no ADRs, no issue tracker and none of this repo\'s scripts, so this ' +
115+
'reads as a reference the newcomer is failing to follow. State the fact ' +
116+
'self-contained, or link a public docs page — do not delete the rationale.',
117+
).toBeNull();
118+
}
119+
},
120+
);
121+
122+
// ── assertion 2: the rationale survives ──────────────────────────────────
123+
// Each entry is the FACT the removed reference was carrying, matched loosely
124+
// enough that rewording is free and deletion is not.
125+
const RATIONALE: { file: string; facts: { what: string; re: RegExp }[] }[] = [
126+
{
127+
file: 'blank/objectstack.config.ts',
128+
facts: [
129+
{ what: 'why the protocol range exists (an incompatible runtime refuses the app)', re: /refuses? this app|refuse this package|incompatible runtime/i },
130+
{ what: 'that the protocol range is stamped for you, not hand-tuned', re: /stamped|scaffold(ing|ed)/i },
131+
{ what: 'why `automation` must stay when a connector is listed', re: /nowhere to register|boot fails/i },
132+
{ what: 'that a declarative mcp stdio transport is denied by default', re: /denied by default/i },
133+
],
134+
},
135+
{
136+
file: 'blank/src/objects/note.object.ts',
137+
facts: [
138+
{ what: 'what the org-wide default means', re: /org-wide default|OWD/i },
139+
{ what: 'that declaring it is required rather than optional', re: /required|refuses/i },
140+
],
141+
},
142+
];
143+
144+
for (const { file, facts } of RATIONALE) {
145+
describe(file, () => {
146+
for (const { what, re } of facts) {
147+
it(`still explains ${what}`, () => {
148+
expect(
149+
read(file),
150+
`${file} no longer explains ${what}. These comments were rewritten to drop ` +
151+
'monorepo-only references while KEEPING what they explain; deleting the ' +
152+
'explanation is not the same fix.',
153+
).toMatch(re);
154+
});
155+
}
156+
});
157+
}
158+
159+
// ── assertion 3: canonical docs links resolve ────────────────────────────
160+
it('every canonical docs URL in the shipped tree resolves to a real page', () => {
161+
// baseUrl '/docs' is mounted over content/docs, so the route path is the
162+
// file path minus the extension; a directory resolves only via an index page.
163+
const candidates = (route: string) => [
164+
`${route}.mdx`,
165+
`${route}.md`,
166+
`${route}/index.mdx`,
167+
`${route}/index.md`,
168+
];
169+
const urls: { rel: string; url: string; route: string }[] = [];
170+
for (const rel of shippedFiles()) {
171+
const text = read(rel);
172+
for (const m of text.matchAll(/https:\/\/objectstack\.ai\/docs\/([\w./-]*[\w-])/g)) {
173+
urls.push({ rel, url: m[0], route: m[1] });
174+
}
175+
}
176+
// Non-vacuity: the rewritten starter comments put docs links in this tree on
177+
// purpose. Zero matches means the extractor broke, not that the tree is clean.
178+
expect(urls.length, 'no canonical docs URLs found — the extractor is broken').toBeGreaterThan(0);
179+
180+
for (const { rel, url, route } of urls) {
181+
const found = candidates(route).some((c) => fs.existsSync(path.join(contentDocs, c)));
182+
expect(
183+
found,
184+
`${rel} links ${url}, which content/docs serves from none of ` +
185+
`${candidates(route).join(', ')}. A link that 404s is the same defect one ` +
186+
'level out — repoint it, or make the comment self-contained instead.',
187+
).toBe(true);
188+
}
189+
});
190+
191+
// ── the exclusion is live, or it is gone ─────────────────────────────────
192+
it.each([...EXCLUDED.keys()])('%s still needs its exclusion', (rel) => {
193+
const text = read(rel);
194+
const hits = MONOREPO_ONLY.filter(({ re }) => re.test(text));
195+
expect(
196+
hits.length,
197+
`${rel} no longer cites anything monorepo-only — remove it from EXCLUDED in ` +
198+
'this file so it is scanned like every other shipped file. An exclusion kept ' +
199+
'past its cause is how a file stops being checked without anyone deciding to ' +
200+
'stop checking it.',
201+
).toBeGreaterThan(0);
202+
});
203+
});

packages/create-objectstack/src/templates/blank/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ RUN npx os build # → dist/objectstack.json
2020
# ── Runtime: the official ObjectStack runtime image ──────────────────
2121
# Ships Node + @objectstack/cli with `os start`, a non-root user, the
2222
# /api/v1/health HEALTHCHECK, and OS_ARTIFACT_PATH/OS_PORT preset (port 8080)
23-
# — see docker/README.md in the framework repo.
23+
# — see the self-hosting guide linked above.
2424
#
2525
# Dependencies were not installed while scaffolding, so the tag below could
2626
# not be resolved for you. `latest` floats to whatever release is newest,

packages/create-objectstack/src/templates/blank/objectstack.config.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,31 @@ export default defineStack({
1212
type: 'app',
1313
name: 'Blank Starter',
1414
description: 'Minimal ObjectStack environment — a clean slate for building.',
15-
// Protocol compatibility range (ADR-0087 D1): lets an incompatible runtime
16-
// refuse this package at the boundary with the exact migration command,
17-
// instead of crashing later. Kept in lockstep with releases by
18-
// scripts/sync-template-versions.mjs.
15+
// Protocol compatibility range: the metadata-protocol major this app is
16+
// authored against. The runtime checks it before it loads anything, so a
17+
// runtime outside the range refuses this app at the boundary with the exact
18+
// migration command instead of crashing later. Scaffolding stamped it to
19+
// match the ObjectStack version you installed — change it when you
20+
// deliberately move to a new protocol major, not to silence a mismatch.
21+
// Guide: https://objectstack.ai/docs/upgrading
1922
engines: { protocol: '^17' },
2023
},
2124

22-
// `automation` backs flow execution and, per ADR-0097, materializes any
23-
// declarative `connectors:` entry into a live, dispatchable connector at boot.
24-
// The connector executors below register their provider factories with it —
25-
// without `automation` loaded they have nowhere to register and boot fails, so
26-
// keep this capability whenever `plugins:` lists a connector.
25+
// `automation` backs flow execution and materializes any declarative
26+
// `connectors:` entry into a live, dispatchable connector at boot. The
27+
// connector executors below register their provider factories with it —
28+
// without `automation` loaded they have nowhere to register and boot fails,
29+
// so keep this capability whenever `plugins:` lists a connector.
2730
requires: ['automation'],
2831

29-
// Generic connector executors (ADR-0022/0023/0024 + ADR-0097), default-present
30-
// so you can add a `connectors:` entry naming `provider: 'rest' | 'openapi' |
31-
// 'mcp'` and have it materialize with zero host code. Zero-arg = contribute the
32-
// provider factory only. Brand connectors (Slack, …) stay marketplace/opt-in.
33-
// Security (#3055): a declarative `mcp` stdio transport spawns a local process
34-
// from metadata and is denied by default — opt in per host with
32+
// Generic connector executors, default-present so you can add a `connectors:`
33+
// entry naming `provider: 'rest' | 'openapi' | 'mcp'` and have it materialize
34+
// with zero host code. Zero-arg = contribute the provider factory only. Brand
35+
// connectors (Slack, …) stay marketplace/opt-in.
36+
// Security: a declarative `mcp` stdio transport spawns a local process from
37+
// metadata, so it is denied by default — opt in per host with
3538
// `new ConnectorMcpPlugin({ declarativeStdio: ['<trusted-command>'] })`.
39+
// Authoring guide: https://objectstack.ai/docs/automation/connectors
3640
plugins: [
3741
new ConnectorRestPlugin(),
3842
new ConnectorOpenApiPlugin(),

packages/create-objectstack/src/templates/blank/src/objects/note.object.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@ export const Note = ObjectSchema.create({
2121
}),
2222
},
2323

24-
// Org-wide default (OWD): who can see records they don't own. The security
25-
// posture gate (ADR-0090) requires an explicit, authored decision here.
24+
// Org-wide default (OWD): who can see records they don't own. `private` is
25+
// owner-only until access is widened by a permission grant or a sharing rule.
26+
// Declaring it is required, deliberately: `objectstack build` refuses an
27+
// object that declares no OWD, so the baseline is always an authored decision
28+
// rather than an accident. The other values, and how to widen access safely:
29+
// https://objectstack.ai/docs/permissions/sharing-rules
2630
sharingModel: 'private',
2731

2832
enable: {

0 commit comments

Comments
 (0)