Skip to content

feat(lint): canonical-envelope page audit gets a shared home; cloud-connection joins the gated population - #11574

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-11480-envelope-detector-shared-home
Aug 24, 2026
Merged

feat(lint): canonical-envelope page audit gets a shared home; cloud-connection joins the gated population#11574
os-steve merged 1 commit into
mainfrom
claude/issue-11480-envelope-detector-shared-home

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes#11480

Clause-② — this PR stays DRAFT, held for the PM's contract review (it widens @objectstack/lint's published API). Do not flip ready or queue it; the PM lands it.

Why a shared home (Option A, per the PM claim on the card)

The three-door canonical-envelope detector landed package-local in platform-objects (PR #11481, for issue #11255) because a packages/spec test reading platform-objects sources trips check:cross-package-test-inputs — so it could not reach the raw-literal Page a sibling published package ships (packages/cloud-connection/src/cloud-connection-ui.ts:19). Copying the detector per package is the failure mode page-walk.ts's own header records as having already produced one dead rule. So the detector moves to @objectstack/lint beside the walk it is built on, and each owning package runs a thin population test over its own Page exports. Route B (a second copy) re-creates the documented dead-rule hazard; route C leaves the card's stated hazard live (the next predicate ships bare, silently).

Exported API (packages/lint/src/page-envelope-audit.ts)

  • auditPageExpressionEnvelopes(page, pageLabel) — runs the three parse doors (PageSchema / PageComponentSchema / ComponentPropsMap[type]) over one authored page and unions their findings; returns PageEnvelopeAudit carrying findings plus the four door-precondition channels (pageParseError, componentParseErrors, unmappedTypes, unreadableProps) and componentCount, so a door that cannot open is reported rather than reading as clean.
  • renderBareExpressionFindings(findings) — the actionable red an author reads in CI (page, path, authored source, door, fix prescription). Exported because every consuming gate asserts on exactly this rendering.
  • Types: BareExpressionFinding, PageEnvelopeAudit, EnvelopeAuditDoor.
  • collectBare (the single-door lockstep walk) is exported at module level only, NOT from the barrel — it exists so page-envelope-audit.test.ts can prove door necessity by running one door in isolation; consumers always want the three-door union.

The module header carries the full TSDoc: the hazard (a raw-literal page is type-checked, never parsed, so bare predicates reach the wire verbatim and objectui's shape-router sends them to the legacy fail-soft evaluator), the door table with each door's blind spot, the behavioural identification rule (a position is flagged only when the author wrote a string and the parse turned that same string into an envelope — no key names hardcoded), and the precondition-reporting contract. The detector's own unit tests (the five fixture-based negative controls / door-necessity proofs from the platform-objects file) moved with it and run once, at its home.

@objectstack/lint keeps no api-surface / export-origins-style generated registry (checked: packages/lint/scripts/ holds only the two doc-check gates; the barrel pin rule-id-barrel-exports.test.ts covers rule-id constants, which this module does not add) — nothing to regenerate.

platform-objects: refactored, not weakened

canonical-expression-envelopes.test.ts now imports the detector from @objectstack/lint (already source-aliased in its vitest config). Everything the landed gate asserted survives: the export-shape population scan over src/ with the barrel-coverage assert and the ≥3 floor, all four per-page door-precondition tests, the real-page findings assertion, and the shipped-page downgrade control (SysUserDetailPage, deep-cloned, envelope downgraded to bare → red naming slots.alerts[0].visibleWhen). That control is also the reverse-verification surface: ablate the lint module and this file reds. All 22 original test intents survive across the two packages (17 here, 5 at the detector's home).

cloud-connection: joins the population

  • @objectstack/lint devDependency + new vitest.config.ts with the anchored array-form source alias (mirrors platform-objects; anchored because @objectstack/lint ships a second ./runtime subpath — the prefix-matching object form would mangle it to ENOTDIR). check:test-source-alias green; the package's ledger entry is unchanged (the new test's only workspace value import is the aliased lint).
  • New src/canonical-expression-envelopes.test.ts: same export-shape population scan pointed at this package's src/, bidirectional (a declared page nobody audits reds, and an audited page the scan cannot see reds — forcing the : Page annotation convention), floor ≥2; the four door preconditions; the verdict; and downgrade controls injecting a bare predicate into deep clones of BOTH shipped pages (red names page + regions[1].components[0].visibleWhen).
  • Population is 2, not the card's 1: while wiring the scan I found a second raw-literal page shipped by this package — MarketplaceInstalledPage (src/marketplace-ui.ts), served through MARKETPLACE_INSTALLED_UI_BUNDLE.pages, invisible to export-shape discovery because it was never annotated : Page. Bounded in-place fix, same defect class as the card: the declaration is now annotated : Page (type-level only, zero runtime change; sibling CloudConnectionSettingsPage in the same package is the evidence for the shape). Verified: the DTS build reads the annotation (bogus-key injection reds with TS2353 against Page; restored byte-identical), and the one downstream consumer of the bundles (packages/cli/test/platform-page-i18n-parity.test.ts) reads them with optional chaining + any-cast and stays green.
  • Door-3 exemptions, recorded exactly: cloud-connection:panel and marketplace:installed-list are console-registered widget types with no ComponentPropsMap row. The test asserts the unmapped set EQUALS the exemption list per page, and additionally asserts every exempted component authors an EMPTY props bag — the moment a real key lands there, the gate forces the decision (props schema row, or a conscious wider exemption). Both pages author zero expression keys today (measured), so the gate lands green over a real population.

The qa/downstream-contract exclusion from #11255 stands unchanged.

Verification (all runs at commit 1291491a36, the PR head; every exit captured before any pipe; heavy commands serialized through scripts/pm/os-verify-lock.sh)

  • pnpm --filter @objectstack/lint test: 81 files / 2263 passed; typecheck exit 0. New detector suite: 5/5.
  • pnpm --filter @objectstack/platform-objects test: 28 files / 447 passed; typecheck exit 0. Gate file verbose run: 17/17.
  • pnpm --filter @objectstack/cloud-connection test: 26 files / 283 passed (25 pre-existing + the new gate; gate file verbose run: 15/15). Package rebuilds green incl. DTS.
  • Reverse verification (ablation): trap-guarded script neutralized the detector's core comparison in packages/lint/src/page-envelope-audit.ts (marker grep = 1 on disk, original spelling = 0; both consuming suites resolve lint through the vitest SOURCE alias, so the mutation leg needs no rebuild and dist is not on any of these resolution paths). Result: lint detector suite 3 failed, platform-objects gate 1 failed (the downgrade control), cloud-connection gate 2 failed — failure text names page + key path (regions[1].components[0].visibleWhen). Restore proven byte-identical (git hash-object = committed blob 1bf3b779…); all suites re-green.
  • Derived gates (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, derived at 1291491a36): all path-derived and convention-triggered gates run and green — check:cross-package-test-inputs, check:test-source-alias, check:type-source-resolution, check:published-files, check:slot-lookup, check:changeset-gate-self-tests, check:objectui-changeset, check:override-consistency, check:query-options-erasure, check:engine-double-contract, check:where-matcher, check:i18n (after building the CLI closure it requires), check:type-check-coverage, check:type-check-debt --re-measure (32 entries, none above ceiling), check:nul-bytes, adr-0087-registration, changeset-fixed, changeset-no-major, ci-filter-parity, empty-changeset, osv-exemptions, plugin-teardown-shape, affected-docs. Each verdict read from the gate's own OK/failure line, not a piped $?.
  • Downstream consumer sweep (prefix filter = dependents): pnpm --filter '...@objectstack/cloud-connection' --filter '...@objectstack/lint' typecheck — exit 0 across the sweep; plus the CLI's bundle-consuming parity test 5/5.

Changeset

One changeset: @objectstack/lintminor (new public API), @objectstack/cloud-connectionpatch (type-level annotation). platform-objects' diff is tests-only and needs no entry. Not declared-breaking, so no ADR-0087 marker is owed (check:adr-0087-registration green).


Generated by Claude Code

…ck/lint; gate cloud-connection's pages
The three-door detector from platform-objects' canonical-expression-envelopes
gate moves to @objectstack/lint beside page-walk.ts as
auditPageExpressionEnvelopes / renderBareExpressionFindings, so every package
shipping raw-literal Page exports can run the same gate. platform-objects'
gate now consumes the shared export (population scan, preconditions, verdict
and shipped-page downgrade control unchanged); cloud-connection gains a thin
gate over its two shipped pages, with @objectstack/lint devDependency and the
anchored vitest source alias. MarketplaceInstalledPage is declared : Page
(type-level only) so export-shape discovery sees it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9cDbY2NBiVJWYx3BpWfH2
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cloud-connection, @objectstack/lint, touching 20 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-handling-server.mdx(via safeParse (symbol))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via safeParse (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/cloud-connection/vitest.config.ts, packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • 1 cross-cutting symbol(s) contributed no route anchor: safeParse (5 routes)
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 45 of 222 client-bound route-ledger rows — the other 177 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 7 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 7bdb1564a0166f46ff7ac006ca05273daf1cbda6packageMentionDocs.

Which tree this was computed on

This run read content/docs from 3a4592fc903543308c7f5e934446c412d23c38f1 — the merge of head 1291491a361db408df9be1a837b1009cf26f730d into base 7bdb1564a0166f46ff7ac006ca05273daf1cbda6, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3a4592fc903543308c7f5e934446c412d23c38f1 && git checkout 3a4592fc903543308c7f5e934446c412d23c38f1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7bdb1564a0166f46ff7ac006ca05273daf1cbda6 1291491a361db408df9be1a837b1009cf26f730d && git checkout -B drift-repro 7bdb1564a0166f46ff7ac006ca05273daf1cbda6 && git merge --no-ff 1291491a361db408df9be1a837b1009cf26f730d
node scripts/docs-audit/affected-docs.mjs --json 7bdb1564a0166f46ff7ac006ca05273daf1cbda6

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 7bdb1564a0166f46ff7ac006ca05273daf1cbda6 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Aug 24, 2026
@os-steve
os-steve marked this pull request as ready for review August 24, 2026 04:17
@os-steve
os-steve added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit 12e306aAug 24, 2026
33 checks passed
@os-steve
os-steve deleted the claude/issue-11480-envelope-detector-shared-home branch August 24, 2026 06:31
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A raw-literal Page also ships from @objectstack/cloud-connection — the *.page.ts sweep missed it, so #11255's gate does not reach it

2 participants

@os-steve@claude