Skip to content

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 into
mainfrom
claude/issue-15150-monorepo-only-escaping-path
Sep 9, 2026
Merged

test(cli, create-objectstack): give MONOREPO_ONLY a pattern for project-escaping relative paths, and re-equalise the two pins' vocabularies#17018
os-project-manager merged 4 commits into
mainfrom
claude/issue-15150-monorepo-only-escaping-path

Conversation

@claude

@claude claude Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #15150

What this is

Both scaffold-comment pins share one MONOREPO_ONLY vocabulary — patterns for a
reference 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.mjs path, a packages/NAME/ path, the framework's
own name in prose. None matched that same unfollowable reference written
relatively, so a scaffolded README carrying a link to ../../content/docs was
read, 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-objectstack grew to five. The two
vocabularies 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. It
will not, and a reviewer waiting for that red would misread this PR:

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-inputs
judges 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.ts renders per placement. defineTemplate's files getter — the map the
    cli pin reads — is filesFor(DEFAULT_PLACEMENT), i.e. standalone.
  • The one legitimate escaping climb this package can emit, rootTsconfigExtends, is
    produced only on the in-repo branch.

Measured directly, per template:

map escaping-path hits
template.files (what the pin reads) 0
filesFor('standalone') 0
filesFor('in-repo') 1 — tsconfig.json extends three levels up

On the create-objectstack side the same holds for a different reason: every
relative 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 climbing
paths 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:

input matches correct?
../../content/docs yes escapes the project
../ yes escapes the project
./src/objects/index.js no downward, left alone
.../weird no the lookbehind earns its keep
foo../bar no word character before
import { x } from '../config' yes ⚠️ intra-project climb, perfectly legal
see ../README.md in this project yes ⚠️ intra-project climb in prose

The 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:

  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 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 HEAD empty), never assumed.

leg injected into red names
A create.ts README literal create:plugin/README.md cites a path that climbs out of the scaffolded project ("../../content/docs")
B create.ts README literal create:plugin/README.md cites a reference to the ObjectStack repo as an unlinked location ("ObjectStack framework repo")
C templates/blank/README.md blank/README.md cites a path that climbs out of the scaffolded project ("../../content/docs")
D create.ts README literal create: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
e37456eb03 against create:example/objectstack.config.ts, a file that no longer
exists; 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 mutation
survives; nothing was left behind for a later run to inherit.

Verification

  • pnpm --filter @objectstack/cli exec vitest run --project unit — 189 files, 2624
    tests, all pass. Only the unit tier is owed locally: the diff touches no
    integration-tier file and no spawn entry point. The integration tier is declared
    to CI.
  • pnpm --filter create-objectstack test — 16 files, 203 tests, all pass.
  • pnpm --filter @objectstack/cli --filter create-objectstack typecheck — pass. Both
    edited files were proved to be in a tsc program with --listFiles, not assumed:
    the create-objectstack pin under its tsconfig.json, the cli pin under
    tsconfig.test.json (the second half of that package's typecheck script).
  • Gates: derived with scripts/pm/dispatch-gates.mjs --commands from the real
    changeset, 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, which
    each 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 was
    built here) and check:type-check-debt (its re-measure leg exhausted the 4 GB heap on
    this shared container). Both are declared to CI, which builds everything.
  • pnpm lint — the whole repo, exit 0, at 64a6304081. Not narrowed, so no
    narrowing argument is needed.

An earlier run showed two cli suites failing; that was the same prerequisite class
(packages/cli had no dist/, which those pins refuse to proceed without) and cleared
on 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.ts files, neither under packages/spec. The contract surface is packages/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 --json and searched every packed path
(not files[], which is not the evidence):

package packed paths packed test files added symbols found control symbol
@objectstack/cli 505 0 0 ScaffoldPlacement in 1 packed file
create-objectstack 23 0 0 objectstack in 16 packed files

Both 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 diffBase does match this PR's base sha, so the tree was never the
problem. ⛔ This must not be recorded as "docs drift: clean".

Swept content/ by hand instead (441 markdown/JSON files), asking whether any page
enumerates 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:

control pattern files matched
ADR-0 232
defineStack 76
scaffold 25
scaffolded project 6
os init 6
os create 5

Target 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-contained matched 11 files and scaffolded project 6; both sets were read
out
, not counted. None states what the pins check. The nearest neighbours describe
the emitted tsconfig.json being self-contained (protocol/kernel/plugin-spec.mdx,
protocol/kernel/index.mdx, deployment/cli.mdx) — that is PR #15535's placement
fact, 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/ needs
editing.
content/docs/releases/v17.mdx appeared in the sweep and was read
only
— release-owned, never edited from a code PR.

Acceptance notes

  • The assertion-1 failure message enumerates the three original shapes ("ships no ADRs,
    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.
  • The prose pattern's only near-hit on the cli side is create.ts's --in-repo flag
    description — 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

…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
@github-actions github-actions Bot added the size/m label Sep 9, 2026
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json d127f9babce9d483ac4c053194360968e1c7ba0apackageMentionDocs.

@github-actions github-actions Bot added the tests label Sep 9, 2026
… 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
os-project-manager marked this pull request as ready for review September 9, 2026 04:21
@os-project-manager
os-project-manager added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit cc986c9 Sep 9, 2026
43 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-15150-monorepo-only-escaping-path branch September 9, 2026 04:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants