Skip to content

Commit cc986c9

Browse files
claude[bot]claude
andauthored
test(cli, create-objectstack): give MONOREPO_ONLY a pattern for project-escaping relative paths, and re-equalise the two pins' vocabularies (#17018)
* test(cli, create-objectstack): give MONOREPO_ONLY a pattern for project-escaping relative paths, and re-equalise the two vocabularies The scaffold-comment pins match unfollowable references in five ABSOLUTE spellings (an ADR id, an issue number, `scripts/<name>.mjs`, `packages/<name>/`, the framework's own name in prose). None matched the same reference written RELATIVELY, so `[ObjectStack Documentation](../../content/docs)` in a scaffolded README was read, matched nothing, and reported as a passing row — worse than not reporting on the file at all. Adds a sixth pattern for a path that climbs out of the scaffolded project, to BOTH pins, and backfills the fifth (the prose pattern) into the cli-side pin, which had kept four while create-objectstack grew to five — the same defect class answered differently depending on which scaffolder shipped it. The two vocabularies are now byte-identical again. Both additions are pure regression guardrails: measured on both swept populations, each matches zero text today. The bare `../` anchor is sound here because the cli pin's population is the DEFAULT placement only — the `files` getter is `filesFor('standalone')` — and the one legitimate escaping `../`, `rootTsconfigExtends`, is emitted only on the `in-repo` branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8 * docs(test): record the escaping-path pattern's real trigger, not just the case that motivated it The pattern matches ANY `../`, including a climb that stays inside the scaffolded project — `import { x } from '../config'` in a nested source file, or a prose "see ../README.md in this project", both legal and both reddened. Only the population keeps that from mattering, and the population is measured at zero `../` today. The cli-side comment recorded one re-read condition (the population widening to the `in-repo` placement) and the create-objectstack side recorded it as a hypothetical. Neither named the likelier case: a template growing an intra-project relative climb, which needs no placement change at all. A future contributor hitting that red would have been told it could not happen. Comment-only; the regex, the vocabulary and both populations are untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 513c495 commit cc986c9

2 files changed

Lines changed: 135 additions & 0 deletions

File tree

packages/cli/test/init-template-comments-self-contained.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,23 @@
8181
// (which owns the canonical-origin constant), for the same reason #10324's
8282
// version does: an import would widen this suite's declared cross-package
8383
// read radius to buy six lines.
84+
//
85+
// ## The vocabulary is the OTHER pin's, and drifting apart is the failure mode
86+
//
87+
// `MONOREPO_ONLY` below is `create-objectstack`'s
88+
// `starter-comments-self-contained.test.ts` vocabulary, restated rather than
89+
// imported — same reason as the candidate-route logic above. Restating buys
90+
// the narrow read radius and costs the guarantee that the two stay equal, and
91+
// they did NOT: #11022 added a fifth pattern to that file and this one kept
92+
// four, so the same defect class had two different answers depending on which
93+
// scaffolder shipped it. #15150 backfills that fifth pattern here and adds a
94+
// sixth to BOTH files in one change.
95+
//
96+
// Both additions are pure regression guardrails: measured on the rendered
97+
// population this file actually sweeps, each matches zero text today. That is
98+
// the point — a shape that cannot grow back — and it is also why each carries
99+
// its own injection control in the PR that landed it, since a pattern matching
100+
// nothing is indistinguishable from a pattern that is broken.
84101

85102
import { describe, it, expect, afterAll } from 'vitest';
86103
import fs from 'node:fs';
@@ -202,12 +219,79 @@ function renderAll(): Rendered[] {
202219
* `starter-comments-self-contained.test.ts` — same defect class, same
203220
* vocabulary — spelled to match the identifier, not any particular
204221
* sentence, so the prose around it stays free to change.
222+
*
223+
* "Verbatim" is an obligation, not a description of how it got here: a
224+
* pattern added to either file belongs in both, or this package's scaffolders
225+
* and that one's answer the same question differently. See the file header
226+
* for the drift #15150 repaired.
205227
*/
206228
const MONOREPO_ONLY = [
207229
{ label: 'an ADR identifier', re: /\bADR-\d{3,4}\b/ },
208230
{ label: 'a bare issue number', re: /(^|[^\w/])#\d{3,6}\b/ },
209231
{ label: 'a repo build-script path', re: /\bscripts\/[\w.-]+\.(?:mjs|mts|cjs|ts|js)\b/ },
210232
{ label: 'a monorepo package path', re: /\bpackages\/[a-z0-9][\w-]*\//i },
233+
// #11022, backfilled here by #15150: `create-objectstack`'s
234+
// `blank/README.md` named "the ObjectStack framework repo" as the home of
235+
// `skills/`, unlinked — a reader with only their own scaffolded project has
236+
// no way to reach it. The four patterns above are syntactic identifiers (an
237+
// ADR id, an issue number, a repo-relative path); this one is the same class
238+
// of defect in PROSE form, so it is spelled to the FRAMEWORK'S OWN NAME next
239+
// to a "repo" word rather than to that one sentence — it survives a reword.
240+
// Deliberately narrower than a bare "repo" or "monorepo" match: a scaffolded
241+
// project is entitled to call ITSELF a monorepo root, which is a correct,
242+
// self-contained, followable statement about a directory the reader has.
243+
//
244+
// Nothing in this package's rendered output matches it today, and the one
245+
// near-hit is deliberately out of population: `create.ts` describes its
246+
// `--in-repo` flag as emitting a project that installs only in this
247+
// monorepo, and that string is CLI help text — printed to whoever runs the
248+
// command, never written into the scaffolded project. This pin reads what
249+
// the emitters write to disk, so it does not see it and must not be
250+
// "fixed" to.
251+
{ label: 'a reference to the ObjectStack repo as an unlinked location', re: /\bObjectStack (?:framework )?(?:mono)?repo\b/i },
252+
// #15150: the five patterns above are all spelled ABSOLUTELY — a repo-rooted
253+
// path, an identifier, or a name. This one is the same class of unfollowable
254+
// reference written RELATIVELY, as a path that climbs out of the project the
255+
// reader actually has. `os create` shipped exactly that,
256+
// `[ObjectStack Documentation](../../content/docs)` in a scaffolded README:
257+
// the project has no parent directory to climb into, so the link resolved
258+
// nowhere while assertion 1 read the file, matched none of its patterns, and
259+
// reported it as a PASSING row.
260+
//
261+
// Anchored on `../`, deliberately NOT on `./`, and deliberately not on the
262+
// depth judgement #15150 proposed (`check:cross-package-test-inputs`-style
263+
// "the shallowest point a path reaches"). What makes the bare anchor sound
264+
// is a property of THIS population rather than of `../`: the population is
265+
// the DEFAULT placement only. `create.ts` renders per `ScaffoldPlacement`,
266+
// and `defineTemplate`'s `files` getter — the map `renderAll()` reads below
267+
// — is `filesFor(DEFAULT_PLACEMENT)`, i.e. `standalone`, whose emitted
268+
// `tsconfig.json` is self-contained. The one legitimate escaping `../` this
269+
// package can produce, `rootTsconfigExtends`, is emitted ONLY on the
270+
// `in-repo` branch and so is never in this sweep. Measured: `filesFor`
271+
// `('in-repo')` yields `"../../../tsconfig.json"`; `template.files` and
272+
// `filesFor('standalone')` yield no `../` at all.
273+
//
274+
// ⚠️ THAT IS THE POPULATION ARGUMENT, NOT A PROPERTY OF THE PATTERN, and the
275+
// difference is worth stating plainly rather than leaving for someone to
276+
// discover from a red: this matches ANY `../`, including a climb that stays
277+
// INSIDE the project. `import { x } from '../config'` in a nested emitted
278+
// source file, or a prose "see ../README.md in this project", are both legal
279+
// in a scaffolded project and both redden here. Only the population keeps
280+
// that from mattering.
281+
//
282+
// So there are TWO conditions to re-read this on, not one, and the second is
283+
// the likelier:
284+
// 1. the population widens to the `in-repo` placement, which emits
285+
// `rootTsconfigExtends`; or
286+
// 2. a template grows an intra-project relative climb — an emitted source
287+
// file nested deeply enough to reach back up toward its own project
288+
// root. This needs NO placement change at all.
289+
//
290+
// In EITHER case the red lands on correctly-shaped, legal text, and the fix
291+
// is the depth judgement (`check:cross-package-test-inputs`-style, "the
292+
// shallowest point a path reaches") — never an exemption, and never a
293+
// widened pattern.
294+
{ label: 'a path that climbs out of the scaffolded project', re: /(?<![\w.])\.\.\/[\w./-]*/ },
211295
];
212296

213297
describe('rendered scaffold templates are followable by a stranger', () => {

packages/create-objectstack/src/starter-comments-self-contained.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,21 @@
5959
// still-unfollowable line untouched. See the fifth `MONOREPO_ONLY` entry
6060
// below for how it is scoped to avoid the reader's own project also being
6161
// called "a monorepo" (`blank/pnpm-workspace.yaml`, correctly).
62+
//
63+
// ## A sixth `MONOREPO_ONLY` pattern: the same reference, written RELATIVELY
64+
//
65+
// All five patterns above match a reference spelled in ABSOLUTE, repo-rooted
66+
// form — `packages/<name>/`, `scripts/<name>.mjs`, an ADR id, an issue
67+
// number, the framework's own name. None of them matches that same
68+
// unfollowable reference written as a path which climbs OUT of the scaffolded
69+
// project. `os create` shipped exactly that into a project README —
70+
// `[ObjectStack Documentation](../../content/docs)` — and assertion 1 read
71+
// the file, matched none of its five patterns, and reported it as a PASSING
72+
// row, which is worse than never having reported on it (#15150).
73+
//
74+
// The sixth pattern closes that spelling. It is a pure regression guardrail:
75+
// this tree ships zero `../` of any kind today, so it catches nothing on the
76+
// day it lands and exists so the shape cannot grow back.
6277

6378
import { describe, it, expect } from 'vitest';
6479
import fs from 'node:fs';
@@ -115,6 +130,42 @@ const MONOREPO_ONLY = [
115130
// "a monorepo root" (`blank/pnpm-workspace.yaml`), which is a correct,
116131
// self-contained, followable statement about a directory they do have.
117132
{ label: 'a reference to the ObjectStack repo as an unlinked location', re: /\bObjectStack (?:framework )?(?:mono)?repo\b/i },
133+
// #15150: the five patterns above are all spelled ABSOLUTELY — a
134+
// repo-rooted path, an identifier, or a name. This one is the same class of
135+
// unfollowable reference written RELATIVELY, as a path that climbs out of
136+
// the project the reader actually has. A scaffolded project has no parent
137+
// directory to climb into, so `../../content/docs` resolves nowhere for
138+
// them however faithfully it describes this monorepo.
139+
//
140+
// Anchored on `../`, deliberately NOT on `./`, and deliberately not on the
141+
// depth judgement #15150 proposed (`check:cross-package-test-inputs`-style
142+
// "the shallowest point a path reaches"). In THIS population the two give
143+
// the same answer, for a measured reason: every relative reference the
144+
// shipped tree carries is `./`-anchored and points DOWNWARD from the file
145+
// that writes it (`./note.object.js`, `./src/objects/index.js`), so the
146+
// count of `../` here — escaping or merely climbing — is zero. The bare
147+
// anchor therefore has no correct text to redden. The leading lookbehind is
148+
// what keeps it that way: it refuses a `..` that is itself part of a longer
149+
// run of dots, so an ellipsis followed by a path is not a hit.
150+
//
151+
// ⚠️ WHAT IT MATCHES IS WIDER THAN WHAT MOTIVATED IT, and that is worth
152+
// stating plainly rather than leaving for someone to discover from a red:
153+
// this matches ANY `../`, including a climb that stays INSIDE the project.
154+
// `import { x } from '../config'` in a nested source file, or a prose
155+
// "see ../README.md in this project", are both legal in a scaffolded project
156+
// and both redden here. Nothing about the pattern distinguishes them; only
157+
// the population does.
158+
//
159+
// So the trade is sound exactly while the swept tree carries no `../` at all
160+
// — measured zero today — and the condition to re-read is: a template grows
161+
// an intra-project relative climb, i.e. a source file nested deeply enough to
162+
// reach back up toward its own project root. That needs no other change to
163+
// happen. When it does, the red is on correctly-shaped, legal text, and the
164+
// fix is the depth judgement (`check:cross-package-test-inputs`-style, "the
165+
// shallowest point a path reaches") — never an exemption for the file, and
166+
// never a widened pattern, which would hand the escaping spelling its
167+
// exemption back.
168+
{ label: 'a path that climbs out of the scaffolded project', re: /(?<![\w.])\.\.\/[\w./-]*/ },
118169
];
119170

120171
const read = (rel: string) => fs.readFileSync(path.join(templateRoot, rel), 'utf8');

0 commit comments

Comments
 (0)