Skip to content

fix(lint)!: refuse a view container whose object names no object - #20253

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-20216-view-container-object-refused
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-20216-view-container-object-refused

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20216
Clause-②: no (narrowing)

Landing order: PR #20228 → PR #20229 → this PR. #20228 has merged. #20229 (the packages[] read in artifactProvidedObjectNames, still in a rework round) edits the same file. This PR does not touch that hunk, its import line, or packages/lint/src/object-graph.ts, and a local git merge-tree of this branch against #20229's head a4b05d6 writes a clean tree. The seat enqueues this PR after #20229 merges, and this branch merges origin/main then.

CI fix round: Test Core (6/6), and a file-surface amendment

  • What went red: packages/cli/test/generate-scaffold-validates.test.ts, "os g 'view' writes a stack os validate accepts", got object-reference-unknown at views[0].object: view container object "probe_thing" ….
  • Why: the harness judged each scaffold in a host stack holding only the collection under test. The view scaffold binds probe_thing, the object os g object probe_thing writes, but no object was in the stack. The new leg refused it correctly: the omission was the harness's, not the template's. Reproduced red locally before the fix (1 failed, 17 passed).
  • Fix (test fixture only): every namesObject generator other than object itself (view, action, flow, app) is now judged beside the object scaffold for the same name, materialized through the same bundle-require loader. A new pin asserts the view's binding and the seeded object's name are the same spelling, and that a non-binding generator (dashboard) and object itself carry no seeded object. ⛔ The rule is not weakened, probe_thing is not special-cased, and no test is skipped.
  • File-surface amendment: packages/cli/test/generate-scaffold-validates.test.ts, test fixture only. No other CLI fixture needed a change (see the runs below).
  • Other generators: action, flow and app also bind probe_thing. They passed before the seeding and still pass with it.

What changed

A view container's own object (ViewSchema.object) is the key the runtime indexes views by (getViewsByObject() / GET /meta/view?object=). Nothing resolved it at authoring time: defineStack's validateCrossReferences reads a container's list.data / form.data bindings, never the container's own key.

  • packages/lint/src/validate-object-references.ts gains a view-container leg beside the relationship-target leg. It uses the same check ladder and the same resolvable set: the stack's objects plus what its packages[] provide.
    • An unresolved unprefixed name is an error, object-reference-unknown at views[N].object.
    • A known platform object passes.
    • A platform-shaped name that nothing registers gets the existing object-reference-unregistered-platform advisory.
  • The refusal is located and carries a prescription:
    • it lists the objects the stack declares (Defined objects: ...);
    • when the bound name is exactly a declared object minus the stack's manifest.namespace prefix, the hint names that object: write "my_app_order_line", not "order_line".
  • It gates like its sibling. It is the same member of the same reference-integrity suite entry, so os validate, os build and os lint all run it at the same tier.
  • Not judged, on purpose:
  • ⛔ No second copy in packages/spec/src/stack.zod.ts.

Measured at the public door (CLI built at this branch)

The scratch project has manifest.namespace: 'my_app', an object my_app_order_line, and a view container with object: 'order_line'.

run os validate os build
leg disabled (ablation, lint rebuilt, marker proven in dist/) exit 0, "Validation passed", nothing about the view not run
this branch, object: 'order_line' exit 1, object-reference-unknown at views[0].object, hint names my_app_order_line exit 1, same rule and path
this branch, object: 'my_app_order_line' (control) exit 0 exit 0

