feat(lint): check:doc-security-posture — gate os:check docs object examples on the os validate security posture - #10930
Conversation
…urity posture (#10618) New check:doc-security-posture in packages/lint: statically evaluates every ObjectSchema.create literal in a {/* os:check */}-marked content/docs block and runs validateSecurityPosture over it — the same gating-tier rule os validate runs — so a docs example that os validate rejects as severity:'error' (SECURITY_OWD_UNSET et al.) turns a gate red instead of shipping. Also fixes the 8 pre-existing instances the gate's first run found on pages outside sweep #10714's audited scope (explicit sharingModel, 'private' per the rule's own recommended default). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw
📓 Docs Drift CheckNothing 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. |
|
Review: ACCEPT (reviewer of record: PM seat domain:spec, session_01B4h3medzvhB9rpfoja9jcw) Verified against GitHub, not the report:
Clause-②: no — repo tooling; no spec acceptance change; Landing: waiting on CI convergence on Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32537777627 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Fixes #10618
check:skill-examplesproves anos:checkdocs block compiles; nothing provedos validateaccepts what the block constructs. #10581 measured the consequence: 22ObjectSchema.create()examples omittedsharingModel— the omission type-checks clean (.optional(), no.default()), whileos validaterejects it asseverity: 'error'(SECURITY_OWD_UNSET). Sweep #10714 fixed those 22 instances; this PR closes the class: a new gate,check:doc-security-posture, runs the same author-time lint ruleos validateruns over everyObjectSchema.createliteral in anos:check-markedcontent/docs/**block.Design judgments (each is mine, per the card; reasoning inline)
1. Which lint rules run:
validateSecurityPosture, error severity only — measured, not assumed.The corpus census (25
.createcalls in 191 marked blocks across 189 files) plus the rule inventory settled the set:validateSecurityPostureis the registry's gating-tier entry for exactly the measured class, and its four error rules that judge a lone object document (security-owd-unset,security-owd-alias,security-external-wider-than-internal,security-controlled-by-parent-no-relation) are all self-contained properties of the literal — no cross-stack reference a single-page example could false-red on. Itswarning/inforules need permission sets / books a doc example never carries and stay structurally silent (permissionSets.length > 0guards).os validaterule set: reference-integrity rules judge a whole stack, and a lone teaching literal legitimately references objects the page does not declare — firing them is the false red that teaches people to add ignores.validateSecurityRoleWord: the repo-levelcheck:role-wordratchet already owns that vocabulary on the docs surface; a second gate adjudicating the same word is a second opinion (Prime Directive Add comprehensive test suite for Zod schema validation #12).validateSecurityPosturefrom@objectstack/lint— the identical registry entryos validate/os compileand the runtime publish gate run. The rule's own docblock blesses pre-parse input ("works both pre- and post-zod-parse"), and none of the keys it reads on this path carries a zod default, so the pre-parse judgment equals the post-parse one.2. How the literal is obtained: static extraction with per-fact conservatism — never execution.
Same choice as the existing gate (
check:skill-examplesrunstsc, executes nothing) and its CEL sibling (check-doc-formula-expressions: "only statically-extractable sources"). The evaluator resolves literals, arrays, nested objects, templates-without-substitution; anything else (aField.text({...})factory call, an identifier) becomes an opaque non-null, non-string sentinel, and each rule verdict is trusted only where its facts are statically known:typeof === 'string');fieldssubtree, so it is suppressed with a printed notice when that subtree is not fully static — aField.masterDetail(...)call must not read as "no relation". (19 of the 22 corpus literals have factory-call fields; this is what keeps the gate at zero false positives without shrinking its population.)Two shapes are refused loudly rather than skipped (absence must be loud): a non-object-literal argument / top-level spread, and a non-static
name(the sys-object exemption needs it). Zero of either in today's corpus.3. How a violation prints.
page.mdx:line object "name" [rule-id], then the rule's ownmessageandhintverbatim (the same textos validateprints), then one paragraph stating the parity:os validateruns this same rule and fails the copied app — fix the example, don't weaken the rule or drop the marker.4. Per-block opt-out: an in-script exemption registry, not a new in-doc marker.
EXEMPT_SITESfollows thecheck-doc-formula-expressionsEXEMPT_EXAMPLEShouse discipline — an entry names the site AND its reason, and is self-invalidating in both directions (stale entry = error, entry on a now-clean site = error; both pinned in the self-test). Born empty: no legitimate teaching example needed one (see the born-green measurement). A new in-doc marker syntax was considered and rejected: it would interact withcheck-skill-examples' orphan-marker guard and MDX comment handling for zero present-day benefit.5. Gate home:
packages/lint/scripts/, not apackages/spec/scripts/sibling — a declared deviation from the claim surface. The verdict lives in@objectstack/lint, andpackages/speccannot import it: lint depends on spec, so a reverse devDependency cycles the turbo build graph. The repo has already ruled where this shape lives —check:doc-formula-expressions' own CI comment: "its home in packages/lint (which owns that verdict)". Wired identically to that precedent: package script (self-test first), CI step in the same post-buildtypecheck-consumersjob, directly beside its CEL sibling. Declared on the issue in a claim-amendment comment before the PR.Born green — with 8 more instances found and fixed (bounded in-class fixes, declared)
The dispatch's born-green premise held for #10581's audited 12 pages (sweep #10714, merged), but the gate's first full run found 8 more
ObjectSchema.createsites missingsharingModelon pages outside that audit's scope — same defect, never measured before:plugins/development.mdx:135greeting_logsharingModel: 'private'protocol/kernel/i18n-standard.mdx:657account'private'protocol/kernel/lifecycle.mdx:482salesforce_account'private'protocol/kernel/plugin-spec.mdx:264account'private'protocol/objectql/schema.mdx:53customer'private'protocol/objectql/schema.mdx:137project'private'protocol/objectql/state-machine.mdx:31purchase_request'private'ui/field-grouping-and-order.mdx:36showcase_contact'private'— mirrors the realshowcase_contact(examples/app-showcase/src/data/objects/contact.object.ts:37)Why fixed here rather than exempted or deferred: exempting live instances of the gate's own class at birth is the "gate that lies" the card forbids; the fix shape is pinned by the merged #10714 sweep (explicit
sharingModeldirectly aftername:;'private'is the rule's own recommended default); no competing claim existed on any of these files (git ls-remotebranches + open-issue search); and the claim surface was amended on #10618 in the same round, before this PR. The two deliberate incorrect-counter-examples inmetadata-driven.mdxalready carrysharingModelpost-sweep, so no opt-out was needed for them either.Full run readout at HEAD
39dcc1a3a1(after the 8 fixes):(22 judged sites = 25
.createcalls minus 3 non-ObjectSchemafactories —App.create,Action.create,Dashboard.create— which carry no OWD posture and are deliberately not read.)Positive control — real corpus, mutation proven on disk
Reverse verification ran from the committed state. Mutation: remove the just-added
sharingModel: 'private',fromplugins/development.mdx(perl -0pi), confirmed on disk by anchored grep counts —sharingModeloccurrences in the file 1 → 0 — never by the editor's exit code. No rebuild is involved in either leg, checked rather than assumed: the gate readscontent/docs/*.mdxfrom source; the only built artifact it consumes (packages/lint/dist) is untouched by the mutation. The gate went red, exit 1 captured before any pipe, naming the exact site:Restore leg:
git restore --source=HEAD --staged --worktree, anchor count back 0 → 1,git status --porcelainclean, gate green again (exit 0, same 22-site verdict line).The self-test (house style, runs first in the package script) pins both directions over the real pipeline: the measured defect at the right page line, the alias value, silence on declared postures / factory-call fields /
sys_objects / unmarked blocks / dynamic values / non-ObjectSchemafactories, the loud spread refusal, the cbp suppression notice, and the exemption ledger's both-direction discipline.Changeset
None owed —
skip-changesetlabel applied, re-derived rather than assumed.@objectstack/lintpublishesfiles: ["dist","README.md","CHANGELOG.md"];scripts/reaches no published surface, and the onlypackage.jsonchange is a dev-onlyscriptsentry. Docs and workflow publish nothing. Same reasoning as PR #10684 (tooling-only) and PR #10714 (docs-only).Gates
Derived with
node scripts/pm/dispatch-gates.mjs(no paths — it read the change set from merge base58563be06): 10 paths, 34 matched families. All run locally at HEAD39dcc1a3a1, every exit captured before any pipe, verdicts from the gates' own lines:Verdict lines rather than bare exit codes, e.g.:
22 ObjectSchema.create example(s) in 191 marked block(s) across 189 docs file(s) carry an os validate-clean security posture(the new gate)227 prose examples type-check against @objectstack/spec— all 8 edited blocks still compile with the added keycheck-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured in 388.2s, 1908 raw tsc error(s) total, none above its recorded number(its plugin-auth "12 below ceiling" surplus note is pre-existing on main;--lowerwould touch a ledger outside this card's surface)check-nul-bytes: OK (scanned 6302 text file(s); no raw ASCII control bytes)doc authoring guard: 389 files clean — no bare metadata literals.check-role-word: OK, no new occurrences of the reserved word.Reasoned additions beyond the derivation (the derived list is a clue, not a spec):
check:skill-examples— 8 of my edits are inside blocks that gate compiles (same reasoning as #10714's run, where dispatch-gates also did not name it);check:nul-bytes— runs on any edit.Narrowings, declared: repo-wide
pnpm lint(eslint . --no-inline-config) is CI's run and was not run here.Out of scope, filed
sharingModel(os validate error); extend check:doc-security-posture to skills/ once fixed #10925 — 4 more instances of the same class in the published skills catalog (objectstack-data/SKILL.mdx3,objectstack-upgrade/SKILL.mdx1) plus the gate-root extension toskills/once they land. Not touched here:skills/**is a governed surface (Prime Directive feat: Comprehensive CRM example demonstrating all ObjectStack protocol features #14), so the fixes cannot ride a code PR; the gate's header records the deferred extension.Generated by Claude Code