diff --git a/packages/cli/test/init-template-comments-self-contained.test.ts b/packages/cli/test/init-template-comments-self-contained.test.ts index bd6f17f225..5e723ad0e7 100644 --- a/packages/cli/test/init-template-comments-self-contained.test.ts +++ b/packages/cli/test/init-template-comments-self-contained.test.ts @@ -81,6 +81,23 @@ // (which owns the canonical-origin constant), for the same reason #10324's // version does: an import would widen this suite's declared cross-package // read radius to buy six lines. +// +// ## The vocabulary is the OTHER pin's, and drifting apart is the failure mode +// +// `MONOREPO_ONLY` below is `create-objectstack`'s +// `starter-comments-self-contained.test.ts` vocabulary, restated rather than +// imported — same reason as the candidate-route logic above. Restating buys +// the narrow read radius and costs the guarantee that the two stay equal, and +// they did NOT: #11022 added a fifth pattern to that file and this one kept +// four, so the same defect class had two different answers depending on which +// scaffolder shipped it. #15150 backfills that fifth pattern here and adds a +// sixth to BOTH files in one change. +// +// Both additions are pure regression guardrails: measured on the rendered +// population this file actually sweeps, each matches zero text today. That is +// the point — a shape that cannot grow back — and it is also why each carries +// its own injection control in the PR that landed it, since a pattern matching +// nothing is indistinguishable from a pattern that is broken. import { describe, it, expect, afterAll } from 'vitest'; import fs from 'node:fs'; @@ -202,12 +219,79 @@ function renderAll(): Rendered[] { * `starter-comments-self-contained.test.ts` — same defect class, same * vocabulary — spelled to match the identifier, not any particular * sentence, so the prose around it stays free to change. + * + * "Verbatim" is an obligation, not a description of how it got here: a + * pattern added to either file belongs in both, or this package's scaffolders + * and that one's answer the same question differently. See the file header + * for the drift #15150 repaired. */ 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 }, + // #11022, backfilled here by #15150: `create-objectstack`'s + // `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 four patterns above 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 rather than to that one sentence — it survives a reword. + // Deliberately narrower than a bare "repo" or "monorepo" match: a scaffolded + // project is entitled to call ITSELF a monorepo root, which is a correct, + // self-contained, followable statement about a directory the reader has. + // + // Nothing in this package's rendered output matches it today, and the one + // near-hit is deliberately out of population: `create.ts` describes its + // `--in-repo` flag as emitting a project that installs only in this + // monorepo, and that string is CLI help text — printed to whoever runs the + // command, never written into the scaffolded project. This pin reads what + // the emitters write to disk, so it does not see it and must not be + // "fixed" to. + { label: 'a reference to the ObjectStack repo as an unlinked location', re: /\bObjectStack (?:framework )?(?:mono)?repo\b/i }, + // #15150: the five patterns above are all spelled ABSOLUTELY — a repo-rooted + // path, an identifier, or a name. This one is the same class of unfollowable + // reference written RELATIVELY, as a path that climbs out of the project the + // reader actually has. `os create` shipped exactly that, + // `[ObjectStack Documentation](../../content/docs)` in a scaffolded README: + // the project has no parent directory to climb into, so the link resolved + // nowhere while assertion 1 read the file, matched none of its patterns, and + // reported it as a PASSING row. + // + // Anchored on `../`, deliberately NOT on `./`, and deliberately not on the + // depth judgement #15150 proposed (`check:cross-package-test-inputs`-style + // "the shallowest point a path reaches"). What makes the bare anchor sound + // is a property of THIS population rather than of `../`: the population is + // the DEFAULT placement only. `create.ts` renders per `ScaffoldPlacement`, + // and `defineTemplate`'s `files` getter — the map `renderAll()` reads below + // — is `filesFor(DEFAULT_PLACEMENT)`, i.e. `standalone`, whose emitted + // `tsconfig.json` is self-contained. The one legitimate escaping `../` this + // package can produce, `rootTsconfigExtends`, is emitted ONLY on the + // `in-repo` branch and so is never in this sweep. Measured: `filesFor` + // `('in-repo')` yields `"../../../tsconfig.json"`; `template.files` and + // `filesFor('standalone')` yield no `../` at all. + // + // ⚠️ THAT IS THE POPULATION ARGUMENT, NOT A PROPERTY OF THE PATTERN, and the + // difference is worth stating plainly rather than leaving for someone to + // discover from a red: this matches ANY `../`, including a climb that stays + // INSIDE the project. `import { x } from '../config'` in a nested emitted + // source file, or a prose "see ../README.md in this project", are both legal + // in a scaffolded project and both redden here. Only the population keeps + // that from mattering. + // + // So there are TWO conditions to re-read this on, not one, and the second is + // the likelier: + // 1. the population widens to the `in-repo` placement, which emits + // `rootTsconfigExtends`; or + // 2. a template grows an intra-project relative climb — an emitted source + // file nested deeply enough to reach back up toward its own project + // root. This needs NO placement change at all. + // + // In EITHER case the red lands on correctly-shaped, legal text, and the fix + // is the depth judgement (`check:cross-package-test-inputs`-style, "the + // shallowest point a path reaches") — never an exemption, and never a + // widened pattern. + { label: 'a path that climbs out of the scaffolded project', re: /(? { 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 e781717bb6..1427f6702f 100644 --- a/packages/create-objectstack/src/starter-comments-self-contained.test.ts +++ b/packages/create-objectstack/src/starter-comments-self-contained.test.ts @@ -59,6 +59,21 @@ // 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). +// +// ## A sixth `MONOREPO_ONLY` pattern: the same reference, written RELATIVELY +// +// All five patterns above match a reference spelled in ABSOLUTE, repo-rooted +// form — `packages//`, `scripts/.mjs`, an ADR id, an issue +// number, the framework's own name. None of them matches that same +// unfollowable reference written as a path which climbs OUT of the scaffolded +// project. `os create` shipped exactly that into a project README — +// `[ObjectStack Documentation](../../content/docs)` — and assertion 1 read +// the file, matched none of its five patterns, and reported it as a PASSING +// row, which is worse than never having reported on it (#15150). +// +// The sixth pattern closes that spelling. It is a pure regression guardrail: +// this tree ships zero `../` of any kind today, so it catches nothing on the +// day it lands and exists so the shape cannot grow back. import { describe, it, expect } from 'vitest'; import fs from 'node:fs'; @@ -115,6 +130,42 @@ const MONOREPO_ONLY = [ // "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 }, + // #15150: the five patterns above are all spelled ABSOLUTELY — a + // repo-rooted path, an identifier, or a name. This one is the same class of + // unfollowable reference written RELATIVELY, as a path that climbs out of + // the project the reader actually has. A scaffolded project has no parent + // directory to climb into, so `../../content/docs` resolves nowhere for + // them however faithfully it describes this monorepo. + // + // Anchored on `../`, deliberately NOT on `./`, and deliberately not on the + // depth judgement #15150 proposed (`check:cross-package-test-inputs`-style + // "the shallowest point a path reaches"). In THIS population the two give + // the same answer, for a measured reason: every relative reference the + // shipped tree carries is `./`-anchored and points DOWNWARD from the file + // that writes it (`./note.object.js`, `./src/objects/index.js`), so the + // count of `../` here — escaping or merely climbing — is zero. The bare + // anchor therefore has no correct text to redden. The leading lookbehind is + // what keeps it that way: it refuses a `..` that is itself part of a longer + // run of dots, so an ellipsis followed by a path is not a hit. + // + // ⚠️ WHAT IT MATCHES IS WIDER THAN WHAT MOTIVATED IT, and that is worth + // stating plainly rather than leaving for someone to discover from a red: + // this matches ANY `../`, including a climb that stays INSIDE the project. + // `import { x } from '../config'` in a nested source file, or a prose + // "see ../README.md in this project", are both legal in a scaffolded project + // and both redden here. Nothing about the pattern distinguishes them; only + // the population does. + // + // So the trade is sound exactly while the swept tree carries no `../` at all + // — measured zero today — and the condition to re-read is: a template grows + // an intra-project relative climb, i.e. a source file nested deeply enough to + // reach back up toward its own project root. That needs no other change to + // happen. When it does, the red is on correctly-shaped, legal text, and the + // fix is the depth judgement (`check:cross-package-test-inputs`-style, "the + // shallowest point a path reaches") — never an exemption for the file, and + // never a widened pattern, which would hand the escaping spelling its + // exemption back. + { label: 'a path that climbs out of the scaffolded project', re: /(? fs.readFileSync(path.join(templateRoot, rel), 'utf8');