os generate view order_line in the same namespaced project (after PR #20214) writes object: 'my_app_order_line', which passes.

Census of producers (H2)

The instrument is one TypeScript-AST scan at 0d60f88760. It reads examples/**, packages/** (tests and fixtures included), skills/**, and the ts/js code fences in content/docs/** md/mdx (generated references/ excluded). It covers 7242 files and counts object literals that carry a view-container slot (list/form/listViews/formViews). A name resolves when it is declared by an object literal (name + fields) anywhere in the corpus, or when it is a platform-provided object.

tree containers carrying object unresolved
examples 10 0 0
packages 367 105 11
skills 3 0 0
content/docs 21 0 0
  • Control from the same instrument: 94 of the 105 containers carrying object resolve.
  • No example or platform package ships a dangling container. No example container carries object at all; they bind through list.data.object.
  • The 11 unresolved:
    • 10 are non-literal object expressions in code, not stored views: schema/strictObject definitions in view.zod.ts, walkers in validate-translation-references.ts / validate-translatable-sections.ts / protocol.ts, a helper in a rest measurement test, and the parameterised helper in this PR's own test.
    • 1 literal: packages/cli/test/format-zod-union.test.ts (union_probe_obj). That specimen fails schema parse first, which that file asserts as exactly one invalid_union issue. os validate exits at the schema step, so author-time rules never run on it. No pin flips.
  • Pin sweep ①: grepping object-reference-unknown and the rule's message across the repo found no pin on a view container. The pins in packages/cli (artifact-packages.test.ts, build-multi-package-artifact.e2e.test.ts, union-fold-command-parity.test.ts) have fixtures with no container object, so none flips. ②: nothing flipped, so no load-bearing re-pin was owed.

Tests (at 60808317dc unless marked)

  • pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2: 109 files / 4242 tests passed.
  • pnpm --filter @objectstack/lint typecheck: exit 0 (tsc --noEmit plus check:test-typecheck: OK).
  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: 227 files / 3219 tests passed. This includes the fixed generate-scaffold-validates.test.ts (19 of 19).
  • CLI integration tier, the 21 files that reference views or scaffolds (--project integration): 21 files / 196 tests passed.
  • pnpm --filter @objectstack/cli typecheck: exit 0 (check:test-typecheck: OK).
  • Nightly tier (OS_TEST_TIERS=nightly), 7 e2e files on views or scaffolds: 6 files passed. One test failed in generate-agent-retired.e2e.test.ts ("os g object … --dry-run still previews a typed object file"). It expects import * as Data from '@objectstack/spec/data', but the object template writes import { ObjectSchema } since fix(cli): os init and os generate object declare the scaffolded object with ObjectSchema.create #20195. That failure is independent of this PR: neither file differs from the merge base (0 diff lines).
  • New pins in validate-object-references.test.ts (two new describe blocks, appended so they stay clear of fix(lint): refuse a present non-array packages at all five stack.packages readers #20229's hunk):
    • object: 'order_line' in a my_app stack is refused, naming my_app_order_line; the prefixed container is the control;
    • the map form of views is read;
    • a non-prefix miss is refused without the namespace prescription;
    • a container over a packages[] sibling's object passes, and the same package alone is refused (control);
    • sys_user passes and sys_approval_process advises;
    • no views, views: [], and a container with no object stay silent;
    • the finding reaches the gating tier of runAuthoringRules for validate, build and lint.
  • Unit ablation (scripts/ablation-replace.mjs, anchor const bound = strName(view.object);, anchor count 1 to 0, blob 27699c9 to 03f6f47): 8 refusal pins red; 48 green, including both controls (prefixed container, silence). Restored with blob equal to HEAD and git diff HEAD empty. Taken at 9fa1ff4c61. Since then only comment lines changed in src.
  • Door ablation: marker planted, @objectstack/lint rebuilt, ablation-dist-preflight found the marker in 4 built files, and os validate exited 0. Then the restore leg: blob equal to HEAD, lint rebuilt, preflight --absent confirmed the marker gone from all 14 built files with a clean tree, and os validate exited 1 again.

Gates (derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at 60808317dc)

  • 60 families derived (one new: check:cli-test-child-env, green) and all run at 60808317dc. --ran reconciliation with exit codes: 59 run, 1 NOT MEASURED, 0 unrun.
  • The ones this diff actually moves are green: check-adr-0087-registration (disposition not-required (no-migration-prescription) accepted), check-changeset-no-major, check-empty-changeset, check:doc-authoring, check:nul-bytes, check-closing-keyword-parity, check:published-files, check:type-check-coverage, check:test-source-alias.
  • check:type-check-debt now measures green (none above its recorded number).
  • NOT MEASURED: check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET: it needs every package's dist/, and this box built the lint and CLI closures only). Declared to CI.
  • Correction to the first round: I declared the packages/cli suites to CI without running them, and the census missed os g view because its container sits inside a template string the AST scan cannot see. That is the red this round fixes. The CLI unit project now runs in full here.

Changeset

.changeset/20216-view-container-object-refused.md:

  • @objectstack/lint: minor, carrying a BREAKING banner and Clause-②: no (narrowing);
  • a before/after accept-set table;
  • ADR-0087 not-required (no-migration-prescription): nothing authorable moves in spec, and which object an author meant is a fact about their stack, not a mechanical conversion.

The table is a behaviour table (door: FROM exit 0, TO exit 1). My first draft headed it "FROM → TO", and the ADR-0087 gate read that heading as a rewrite prescription and refused the exemption. The heading now says "before and after", and the table carries no rewrite row.

Acceptance notes (not filed)

  • The namespace prescription is on this leg only. The other sites on the same rule (a field reference, action params, dataset base object) could give the same hint for the same missing-prefix miss. I noted it and did not widen it here. Carrier: none.
  • A container whose object and list.data.object name different objects is not judged by any rule. It is out of this card's scope, and I found no instance in the census.

Generated by Claude Code

The container's own `object` is the key getViewsByObject() indexes by,
and nothing resolved it at authoring time. It now joins the
object-reference ladder beside the relationship-target leg, resolved
against the same set (own objects plus packages[]), and names the
namespace-prefixed spelling when that is the one declared.

Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 27, 2026
@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 2 documentable anchor(s).

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

  • content/docs/deployment/cli.mdx (via order_line (literal, a string literal in a comment in validateObjectReferences))
  • content/docs/getting-started/common-patterns.mdx (via order_line (literal, a string literal in a comment in validateObjectReferences))
What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 4 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 443b2f4fdce681f1d3656475c9c72f3ffacfee4c → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7750764a9f6433145424620213c4eca3e6e125e3 — the merge of head 60808317dca031fc55bff733d60cafe762283988 into base 443b2f4fdce681f1d3656475c9c72f3ffacfee4c, 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 7750764a9f6433145424620213c4eca3e6e125e3 && git checkout 7750764a9f6433145424620213c4eca3e6e125e3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 443b2f4fdce681f1d3656475c9c72f3ffacfee4c 60808317dca031fc55bff733d60cafe762283988 && git checkout -B drift-repro 443b2f4fdce681f1d3656475c9c72f3ffacfee4c && git merge --no-ff 60808317dca031fc55bff733d60cafe762283988

node scripts/docs-audit/affected-docs.mjs --json 443b2f4fdce681f1d3656475c9c72f3ffacfee4c

⚠️ 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 443b2f4fdce681f1d3656475c9c72f3ffacfee4c → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…rites

The scaffold-validates harness judged `os g view` in a stack that declared
no object, so once the author-time rules resolve a view container's
`object` the harness's own omission read as the template's defect. Every
`namesObject` generator is now validated beside the object scaffold for
the same name, materialized through the same loader.

Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 18:37
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit ae8e3ca Sep 27, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20216-view-container-object-refused branch September 27, 2026 18:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants