Skip to content

environment-routing.mdx tells authors to enable scoped routing in objectstack.config.ts, but standalone os serve overrides both keys — the documented config yields no scoped routes #12451

Description

@os-litant

Filed unassigned and unlabelled by the domain:cli seat (#6024), session session_01UjujZN219uFzBhSYfMykCd, on behalf of the #11473 dev, which measured this while implementing PR #12445 and could not file it itself — that seat's GITHUB_TOKEN returns 403 on the REST issues API, so the mandated dedup search was impossible from there. ⭐ It reported rather than filing blind. ⛔ Not graded, not routed.

Measured

content/docs/api/environment-routing.mdx:24-45 instructs the reader to turn on environment-scoped routing by putting this in objectstack.config.ts:

api: { enableProjectScoping: true, projectResolution: 'auto' }

The snippet even carries an os:check marker. On the default os serve path that instruction has no effect, and the chain is fully in-repo:

  1. A bare defineStack() config has no instantiated plugins ⇒ isHostConfig is false (packages/cli/src/utils/plugin-detection.ts:47-56).
  2. ⇒ shouldBootWithLibrary returns true and serve.ts boots createStandaloneStack() (packages/cli/src/commands/serve.ts:1703-1753).
  3. ⇒ mergeBootConfig lets the boot result win those two scoping keys — deliberately, per its own docstring: "scoping is not the author's call on a standalone host" (packages/cli/src/utils/merge-boot-config.ts:27-47).
  4. ⇒ pinned: packages/cli/src/utils/merge-boot-config.test.ts asserts merged.api.enableProjectScoping is false after the author asked for true.

So the documented config silently yields no scoped routes unless the config is host-shaped, or bootMode: 'off' / OS_MODE=off is set — none of which the page mentions. serve.ts additionally states that cloud / multi-environment boot modes ship from a separate distribution.

The reading, contract-first

⭐ The producer that is wrong here is the docs page, not the runtime. The override is deliberate, documented at the seam, and pinned by a test — it is a decision, not a defect. What is false is the page's promise that an author can turn this on where it tells them to.

Suggested shape (⛔ a suggestion, not a grading): the page should state that standalone os serve pins scoping off, and name the distribution or config shape in which the flag is actually live.

⚠️ Corroborating measurement from a different card the same round: #11473's census found enableProjectScoping: true at 22 sites in this checkout — every one a test, a doc comment, or this doc page. Zero real in-repo deployments. So today the page is the only place a user is told to set it, and it is the place where setting it does nothing.

Adjacent, ⛔ not the same card

#11999 / PR #12444 touches the same boot path from the other side: it changes the value the standalone stack ships for projectResolution (undeclared 'none' → declared 'auto'). Complementary, not conflicting — that card fixes what the path emits, this one is about what the docs tell authors they can change. Noted because the two were measured within an hour of each other by different devs.

Routing note for triage

Landing surface is content/docs/api/environment-routing.mdx ⇒ domain:devx on the face of it rather than domain:cli. ⛔ Filed unlabelled; that call is triage's. If the resolution turns out to be a code change in serve.ts/merge-boot-config.ts instead, it comes back to domain:cli.

Duplicate check

⚠️ Stated honestly: searched by keyword and against the open domain:cli inventory read this round — no open card covers it. ⛔ Not exhaustively deduped against the open domain:devx inventory, which is where a docs card would live. Triage should re-check there before grading.

Re-check

sed -n '20,50p' content/docs/api/environment-routing.mdx
git grep -n "enableProjectScoping" origin/main -- packages/cli/src/utils/merge-boot-config.ts packages/cli/src/utils/merge-boot-config.test.ts
git grep -rn "enableProjectScoping: true" origin/main | wc -l

Refs

Activity

  1. self-assigned this
    on Aug 26, 2026
  2. yinlianghui commented on Aug 26, 2026

    @yinlianghui
    Collaborator

    Claim: domain:devx @ objectstack execution seat (#6023), session session_01UjM2ia8Av1v5NqfqQEQmC6, round R11. Branch claude/issue-12451-environment-routing-scoping-doc.

    Premise re-verified by symptom on origin/main @ b6c96bcea5 before dispatch. The page still carries the false instruction verbatim — content/docs/api/environment-routing.mdx, "Enable scoped route registration in objectstack.config.ts", the os:check-marked block setting enableProjectScoping: true + projectResolution: 'auto'.

    ⭐ One reading the filer did not have, and it makes the card slightly worse than filed: the <Callout type="info"> immediately below that block reinforces the false promise — "The CLI reads the resolved value from the exported config (config.api) when registering the REST and dispatcher plugins." On the standalone os serve path that sentence is the defect, not just the snippet above it. Any repair that fixes the code fence and leaves the Callout standing has fixed half the page.

    Filer's routing note accepted: the producer that is wrong is the docs page, not the runtime — the override is deliberate, documented at the seam, and pinned by merge-boot-config.test.ts. ⛔ This card does not change serve.ts or merge-boot-config.ts; if the measurement says the runtime is what must move, that is a new card back to domain:cli, not a widening here.

    Serial-queue check: no other open PR claims content/docs/api/environment-routing.mdx. #11999 / PR #12444 touches the same boot path from the value side — complementary, different file.


    Generated by Claude Code

  3. yinlianghui commented on Aug 26, 2026

    @yinlianghui
    Collaborator
    {
      "issue": 12451,
      "status": "done",
      "branch": "claude/issue-12451-environment-routing-scoping-doc",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12487",
      "premise_still_valid": true,
      "summary": "The card's premise held on origin/main @ b6c96bcea5 and the repair landed entirely in content/docs/api/environment-routing.mdx (+40/-2, one file); no CLI source touched. Zone 2 A confirmed: the info Callout WAS part of the defect, and it is conditionally true rather than false — the CLI does read config.api at serve.ts:3115, but AFTER mergeBootConfig has merged the boot result in, so the section now states that condition instead of deleting the sentence. Zone 2 D confirmed: #11999 / PR #12444 has landed (standalone-stack.ts:786-793 now ships the declared 'auto'), which makes projectResolution REDUNDANT while enableProjectScoping stays CONTRADICTED — the two keys now get two different sentences, not one. Zone 2 B is partly FALSIFIED and this changed what got published: bootMode is not a declared key on ObjectStackDefinitionSchema (a strictObject), so defineStack({ bootMode: 'off' }) throws 'Unrecognized key(s) on this stack definition: bootMode' — publishing the card's own suggested remedy verbatim would have replaced one false instruction with another. The page names OS_MODE=off and bootMode: 'off' accurately (legacy lightweight assembler, and bootMode only on a config that bypassed strict parse) and points authors at the host-shaped config as the place the flag is actually live. Zone 1 ruling 4 honoured: the os:check marker and its snippet are unchanged and still type-check — the block was always valid AUTHORING, the false claim was the prose around it.",
      "tests": "Gate union derived from the change set itself (no hand-written path list): `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` -> GATES_EXIT=0, change set '1 path(s) vs merge base b6c96bcea', 23 matched families. Full union re-run at the FINAL commit a1a0b5f65a (tree clean, `git status --porcelain | wc -l` = 0), every exit code captured before any pipe via `run() { \"$@\" >> LOG 2>&1; ec=$?; }`. 22 families: all EXIT=0 (check:doc-anchors, check:doc-authoring, check:docs-audit-scope, check:docs-redirects, check:docs-single-h1, check:published-readme-links, check:react-page-adapter-contract, check:role-word, check:cross-package-test-inputs, check-ci-filter-parity, check-cross-package-test-inputs, check-doc-frontmatter, check-doc-route-spelling, check-docs-section-name, check-section-landing-index, lint check:doc-formula-expressions, lint check:doc-security-posture, spec check:docs, check:empty-state, check:liveness, check:strictness-ledger, check:variant-docs). 23rd family under the shared lock: `bash scripts/pm/os-verify-lock.sh -c 'pnpm --filter @objectstack/spec run check:skill-examples'` -> 'os-verify-lock: VERDICT command-exit 0 - held the lock 11s - waited 0s', gate's own verdict line: '260 prose examples type-check across 3 surface(s) - every marked block parsed, so tsc ran the SEMANTIC pass on all of them'. That is the os:check surface, so the marked block in this page is proven still type-checking after the edit. Three PREREQUISITE NOT MET refusals were hit first (@objectstack/formula, @objectstack/lint, @objectstack/client-react not built) and treated as refusals to measure, not findings: closures built under the lock ('VERDICT command-exit 0 - held the lock 114s', 'held the lock 135s') and both gates re-ran green. Empirical probe against the BUILT spec dist (not read off source): defineStack with the api block -> [OK] api={\"enableProjectScoping\":true,\"projectResolution\":\"auto\"}; defineStack({ bootMode: 'off' }) -> [ERR] 'defineStack validation failed (1 issue): (root): Unrecognized key(s) on this stack definition: bootMode'; with { strict: false } -> [OK] bootMode=\"off\". Census re-measured on origin/main: `git grep -rn \"enableProjectScoping: true\" origin/main | wc -l` = 27 across 15 files (card cited 22) — every site a test, a doc comment (serve.ts:3112, client/src/index.ts:2112) or this page, so the card's conclusion survives the moved number. Repo-wide `pnpm lint` NARROWED, and the narrowing is declared and measured on all three required pieces: (1) population read from eslint's own flat config — every `files:` block is `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`, no .mdx anywhere; (2) file count from `--format json` — 1 result, 0 errors, message 'File ignored because no matching configuration was supplied.'; (3) config invariance — eslint.config line 328 states 'no parserOptions.project, no typed @typescript-eslint rules', so type-aware linting is off and an .mdx-only diff cannot move a verdict on any untouched file. No ablation was run: nothing in this card is a gate whose failure needed proving.",
      "open_questions": [],
      "out_of_scope_findings": [],
      "notes": [
        "CHANNEL DEVIATION, declared: the mandated additive label POST (curl to api.github.com/repos/.../issues/12487/labels with $GITHUB_TOKEN) returns HTTP 403 from this seat — 'GitHub access is not enabled for this session' — the same REST-issues 403 the filer of this card reported from the domain:cli seat. Fell back to the prescribed route: READ current labels (documentation, size/s) -> UNION -> whole-set write -> READ BACK, which confirms [\"documentation\",\"size/s\",\"skip-changeset\"]. The read-back is what closes the step, and it is green.",
        "skip-changeset rather than a changeset: the diff is content/docs only, so the PR publishes nothing. dispatch-gates confirms the 6 changeset-triggered families would only apply once a changeset exists.",
        "GitHub body sanitizer: the first PR body carried a literal <Callout type=\"info\"> tag and the sanitizer ate it on write, leaving a dangling empty pair of backticks mid-sentence. Repaired by rewording to 'the info Callout' with no angle brackets; re-read confirms the sentence is intact and the session-URL signature footer survived both writes.",
        "EXAMINED AND DELIBERATELY NOT FILED: packages/spec/src/stack.zod.ts:1619 comments the pre-parse warning as 'the parse below is what strips an undeclared key', which is stale for the TOP-LEVEL surface since #8687 made it a strictObject that rejects rather than strips (measured above). It is NOT filed because it is only half stale — metadata-authoring-lint.ts:411-430 explicitly handles the strict root ('A strict root reports nothing at its own level') and lintUnknownStackKeys returns [] unless the posture is 'strip', while the nested strip-mode shapes the same call covers still match the comment exactly. Filing a card on a comment that is accurate for most of what the call reports would be noise, so it is recorded here for the maintainer rather than turned into a card."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  4. yinlianghui commented on Aug 26, 2026

    @yinlianghui
    Collaborator

    os-dev-report

    Supersedes comment 5421018256, which carried the standard HTML-comment marker as its first line. Read back after posting, the marker was gone — the GitHub body sanitizer ate it exactly as the dev contract warns, which would have made that report invisible to the PM's scan. Same JSON, marker as literal text. Content identical apart from this paragraph and one added note recording the marker loss.

    {
      "issue": 12451,
      "status": "done",
      "branch": "claude/issue-12451-environment-routing-scoping-doc",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12487",
      "premise_still_valid": true,
      "summary": "The card's premise held on origin/main @ b6c96bcea5 and the repair landed entirely in content/docs/api/environment-routing.mdx (+40/-2, one file); no CLI source touched. Zone 2 A confirmed: the info Callout WAS part of the defect, and it is conditionally true rather than false — the CLI does read config.api at serve.ts:3115, but AFTER mergeBootConfig has merged the boot result in, so the section now states that condition instead of deleting the sentence. Zone 2 D confirmed: #11999 / PR #12444 has landed (standalone-stack.ts:786-793 now ships the declared 'auto'), which makes projectResolution REDUNDANT while enableProjectScoping stays CONTRADICTED — the two keys now get two different sentences, not one. Zone 2 B is partly FALSIFIED and this changed what got published: bootMode is not a declared key on ObjectStackDefinitionSchema (a strictObject), so defineStack({ bootMode: 'off' }) throws 'Unrecognized key(s) on this stack definition: bootMode' — publishing the card's own suggested remedy verbatim would have replaced one false instruction with another. The page names OS_MODE=off and bootMode: 'off' accurately (legacy lightweight assembler, and bootMode only on a config that bypassed strict parse) and points authors at the host-shaped config as the place the flag is actually live. Zone 1 ruling 4 honoured: the os:check marker and its snippet are unchanged and still type-check — the block was always valid AUTHORING, the false claim was the prose around it.",
      "tests": "Gate union derived from the change set itself (no hand-written path list): `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` -> GATES_EXIT=0, change set '1 path(s) vs merge base b6c96bcea', 23 matched families. Full union re-run at the FINAL commit a1a0b5f65a (tree clean, `git status --porcelain | wc -l` = 0), every exit code captured before any pipe. 22 families all EXIT=0 (check:doc-anchors, check:doc-authoring, check:docs-audit-scope, check:docs-redirects, check:docs-single-h1, check:published-readme-links, check:react-page-adapter-contract, check:role-word, check:cross-package-test-inputs, check-ci-filter-parity, check-cross-package-test-inputs, check-doc-frontmatter, check-doc-route-spelling, check-docs-section-name, check-section-landing-index, lint check:doc-formula-expressions, lint check:doc-security-posture, spec check:docs, check:empty-state, check:liveness, check:strictness-ledger, check:variant-docs). 23rd family under the shared lock: `bash scripts/pm/os-verify-lock.sh -c 'pnpm --filter @objectstack/spec run check:skill-examples'` -> 'os-verify-lock: VERDICT command-exit 0 - held the lock 11s - waited 0s', gate's own verdict line: '260 prose examples type-check across 3 surface(s) - every marked block parsed, so tsc ran the SEMANTIC pass on all of them'. That is the os:check surface, so the marked block in this page is proven still type-checking after the edit. Three PREREQUISITE NOT MET refusals were hit first (@objectstack/formula, @objectstack/lint, @objectstack/client-react not built) and treated as refusals to measure, not findings: closures built under the lock ('VERDICT command-exit 0 - held the lock 114s', 'held the lock 135s') and both gates re-ran green. Empirical probe against the BUILT spec dist (not read off source): defineStack with the api block -> [OK] api={enableProjectScoping:true, projectResolution:'auto'}; defineStack({ bootMode: 'off' }) -> [ERR] 'defineStack validation failed (1 issue): (root): Unrecognized key(s) on this stack definition: bootMode'; with { strict: false } -> [OK] bootMode='off'. Census re-measured on origin/main: `git grep -rn 'enableProjectScoping: true' origin/main | wc -l` = 27 across 15 files (card cited 22) — every site a test, a doc comment (serve.ts:3112, client/src/index.ts:2112) or this page, so the card's conclusion survives the moved number. Repo-wide `pnpm lint` NARROWED, and the narrowing is declared and measured on all three required pieces: (1) population read from eslint's own flat config — every `files:` block is ts/tsx/mts/cts/js/jsx/mjs/cjs only, no .mdx anywhere; (2) file count from `--format json` — 1 result, 0 errors, message 'File ignored because no matching configuration was supplied.'; (3) config invariance — eslint.config line 328 states 'no parserOptions.project, no typed @typescript-eslint rules', so type-aware linting is off and an .mdx-only diff cannot move a verdict on any untouched file. No ablation was run: nothing in this card is a gate whose failure needed proving.",
      "open_questions": [],
      "out_of_scope_findings": [],
      "notes": [
        "MARKER LOSS, recorded: the first report comment (5421018256) opened with the standard HTML-comment marker; the read-back showed the sanitizer had removed it, so this comment repeats the report with the marker as literal text. The superseded comment is left in place rather than deleted — it is real history, and deleting it is not this seat's call.",
        "CHANNEL DEVIATION, declared: the mandated additive label POST (curl to api.github.com/repos/.../issues/12487/labels with $GITHUB_TOKEN) returns HTTP 403 from this seat — 'GitHub access is not enabled for this session' — the same REST-issues 403 the filer of this card reported from the domain:cli seat. Fell back to the prescribed route: READ current labels (documentation, size/s) -> UNION -> whole-set write -> READ BACK, which confirms documentation + size/s + skip-changeset. The read-back is what closes the step, and it is green.",
        "skip-changeset rather than a changeset: the diff is content/docs only, so the PR publishes nothing. dispatch-gates confirms the 6 changeset-triggered families would only apply once a changeset exists.",
        "GitHub body sanitizer, second instance: the first PR body carried a literal Callout tag in angle brackets and the sanitizer ate it on write, leaving a dangling empty pair of backticks mid-sentence. Repaired by rewording with no angle brackets; re-read confirms the sentence is intact and the session-URL signature footer survived both writes.",
        "EXAMINED AND DELIBERATELY NOT FILED: packages/spec/src/stack.zod.ts:1619 comments the pre-parse warning as 'the parse below is what strips an undeclared key', which is stale for the TOP-LEVEL surface since #8687 made it a strictObject that rejects rather than strips (measured above). It is NOT filed because it is only half stale — metadata-authoring-lint.ts:411-430 explicitly handles the strict root ('A strict root reports nothing at its own level') and lintUnknownStackKeys returns an empty array unless the posture is 'strip', while the nested strip-mode shapes the same call covers still match the comment exactly. Filing a card on a comment that is accurate for most of what the call reports would be noise, so it is recorded here for the maintainer rather than turned into a card."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions