Skip to content

docs(skills): objectstack-data teaches that a credential column is queried only for presence - #27772

Merged
os-zhuang merged 4 commits into
mainfrom
claude/project-thread-laa8t4
Oct 30, 2026
Merged

os-zhuang merged 4 commits into
mainfrom
claude/project-thread-laa8t4

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Fixes #27660
Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:对外发布的 objectstack-data skill 在 rules/security.md 的 secret 示例旁加一条规则:凭据列(secret / password)只能查「有没有值」(null、$null、$exists、$empty),不能做过滤、排序、分组或导入匹配键,搜索也不会扫它;另加一条 eval(按 secret 字段过滤的列表视图,期望改写成「是否有值」)。
  • 为什么改:fix(objectql): extend the credential-column guard from aggregates to filter and sort keys #27477 已让引擎对这些用法返回 400 INVALID_FIELD,但 skill 没教,AI 照着 skill 仍会写出被拒的视图过滤、排序、报表或导入映射。
  • 风险与回滚:只改 skills/** 和一条 @objectstack/skills patch changeset,不动运行时。security.md 的新增靠删同包已有的重复句付账,token 不升(3686/3687)。eval 文件超出上限 93 token(1666/1573),ratchet 门禁为红,这一半需要决定。回滚 = revert 本 PR(两个 commit,eval 在第二个,可单独 revert)。
  • 先看:① skills/objectstack-data/rules/security.md 的 secret 段(规则与示例,以及被删的重复句是否真的重复);② skills/objectstack-data/evals/hooks-security-seeds-datasources.json 的 case 9 与 case 2 / 8 的删减。
  • 两分钟验证:node scripts/check-skills-token-ratchet.mjs,预期 security.md 显示 3686 / 3687 通过,eval 文件显示 1666 / 1573 (+93) 失败。
  • 需要你做的:还欠一次 contract review(Tier H,skills/**),由席位来做;目前不请求批准。eval 超出的 token 由席位带去决定。

What changes

  • skills/objectstack-data/rules/security.md, next to the type: 'secret' example: Query a credential only for presence. A secret / password column is never a filter, sort, group-by or import matchFields key (400 INVALID_FIELD; an import row naming it fails). Search never scans it, so keep it out of searchableFields. The example gains filter: { api_key: { $null: false } } (allowed; also null, $exists, $empty) and a commented-out refused filter: { api_key: 'sk_live_…' }.
  • Paid for by deleting sentences the same package already holds:
    • the secret type's purpose, encryption and masking, which rules/field-types.md's secret row states;
    • the queue-parking aside in the sharing-rule bullet, which SKILL.md § Ownership queues teaches;
    • the four-mechanism list in the external tier bullet, which the choice list just above it names;
    • — NOT permissionSets in a code comment, which the bullet below it states with the refusal text.
  • skills/objectstack-data/evals/hooks-security-seeds-datasources.json: case 9 asks for a vendor list view filtered on a secret api_key starting sk_live and expects a redirect to { api_key: { $null: false } } with the 400 INVALID_FIELD refusal named. Cases 2 and 8 each drop a clause that restated their own must_not_contain assertions.
  • .changeset/27660-skills-credential-presence-only.md: @objectstack/skills patch.

What the rule claims, measured against origin/main 481ade61

  • Filter: refused. rejectCredentialEvaluation (packages/objectql/src/credential-evaluation-guard.ts) judges where (every branch), a { $field } comparand, each aggregations[i].filter and each expand entry's where. It answers INVALID_FIELD / 400 for every caller except a system context.
  • Sort: refused by the same guard (orderBy, and expand entries' orderBy).
  • Group: refused. rejectCredentialAggregation (packages/objectql/src/engine-count-aggregate.ts) refuses a groupBy or aggregation field on a credential column, INVALID_FIELD / 400. This guard has no system exemption.
  • Import match: an upsert / update import whose matchFields names a credential column looks rows up through the guarded read, so each such row is reported failed and is not written. The rule says "an import row naming it fails", not "the import is refused".
  • Search: withheld, not refused. expandSearchOnAst drops credential columns from the scan set, including one an object lists in searchableFields (packages/objectql/src/engine.ts, the collectCredentialFields line in the search expansion). No 400 comes back. assertSearchFieldsAreSearchable refuses an explicit $searchFields override only for internal: true fields, not for credential types. So the rule says "search never scans it" and does not claim a refusal.
  • Allowed: isCredentialPresenceCondition accepts bare null, $null / $exists / $empty with a boolean, $eq: null, $ne: null, and $not over those. The rule names the four the card named.

Tests

No tested behaviour changes, so there is no ablation. Skills token readings (ceil(utf8 bytes / 4)), before → after:

File Lines Tokens Ceiling
skills/objectstack-data/rules/security.md 307 → 309 (+2) 3681 → 3686 3687 ✓
skills/objectstack-data/evals/hooks-security-seeds-datasources.json 85 → 95 (+10) 1573 → 1666 1573 ✗ (+93)
whole objectstack-data/SKILL.md unchanged unchanged —

Whole published bundle (ratcheted, authored): 208076 tokens after, against ceilings summing to 217598. The only SKILL.md that could have moved is objectstack-data/SKILL.md, and it is untouched.

Acceptance notes

  • The eval half is over its ceiling. hooks-security-seeds-datasources.json was at zero headroom on origin/main. The two restating clauses cut here pay 71 bytes, and the new case costs about 440 bytes. Nothing else in the file restates itself without changing what a case expects. There are two ways out. One is the standing ruling 「A:先削重复,余额按实测提高」 (maintainer, 2026-10-27, decision card 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」), which would raise the row in scripts/check-skills-token-ratchet.mjs from 1573 to 1666. That file is outside this PR's declared file surface, so it is not edited here. The other is to drop the second commit and land only the rule. The seat decides which.
  • The card asked to teach "search" as a refused position. The engine withholds rather than refuses (measured above), so the text says "search never scans it" and gives the searchableFields advice the card asked for.

Verification

Gates derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on head 1ad3325, with the exit code recorded before any pipe:

44 commands: 43 derived plus `pnpm check:changeset-fixed` (its roster is under `.changeset/`); one red, the token ratchet
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-doc-route-spelling.mjs --advisory :: exit 0
node scripts/check-doc-route-spelling.mjs --self-test :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-skills-token-ratchet.mjs :: exit 1
node scripts/check-skills-token-ratchet.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:corpus-claim-drift :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:pm-governed-merges :: exit 0
pnpm check:pm-skill-id-lint :: exit 0
pnpm check:published-files :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:role-word :: exit 0
pnpm check:skill-compatibility :: exit 0
pnpm check:skill-frame-sync :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:changeset-fixed :: exit 0

The five gates that first exited 3 (PREREQUISITE NOT MET: no dist/) were re-run after turbo run build --concurrency=2 (115/115 tasks, lock VERDICT command-exit 0), and all five exit 0. --ran reconciliation: 43 derived, 43 run, 0 NOT-MEASURED, 0 UNRUN. Outside the derived list: pnpm --filter @objectstack/spec check:skill-examples exits 0 (274 prose examples type-check).

node scripts/check-skills-token-ratchet.mjs exits 1 with one failing row: hooks-security-seeds-datasources.json is 1666 tokens; the ratchet ceiling is 1573 (over by 93). security.md passes at 3686, ceiling 3687.

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4d1QkPVVzNz3FJqb2iS

…eried only for presence

A `secret` / `password` column is never a filter, sort, group-by or
import matchFields key (400 INVALID_FIELD); the only condition allowed
asks whether it has a value, and search never scans it. Paid for by
folding sentences the package already holds in rules/field-types.md and
SKILL.md; the file stays under its token ceiling.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4d1QkPVVzNz3FJqb2iS
…a secret field

The expected answer redirects to a has-a-value filter and names the
400 INVALID_FIELD refusal. Two existing model answers drop a clause that
restated their own must_not_contain assertions.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4d1QkPVVzNz3FJqb2iS
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tooling labels Oct 29, 2026
…-> 1666, pinned at the measurement

The list-view-on-a-secret-field case costs 447 bytes; two self-restating
clauses (73 bytes) were cut first. The balance is raised under the
standing maintainer ruling recorded in the row's comment block.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4d1QkPVVzNz3FJqb2iS
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Ceiling raise, head f2c61c49: skills/objectstack-data/evals/hooks-security-seeds-datasources.json goes from 1573 to 1666 in scripts/check-skills-token-ratchet.mjs. The new ceiling is pinned at the measurement, with zero headroom.

Authority: a seat decision on this card's open question. It applies the standing maintainer ruling of 2026-10-27 on the decision card 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」, tap A, quoted verbatim and untranslated: 「A:先削重复,余额按实测提高」. This is the same authority the row's earlier 1404 -> 1573 raise (card #26834) quotes. Claim 6509312450 was amended to add the row to this card's file surface.

Arithmetic, counted as ceil(utf8 bytes / 4):

  • Paid down first: two clauses that restated their own case's must_not_contain, 73 bytes (18 units) in total.
    • Case 2's "Permission sets, not the object: " restated permissions: { (33 bytes).
    • Case 8's " — no picklist, counter or insert hook" restated assigned_to / beforeInsert (40 bytes).
    • The PR body's "71 bytes" is a pre-measurement estimate. The measured figure is 73.
  • Case 9 adds 447 bytes (112 units).
  • The file: 6289 bytes on origin/main (1573 tokens, headroom 0), 6663 bytes landed (1666 tokens). The ceiling rises by 93.
  • Check: ceil($(wc -c < skills/objectstack-data/evals/hooks-security-seeds-datasources.json) / 4) == ceil(6663 / 4) holds.

Gates on f2c61c49:

  • node scripts/check-skills-token-ratchet.mjs now exits 0: "84 authored bundle file(s) within their ceilings". The eval file reads 1666 / 1666 and rules/security.md reads 3686 / 3687.
  • The raise adds 19 families to the derived list. All of them exit 0, among them pnpm check:scripts-symbol-anchors, pnpm check:ratchet-remedy-authority and pnpm check:pm-dispatch-gates. The fast tier ran 1937 cases, and that exit was read from the gate's own pass line.
  • --ran reconciliation: 61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN.

This arithmetic is posted as a comment rather than a body edit because os-dev writes a PR body once, at pr_create, and does not PATCH it afterwards. A seat may fold it into the body.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#2 · os-elon-musk · session_016WV4d1QkPVVzNz3FJqb2iS

Contract-review-claim: session_016WV4d1QkPVVzNz3FJqb2iS · head f2c61c4 · 2026-10-29T23:02Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#2 · os-elon-musk · session_016WV4d1QkPVVzNz3FJqb2iS

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f2c61c493590d0c41bffdc55791b16db7b0f1ab9
Local-runs: none
Scope: skills/objectstack-data/evals/hooks-security-seeds-datasources.json, skills/objectstack-data/rules/security.md + scripts/check-skills-token-ratchet.mjs, packages/objectql/src/credential-evaluation-guard.ts, packages/objectql/src/engine-count-aggregate.ts, packages/objectql/src/engine.ts, packages/objectql/src/secret-fields.ts, packages/metadata-protocol/src/protocol-query-validation.ts, packages/core/src/utils/import-runner.ts, packages/spec/src/data/filter-comparand-operators.ts, packages/spec/src/data/filter.zod.ts, packages/spec/src/data/field.zod.ts, packages/spec/src/ui/view.zod.ts, skills/objectstack-data/rules/field-types.md, skills/objectstack-data/SKILL.md, skills/objectstack-data/references/data-hooks.md, skills/objectstack-ui/rules/list-views.md, .changeset/27660-skills-credential-presence-only.md

Inputs: card #27660 (body and all three comments, the dev report 6510309018 included); PR #27772 (body, both comments — the arithmetic comment 6510542306 corrects the body's 71-byte figure to 73 — the file list, and the net diff origin/main...f2c61c49 against the merge base 481ade6; origin/main read at d7625ea); the check-runs on the head. The remainder of the diff outside hits and the cited files is .changeset/27660-skills-credential-presence-only.md alone — the lane seat's ACCEPT at TIER_DEFAULT, read here for ② only. The ratchet row is the gate half of the eval file and is judged with it in ④.

Gate verdicts on the head: all 42 check-runs have concluded — every conclusion is success or a path-filtered skipped; none failed, none pending. The ten required contexts: Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard, Test UI, Live E2E UI success; Lint UI, Type Check UI, Build & E2E UI skipped by path filter. Check Changeset success. The token ratchet is green on this head (it was red on 1ad3325, before the raise commit).

① Derived judgments

  1. Published surface @objectstack/skills (packages/skills, which sync-catalog.mjs fills by copying the skills/** tree whole, so both hits files ship to customer context windows). rules/security.md gains one rule plus a presence example and loses four restating sentences — right (④). evals/hooks-security-seeds-datasources.json gains case 9 and trims the prose of cases 2 and 8 with their assertions byte-identical to main — the trims right, case 9 wrong for the surface its own prompt names (④ M1).
  2. Accept-set: nothing the engine accepts or refuses moves; no spec key, runtime path or metadata type changes. Metadata-protocol: none — right. The skill's claim is exactly as narrow as the enforcement on main — refusals where the engine refuses (filter, sort, group-by, import match), a withhold where it withholds (search) — right by Prime Directive chore: version packages #10's corollary.
  3. Gate ceiling: the CEILINGS row for the eval file rises 1573 → 1666 in scripts/check-skills-token-ratchet.mjs, a shrink-only ratchet raised only under a quoted maintainer ruling, pinned at the measurement with zero headroom — right for this head (④); it must be re-measured with M1.
  4. Governed surface: skills/** is a Tier H register row; this record is the contract-tier review the dispatch named as the compensating control for the model downgrade. No register row, .claude/**, ADR or workflow path is touched — right.
  5. The dev's out-of-scope observation (an explicit $searchFields naming a credential column the object lists in searchableFields is intersected away and the search falls back to the default set, no 400) is a property of fix(objectql): extend the credential-column guard from aggregates to filter and sort keys #27477's design on main, not a change this diff implies — escalated in ③, no action here.

② Semver level

.changeset/27660-skills-credential-presence-only.md: '@objectstack/skills': patch, with Clause-②: no and Metadata-protocol: none as line-initial declarations; the PR body carries the same two lines. The diff publishes documentation inside one released package and nothing else, so a none kind takes patch — right; no skip-changeset, right, since the package publishes. The changeset's Before/Now reads true against main (group-by or min/max key refused by rejectCredentialAggregation; the search scan never reads a credential column). Clause-②: no — right: no capability is widened or narrowed at any consumer; a skill teaching a refusal the engine already enforces is not a narrowing. Check Changeset and check:adr-0087-registration green on the head.

③ Boundary flags

Dev report 6510309018 — five deviations, one open question, one out-of-scope finding:

  1. "Token ratchet stays red on the eval file (+93); the ceiling was not self-raised" — answered: the seat's commit f2c61c4 raises the row under the standing ruling, claim 6509312450 was amended to add the row to the file surface, and Lint & Repo Gates is green on this head. Arithmetic verified in ④.
  2. "The eval case is its own commit 1ad3325 so it can be reverted alone" — noted; the remedy here is to fix the case (M1), not revert it.
  3. "PR carries the card's assignee; os-dev.md rule 2 vs the AGENTS.md Claim paragraph" — AGENTS.md outranks by its own precedence line, and the PR's assignee mirrors the card's — right, no action.
  4. "Cleanup step not run, worktree kept" — housekeeping for the dev's next round on this branch; not a review finding.
  5. "'search' is taught as withheld, not refused" — verified on main (④, search); right, and the card's "search ... key" wording was correctly narrowed to what the engine enforces.

Open question "how to pay the +93 on the eval file" — the seat answered A; the head carries it; the raise equals the measured balance after the pay-down (④).

Out-of-scope finding (explicit $searchFields naming a secret/password column is intersected away, not refused) — read in engine.ts expandSearchOnAst: the engine's own comment calls this "the engine's existing tolerant posture for an unknown requested search field", and the REST door assertSearchFieldsAreSearchable (metadata-protocol) refuses an explicit override only for internal: true. Not this PR's: the rule's "search never scans it" holds either way. Escalated to the seat: whether the #4254 posture (an unhonoured explicit narrowing is a 400, never a silent widening) should extend to credential types on the explicit-override axis is a card against #27477's design under Prime Directive #10, if the seat reads it as a contract violation rather than the documented withhold.

④ Design judgment

skills/objectstack-data/rules/security.md — the rule against the code it cites, on main:

  • Filter key: rejectCredentialEvaluation (packages/objectql/src/credential-evaluation-guard.ts) walks where through every $and/$or/$not branch, every { $field } comparand, each aggregations[i].filter and each expand entry's where against the target object, and throws INVALID_FIELD with status/httpStatus 400 for every caller but isSystem === true; it runs in find, findOne, count, aggregate, update and delete after the array-form lowering and before any driver. A refusal — right.
  • Sort: the same guard on orderBy and expand entries' orderBy. A refusal — right.
  • Group-by: rejectCredentialAggregation (packages/objectql/src/engine-count-aggregate.ts) refuses a groupBy entry or an aggregation field on a credential column, INVALID_FIELD / 400, with no system exemption. A refusal — right.
  • Import matchFields: import-runner.ts findExisting builds where from the row's key-set cell values and calls p.findData with the caller's own context (no system elevation anywhere in the runner), so the guard throws; the per-row catch hands it to toFailedResult, and that row is reported failed and not written. "An import row naming it fails" — right, and correctly not "the import is refused".
  • Search: expandSearchOnAst (packages/objectql/src/engine.ts) removes collectCredentialFields(schema) from the scan set, an author-declared searchableFields entry included, with no 400 (the only 400 on that path is an empty resolved set, emptySearchSetError); assertSearchFieldsAreSearchable refuses an explicit override only for internal: true. "Search never scans it: keep it out of searchableFields" is a withhold phrased as a withhold — nothing is claimed refused that is merely withheld. Right.
  • secret / password: collectCredentialFields (secret-fields.ts) is type === 'secret' || type === 'password', and both are FieldTypeSchema members (field.zod.ts). Right.
  • Allowed shapes: isCredentialPresenceCondition accepts a bare null, $null / $exists / $empty with a boolean operand, $eq/$ne with null, and $not over those. The rule names null, $null, $exists, $empty — a subset, never wider than the enforcement. Right. The example { api_key: { $null: false } } is accepted by the guard and $null is a declared comparand operator (filter-comparand-operators.ts). Valid as an ObjectQL predicate.
  • The four deleted sentences, each read for a surviving home in the package: (1) the code comment's — NOT permissionSets → the bullet two lines below, "permissionSets: is refused at load (Unrecognized key(s) on this stack definition)", holds; (2) the secret type's purpose, ICryptoProvider encrypt-on-write and mask-on-read → rules/field-types.md secret row, "Reversible encrypted-at-rest credential (DB password, API key, token) — encrypted on write via ICryptoProvider, masked on read, fail-closed (ADR-0100)", holds verbatim; (3) "it shares the rule's rows, it parks none — only the record's queue column does that" → SKILL.md § Ownership queues, "A record sits in a queue when owner_id is empty and queue names it", the queue name being the one "a sharing recipient" uses, holds, and the pointer stays in the bullet; (4) the external tier's mechanism list → "the explicit grants above", the preceding section's "Choose by what the rows are keyed on" list (owner scoping, positions on the policy, a membership key, a sharing rule or a manual share, a sharing rule to a queue), holds. Note, no action: "above" now reads across a section boundary.
  • Token: 14722 → 14741 bytes, 3681 → 3686, ceiling 3687 unchanged — verified by byte count on both trees. The new fenced block carries no os:check marker (as its requiredPermissions sibling does not), so check:skill-examples does not type-check it; the edited os:check block still passes (gate green).
  • S1 — should-fix. The example line filter: { api_key: { $null: false } } names no surface. In this bundle where: is the api spelling (data-hooks.md, find({ where })), filter: {…} object-form is the hook's query.filter and the rollup-field key, and the view surface takes filter: [{ field, operator, value }]. Dropped under a bare fields: block in a file whose other recipes are object-level keys (tenancy:, requiredPermissions:), it reads as a key on something. One comment naming the surface ("in a where / hook query.filter") fixes it; with M1 (a) it reads consistently. Not a FAIL on its own.

skills/objectstack-data/evals/hooks-security-seeds-datasources.json, judged against the card's eval prompt and the spec on main, with its gate half:

  • Cases 2 and 8: only expected_output moved; assertions are byte-identical to main. Case 2 still says "the rep grant carries readScope: 'own'" and must_not_contain: "permissions: {" holds the cut lead-in's lesson; case 8's must_not_contain: ["assigned_to", "beforeInsert"] holds the cut clause's. Genuine restatement cuts — right, as the ruling orders (先削重复).
  • Case 9 against the bundle's own text: $startsWith occurs nowhere in the bundle's teaching (only inside this assertion); INVALID_FIELD (security.md ×2, SKILL.md ×2) and $null (security.md) are taught as whole tokens. A correct answer quoting the rule does not trip itself.
  • M1 — must-fix. Case 9's prompt asks for "A vendor list view of the rows whose api_key … starts with sk_live", and its expected_output and must_contain pin the ObjectQL predicate { api_key: { $null: false } } as the answer. On main a list view's filter is z.array(ViewFilterRuleSchema) on a strict object (packages/spec/src/ui/view.zod.ts, the filter key and ViewFilterRuleSchema), whose operator enum VIEW_FILTER_OPERATORS spells presence is_null / is_not_null / is_empty / is_not_empty and prefix starts_with; skills/objectstack-ui/rules/list-views.md § Filtering teaches exactly that vocabulary (starts_with, is_not_empty); this skill's own SKILL.md routes "Build views, dashboards, or apps" to objectstack-ui and teaches no view at all (no defineView in the bundle); and the catalog loads whole into a customer session. The view form does reach the guard — filter.zod.ts's alias map lowers starts_with → $startsWith and is_not_null → $null before where is judged — so the rule's refusal holds on the view path. The grading does not: the on-spec correct answer for the surface the prompt names, filter: [{ field: 'api_key', operator: 'is_not_null' }], contains no $null and fails must_contain; the on-spec wrong answer, operator: 'starts_with', contains no $startsWith and is not refused by must_not_contain; and the answer the eval rewards, { api_key: { $null: false } } placed where the prompt puts it, is the shape the view schema refuses. For the one scenario the card ordered the eval to exercise, it passes the shape the spec refuses and fails the shape the spec accepts — the declared-not-enforced shape of Prime Directive chore: version packages #10, on the skill's own test, shipped to customers as an exemplar. Remedy, one of: (a) re-word the prompt to the surface this skill owns — a find or hook query.filter over vendor whose api_key starts with sk_live — keeping the assertions as they are, so $null and $startsWith are the right tokens; or (b) keep "list view" and grade the view dialect (must_contain INVALID_FIELD plus is_not_null or is_not_empty, must_not_contain starts_with), with the rule's example then showing that spelling as well. Either moves the file's bytes, so the ratchet row is re-measured in the same fix and pinned at the new measurement under the same ruling.
  • Ratchet row (scripts/check-skills-token-ratchet.mjs, read with the eval), for this head: arithmetic verified from the two trees — case 2's cut "Permission sets, not the object: " is 33 bytes and case 8's " — no picklist, counter or insert hook" is 40 bytes (73 bytes, 18 units; both present on main, both absent at head); case 9 adds 447 bytes (112 units); 6289 → 6663 bytes, ceil(6663 / 4) = 1666; 1573 → 1666 is +93, exactly the measured balance after the pay-down, zero headroom. The row comment carries the corrected 73 (comment 6510542306 corrects the body's 71). It quotes the standing ruling verbatim and untranslated, 「A:先削重复,余额按实测提高」, naming the decision card 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」, tap A, maintainer ruling 2026-10-27 — the same spelling the row's earlier 1404 → 1573 raise for card skills(objectstack-data): no eval exercises a hook's refusals catalogue, because hooks-security-seeds-datasources.json sits at its token ceiling (follow-up owed by #26170) #26834 and the map's other rows carry — and the PR body quotes the same ruling, which is the ratchet header's evidence bar for a raise. Paid down first, as the ruling orders. Right for this head; re-measure with M1.

Implemented-by: claude/project-thread-laa8t4
Reviewed-by: session_016WV4d1QkPVVzNz3FJqb2iS

VERDICT: FAIL

…name both filter spellings

The case asks for a list view, so it now expects the view rule
`operator: 'is_not_null'` and refuses `starts_with`; the security.md
example labels the `where` / hook spelling and the list-view spelling.
Both ratchet rows move to the new measurement under the standing ruling
quoted in their comment blocks, after paying down a restating clause.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4d1QkPVVzNz3FJqb2iS
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract-review fix for record 6510851915, at head fc274710.

M1, remedy (b): the eval is graded in the list-view dialect. Case 9 keeps its list-view prompt and now expects filter: [{ field: 'api_key', operator: 'is_not_null' }] and a named 400 INVALID_FIELD.

  • Assertions: must_contain is ["INVALID_FIELD", "is_not_null"] and must_not_contain is ["starts_with"].
  • Lowering, checked in packages/spec/src/data/filter.zod.ts: is_not_null lowers to { field: { $null: false } }, which the credential guard accepts as has-a-value. starts_with lowers to $startsWith, which the guard refuses. So the refusal claim holds on the view path.
  • Neither token trips on the bundle's own text. is_not_null is taught by the new security.md example, and starts_with appears nowhere in objectstack-data except this assertion.

S1: the example now labels both spellings. where: { api_key: { $null: false } } is labelled ObjectQL and hook query.filter, and filter: [{ field: 'api_key', operator: 'is_not_null' }] is labelled list view. Two lines added earlier in this PR were dropped:

  • is it set? , which restated the rule's heading;
  • the commented-out refused example, which restated the rule's 400 INVALID_FIELD.

Neither line was on main, so neither is counted as pay-down below.

Ceilings: under the standing maintainer ruling of 2026-10-27, decision card 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」, tap A, verbatim and untranslated 「A:先削重复,余额按实测提高」. Both are counted as ceil(utf8 bytes / 4), pinned at the measurement, with zero headroom.

skills/objectstack-data/evals/hooks-security-seeds-datasources.json: 1573 -> 1674. This replaces the head's 1666.

  • Paid down first: 73 bytes (18 units).
    • Case 2's "Permission sets, not the object: " restated permissions: { (33 bytes).
    • Case 8's " — no picklist, counter or insert hook" restated assigned_to / beforeInsert (40 bytes).
  • Case 9 adds 479 bytes (120 units).
  • The file goes from 6289 to 6695 bytes, so the ceiling rises by 101.
  • Check: ceil($(wc -c < skills/objectstack-data/evals/hooks-security-seeds-datasources.json) / 4) == ceil(6695 / 4).

skills/objectstack-data/rules/security.md: 3687 -> 3698. The S1 example took the file 11 tokens over its old ceiling.

  • Paid down first: 373 bytes (93 units), four passages the package already holds elsewhere.
    • The secret type's purpose, encryption and masking, kept in rules/field-types.md (net 150 bytes).
    • The queue-parking aside, kept in SKILL.md § Ownership queues (91 bytes).
    • The external tier's mechanism list, kept in the choice list above it (net 107 bytes).
    • — NOT permissionSets, kept in the bullet below it (25 bytes).
  • The rule adds 442 bytes (111 units).
  • The file goes from 14722 bytes (3681 tokens, headroom 6) to 14791 bytes (3698 tokens), so the ceiling rises by 11.
  • Check: ceil($(wc -c < skills/objectstack-data/rules/security.md) / 4) == ceil(14791 / 4).

Both rows in scripts/check-skills-token-ratchet.mjs carry this arithmetic. The changeset now says the balance was raised by measurement and shows both spellings.

Gates at fc274710:

  • The ratchet exits 0: "84 authored bundle file(s) within their ceilings", with 1674 / 1674 and 3698 / 3698.
  • pnpm --filter @objectstack/spec check:skill-examples exits 0 (274 prose examples type-check).
  • The 61 derived families all exit 0, pnpm check:pm-dispatch-gates included, with its exit code captured.
  • check-comment-mask-corpus first exited 124 (a 200 s timeout under box contention). Re-run alone, it exits 0 (19039 files, 0 disagree).
  • pnpm check:changeset-fixed exits 0.
  • --ran reconciliation: 61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#2 · os-elon-musk · session_016WV4d1QkPVVzNz3FJqb2iS

Contract-review-claim: session_016WV4d1QkPVVzNz3FJqb2iS · head fc27471 · 2026-10-29T23:47Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#2 · os-elon-musk · session_016WV4d1QkPVVzNz3FJqb2iS

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: fc274710340cb52bc7ae6d94fda9518b804f7642
Local-runs: none
Scope: skills/objectstack-data/evals/hooks-security-seeds-datasources.json, skills/objectstack-data/rules/security.md + scripts/check-skills-token-ratchet.mjs, packages/objectql/src/credential-evaluation-guard.ts, packages/objectql/src/engine-count-aggregate.ts, packages/objectql/src/engine.ts, packages/objectql/src/secret-fields.ts, packages/metadata-protocol/src/protocol-query-validation.ts, packages/core/src/utils/import-runner.ts, packages/spec/src/data/filter-comparand-operators.ts, packages/spec/src/data/filter.zod.ts, packages/spec/src/data/field.zod.ts, packages/spec/src/ui/view.zod.ts, skills/objectstack-data/rules/field-types.md, skills/objectstack-data/SKILL.md, skills/objectstack-data/references/data-hooks.md, skills/objectstack-ui/rules/list-views.md, .changeset/27660-skills-credential-presence-only.md

This record is a delta over record 6510851915 (FAIL on head f2c61c4, one must-fix M1 and one should-fix S1). Inputs: that record; the one commit fc27471 on top of f2c61c4 (git diff f2c61c49..fc274710, four files) and the net diff origin/main...fc274710 for the two hits files; PR #27772's thread (the dev's fix account 6511244281 and the claim 6511257990 are read as accounts, the tree is judged); the check-runs on fc27471. Everything 6510851915 judged right and the delta does not touch is carried, not re-judged; the ③ escalation there stands. The remainder of the diff outside hits and the cited files is .changeset/27660-skills-credential-presence-only.md alone — the lane seat's ACCEPT at TIER_DEFAULT, read here for ② only.

Gate verdicts on fc27471: all 42 check-runs have concluded — 29 success, 13 path-filtered skipped; none failed, none running. The ten required contexts: Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard, Test UI, Live E2E UI success; Lint UI, Type Check UI, Build & E2E UI skipped by path filter. Check Changeset success. The token ratchet is green on both re-measured rows.

① Derived judgments

  1. Published surface @objectstack/skills (the skills/** tree, copied whole by sync-catalog.mjs): rules/security.md now shows the has-a-value check in both spellings with each labelled (where / hook query.filter, and a list view's filter rule); evals/hooks-security-seeds-datasources.json case 9 is graded in the dialect of the surface its prompt names. Both right (④). Nothing else in either file moved since 6510851915.
  2. Accept-set: unchanged — nothing the engine accepts or refuses moves; Metadata-protocol: none right; the skill's claims stay exactly as narrow as the enforcement on main (carried from 6510851915).
  3. Gate ceilings: two CEILINGS rows raised, rules/security.md 3687 → 3698 and the eval file 1573 → 1674 (replacing the previous head's 1666), each under the standing ruling, pinned at the measurement, zero headroom — right (④).
  4. Governed surface: skills/** only, as before; no register row, .claude/**, ADR or workflow path — right.

② Semver level

.changeset/27660-skills-credential-presence-only.md: '@objectstack/skills': patch, Clause-②: no, Metadata-protocol: none, matching the PR body's two lines; the delta rewrites its Now paragraph to the view's is_not_null redirect and names the by-measurement raise of both files — true against the tree. Documentation in one released package → patch — right; Clause-②: no — right (no capability widened or narrowed at any consumer). Check Changeset green.

③ Boundary flags

  • M1 of 6510851915 — fixed by remedy (b), verified in ④.
  • S1 of 6510851915 — fixed, verified in ④.
  • The PR body still carries round-1 figures: "71 bytes", the eval file at 1666 over a 1573 ceiling (+93), security.md at 3686 / 3687, a "两分钟验证" step that expects the ratchet to fail on the eval file, and an acceptance note presenting the raise as an open decision and the ratchet file as unedited. The thread supersedes them (6510542306 corrects 71 to 73; 6511244281 carries both raises; the two rows cite "the raising PR's thread"), the body's gate-read lines (Clause-②, Metadata-protocol, Fixes) are right, and the body does quote the standing ruling verbatim, which is the ratchet header's evidence bar for a raise. Acceptable — not a must-fix: no gate reads those figures, nothing in the tree is wrong, and os-dev writes the body once by rule. Should-fix, for the owning seat before landing: fold the two raises (3687 → 3698, 1573 → 1674, with the 73 / 373-byte pay-downs and the ruling as the authority applied) into the body's Tests table and 速读, and retire the stale verification step and acceptance note — the body is what a maintainer reads first and it currently describes a red gate and an open decision that the head has resolved. A seat PATCH, no commit; read the body back after the write.
  • The out-of-scope escalation in 6510851915 (explicit $searchFields naming a declared-searchable credential column is intersected away, not refused — a possible REST 读路径:searchFields / groupBy / aggregations 指向不存在的字段时被静默降级(#4226 收口后剩下的三条轴) #4254-posture gap in fix(objectql): extend the credential-column guard from aggregates to filter and sort keys #27477's design) stands for the seat; not this PR's.
  • The dev's account 6511244281 names a check-comment-mask-corpus timeout (exit 124) re-run green alone; Lint & Repo Gates is green on the head, which is the verdict that counts.

④ Design judgment

Judged on the delta, against the cited code on main.

M1 — fixed, remedy (b). Case 9 keeps "A vendor list view of the rows whose api_key (a secret field) starts with sk_live." and now expects filter: [{ field: 'api_key', operator: 'is_not_null' }] with 400 INVALID_FIELD named; must_contain: ["INVALID_FIELD", "is_not_null"], must_not_contain: ["starts_with"].

  • Against view.zod.ts: filter on a list view is z.array(ViewFilterRuleSchema); { field: 'api_key', operator: 'is_not_null' } is a complete rule — field and operator present, operator in VIEW_FILTER_OPERATORS, value optional for the unary presence operators. On-spec.
  • Against filter.zod.ts's lowering: is_not_null lowers to { api_key: { $null: false } } and is_not_empty to { api_key: { $empty: false } } — direction from the operator name, boolean operand — which isCredentialPresenceCondition in the credential guard accepts ($null / $empty with a boolean); starts_with lowers through astOperatorLowering to { api_key: { $startsWith: value } }, which the guard refuses with INVALID_FIELD / 400 for every non-system caller. So on the view path the right answer is accepted and the wrong one refused, and the eval now grades exactly those two spellings: the on-spec correct answer passes, the on-spec wrong answer is refused.
  • Against the bundle's own text at fc27471: is_not_null is taught as a whole token (security.md line 229, the list-view line); INVALID_FIELD is taught (security.md line 219, SKILL.md ×2); starts_with occurs nowhere in the bundle but this assertion, and the prompt spells "starts with" without the underscore. A correct answer quoting the rule does not trip itself. Note, no action: as with every sibling case, an answer that names the refused operator in words trips must_not_contain, and an answer choosing is_not_empty over is_not_null fails must_contain — the eval set's convention pins the taught spelling, and the rule's example leads with is_not_null.
  • Cases 2 and 8 unchanged since f2c61c4: prose trims only, assertions byte-identical to main.

S1 — fixed. The example block now reads: a comment naming the other presence spellings (null, $exists, $empty / is_not_empty), then where: { api_key: { $null: false } } labelled "ObjectQL, hook query.filter" (the bundle's api spelling in data-hooks.md find({ where }), and its hook query.filter object form at data-hooks.md line 687), then filter: [{ field: 'api_key', operator: 'is_not_null' }] labelled "list view". Each line names its surface; both are valid on main (the where form accepted by the guard, the rule form by ViewFilterRuleSchema); is_not_empty lowers to an accepted $empty: false. The commented-out refused example and the "set?" aside are dropped — the rule's prose still names 400 INVALID_FIELD, so nothing taught is lost. The block still carries no os:check marker, like its requiredPermissions sibling, so check:skill-examples does not read it; the gate is green. The rule paragraph and the four pay-down passages are untouched by the delta: each surviving home verified in 6510851915 holds (the permissionSets: refusal bullet; rules/field-types.md's secret row; SKILL.md § Ownership queues; the "Choose by what the rows are keyed on" list).

Ratchet rows, read with the files they gate — arithmetic verified from the two trees, main and fc27471:

  • rules/security.md: 14722 → 14791 bytes (+69), ceil(14791 / 4) = 3698; main was 3681 against 3687 (headroom 6), so the ceiling moves +11 to 3698, pinned at the measurement, zero headroom. Pay-down measured passage by passage against main: — NOT permissionSets 45 → 20 (net 25); the secret paragraph 362 → 212 (net 150); the queue-parking aside 159 → 68 (net 91); the external tier's mechanism list 183 → 76 (net 107); 373 bytes in all, every old text present on main and absent at head. The rule and example add 442 bytes (373 + 69). The row comment states these same numbers and quotes the standing ruling verbatim and untranslated, 「A:先削重复,余额按实测提高」, with the decision card 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」, tap A, maintainer ruling 2026-10-27, in the spelling the map's other rows carry. Right.
  • evals/hooks-security-seeds-datasources.json: 6289 → 6695 bytes (+406), ceil(6695 / 4) = 1674; main was 1573 at headroom 0, so the ceiling moves +101 to 1674 (replacing f2c61c4's 1666), pinned at the measurement, zero headroom. Pay-down: case 2's "Permission sets, not the object: " 33 bytes and case 8's " — no picklist, counter or insert hook" 40 bytes, 73 in all, both present on main and absent at head; case 9 adds 479 bytes (406 + 73). The row comment states these numbers, names the new tokens (is_not_null, no starts_with), and quotes the same ruling verbatim. Right.
  • Both raises equal the measured balance after the pay-down the ruling orders first; neither row carries headroom. The PR body's figures for both rows are stale (③), the thread's are the ones the rows cite.

Nothing new in the delta is wrong. No must-fix.

Implemented-by: claude/project-thread-laa8t4
Reviewed-by: session_016WV4d1QkPVVzNz3FJqb2iS

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#2 · os-elon-musk · session_016WV4d1QkPVVzNz3FJqb2iS

维护者速读

  • 改了什么:已发布的 objectstack-data 技能新增一条规则:secret / password 字段永不作过滤、排序、分组或导入匹配键(引擎答 400 INVALID_FIELD,导入那一行失败),搜索也永不扫它;唯一允许的查询是「有没有值」,并给出 ObjectQL(where: { api_key: { $null: false } })与列表视图(filter: [{ field: 'api_key', operator: 'is_not_null' }])两种写法;配一条 eval 用例。
  • 为什么改:fix(objectql): extend the credential-column guard from aggregates to filter and sort keys #27477 已让引擎拒绝这些查询,但技能没教,AI 照技能写出的视图过滤、排序、报表、导入映射会被运行时拒掉。
  • 风险与回滚:纯技能文本与 eval,无运行时变化;revert 即回滚。按 10-27 常设裁决「A:先削重复,余额按实测提高」先删了重复句,余额抬了两行 token 上限:security.md 3687 → 3698、eval 文件 1573 → 1674(PR 正文里的 71 字节 / 1666 是第一轮旧数,以本条与评论 6511244281 为准)。
  • 先看:skills/objectstack-data/rules/security.md 新规则与两种写法示例;eval 第 9 条。
  • 两分钟验证:node scripts/check-skills-token-ratchet.mjs 退出 0(1674/1674、3698/3698)。
  • 需要你做的:受管面 Tier H,请批准本 PR(契约复核 PASS 6511352146,Head-sha: fc274710340cb52bc7ae6d94fda9518b804f7642)。

席位意见:ACCEPT。契约复核首轮 FAIL(eval 问列表视图却按 ObjectQL 写法判分)已按方案 (b) 改正,增量复核 PASS,CI 全绿;批准后由席位落地。

席位核对

  • 形态: one PR, Fixes #27660, 4 files: security.md (+14/−11, net +3 lines, budget ≤ +12), one eval case, two ratchet rows, a @objectstack/skills patch changeset. Tier H (skills/**); far under 3000 lines.
  • 前提实测: security.md on origin/main 481ade61 says nothing about querying a credential column. The engine's refusals were measured on main by the dev and the review (credential guard on filter / $field / sort / expand; rejectCredentialAggregation on group-by; import match fails per row; search withholds, not refuses — taught as such).
  • 门禁 check-runs: head fc274710340cb52bc7ae6d94fda9518b804f7642: 42 check-runs, 29 success, 13 skipped (path-filtered), 0 failed, 0 pending; Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Test UI, Live E2E UI, Check Changeset, Governed Surface Queue Guard green.
  • 接受的偏差: two ratchet rows raised under the standing ruling 2026-10-27 「A:先削重复,余额按实测提高」 after pay-down (73 bytes in the eval, 373 bytes in security.md; arithmetic in the rows and in 6511244281); the PR body keeps round-1 figures, superseded by this record and the thread (the relay cannot patch a PR body without displacing its first lines); scripts/check-skills-token-ratchet.mjs added to the file surface by claim amendment.
  • 范围外去向: an explicit $searchFields naming a credential column is intersected away rather than refused (assertSearchFieldsAreSearchable refuses only internal: true) — documented as withholding by fix(objectql): extend the credential-column guard from aggregates to filter and sort keys #27477's design; held as a seat observation, not carded (a card only if the maintainer reads it as a contract gap). Skills-docs: n/a (Metadata-protocol: none).
  • 落地条款: Tier H: a GOVERNED_APPROVERS approval, then the seat lands it under the maintainer's landing grant for this term (seat thread, 2026-10-29 22:04Z).

@os-zhuang
os-zhuang marked this pull request as ready for review October 30, 2026 01:11
@os-zhuang
os-zhuang enabled auto-merge October 30, 2026 01:11
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 61c3fdd Oct 30, 2026
51 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-laa8t4 branch October 30, 2026 01:37
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/s tooling

Projects

None yet

3 participants