Skip to content

feat(lint)!: refuse a bulk action's confirmText hole over an unbound root, a single-brace token, a hole holding logic and a blank sentence at validate, compile and the publish gate - #25451

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-gtwowq
Oct 23, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-gtwowq

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Fixes #25205

Clause-②: no (narrowing)
Metadata-protocol: narrowed

The refusal half of ruling 6285948976 item 1 on #24798, verbatim: "an unbound root and a single-brace token are refused by name at objectstack validate, compile and the publish gate through the one judge the flow text slots use (automation/flow-text-slot-template.ts)". #24947 (merged as 925fd0772c) typed the slot and shipped every finding as a warning. Under the amended 「甲」 (#6015 6303865025) this narrowing ships now, in 18.x, as its own BREAKING minor, shaped like #25002's page-predicate bounds.

What changes

  • The bulk confirmText pass refuses (packages/lint/src/validate-expressions.ts, bulkConfirmTextIssues). Every finding is now severity: 'error', pushed the way the flow text slots push theirs, and the BULK_CONFIRM_TEXT_WARNING_ONLY suffix is gone. The judge, the walk and the sentences are unchanged. The four refused texts are a hole over a root the def does not bind, a single-brace {x} token, a hole holding logic or an unknown formatter, and a blank sentence.
  • How it reaches each door. objectstack validate and compile fail on an error from validateStackExpressions. The publish gate splits errors from advisories by severity (runtime-gate.ts), so an active view or object write, and a draft promotion, now answer 422 INVALID_METADATA with an expression-invalid issue at bulkActionDefs[N] confirmText. No door code moves.
  • The blank '' arm is kept (packages/spec/src/ui/bulk-action.zod.ts, comment and describe only). The doors refuse a blank sentence in this same change, as they refuse the other three. The parse is what the boot load (computeMetadataDiagnostics in loadMetaFromDb) and the read path judge a stored view with. Dropping the arm would make a stored blank sentence an invalid row on load, a narrowing wider than the three doors the ruling names, and one lint 18.x: page-predicate-over-budget becomes an error, refusing a page component props CEL slot over the platform parse bounds (the refusal half of #24953) #25002's precedent avoids ("Stored pages are not migrated and are not refused on read"). With the arm kept, every bulk confirmText finding is one kind of verdict: a door refusal over text that parses. A stored view keeps loading until its next save, and that save is judged. The new door test (d) pins this for a blank and for an unbound hole.
  • Prose made false by the flip, corrected: the slot's .describe() (it said "as a warning"), the regenerated references (content/docs/references/ui/{bulk-action,component,view}.mdx), the authoring-rules.ts registration comment, a runtime-gate.test.ts comment, and the dogfood expression-conformance.ledger.ts row's enforcement text.
  • Changeset .changeset/25205-bulk-confirm-text-refusal.md: @objectstack/lint and @objectstack/spec minor, both declaration lines, a FROM/TO block, and the ADR-0087 disposition not-required (no-migration-prescription), worded like lint 18.x: page-predicate-over-budget becomes an error, refusing a page component props CEL slot over the platform parse bounds (the refusal half of #24953) #25002's.

Producers, measured first (through the built @objectstack/lint)

  • Shipped corpus at 925fd0772c: the only non-test bulk defs with a confirmText are the 6 in examples/app-showcase/src/ui/views/project.view.ts, all holeless: 0 findings, against 1 for a planted {{ nope }} control. No platform-objects, skills/ or other example module declares one.
  • HotCRM origin/main 1e71d5e (read-only): src/sales/views/account.view.ts declares the 3 bulk sentences ({{count}}, and {{tier}} on the def whose param is tier): 0 findings, against an error for a planted {count} control. The other 9 of the card's 12 are their es-ES / ja-JP / zh-CN translations in locale bundles, which this pass does not read; they use the same two roots. No producer is refused.

Tests (head 6a4c0663)

  • validate-expressions.bulk-confirm-text.test.ts: the pins flip from warning to error, the "no error at all" case inverts to "no warning at all", and the gate block now asserts errors with empty advisories on all three view shapes, plus a single-brace and a blank case per shape.
  • New door block in packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, through the real saveMetaItem: (a) each of the four texts is refused with INVALID_METADATA / 422 and nothing is stored, and the draft promotion is refused too; (b) the door's finding equals runAuthoringRules('build', …)'s on rule, where and message; (c) control: the HotCRM sentence saves; (d) a stored view with a blank or an unbound sentence loads (invalid: 0) and its next save is refused, the row untouched.
  • Ablation (scripts/ablation-replace.mjs, severity: 'error' back to 'warning' in the push helper; blob b86ef1862f5b → 7ab1d8f7b6ad; ablation-dist-preflight.mjs found the marker in 4 built files of packages/lint/dist): lint Tests 11 failed | 6 passed, door Tests 8 failed | 1 passed (the passing one is control (c)). Restore leg: blob back to HEAD, git diff HEAD empty, lint rebuilt, preflight --absent clean, door 9/9 green. A first attempt's preflight spelled the dist marker with single quotes and read ABSENT although the build carried it; that run is not counted, and the run above is the re-take with the emitted spelling.
  • pnpm --filter @objectstack/lint test: 156/156 files, 7196 tests; typecheck exit 0.
  • pnpm --filter @objectstack/metadata-protocol test: 261 passed / 3 skipped files, 21456 passed / 19 skipped; typecheck exit 0.
  • pnpm --filter @objectstack/spec test: 712/712 files, 21657 passed, 1 todo; typecheck exit 0. check:generated green after --fix regenerated the three references.
  • Full dogfood (pnpm --filter @objectstack/dogfood test, owed by the (narrowing) arm) at e644a80a: 290 passed / 3 skipped files, 2494 passed / 18 skipped. 6a4c0663 changes one string in the ledger row (it dropped a tracker id that check:doc-authoring named); expression-conformance.test.ts re-ran there: 8/8.
  • Derived gates: dispatch-gates --commands gives 112 families, the same list at both heads. Results are in the os-dev-report comment.

Acceptance notes

🤖 Generated with Claude Code

https://claude.ai/code/session_01MrzJbTRTRRTVangBSZz6JR

…ingle-brace token, a hole holding logic and a blank sentence at validate, compile and the publish gate

The bulk confirmText pass in validateStackExpressions now emits every
finding as an error, as the flow text slots do, so `objectstack validate`
and `compile` fail and the publish gate answers 422 INVALID_METADATA. The
parse is unchanged: the blank-string arm stays, so a stored view keeps
loading until its next save.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrzJbTRTRRTVangBSZz6JR
…REAKING minor changeset with its ADR-0087 disposition

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrzJbTRTRRTVangBSZz6JR
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/deployment/validating-metadata.mdx (via AUTHORING_RULES (symbol, a top-level const object))
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 — 144 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 2b7c331537ae54c2060b8a64bd025f9ca6fbcdb9 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 87ac96bc6050bc8c1e7389b29d0d998c386b0e9c — the merge of head 6a4c06637b95c94871cec630475fc6c7ebaa5473 into base 2b7c331537ae54c2060b8a64bd025f9ca6fbcdb9, 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 87ac96bc6050bc8c1e7389b29d0d998c386b0e9c && git checkout 87ac96bc6050bc8c1e7389b29d0d998c386b0e9c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2b7c331537ae54c2060b8a64bd025f9ca6fbcdb9 6a4c06637b95c94871cec630475fc6c7ebaa5473 && git checkout -B drift-repro 2b7c331537ae54c2060b8a64bd025f9ca6fbcdb9 && git merge --no-ff 6a4c06637b95c94871cec630475fc6c7ebaa5473

node scripts/docs-audit/affected-docs.mjs --json 2b7c331537ae54c2060b8a64bd025f9ca6fbcdb9

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6a4c06637b95c94871cec630475fc6c7ebaa5473
Local-runs: none
Scope: packages/spec/src/ui/bulk-action.zod.ts + packages/lint/src/validate-expressions.ts, packages/lint/src/runtime-gate.ts (unchanged; the severity split the door claim rests on, read at lines 1174–1175), packages/lint/src/authoring-rules.ts, packages/lint/src/validate-expressions.bulk-confirm-text.test.ts, packages/lint/src/runtime-gate.test.ts, packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, packages/qa/dogfood/test/expression-conformance.ledger.ts, .changeset/25205-bulk-confirm-text-refusal.md; remainder (lane seat's ACCEPT at TIER_DEFAULT, read here only as the describe's projection): content/docs/references/ui/{bulk-action,component,view}.mdx.

Inputs read at 2026-10-23T14:54Z: card #25205 (body + 3 comments: 6355077431 blocked, 6365520957 claim, 6367599529 os-dev-report), PR #25451 (body, 11-file list, git diff origin/main...refs/review/pr-25451), the 42 check-runs on the head, ruling 6285948976 on #24798 (2026-10-20T07:40Z), the amended 「甲」 pointer 6303865025 on #6015 (2026-10-20T23:11Z), the #25002 card and its merged changeset .changeset/25002-page-predicate-over-budget-error.md (landed as 31f0f279), AGENTS.md §3 and scripts/check-adr-0087-registration.mjs (the category definitions), and the producers under examples/, packages/platform-objects, skills/ and HotCRM at /home/claude/hotcrm (4166f21).

① Derived judgments

  1. The parse does not move — right. The diff on bulk-action.zod.ts is a comment and the confirmText .describe() string only. BulkConfirmTextSchema is byte-identical on the head: the templateExpressionInput(...) arm and the z.string().regex(/^\s*$/) blank arm, with the same union error. No accept-set change at BulkActionDefSchema.parse, no JSON Schema shape change, no export added or removed. The three content/docs/references/ui/*.mdx rows are the describe's regeneration and nothing else (Lint & Repo Gates, which carries check:generated, is green).
  2. The door accept set narrows, through existing door code — right. bulkConfirmTextIssues now pushes severity: 'error' for the blank sentence, the closedScopeTemplateRefusal finding (unbound root, single-brace token) and every validateExpression('template', …) error (logic in a hole, unknown formatter). objectstack validate / compile fail on an error from validateStackExpressions; the publish gate's split at runtime-gate.ts:1174–1175 (errors = severity === 'error', advisories otherwise) turns the same finding into 422 INVALID_METADATA on an active view or object write and on publishMetaItem. No door code is edited, so the "same finding at door and build" claim (test (b)) rests on one function. The module-private BULK_CONFIRM_TEXT_WARNING_ONLY suffix constant is removed; it was not exported, so no public surface moves.
  3. Width against ruling 6285948976 item 1 — right. The ruling binds "an unbound root and a single-brace token are refused by name at objectstack validate, compile and the publish gate through the one judge the flow text slots use" and "⛔ no CEL logic inside a hole". The PR refuses four texts, not two: the extra two are the template compile step's own errors (logic in a hole, an unknown formatter) — the one-judge mandate carries them, as the flow text slots already refuse them — and the blank sentence, which card spec + lint 18.x: refuse a bulk confirmText hole over an unbound root or a single-brace token at validate / compile / the publish gate (the refusal half of #24798, under the amended 「甲」) #25205's scope line delegated to the dev ("refuse it as every template slot does, or keep it. Say which and why"). The dev said which (refuse at the doors, keep the parse arm) and why. Judged within the ruling, not beyond it.
  4. Blank '' arm kept in the schema while the doors refuse it — right, and the lint 18.x: page-predicate-over-budget becomes an error, refusing a page component props CEL slot over the platform parse bounds (the refusal half of #24953) #25002 shape. The dev's reason holds in code: loadMetaFromDb runs computeMetadataDiagnostics with the parse and no authoring rule, so dropping the arm would make a stored blank row invalid on boot — a read-path narrowing the ruling does not name and lint 18.x: page-predicate-over-budget becomes an error, refusing a page component props CEL slot over the platform parse bounds (the refusal half of #24953) #25002's "stored pages are not migrated and are not refused on read" avoids. Door test (d) pins the posture for a blank and for an unbound hole (loaded: 1, invalid: 0, then 422 on the next save, row untouched). The residual oddity is cosmetic: the JSON Schema still carries a pattern: ^\s*$ arm that its own description says the doors refuse. That is the documented 18.x posture, the same as the warning-era text said in the other direction; it is not a defect.
  5. Producers — none refused. In-repo, the only bulk defs with a confirmText are the 6 in examples/app-showcase/src/ui/views/project.view.ts; all are holeless (no brace at all), so neither judge step fires. examples/app-todo/src/actions/task.actions.ts:145 and the platform-objects en.objects.generated.ts rows are an action's confirmText (a different slot bulkConfirmTextIssues does not walk); skills/ declares none. HotCRM at the local checkout 4166f21 (the PR measured 1e71d5e; the three sentences are unchanged between them): src/sales/views/account.view.ts — update_tier binds a param named tier and writes {{count}} / {{tier}}; transfer_owner and delete write {{count}} only. All roots bound, no single brace, no logic, none blank: 0 findings. The other 9 of the card's 12 are es-ES / ja-JP / zh-CN translations in locale bundles, which this pass does not read (ruling item 3: HotCRM's 12 strings stand unchanged; the bundle gap is carried by spec: type the remaining person-facing text slots as template slots — confirmText ×3, successMessage / errorMessage ×6, emptyText ×2 (ADR-0032 Decision 2 and 3; the same mechanism as #24798) #24814).
  6. Declaration lines — right. Clause-②: no (narrowing) is BREAKING (AGENTS §3) and matches feat(lint)!:; Metadata-protocol: narrowed fits "a value refused or a requirement tightened" — the accepted set at the three doors tightens, and no key is added or re-meant. Both lines are line-initial in the PR body and in the changeset.
  7. Tests pin the behaviour, not the prose. The lint suite flips four severities and inverts the "no error at all" pin into "no warning at all" with a non-empty result; the gate block now asserts errors with empty advisories on all three view shapes plus single-brace and blank per shape; the metadata-protocol block drives the real saveMetaItem for (a) four refusals and the draft promotion, (b) door = build on rule/where/message, (c) the HotCRM control saves, (d) stored blank/unbound loads then is refused on save. Read against the production diff, each assertion follows from item 2.

② Semver level

③ Boundary flags

Dev flags (os-dev-report 6367599529, deviations), each answered:

  1. Files outside the claim's surface (authoring-rules.ts comment, runtime-gate.test.ts comment, dogfood ledger prose, three regenerated references). Each is prose the flip made false; the references are owed by check:generated. Answered: in scope, correct.
  2. bulk-action.zod.ts edited although the blank arm is kept. Comment and describe only, verified byte-for-byte on the schema expression. Answered.
  3. Door test placed in metadata-protocol's protocol.runtime-authoring-gate.test.ts. The lint 18.x: page-predicate-over-budget becomes an error, refusing a page component props CEL slot over the platform parse bounds (the refusal half of #24953) #25002 precedent's file and the only place the real saveMetaItem is reachable; it reads @objectstack/lint through its built dist/, so it measures only after a lint build — CI builds before Test Core; shards 1, 2, 4, 5, 6 of 6 are green. Answered.
  4. Full dogfood at e644a80a, not the head. The head changes one string in the dogfood ledger row; Dogfood Regression Gate on the head itself is success, which supersedes the local run. Answered.
  5. Branch claude/project-thread-gtwowq, not claude/issue-25205-slug. The claim records that branch; The card this PR closes must claim this branch is success. Answered.

open_questions: none declared. out_of_scope_findings: translated bulk confirmText strings in locale bundles are not judged for holes — pre-existing, carried by #24814, and ruling item 3 keeps HotCRM's strings unchanged; not this PR's debt.

Reviewer flags:

Implemented-by: claude/project-thread-gtwowq
Reviewed-by: contract-review subagent of session_013odhA26dvzy7pQmevdTTvy

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:spec seat 1 (seat post #6017) · os-tesla · session session_013odhA26dvzy7pQmevdTTvy · 2026-10-23T14:55Z

Head 6a4c06637b · card #25205 · claim 6365520957 · report 6367599529 · ruling 6285948976 item 1 on #24798 and the amended 「甲」 6303865025 · contract review 6367779649 (PASS).


Generated by Claude Code

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 protocol:ui size/m tests tooling

Projects

None yet

2 participants