test(cli, create-objectstack): give MONOREPO_ONLY a pattern for project-escaping relative paths, and re-equalise the two pins' vocabularies - #17018
Merged
os-project-manager merged 4 commits intoSep 9, 2026
Conversation
…ct-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
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
… 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
os-project-manager
marked this pull request as ready for review
September 9, 2026 04:21
os-project-manager
enabled auto-merge
September 9, 2026 04:21
os-project-manager
deleted the
claude/issue-15150-monorepo-only-escaping-path
branch
September 9, 2026 04:41
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #15150
What this is
Both scaffold-comment pins share one
MONOREPO_ONLYvocabulary — patterns for areference that a reader who has only their own scaffolded project cannot follow.
Every pattern in it matched an absolute, repo-rooted spelling: an ADR id, an
issue number, a
scripts/NAME.mjspath, apackages/NAME/path, the framework'sown name in prose. None matched that same unfollowable reference written
relatively, so a scaffolded README carrying a link to
../../content/docswasread, matched nothing, and reported as a passing row — worse than never having
reported on the file.
This adds a sixth pattern, for a path that climbs out of the scaffolded project,
to both pins, and backfills the fifth (the prose pattern from #11022) into the
cli-side pin, which had kept four while
create-objectstackgrew to five. The twovocabularies are byte-identical again — verified by diffing the six entry lines.
Premise check: the card's own "Shape of the fix" is stale, on purpose
The card predicts the new pattern will go red on
create:example/README.md. Itwill not, and a reviewer waiting for that red would misread this PR:
os createemit a project that installs outside this monorepo #15535 reworkedcreate.tsinto aScaffoldPlacementsplit and deleted thetwo named instances.
os create example— it is a weakeros initplus a README; docs point atos init(item 2 of #15531) #16483 retired thecreate:exampletemplate outright — the surviving pinalready asserts its absence (
init-template-comments-self-contained.test.ts).So this card is now a pure regression guardrail: measured on both swept
populations, every new pattern matches zero text today. It is added so the
shape cannot grow back. Green is therefore the null hypothesis here, not a result,
which is why every pattern below carries a firing control.
Why a plain two-dot-slash anchor, and not the depth judgement the card proposed
The card proposed judging escape by depth, the way
check:cross-package-test-inputsjudges the shallowest point a path reaches. That is not needed here, and the reason
is a measured property of the population rather than of the pattern:
create.tsrenders per placement.defineTemplate'sfilesgetter — the map thecli pin reads — is
filesFor(DEFAULT_PLACEMENT), i.e.standalone.rootTsconfigExtends, isproduced only on the
in-repobranch.Measured directly, per template:
template.files(what the pin reads)filesFor('standalone')filesFor('in-repo')tsconfig.jsonextends three levels upOn the
create-objectstackside the same holds for a different reason: everyrelative reference the shipped tree carries is anchored downward from the file that
writes it (
./note.object.js,./src/objects/index.js), so the count of climbingpaths there is zero.
The pattern is anchored on the two-dot-slash sequence, never on the single-dot form,
and carries a negative lookbehind refusing a word character or a further dot before
it, so an ellipsis followed by a path is not a hit.
What the pattern actually matches, stated wider than the case that motivated it
Driven against a case set, reading each result out rather than assuming it:
../../content/docs.././src/objects/index.js.../weirdfoo../barimport { x } from '../config'see ../README.md in this projectThe last two are legal inside a scaffolded project and this pattern reddens them.
That is a deliberate trade and it is sound only while the swept population
carries no climbing path at all, which is measured at zero on both sides today.
It is not a property of the pattern — nothing in the regex distinguishes an
escaping climb from an internal one; only the population does.
So there are two conditions to re-read this on, and an earlier revision of this
PR recorded only the first — the narrower scope being exactly the defect class this
card exists to stop, one layer up:
in-repoplacement, which emitsrootTsconfigExtends; ornested deeply enough to reach back up toward its own project root. This needs
no placement change at all and is the likelier of the two.
In either case the red lands on correctly-shaped, legal text, and the fix is the
depth judgement — never an exemption, and never a widened pattern. Both files'
comments now carry this in full.
Firing controls — each pattern proved able to fail
A pattern that matches nothing is indistinguishable from a pattern that is broken.
For each new entry, a violating string was injected into the real population, the
pin was run, and the restore was proved (blob hash back to the HEAD blob and
git diff HEADempty), never assumed.create.tsREADME literalcreate:plugin/README.md cites a path that climbs out of the scaffolded project ("../../content/docs")create.tsREADME literalcreate:plugin/README.md cites a reference to the ObjectStack repo as an unlinked location ("ObjectStack framework repo")templates/blank/README.mdblank/README.md cites a path that climbs out of the scaffolded project ("../../content/docs")create.tsREADME literalcreate:plugin/README.md cites an ADR identifier ("ADR-0999")Leg D is the card's own positive control, re-run. The original was measured at
e37456eb03againstcreate:example/objectstack.config.ts, a file that no longerexists; re-running it against the surviving template re-establishes that the swept
file is genuinely in the population on this tree, separately from whether the new
vocabulary works.
Each leg: exit 1 while mutated, exactly one probe line on disk, blob hash differing
from HEAD, then blob hash back to HEAD and an empty
git diff HEAD. No mutationsurvives; nothing was left behind for a later run to inherit.
Verification
pnpm --filter @objectstack/cli exec vitest run --project unit— 189 files, 2624tests, all pass. Only the
unittier is owed locally: the diff touches nointegration-tier file and no spawn entry point. The
integrationtier is declaredto CI.
pnpm --filter create-objectstack test— 16 files, 203 tests, all pass.pnpm --filter @objectstack/cli --filter create-objectstack typecheck— pass. Bothedited files were proved to be in a tsc program with
--listFiles, not assumed:the create-objectstack pin under its
tsconfig.json, the cli pin undertsconfig.test.json(the second half of that package'stypecheckscript).scripts/pm/dispatch-gates.mjs --commandsfrom the realchangeset, then reconciled — 50 derived, 50 run, 0 unrun. 48 green. The two
non-zero results are both the gates' own exit 3,
PREREQUISITE NOT MET, whicheach gate's text states is "NOT a pass" and measures nothing:
check:dual-build-cjs-loads(needs a whole-repo build; only the affected closure wasbuilt here) and
check:type-check-debt(its re-measure leg exhausted the 4 GB heap onthis shared container). Both are declared to CI, which builds everything.
pnpm lint— the whole repo, exit 0, at64a6304081. Not narrowed, so nonarrowing argument is needed.
An earlier run showed two cli suites failing; that was the same prerequisite class
(
packages/clihad nodist/, which those pins refuse to proceed without) and clearedon a real build. It is recorded here so the number is not read as a flake.
Clause-②: no
Re-derived from the delivered diff rather than copied from the claim: the diff is two
*.test.tsfiles, neither underpackages/spec. The contract surface ispackages/spec,and the widening tells explicitly exclude test files and paths off that surface — no
schema key, closed-set member, published export or registry entry moves.
skip-changeset — measured, not guessed
Built both packages, then
npm pack --dry-run --jsonand searched every packed path(not
files[], which is not the evidence):@objectstack/cliScaffoldPlacementin 1 packed filecreate-objectstackobjectstackin 16 packed filesBoth controls fire, both added symbols reach zero packed paths, and neither tarball packs
a test file at all. Nothing published moves.
Docs drift: swept by hand, because the bot declined rather than passed
The docs-drift bot returned no opinion on this PR, and that is not a clean bill.
Its words: "Nothing in this diff resolved to a documentable surface (no symbol, route
or SDK anchor derived from 0 changed package(s)), so this run has no opinion
about the docs." The 0 is definitional, not a reading of the tree — a test-only
diff resolves to no documentable surface, so the tool derives no anchors and declines
to answer. Its
diffBasedoes match this PR's base sha, so the tree was never theproblem. ⛔ This must not be recorded as "docs drift: clean".
Swept
content/by hand instead (441 markdown/JSON files), asking whether any pageenumerates what the scaffold-comment pins check or which references a scaffolded
project's comments may carry — a page enumerating the vocabulary would now be short
by one pattern on both sides, or two on the cli side.
Positive controls first, so a zero is a reading and not a silent miss:
ADR-0defineStackscaffoldscaffolded projectos initos createTarget probes, all zero:
MONOREPO_ONLY,monorepo-only,starter-comments-self-contained,init-template-comments-self-contained,unfollowable,ADR identifier,template comment,comments that ship,issue number,followable.self-containedmatched 11 files andscaffolded project6; both sets were readout, not counted. None states what the pins check. The nearest neighbours describe
the emitted
tsconfig.jsonbeing self-contained (protocol/kernel/plugin-spec.mdx,protocol/kernel/index.mdx,deployment/cli.mdx) — that is PR #15535's placementfact, not the comment-vocabulary fact, and this diff changes no emitted output, so
those pages stay accurate. The rest describe what a scaffolded project ships or
wires (executors, the skills bundle, npm scripts).
⇒ No documentation page is short by this change; nothing under
content/needsediting.
content/docs/releases/v17.mdxappeared in the sweep and was readonly — release-owned, never edited from a code PR.
Acceptance notes
no issue tracker and none of this repo's scripts"). With six patterns that enumeration
is narrower than the vocabulary, though its prescription still reads correctly for an
escaping path. Left alone to keep the diff to the vocabulary; noted, not filed.
create.ts's--in-repoflagdescription — CLI help text printed to whoever runs the command, never written into a
scaffolded project, so it is outside this pin's population by the pin's own population
rule. Deliberately not touched; the reason is now recorded in the comment beside the
pattern so a future reader does not "fix" it.
Generated by Claude Code