Skip to content

docs(skills): objectstack-data factual sweep (1/3) — field-types, relationships, indexing - #13687

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-13675-data-skill-sweep
Aug 31, 2026
Merged

docs(skills): objectstack-data factual sweep (1/3) — field-types, relationships, indexing#13687
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-13675-data-skill-sweep

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Part of #13675

Program anchor: #13658 — flight ② of the published-skills factual sweep, PR 1 of up to three at whole-file boundaries. The card and the anchor both stay open; only the last PR in this sequence carries a closing keyword.

Method precedent: PR #13577 (the class proof) and PR #13674 (flight ①, the worked example). Session, for durable attribution: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de

Governed surface — human merge, deliberately parked

skills/** is the published, customer-facing surface. This PR is a draft on purpose: auto-merge is not armed, it is not marked ready, no review is requested, and this seat will not arm it. A draft awaiting the maintainer's own merge is the correct terminal state here, not a stuck PR.

File order, and why

skills/objectstack-data is 12 files / 4,935 lines. Flight ①'s calibration says to point execution at surfaces and mapping tables first, so PR 1 takes the three rule files with the highest table density and the most engine-asserting columns: rules/field-types.md (49 type rows, each with a config column), rules/relationships.md (the lookup-vs-master_detail capability matrix), rules/indexing.md (the declaration surface and the "what is automatic" list). SKILL.md follows in PR 2, the hooks reference and the remaining rule files in PR 3.

Non-vacuity control — claims proven TRUE by execution

1. The field-type roster.rules/field-types.md says "49 available options" and enumerates them across ten category tables. Probed against FieldType from @objectstack/spec/data:

FIELD TYPE COUNT = 49
SKILL LISTED COUNT = 49
IN SPEC NOT IN SKILL: []
IN SKILL NOT IN SPEC: []

2. The ADR-0120 unique-scope materialization. The unique-scope table states that unique: 'organization' materializes as (COALESCE(organization_id, '__global__'), …fields). Executed against organizationKeyPartSql from @objectstack/driver-sql:

organizationKeyPartSql => "COALESCE(organization_id, '__global__')"

3. The auto-generated index-name pattern. Stated as idx_{object}_{field1}_{field2}_{...}. Executed against buildIndexName:

buildIndexName(account, [status,created_at], unique=false) => idx_account_status_created_at
buildIndexName(task, [a,b,c], unique=false) => idx_task_a_b_c

4. The autonumber token table, row by row. Executed against parseAutonumberFormat + renderAutonumber at a pinned now of 2026-06-17 UTC:

CASE-{0000} seq=42 => CASE-0042 (table claims CASE-0042)
AD{YYYYMMDD}{0000} seq=1 => AD202606170001 (table claims AD202606170001)
{plan_no}{000} plan_no='PLAN-001' seq=1=> PLAN-001001 (table claims PLAN-001001)
{000} seq=1 / 999 / 1000 => 001 / 999 / 1000 (rule 3: minimum width, grows)
'{a}{b}{000}' scope for (AB,C) => "ABC"
'{a}{b}{000}' scope for (A,BC) => "ABC" (rule 2: shared counter)
'{a}-{b}{000}' scope for (AB,C)/(A,BC) => "AB-C" / "A-BC" (rule 2: delimiter separates)
'CASE-{0000}' scope => "" (fixed prefix = one global counter)

5. Per-type config columns. All 49 rows' config keys were parsed through FieldSchema; every claimed key was accepted and retained on its own type (text → maxLength/minLength/defaultValue, number → min/max/precision/scale, lookup → reference/lookupFilters/multiple/deleteBehavior, slider → min/max/step, vector → dimensions, and so on). Enum columns were pinned by rejection as well as acceptance: returnType refuses 'currency' ("expected one of number|text|boolean|date"), summaryOperations.function refuses 'median', currencyMode refuses anything but dynamic|fixed, and a field-level unique: 'tenant' / 'org' is refused by name with the ADR-0120 message.

The six FALSE claims, and what measured them

落点beforeafter
field-types.md L6-8"an unknown field key is silently stripped at parse (dead metadata), so don't invent config like …""an unknown field key is REFUSED at parse (unrecognized_keys), so don't invent …"
field-types.md L357"an authored vectorConfig is silently stripped (dead metadata)""an authored vectorConfig is refused at parse"
field-types.md L346"Date tokens are case-sensitive and exact ({YYYY}, not {yyyy} or {YYYY-MM}). An unrecognized {...} is emitted literally into the number""Date tokens are case-sensitive{yyyy} parses as a {field} reference and renders empty (rule 1 applies). Only a spelling no field could have — { YYYY }, {YYYY-MM} — is emitted literally."
relationships.md L9lookup row, Roll-ups column — "Not available""Supported via summary"
relationships.md L10master_detail row, Required column — "Always required""Forced only under controlled_by_parent; else lint-warned"
indexing.md L9"- Foreign keys (lookup/master_detail fields)" listed under "ObjectStack automatically creates indexes for" (restated twice more)bullet deleted; "- Field-level uniquenot foreign keys: declare those"

1 and 2. Unknown field keys are refused, not silently stripped

This is the single highest-consequence correction in the PR, because it inverts the cost of a typo for an AI author: "dead metadata" says a misspelled key is harmless, when it actually stops the build. Measured through FieldSchema.safeParse:

REJECT | text + theme (invented) | unrecognized_keys
REJECT | text + rows (invented) | unrecognized_keys
REJECT | file + fileAttachmentConfig (invented) | unrecognized_keys
REJECT | vector + vectorConfig | unrecognized_keys

The error's own text names the retired behaviour the skill was still describing:

Unrecognized key(s) on this field: `theme`. Until this shape was closed these were
dropped silently — the field was still created, minus whatever the key was meant to
constrain, protect or compute.

So the silent-strip sentence was true once and is now exactly backwards. The same pass re-measured the genuinely-retired keys and confirms they are refused with their migration prescription rather than dropped: referenceFilters ("removed in the 16.x line … Use lookupFilters"), encryptionConfig ("pruned in 2026-06 … The real channel is type: 'secret'"), columnName, and conditionalRequired ("removed in @objectstack/spec 17 — use requiredWhen"). Those four sentences in the skill are correct and were left alone.

3. A wrong-cased date token renders EMPTY, not literally

The rule named {yyyy} as an unrecognized spelling and then promised unrecognized spellings appear literally. Both halves cannot hold, and the measurement says the second one fails on exactly the example the first one names:

{ YYYY } tokens=[{"kind":"literal","text":"{ YYYY }"}]
{YYYY-MM} tokens=[{"kind":"literal","text":"{YYYY-MM}"}]
{yyyy} tokens=[{"kind":"field","field":"yyyy"}] <-- a FIELD reference
{YYYY} tokens=[{"kind":"date","pattern":"YYYY"}]
render "{yyyy}-{000}" with no field named yyyy => "-001"
referencedFields("{yyyy}-{000}") => ["yyyy"]
missingFieldValues("{yyyy}-{000}", {}) => ["yyyy"]

{yyyy} is a legal field-name spelling, so the tokenizer reads it as {field_name}; only a spelling no field could carry (a space, a hyphen) falls back to a literal. The practical consequence is worse than a cosmetic difference: the year silently vanishes from the record number, and the format now carries a phantom {field} reference, which puts it under rule 1 of the same list ("Every {field} you interpolate must be required: true" — an empty interpolated field throws at create). The replacement states the tokenizer's actual boundary and points back at rule 1.

4. Roll-up summaries DO work over a lookup child

The capability matrix said roll-ups are "Not available" on lookup and "Supported via summary" only on master_detail. Measured with the engine and the roll-up test's own driver stub — a parent summary over a child whose FK to the parent is a plain lookup:

MEASURED lookup-child rollup => {"opp_total":42,"opp_count":2}
Test Files 1 passed (1)
Tests 1 passed (1)

The spec agrees in its own describe text: summaryOperations.relationshipField is "Auto-detected from the child's lookup/master_detail field referencing this object when omitted". This row was not merely inaccurate, it was pushing authors toward a destructive shape — a reader who wants a roll-up and believes it needs master_detail converts an independent-lifecycle relationship into a cascade-delete one. The two adjacent prose bullets that repeated the claim ("No roll-up aggregations needed" under use lookup, "Need roll-up summaries" under use master_detail) were removed for the same reason; the deletion is also what pays for this PR's additions under the ratchet.

The tree row still reads "Not available" and was left alone: a self-referential FK is neither of the two shapes the auto-detection names, and this flight did not measure it. It is recorded as NOT MEASURABLE rather than corrected on inference.

5. master_detail is required only under controlled_by_parent

"Always required" reads as a platform invariant. Measured through ObjectSchema.create() across sharing models:

ACCEPT | sharingModel=(unset) required=undefined | stored required=false
ACCEPT | sharingModel=private required=false | stored required=false
ACCEPT | sharingModel=controlled_by_parent required=undefined | stored required=true
THROW | sharingModel=controlled_by_parent required=false | "declares `required: false` on a
`master_detail` reference under `sharingModel: 'controlled_by_parent'` — a contradiction
with no honest reading."

The controlled_by_parent rows are the control: they prove the probe reaches real enforcement, and they locate exactly where the invariant does hold. Everywhere else a master_detail defaults to required: false and an explicit false is accepted; the only thing that objects is relationship/master-detail-required, which packages/lint/src/data-model-rules.ts:592 emits at severity: 'warning'. The skill's own lint table (in SKILL.md, PR 2's file) already says "warning", so the two documents disagreed with each other and one of them disagreed with the code.

6. Foreign keys are NOT auto-indexed

indexing.md opened with "ObjectStack automatically creates indexes for: … Foreign keys (lookup/master_detail fields)", and repeated it twice ("Foreign keys — Automatic, but verify" under Always Index; "Index foreign keys — Always (automatic in ObjectStack)" under Best Practices). Measured against the driver's own index oracle, for an object carrying both a lookup and a master_detail and no declarations:

uniqueIndexesFromFields(no unique declared) => []
expectedIndexes(same object) => []
control - the same object with one field-level unique:
uniqueIndexesFromFields(email unique) =>
[{"name":"uniq_task_organization_id_email","columns":["organization_id","email"],
"unique":true,"nullSafeColumns":["organization_id"]}]

The control row is the point: the probe is live, and it returns nothing for foreign keys. SqlDriver.syncTableIndexes (sql-driver.ts:11437) is the single entry point and its body is uniqueIndexesFromFields(...) plus the object's declared indexes — there is no third source, and a repo-wide search for any foreign-key indexing path finds none. relationships.md had it right all along ("Index foreign keys — Always create indexes on lookup/master_detail fields"), so the two files in this very diff contradicted each other; indexing.md was the wrong one.

Written to the token ratchet, not around it

All three files sat at zero headroom, so every correction is paid for inside its own file by genuine deletion — the removed roll-up bullets in relationships.md, the removed foreign-key bullet in indexing.md, and tightened wording in field-types.md. No ceiling was raised; that is a maintainer's call, not a dev's.

readingbeforeafter
rules/field-types.md lines428428
rules/relationships.md lines415413
rules/indexing.md lines376375
objectstack-data/** package lines (all .md)49354932
rules/field-types.md tokens (ceiling 3584)35843578
rules/relationships.md tokens (ceiling 3778)37783773
rules/indexing.md tokens (ceiling 3241)32413240

The ratchet's own verdict lines at head 2d9e2a02c:

✓ check-skills-token-ratchet: skills/objectstack-data/rules/field-types.md is 3578 tokens (ceiling 3584; headroom 6).
✓ check-skills-token-ratchet: skills/objectstack-data/rules/indexing.md is 3240 tokens (ceiling 3241; headroom 1).
✓ check-skills-token-ratchet: skills/objectstack-data/rules/relationships.md is 3773 tokens (ceiling 3778; headroom 5).
✓ check-skills-token-ratchet: 38 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted.

Two nuances were not stuffed into ratcheted text and are recorded for a content/docs/** follow-up instead: (a) the fuller foreign-key indexing guidance the corrected bullet compresses — which FK columns actually earn an index, and the composite shapes that serve filter-plus-sort; (b) user field config — multiple: true and defaultValue: 'current_user' are each legal alone but are refused together, with a located message explaining that the runtime token resolves to one scalar and a multi-value field stores an array. The user row lists both keys and does not claim they compose, so it was left alone.

Gates — derived, not recalled, run locally at head 2d9e2a02c

Family derived from the real diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (13 families; harvested with --commands so both invocation spellings survive). 12 green, 1 NOT MEASURED:

EXIT=0 :: node scripts/check-ci-filter-parity.mjs
EXIT=0 :: node scripts/check-cross-package-test-inputs.mjs
EXIT=0 :: node scripts/check-shard-attestation.mjs
EXIT=0 :: node scripts/check-skills-token-ratchet.mjs
EXIT=3 :: node scripts/check-test-completeness.mjs <-- PREREQUISITE NOT MET
EXIT=0 :: pnpm --filter @objectstack/lint run check:doc-formula-expressions
EXIT=0 :: pnpm check:agent-test-spelling
EXIT=0 :: pnpm check:cross-package-test-inputs
EXIT=0 :: pnpm check:doc-authoring
EXIT=0 :: pnpm check:pm-governed-merges
EXIT=0 :: pnpm check:role-word
EXIT=0 :: pnpm check:skill-compatibility
EXIT=0 :: pnpm check:skill-frame-sync

Exit codes were captured before any pipe (each command redirected to its own file first, then read), so these are the gates' statuses and not a tail's.

check-test-completeness.mjs exits 3 and says why in its own words — "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named". Recorded as NOT MEASURED, not as a red.

check:doc-formula-expressions needed @objectstack/formula and @objectstack/lint built first; built, re-run, and it quotes itself:

✓ check:doc-formula-expressions self-test: 58 cases passed
✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 426 files / 1451 TS blocks judged clean by @objectstack/formula.
✓ check:doc-formula-expressions (spec TSDoc, #6763): 9 @example(s) judged clean across 1092 packages/spec/src files
✓ check:doc-formula-expressions (field-level `*When`, #11407): 14 predicate(s) judged clean; 6 skipped as undeterminable.

check-nul-bytes clean: OK (scanned 7555 text file(s) … no raw ASCII control bytes), plus a direct control-byte scan over the three changed files with no hits.

check:skill-examples is NOT MEASURED for this PR, and vacuous for it either way. It refuses to run until @objectstack/client-react is built ("packages/client-react/dist holds no .d.ts declarations … A verdict now would be computed against a build that no longer matches src") — a prerequisite refusal, not a finding, and the refusal is the gate working as designed. Independently, none of this PR's three files carries an os:check marker (measured: 0, 0, 0), so the gate has nothing to say about this diff. It becomes load-bearing in PR 2 (SKILL.md carries 4) and PR 3 (data-hooks.md carries 1), and will be run there against a real build.

ESLint was not run repo-wide, and the narrowing is measured, not assumed. Three readings: (1) the population read from eslint.config.mjs itself — every files: block names {ts,tsx,mts,cts,js,jsx,mjs,cjs}, and the strings .md and markdown appear nowhere in the config; (2) --format json over the diff's three files returns 3 results, 0 errors, and one message each whose ruleId is null and whose text is "File ignored because no matching configuration was supplied."; (3) type-aware linting is not enabled anywhere — the five parserOptions occurrences are all { ecmaVersion, sourceType } and none is project, which the config's own line 327 states in prose — so this diff cannot move any verdict on an untouched file. Zero of the changed files are in that gate's population in either direction.

No changeset

This PR releases nothing from any package — a pure skills/** documentation correction, the repo's live convention for which is no changeset. skip-changeset is applied.

Calibration numbers for the anchor

Counting rule: one claim per assertable proposition about platform behaviour — each type row's existence, each config key attributed to a type, each mapping-table cell, each numbered rule, each behavioural sentence, each asserted example output. Front-matter, section headings, the field-type decision tree (navigation over types already inventoried) and pure style prescriptions are out of the inventory by the anchor's rule; that is ~158 of these 1,219 lines.

314 behavioral claims inventoried across PR 1's three filesfield-types.md 166, relationships.md 77, indexing.md 71. 6 FALSE (1.9%), ~34 NOT MEASURABLE (concentrated in indexing.md's performance heuristics — the "Max Indexes by table size" table, the selectivity folklore, the "more than 5 levels" tree-depth warning — which assert database-tuning judgement no repo artifact settles), the remainder VERIFIED, with executed probes wherever the claim is behaviour-bearing.

Flight ①'s ~8x table concentration did NOT reproduce here, and the reading depends on how "table" is defined — which is itself worth the anchor's attention:

  • By markdown rendering: 2 of 6 falsehoods sit in an actual pipe table (both relationships.md capability-matrix cells) — 2/~150 in-table (1.3%) against 4/~164 elsewhere (2.4%). That is a mild inversion of flight ①'s finding, not an 8x concentration.
  • By content class (what flight ① was really pointing at — assertions about what surfaces exist and what the engine does with them): 5 of 6 are surface/mapping claims — the unknown-key posture, vectorConfig, the FK auto-index list, and the two matrix cells. Only the autonumber tokenizer rule is a pure behavioural rule.

The actionable version for flights ③–⑫ is therefore the content class, not the markdown syntax: point execution at every "what is automatic / what keys exist / what is retired" assertion regardless of whether it is rendered as a table, a bullet list, or a callout. Three of this flight's six falsehoods were in bullet lists and callouts that a table-shaped search would have walked straight past.

A second pattern worth carrying forward: two of the six were caught by cross-file contradiction inside the same package (indexing.md vs relationships.md on FK indexing; relationships.md vs SKILL.md on master_detail requiredness). Neither is a document-vs-document verdict — both were then settled against the implementation — but the disagreement is a cheap, high-yield way to find candidates before spending a probe.

What is not here, on purpose

  • No implementation was touched. Every disagreement was the document being wrong about settled code, not code contradicting itself. No implementation defect was found worth filing from these three files.
  • Probes were written in the worktree, run, and deleted — they are evidence, not commits. None earned a place as a permanent pin: the roll-up-over-lookup behaviour is already pinned by packages/objectql/src/summary-rollup.test.ts, and the field-key posture by packages/spec/src/data/field.test.ts.
  • references/_index.md is generator-owned (pnpm --filter @objectstack/spec gen:skill-refs) and is not in this PR's diff. Its node_modules/@objectstack/spec/src/** pointers were nevertheless spot-measured against a real npm pack --dry-run --json: the tarball carries 209 src/ entries and every .zod.ts path this package points at is present, so the flight-① "unshipped-path pointer" flavour does not apply to @objectstack/spec (it applied to @objectstack/formula, which ships dist only).
  • The performance heuristics in indexing.md were left alone. They are advice, not platform claims, and rewriting advice is not a factual correction.

Generated by Claude Code

…ationships, indexing
Six measured-false behavioral claims corrected against the implementation:
- field-types.md: an unknown field key is REFUSED at parse (unrecognized_keys),
not "silently stripped"; same for an authored vectorConfig.
- field-types.md: a wrong-cased date token ({yyyy}) parses as a {field}
reference and renders EMPTY — it is not emitted literally.
- relationships.md: roll-up summary fields DO work over a lookup child.
- relationships.md: master_detail required is forced only under
sharingModel: 'controlled_by_parent'; elsewhere it is a lint warning.
- indexing.md: foreign keys are NOT auto-indexed.
Token ratchet: all three files shrink (-6 / -5 / -1 = -12).
Part of #13675
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de
@os-zhuang
os-zhuang marked this pull request as ready for review August 31, 2026 07:15
@os-zhuang
os-zhuang enabled auto-merge August 31, 2026 07:15
@os-zhuang
os-zhuang added this pull request to the merge queueAug 31, 2026
@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Aug 31, 2026
@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

Queue dequeue root-caused — not this PR's failure; re-queueing once.

Failing check: merge-queue CI, job Temporal Conformance (live PG + MySQL), run 33367664966 on gh-readonly-queue/main/pr-13687-90578117f: MySQL 8.0.46 refused CREATE TABLE os11565_narrow with [MY-011825] record size will exceed the maximum allowed size — the narrow contrast fixture of sql-driver-11565-row-byte-budget.test.ts, i.e. the table that exists to FIT.

Why it is not this PR's: (1) this diff is three markdown files under skills/objectstack-data/** — zero code, and the whole queue batch ahead of it (#13674) is markdown too; a live-driver conformance job cannot read either; (2) the test is a week old (7e8393262, 2026-08-24), not newly landed; (3) the same job is green on main's own CI at 2cce3fd30 (06:55Z), and the only commits between that head and the queue base 90578117f are two lint fixes and a docs correction — none touches driver-sql or MySQL DDL emission; (4) the narrow fixture being refused points at service-container environment variance on the freshly initialized MySQL (row-format/charset dependent effective budget), not at any change in what the driver emits.

Actions: re-queued via auto-merge as the one legitimate re-run. If the queue fails on the same test a second time, that is real evidence the fixture's byte budget sits too close to an environment-dependent limit — I will file it for the driver lane with both run ids and park this PR on that fix.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo added this pull request to the merge queueAug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33367664966 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Temporal Conformance (live PG + MySQL) — 失败步骤: Run driver-sql suite against both live servers

    × converges — a second detect finds neither the index nor an orphan column 6123ms
    FAIL src/sql-driver-13056-orphan-shadow-column-cleanup.test.ts > a retired shadow-carried UNIQUE leaves no column behind (#13056) > converges — a second detect finds neither the index nor an orphan c
    ↳ 失败原因: Error: Test timed out in 5000ms.
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • src/sql-driver-13056-orphan-shadow-column-cleanup.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 3 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

Correction to my standing-down comment above, prompted by the triage bot's extraction: the failing test was sql-driver-13056-orphan-shadow-column-cleanup.test.ts (「converges — a second detect finds neither the index nor an orphan column」, Test timed out in 5000ms) — a timeout, not an assertion. The os11565_narrow MySQL error I quoted from the service-container log is the row-byte-budget test's deliberately provoked refusal (its whole subject), not the failure; I mis-attributed it.

The verdict is unchanged and now stronger: a 5s timeout in a live-driver convergence test, on a markdown-only diff, first occurrence in 24h per the bot's ledger — load/timing class by the bot's own criterion (「超时…多半是负载/时序,不是本 PR 的回归」). The single re-queue was already spent before this comment and the PR is back in the queue; per the bot's guidance I will NOT re-queue again on a second failure — if the same signature fires once more (here or on another PR, which auto-opens the aggregation issue), the timeout budget of that convergence case goes to the driver lane as a card with both run ids.


Generated by Claude Code

Merged via the queue into main with commit 476ec0aAug 31, 2026
34 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-13675-data-skill-sweep branch August 31, 2026 07:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@zhuangjianguo@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
docs(skills): objectstack-data factual sweep (1/3) — field-types, relationships, indexing by zhuangjianguo · Pull Request #13687 · objectstack-ai/objectstack · GitHub
Skip to content

docs(skills): objectstack-data factual sweep (1/3) — field-types, relationships, indexing - #13687

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-13675-data-skill-sweep
Aug 31, 2026
Merged

docs(skills): objectstack-data factual sweep (1/3) — field-types, relationships, indexing#13687
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-13675-data-skill-sweep

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Part of #13675

Program anchor: #13658 — flight ② of the published-skills factual sweep, PR 1 of up to three at whole-file boundaries. The card and the anchor both stay open; only the last PR in this sequence carries a closing keyword.

Method precedent: PR #13577 (the class proof) and PR #13674 (flight ①, the worked example). Session, for durable attribution: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de

Governed surface — human merge, deliberately parked

skills/** is the published, customer-facing surface. This PR is a draft on purpose: auto-merge is not armed, it is not marked ready, no review is requested, and this seat will not arm it. A draft awaiting the maintainer's own merge is the correct terminal state here, not a stuck PR.

File order, and why

skills/objectstack-data is 12 files / 4,935 lines. Flight ①'s calibration says to point execution at surfaces and mapping tables first, so PR 1 takes the three rule files with the highest table density and the most engine-asserting columns: rules/field-types.md (49 type rows, each with a config column), rules/relationships.md (the lookup-vs-master_detail capability matrix), rules/indexing.md (the declaration surface and the "what is automatic" list). SKILL.md follows in PR 2, the hooks reference and the remaining rule files in PR 3.

Non-vacuity control — claims proven TRUE by execution

1. The field-type roster.rules/field-types.md says "49 available options" and enumerates them across ten category tables. Probed against FieldType from @objectstack/spec/data:

FIELD TYPE COUNT = 49
SKILL LISTED COUNT = 49
IN SPEC NOT IN SKILL: []
IN SKILL NOT IN SPEC: []

2. The ADR-0120 unique-scope materialization. The unique-scope table states that unique: 'organization' materializes as (COALESCE(organization_id, '__global__'), …fields). Executed against organizationKeyPartSql from @objectstack/driver-sql:

organizationKeyPartSql => "COALESCE(organization_id, '__global__')"

3. The auto-generated index-name pattern. Stated as idx_{object}_{field1}_{field2}_{...}. Executed against buildIndexName:

buildIndexName(account, [status,created_at], unique=false) => idx_account_status_created_at
buildIndexName(task, [a,b,c], unique=false) => idx_task_a_b_c

4. The autonumber token table, row by row. Executed against parseAutonumberFormat + renderAutonumber at a pinned now of 2026-06-17 UTC:

CASE-{0000} seq=42 => CASE-0042 (table claims CASE-0042)
AD{YYYYMMDD}{0000} seq=1 => AD202606170001 (table claims AD202606170001)
{plan_no}{000} plan_no='PLAN-001' seq=1=> PLAN-001001 (table claims PLAN-001001)
{000} seq=1 / 999 / 1000 => 001 / 999 / 1000 (rule 3: minimum width, grows)
'{a}{b}{000}' scope for (AB,C) => "ABC"
'{a}{b}{000}' scope for (A,BC) => "ABC" (rule 2: shared counter)
'{a}-{b}{000}' scope for (AB,C)/(A,BC) => "AB-C" / "A-BC" (rule 2: delimiter separates)
'CASE-{0000}' scope => "" (fixed prefix = one global counter)

5. Per-type config columns. All 49 rows' config keys were parsed through FieldSchema; every claimed key was accepted and retained on its own type (text → maxLength/minLength/defaultValue, number → min/max/precision/scale, lookup → reference/lookupFilters/multiple/deleteBehavior, slider → min/max/step, vector → dimensions, and so on). Enum columns were pinned by rejection as well as acceptance: returnType refuses 'currency' ("expected one of number|text|boolean|date"), summaryOperations.function refuses 'median', currencyMode refuses anything but dynamic|fixed, and a field-level unique: 'tenant' / 'org' is refused by name with the ADR-0120 message.

The six FALSE claims, and what measured them

落点beforeafter
field-types.md L6-8"an unknown field key is silently stripped at parse (dead metadata), so don't invent config like …""an unknown field key is REFUSED at parse (unrecognized_keys), so don't invent …"
field-types.md L357"an authored vectorConfig is silently stripped (dead metadata)""an authored vectorConfig is refused at parse"
field-types.md L346"Date tokens are case-sensitive and exact ({YYYY}, not {yyyy} or {YYYY-MM}). An unrecognized {...} is emitted literally into the number""Date tokens are case-sensitive{yyyy} parses as a {field} reference and renders empty (rule 1 applies). Only a spelling no field could have — { YYYY }, {YYYY-MM} — is emitted literally."
relationships.md L9lookup row, Roll-ups column — "Not available""Supported via summary"
relationships.md L10master_detail row, Required column — "Always required""Forced only under controlled_by_parent; else lint-warned"
indexing.md L9"- Foreign keys (lookup/master_detail fields)" listed under "ObjectStack automatically creates indexes for" (restated twice more)bullet deleted; "- Field-level uniquenot foreign keys: declare those"

1 and 2. Unknown field keys are refused, not silently stripped

This is the single highest-consequence correction in the PR, because it inverts the cost of a typo for an AI author: "dead metadata" says a misspelled key is harmless, when it actually stops the build. Measured through FieldSchema.safeParse:

REJECT | text + theme (invented) | unrecognized_keys
REJECT | text + rows (invented) | unrecognized_keys
REJECT | file + fileAttachmentConfig (invented) | unrecognized_keys
REJECT | vector + vectorConfig | unrecognized_keys

The error's own text names the retired behaviour the skill was still describing:

Unrecognized key(s) on this field: `theme`. Until this shape was closed these were
dropped silently — the field was still created, minus whatever the key was meant to
constrain, protect or compute.

So the silent-strip sentence was true once and is now exactly backwards. The same pass re-measured the genuinely-retired keys and confirms they are refused with their migration prescription rather than dropped: referenceFilters ("removed in the 16.x line … Use lookupFilters"), encryptionConfig ("pruned in 2026-06 … The real channel is type: 'secret'"), columnName, and conditionalRequired ("removed in @objectstack/spec 17 — use requiredWhen"). Those four sentences in the skill are correct and were left alone.

3. A wrong-cased date token renders EMPTY, not literally

The rule named {yyyy} as an unrecognized spelling and then promised unrecognized spellings appear literally. Both halves cannot hold, and the measurement says the second one fails on exactly the example the first one names:

{ YYYY } tokens=[{"kind":"literal","text":"{ YYYY }"}]
{YYYY-MM} tokens=[{"kind":"literal","text":"{YYYY-MM}"}]
{yyyy} tokens=[{"kind":"field","field":"yyyy"}] <-- a FIELD reference
{YYYY} tokens=[{"kind":"date","pattern":"YYYY"}]
render "{yyyy}-{000}" with no field named yyyy => "-001"
referencedFields("{yyyy}-{000}") => ["yyyy"]
missingFieldValues("{yyyy}-{000}", {}) => ["yyyy"]

{yyyy} is a legal field-name spelling, so the tokenizer reads it as {field_name}; only a spelling no field could carry (a space, a hyphen) falls back to a literal. The practical consequence is worse than a cosmetic difference: the year silently vanishes from the record number, and the format now carries a phantom {field} reference, which puts it under rule 1 of the same list ("Every {field} you interpolate must be required: true" — an empty interpolated field throws at create). The replacement states the tokenizer's actual boundary and points back at rule 1.

4. Roll-up summaries DO work over a lookup child

The capability matrix said roll-ups are "Not available" on lookup and "Supported via summary" only on master_detail. Measured with the engine and the roll-up test's own driver stub — a parent summary over a child whose FK to the parent is a plain lookup:

MEASURED lookup-child rollup => {"opp_total":42,"opp_count":2}
Test Files 1 passed (1)
Tests 1 passed (1)

The spec agrees in its own describe text: summaryOperations.relationshipField is "Auto-detected from the child's lookup/master_detail field referencing this object when omitted". This row was not merely inaccurate, it was pushing authors toward a destructive shape — a reader who wants a roll-up and believes it needs master_detail converts an independent-lifecycle relationship into a cascade-delete one. The two adjacent prose bullets that repeated the claim ("No roll-up aggregations needed" under use lookup, "Need roll-up summaries" under use master_detail) were removed for the same reason; the deletion is also what pays for this PR's additions under the ratchet.

The tree row still reads "Not available" and was left alone: a self-referential FK is neither of the two shapes the auto-detection names, and this flight did not measure it. It is recorded as NOT MEASURABLE rather than corrected on inference.

5. master_detail is required only under controlled_by_parent

"Always required" reads as a platform invariant. Measured through ObjectSchema.create() across sharing models:

ACCEPT | sharingModel=(unset) required=undefined | stored required=false
ACCEPT | sharingModel=private required=false | stored required=false
ACCEPT | sharingModel=controlled_by_parent required=undefined | stored required=true
THROW | sharingModel=controlled_by_parent required=false | "declares `required: false` on a
`master_detail` reference under `sharingModel: 'controlled_by_parent'` — a contradiction
with no honest reading."

The controlled_by_parent rows are the control: they prove the probe reaches real enforcement, and they locate exactly where the invariant does hold. Everywhere else a master_detail defaults to required: false and an explicit false is accepted; the only thing that objects is relationship/master-detail-required, which packages/lint/src/data-model-rules.ts:592 emits at severity: 'warning'. The skill's own lint table (in SKILL.md, PR 2's file) already says "warning", so the two documents disagreed with each other and one of them disagreed with the code.

6. Foreign keys are NOT auto-indexed

indexing.md opened with "ObjectStack automatically creates indexes for: … Foreign keys (lookup/master_detail fields)", and repeated it twice ("Foreign keys — Automatic, but verify" under Always Index; "Index foreign keys — Always (automatic in ObjectStack)" under Best Practices). Measured against the driver's own index oracle, for an object carrying both a lookup and a master_detail and no declarations:

uniqueIndexesFromFields(no unique declared) => []
expectedIndexes(same object) => []
control - the same object with one field-level unique:
uniqueIndexesFromFields(email unique) =>
[{"name":"uniq_task_organization_id_email","columns":["organization_id","email"],
"unique":true,"nullSafeColumns":["organization_id"]}]

The control row is the point: the probe is live, and it returns nothing for foreign keys. SqlDriver.syncTableIndexes (sql-driver.ts:11437) is the single entry point and its body is uniqueIndexesFromFields(...) plus the object's declared indexes — there is no third source, and a repo-wide search for any foreign-key indexing path finds none. relationships.md had it right all along ("Index foreign keys — Always create indexes on lookup/master_detail fields"), so the two files in this very diff contradicted each other; indexing.md was the wrong one.

Written to the token ratchet, not around it

All three files sat at zero headroom, so every correction is paid for inside its own file by genuine deletion — the removed roll-up bullets in relationships.md, the removed foreign-key bullet in indexing.md, and tightened wording in field-types.md. No ceiling was raised; that is a maintainer's call, not a dev's.

readingbeforeafter
rules/field-types.md lines428428
rules/relationships.md lines415413
rules/indexing.md lines376375
objectstack-data/** package lines (all .md)49354932
rules/field-types.md tokens (ceiling 3584)35843578
rules/relationships.md tokens (ceiling 3778)37783773
rules/indexing.md tokens (ceiling 3241)32413240

The ratchet's own verdict lines at head 2d9e2a02c:

✓ check-skills-token-ratchet: skills/objectstack-data/rules/field-types.md is 3578 tokens (ceiling 3584; headroom 6).
✓ check-skills-token-ratchet: skills/objectstack-data/rules/indexing.md is 3240 tokens (ceiling 3241; headroom 1).
✓ check-skills-token-ratchet: skills/objectstack-data/rules/relationships.md is 3773 tokens (ceiling 3778; headroom 5).
✓ check-skills-token-ratchet: 38 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted.

Two nuances were not stuffed into ratcheted text and are recorded for a content/docs/** follow-up instead: (a) the fuller foreign-key indexing guidance the corrected bullet compresses — which FK columns actually earn an index, and the composite shapes that serve filter-plus-sort; (b) user field config — multiple: true and defaultValue: 'current_user' are each legal alone but are refused together, with a located message explaining that the runtime token resolves to one scalar and a multi-value field stores an array. The user row lists both keys and does not claim they compose, so it was left alone.

Gates — derived, not recalled, run locally at head 2d9e2a02c

Family derived from the real diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (13 families; harvested with --commands so both invocation spellings survive). 12 green, 1 NOT MEASURED:

EXIT=0 :: node scripts/check-ci-filter-parity.mjs
EXIT=0 :: node scripts/check-cross-package-test-inputs.mjs
EXIT=0 :: node scripts/check-shard-attestation.mjs
EXIT=0 :: node scripts/check-skills-token-ratchet.mjs
EXIT=3 :: node scripts/check-test-completeness.mjs <-- PREREQUISITE NOT MET
EXIT=0 :: pnpm --filter @objectstack/lint run check:doc-formula-expressions
EXIT=0 :: pnpm check:agent-test-spelling
EXIT=0 :: pnpm check:cross-package-test-inputs
EXIT=0 :: pnpm check:doc-authoring
EXIT=0 :: pnpm check:pm-governed-merges
EXIT=0 :: pnpm check:role-word
EXIT=0 :: pnpm check:skill-compatibility
EXIT=0 :: pnpm check:skill-frame-sync

Exit codes were captured before any pipe (each command redirected to its own file first, then read), so these are the gates' statuses and not a tail's.

check-test-completeness.mjs exits 3 and says why in its own words — "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named". Recorded as NOT MEASURED, not as a red.

check:doc-formula-expressions needed @objectstack/formula and @objectstack/lint built first; built, re-run, and it quotes itself:

✓ check:doc-formula-expressions self-test: 58 cases passed
✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 426 files / 1451 TS blocks judged clean by @objectstack/formula.
✓ check:doc-formula-expressions (spec TSDoc, #6763): 9 @example(s) judged clean across 1092 packages/spec/src files
✓ check:doc-formula-expressions (field-level `*When`, #11407): 14 predicate(s) judged clean; 6 skipped as undeterminable.

check-nul-bytes clean: OK (scanned 7555 text file(s) … no raw ASCII control bytes), plus a direct control-byte scan over the three changed files with no hits.

check:skill-examples is NOT MEASURED for this PR, and vacuous for it either way. It refuses to run until @objectstack/client-react is built ("packages/client-react/dist holds no .d.ts declarations … A verdict now would be computed against a build that no longer matches src") — a prerequisite refusal, not a finding, and the refusal is the gate working as designed. Independently, none of this PR's three files carries an os:check marker (measured: 0, 0, 0), so the gate has nothing to say about this diff. It becomes load-bearing in PR 2 (SKILL.md carries 4) and PR 3 (data-hooks.md carries 1), and will be run there against a real build.

ESLint was not run repo-wide, and the narrowing is measured, not assumed. Three readings: (1) the population read from eslint.config.mjs itself — every files: block names {ts,tsx,mts,cts,js,jsx,mjs,cjs}, and the strings .md and markdown appear nowhere in the config; (2) --format json over the diff's three files returns 3 results, 0 errors, and one message each whose ruleId is null and whose text is "File ignored because no matching configuration was supplied."; (3) type-aware linting is not enabled anywhere — the five parserOptions occurrences are all { ecmaVersion, sourceType } and none is project, which the config's own line 327 states in prose — so this diff cannot move any verdict on an untouched file. Zero of the changed files are in that gate's population in either direction.

No changeset

This PR releases nothing from any package — a pure skills/** documentation correction, the repo's live convention for which is no changeset. skip-changeset is applied.

Calibration numbers for the anchor

Counting rule: one claim per assertable proposition about platform behaviour — each type row's existence, each config key attributed to a type, each mapping-table cell, each numbered rule, each behavioural sentence, each asserted example output. Front-matter, section headings, the field-type decision tree (navigation over types already inventoried) and pure style prescriptions are out of the inventory by the anchor's rule; that is ~158 of these 1,219 lines.

314 behavioral claims inventoried across PR 1's three filesfield-types.md 166, relationships.md 77, indexing.md 71. 6 FALSE (1.9%), ~34 NOT MEASURABLE (concentrated in indexing.md's performance heuristics — the "Max Indexes by table size" table, the selectivity folklore, the "more than 5 levels" tree-depth warning — which assert database-tuning judgement no repo artifact settles), the remainder VERIFIED, with executed probes wherever the claim is behaviour-bearing.

Flight ①'s ~8x table concentration did NOT reproduce here, and the reading depends on how "table" is defined — which is itself worth the anchor's attention:

  • By markdown rendering: 2 of 6 falsehoods sit in an actual pipe table (both relationships.md capability-matrix cells) — 2/~150 in-table (1.3%) against 4/~164 elsewhere (2.4%). That is a mild inversion of flight ①'s finding, not an 8x concentration.
  • By content class (what flight ① was really pointing at — assertions about what surfaces exist and what the engine does with them): 5 of 6 are surface/mapping claims — the unknown-key posture, vectorConfig, the FK auto-index list, and the two matrix cells. Only the autonumber tokenizer rule is a pure behavioural rule.

The actionable version for flights ③–⑫ is therefore the content class, not the markdown syntax: point execution at every "what is automatic / what keys exist / what is retired" assertion regardless of whether it is rendered as a table, a bullet list, or a callout. Three of this flight's six falsehoods were in bullet lists and callouts that a table-shaped search would have walked straight past.

A second pattern worth carrying forward: two of the six were caught by cross-file contradiction inside the same package (indexing.md vs relationships.md on FK indexing; relationships.md vs SKILL.md on master_detail requiredness). Neither is a document-vs-document verdict — both were then settled against the implementation — but the disagreement is a cheap, high-yield way to find candidates before spending a probe.

What is not here, on purpose

  • No implementation was touched. Every disagreement was the document being wrong about settled code, not code contradicting itself. No implementation defect was found worth filing from these three files.
  • Probes were written in the worktree, run, and deleted — they are evidence, not commits. None earned a place as a permanent pin: the roll-up-over-lookup behaviour is already pinned by packages/objectql/src/summary-rollup.test.ts, and the field-key posture by packages/spec/src/data/field.test.ts.
  • references/_index.md is generator-owned (pnpm --filter @objectstack/spec gen:skill-refs) and is not in this PR's diff. Its node_modules/@objectstack/spec/src/** pointers were nevertheless spot-measured against a real npm pack --dry-run --json: the tarball carries 209 src/ entries and every .zod.ts path this package points at is present, so the flight-① "unshipped-path pointer" flavour does not apply to @objectstack/spec (it applied to @objectstack/formula, which ships dist only).
  • The performance heuristics in indexing.md were left alone. They are advice, not platform claims, and rewriting advice is not a factual correction.

Generated by Claude Code

…ationships, indexing
Six measured-false behavioral claims corrected against the implementation:
- field-types.md: an unknown field key is REFUSED at parse (unrecognized_keys),
not "silently stripped"; same for an authored vectorConfig.
- field-types.md: a wrong-cased date token ({yyyy}) parses as a {field}
reference and renders EMPTY — it is not emitted literally.
- relationships.md: roll-up summary fields DO work over a lookup child.
- relationships.md: master_detail required is forced only under
sharingModel: 'controlled_by_parent'; elsewhere it is a lint warning.
- indexing.md: foreign keys are NOT auto-indexed.
Token ratchet: all three files shrink (-6 / -5 / -1 = -12).
Part of #13675
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de
@os-zhuang
os-zhuang marked this pull request as ready for review August 31, 2026 07:15
@os-zhuang
os-zhuang enabled auto-merge August 31, 2026 07:15
@os-zhuang
os-zhuang added this pull request to the merge queueAug 31, 2026
@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Aug 31, 2026
@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

Queue dequeue root-caused — not this PR's failure; re-queueing once.

Failing check: merge-queue CI, job Temporal Conformance (live PG + MySQL), run 33367664966 on gh-readonly-queue/main/pr-13687-90578117f: MySQL 8.0.46 refused CREATE TABLE os11565_narrow with [MY-011825] record size will exceed the maximum allowed size — the narrow contrast fixture of sql-driver-11565-row-byte-budget.test.ts, i.e. the table that exists to FIT.

Why it is not this PR's: (1) this diff is three markdown files under skills/objectstack-data/** — zero code, and the whole queue batch ahead of it (#13674) is markdown too; a live-driver conformance job cannot read either; (2) the test is a week old (7e8393262, 2026-08-24), not newly landed; (3) the same job is green on main's own CI at 2cce3fd30 (06:55Z), and the only commits between that head and the queue base 90578117f are two lint fixes and a docs correction — none touches driver-sql or MySQL DDL emission; (4) the narrow fixture being refused points at service-container environment variance on the freshly initialized MySQL (row-format/charset dependent effective budget), not at any change in what the driver emits.

Actions: re-queued via auto-merge as the one legitimate re-run. If the queue fails on the same test a second time, that is real evidence the fixture's byte budget sits too close to an environment-dependent limit — I will file it for the driver lane with both run ids and park this PR on that fix.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo added this pull request to the merge queueAug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33367664966 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Temporal Conformance (live PG + MySQL) — 失败步骤: Run driver-sql suite against both live servers

    × converges — a second detect finds neither the index nor an orphan column 6123ms
    FAIL src/sql-driver-13056-orphan-shadow-column-cleanup.test.ts > a retired shadow-carried UNIQUE leaves no column behind (#13056) > converges — a second detect finds neither the index nor an orphan c
    ↳ 失败原因: Error: Test timed out in 5000ms.
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • src/sql-driver-13056-orphan-shadow-column-cleanup.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 3 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

Correction to my standing-down comment above, prompted by the triage bot's extraction: the failing test was sql-driver-13056-orphan-shadow-column-cleanup.test.ts (「converges — a second detect finds neither the index nor an orphan column」, Test timed out in 5000ms) — a timeout, not an assertion. The os11565_narrow MySQL error I quoted from the service-container log is the row-byte-budget test's deliberately provoked refusal (its whole subject), not the failure; I mis-attributed it.

The verdict is unchanged and now stronger: a 5s timeout in a live-driver convergence test, on a markdown-only diff, first occurrence in 24h per the bot's ledger — load/timing class by the bot's own criterion (「超时…多半是负载/时序,不是本 PR 的回归」). The single re-queue was already spent before this comment and the PR is back in the queue; per the bot's guidance I will NOT re-queue again on a second failure — if the same signature fires once more (here or on another PR, which auto-opens the aggregation issue), the timeout budget of that convergence case goes to the driver lane as a card with both run ids.


Generated by Claude Code

Merged via the queue into main with commit 476ec0aAug 31, 2026
34 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-13675-data-skill-sweep branch August 31, 2026 07:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@zhuangjianguo@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' docs(skills): objectstack-data factual sweep (1/3) — field-types, relationships, indexing by zhuangjianguo · Pull Request #13687 · objectstack-ai/objectstack · GitHub
Skip to content

docs(skills): objectstack-data factual sweep (1/3) — field-types, relationships, indexing - #13687

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-13675-data-skill-sweep
Aug 31, 2026
Merged

docs(skills): objectstack-data factual sweep (1/3) — field-types, relationships, indexing#13687
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-13675-data-skill-sweep

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Part of #13675

Program anchor: #13658 — flight ② of the published-skills factual sweep, PR 1 of up to three at whole-file boundaries. The card and the anchor both stay open; only the last PR in this sequence carries a closing keyword.

Method precedent: PR #13577 (the class proof) and PR #13674 (flight ①, the worked example). Session, for durable attribution: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de

Governed surface — human merge, deliberately parked

skills/** is the published, customer-facing surface. This PR is a draft on purpose: auto-merge is not armed, it is not marked ready, no review is requested, and this seat will not arm it. A draft awaiting the maintainer's own merge is the correct terminal state here, not a stuck PR.

File order, and why

skills/objectstack-data is 12 files / 4,935 lines. Flight ①'s calibration says to point execution at surfaces and mapping tables first, so PR 1 takes the three rule files with the highest table density and the most engine-asserting columns: rules/field-types.md (49 type rows, each with a config column), rules/relationships.md (the lookup-vs-master_detail capability matrix), rules/indexing.md (the declaration surface and the "what is automatic" list). SKILL.md follows in PR 2, the hooks reference and the remaining rule files in PR 3.

Non-vacuity control — claims proven TRUE by execution

1. The field-type roster.rules/field-types.md says "49 available options" and enumerates them across ten category tables. Probed against FieldType from @objectstack/spec/data:

FIELD TYPE COUNT = 49
SKILL LISTED COUNT = 49
IN SPEC NOT IN SKILL: []
IN SKILL NOT IN SPEC: []

2. The ADR-0120 unique-scope materialization. The unique-scope table states that unique: 'organization' materializes as (COALESCE(organization_id, '__global__'), …fields). Executed against organizationKeyPartSql from @objectstack/driver-sql:

organizationKeyPartSql => "COALESCE(organization_id, '__global__')"

3. The auto-generated index-name pattern. Stated as idx_{object}_{field1}_{field2}_{...}. Executed against buildIndexName:

buildIndexName(account, [status,created_at], unique=false) => idx_account_status_created_at
buildIndexName(task, [a,b,c], unique=false) => idx_task_a_b_c

4. The autonumber token table, row by row. Executed against parseAutonumberFormat + renderAutonumber at a pinned now of 2026-06-17 UTC:

CASE-{0000} seq=42 => CASE-0042 (table claims CASE-0042)
AD{YYYYMMDD}{0000} seq=1 => AD202606170001 (table claims AD202606170001)
{plan_no}{000} plan_no='PLAN-001' seq=1=> PLAN-001001 (table claims PLAN-001001)
{000} seq=1 / 999 / 1000 => 001 / 999 / 1000 (rule 3: minimum width, grows)
'{a}{b}{000}' scope for (AB,C) => "ABC"
'{a}{b}{000}' scope for (A,BC) => "ABC" (rule 2: shared counter)
'{a}-{b}{000}' scope for (AB,C)/(A,BC) => "AB-C" / "A-BC" (rule 2: delimiter separates)
'CASE-{0000}' scope => "" (fixed prefix = one global counter)

5. Per-type config columns. All 49 rows' config keys were parsed through FieldSchema; every claimed key was accepted and retained on its own type (text → maxLength/minLength/defaultValue, number → min/max/precision/scale, lookup → reference/lookupFilters/multiple/deleteBehavior, slider → min/max/step, vector → dimensions, and so on). Enum columns were pinned by rejection as well as acceptance: returnType refuses 'currency' ("expected one of number|text|boolean|date"), summaryOperations.function refuses 'median', currencyMode refuses anything but dynamic|fixed, and a field-level unique: 'tenant' / 'org' is refused by name with the ADR-0120 message.

The six FALSE claims, and what measured them

落点beforeafter
field-types.md L6-8"an unknown field key is silently stripped at parse (dead metadata), so don't invent config like …""an unknown field key is REFUSED at parse (unrecognized_keys), so don't invent …"
field-types.md L357"an authored vectorConfig is silently stripped (dead metadata)""an authored vectorConfig is refused at parse"
field-types.md L346"Date tokens are case-sensitive and exact ({YYYY}, not {yyyy} or {YYYY-MM}). An unrecognized {...} is emitted literally into the number""Date tokens are case-sensitive{yyyy} parses as a {field} reference and renders empty (rule 1 applies). Only a spelling no field could have — { YYYY }, {YYYY-MM} — is emitted literally."
relationships.md L9lookup row, Roll-ups column — "Not available""Supported via summary"
relationships.md L10master_detail row, Required column — "Always required""Forced only under controlled_by_parent; else lint-warned"
indexing.md L9"- Foreign keys (lookup/master_detail fields)" listed under "ObjectStack automatically creates indexes for" (restated twice more)bullet deleted; "- Field-level uniquenot foreign keys: declare those"

1 and 2. Unknown field keys are refused, not silently stripped

This is the single highest-consequence correction in the PR, because it inverts the cost of a typo for an AI author: "dead metadata" says a misspelled key is harmless, when it actually stops the build. Measured through FieldSchema.safeParse:

REJECT | text + theme (invented) | unrecognized_keys
REJECT | text + rows (invented) | unrecognized_keys
REJECT | file + fileAttachmentConfig (invented) | unrecognized_keys
REJECT | vector + vectorConfig | unrecognized_keys

The error's own text names the retired behaviour the skill was still describing:

Unrecognized key(s) on this field: `theme`. Until this shape was closed these were
dropped silently — the field was still created, minus whatever the key was meant to
constrain, protect or compute.

So the silent-strip sentence was true once and is now exactly backwards. The same pass re-measured the genuinely-retired keys and confirms they are refused with their migration prescription rather than dropped: referenceFilters ("removed in the 16.x line … Use lookupFilters"), encryptionConfig ("pruned in 2026-06 … The real channel is type: 'secret'"), columnName, and conditionalRequired ("removed in @objectstack/spec 17 — use requiredWhen"). Those four sentences in the skill are correct and were left alone.

3. A wrong-cased date token renders EMPTY, not literally

The rule named {yyyy} as an unrecognized spelling and then promised unrecognized spellings appear literally. Both halves cannot hold, and the measurement says the second one fails on exactly the example the first one names:

{ YYYY } tokens=[{"kind":"literal","text":"{ YYYY }"}]
{YYYY-MM} tokens=[{"kind":"literal","text":"{YYYY-MM}"}]
{yyyy} tokens=[{"kind":"field","field":"yyyy"}] <-- a FIELD reference
{YYYY} tokens=[{"kind":"date","pattern":"YYYY"}]
render "{yyyy}-{000}" with no field named yyyy => "-001"
referencedFields("{yyyy}-{000}") => ["yyyy"]
missingFieldValues("{yyyy}-{000}", {}) => ["yyyy"]

{yyyy} is a legal field-name spelling, so the tokenizer reads it as {field_name}; only a spelling no field could carry (a space, a hyphen) falls back to a literal. The practical consequence is worse than a cosmetic difference: the year silently vanishes from the record number, and the format now carries a phantom {field} reference, which puts it under rule 1 of the same list ("Every {field} you interpolate must be required: true" — an empty interpolated field throws at create). The replacement states the tokenizer's actual boundary and points back at rule 1.

4. Roll-up summaries DO work over a lookup child

The capability matrix said roll-ups are "Not available" on lookup and "Supported via summary" only on master_detail. Measured with the engine and the roll-up test's own driver stub — a parent summary over a child whose FK to the parent is a plain lookup:

MEASURED lookup-child rollup => {"opp_total":42,"opp_count":2}
Test Files 1 passed (1)
Tests 1 passed (1)

The spec agrees in its own describe text: summaryOperations.relationshipField is "Auto-detected from the child's lookup/master_detail field referencing this object when omitted". This row was not merely inaccurate, it was pushing authors toward a destructive shape — a reader who wants a roll-up and believes it needs master_detail converts an independent-lifecycle relationship into a cascade-delete one. The two adjacent prose bullets that repeated the claim ("No roll-up aggregations needed" under use lookup, "Need roll-up summaries" under use master_detail) were removed for the same reason; the deletion is also what pays for this PR's additions under the ratchet.

The tree row still reads "Not available" and was left alone: a self-referential FK is neither of the two shapes the auto-detection names, and this flight did not measure it. It is recorded as NOT MEASURABLE rather than corrected on inference.

5. master_detail is required only under controlled_by_parent

"Always required" reads as a platform invariant. Measured through ObjectSchema.create() across sharing models:

ACCEPT | sharingModel=(unset) required=undefined | stored required=false
ACCEPT | sharingModel=private required=false | stored required=false
ACCEPT | sharingModel=controlled_by_parent required=undefined | stored required=true
THROW | sharingModel=controlled_by_parent required=false | "declares `required: false` on a
`master_detail` reference under `sharingModel: 'controlled_by_parent'` — a contradiction
with no honest reading."

The controlled_by_parent rows are the control: they prove the probe reaches real enforcement, and they locate exactly where the invariant does hold. Everywhere else a master_detail defaults to required: false and an explicit false is accepted; the only thing that objects is relationship/master-detail-required, which packages/lint/src/data-model-rules.ts:592 emits at severity: 'warning'. The skill's own lint table (in SKILL.md, PR 2's file) already says "warning", so the two documents disagreed with each other and one of them disagreed with the code.

6. Foreign keys are NOT auto-indexed

indexing.md opened with "ObjectStack automatically creates indexes for: … Foreign keys (lookup/master_detail fields)", and repeated it twice ("Foreign keys — Automatic, but verify" under Always Index; "Index foreign keys — Always (automatic in ObjectStack)" under Best Practices). Measured against the driver's own index oracle, for an object carrying both a lookup and a master_detail and no declarations:

uniqueIndexesFromFields(no unique declared) => []
expectedIndexes(same object) => []
control - the same object with one field-level unique:
uniqueIndexesFromFields(email unique) =>
[{"name":"uniq_task_organization_id_email","columns":["organization_id","email"],
"unique":true,"nullSafeColumns":["organization_id"]}]

The control row is the point: the probe is live, and it returns nothing for foreign keys. SqlDriver.syncTableIndexes (sql-driver.ts:11437) is the single entry point and its body is uniqueIndexesFromFields(...) plus the object's declared indexes — there is no third source, and a repo-wide search for any foreign-key indexing path finds none. relationships.md had it right all along ("Index foreign keys — Always create indexes on lookup/master_detail fields"), so the two files in this very diff contradicted each other; indexing.md was the wrong one.

Written to the token ratchet, not around it

All three files sat at zero headroom, so every correction is paid for inside its own file by genuine deletion — the removed roll-up bullets in relationships.md, the removed foreign-key bullet in indexing.md, and tightened wording in field-types.md. No ceiling was raised; that is a maintainer's call, not a dev's.

readingbeforeafter
rules/field-types.md lines428428
rules/relationships.md lines415413
rules/indexing.md lines376375
objectstack-data/** package lines (all .md)49354932
rules/field-types.md tokens (ceiling 3584)35843578
rules/relationships.md tokens (ceiling 3778)37783773
rules/indexing.md tokens (ceiling 3241)32413240

The ratchet's own verdict lines at head 2d9e2a02c:

✓ check-skills-token-ratchet: skills/objectstack-data/rules/field-types.md is 3578 tokens (ceiling 3584; headroom 6).
✓ check-skills-token-ratchet: skills/objectstack-data/rules/indexing.md is 3240 tokens (ceiling 3241; headroom 1).
✓ check-skills-token-ratchet: skills/objectstack-data/rules/relationships.md is 3773 tokens (ceiling 3778; headroom 5).
✓ check-skills-token-ratchet: 38 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted.

Two nuances were not stuffed into ratcheted text and are recorded for a content/docs/** follow-up instead: (a) the fuller foreign-key indexing guidance the corrected bullet compresses — which FK columns actually earn an index, and the composite shapes that serve filter-plus-sort; (b) user field config — multiple: true and defaultValue: 'current_user' are each legal alone but are refused together, with a located message explaining that the runtime token resolves to one scalar and a multi-value field stores an array. The user row lists both keys and does not claim they compose, so it was left alone.

Gates — derived, not recalled, run locally at head 2d9e2a02c

Family derived from the real diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (13 families; harvested with --commands so both invocation spellings survive). 12 green, 1 NOT MEASURED:

EXIT=0 :: node scripts/check-ci-filter-parity.mjs
EXIT=0 :: node scripts/check-cross-package-test-inputs.mjs
EXIT=0 :: node scripts/check-shard-attestation.mjs
EXIT=0 :: node scripts/check-skills-token-ratchet.mjs
EXIT=3 :: node scripts/check-test-completeness.mjs <-- PREREQUISITE NOT MET
EXIT=0 :: pnpm --filter @objectstack/lint run check:doc-formula-expressions
EXIT=0 :: pnpm check:agent-test-spelling
EXIT=0 :: pnpm check:cross-package-test-inputs
EXIT=0 :: pnpm check:doc-authoring
EXIT=0 :: pnpm check:pm-governed-merges
EXIT=0 :: pnpm check:role-word
EXIT=0 :: pnpm check:skill-compatibility
EXIT=0 :: pnpm check:skill-frame-sync

Exit codes were captured before any pipe (each command redirected to its own file first, then read), so these are the gates' statuses and not a tail's.

check-test-completeness.mjs exits 3 and says why in its own words — "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named". Recorded as NOT MEASURED, not as a red.

check:doc-formula-expressions needed @objectstack/formula and @objectstack/lint built first; built, re-run, and it quotes itself:

✓ check:doc-formula-expressions self-test: 58 cases passed
✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 426 files / 1451 TS blocks judged clean by @objectstack/formula.
✓ check:doc-formula-expressions (spec TSDoc, #6763): 9 @example(s) judged clean across 1092 packages/spec/src files
✓ check:doc-formula-expressions (field-level `*When`, #11407): 14 predicate(s) judged clean; 6 skipped as undeterminable.

check-nul-bytes clean: OK (scanned 7555 text file(s) … no raw ASCII control bytes), plus a direct control-byte scan over the three changed files with no hits.

check:skill-examples is NOT MEASURED for this PR, and vacuous for it either way. It refuses to run until @objectstack/client-react is built ("packages/client-react/dist holds no .d.ts declarations … A verdict now would be computed against a build that no longer matches src") — a prerequisite refusal, not a finding, and the refusal is the gate working as designed. Independently, none of this PR's three files carries an os:check marker (measured: 0, 0, 0), so the gate has nothing to say about this diff. It becomes load-bearing in PR 2 (SKILL.md carries 4) and PR 3 (data-hooks.md carries 1), and will be run there against a real build.

ESLint was not run repo-wide, and the narrowing is measured, not assumed. Three readings: (1) the population read from eslint.config.mjs itself — every files: block names {ts,tsx,mts,cts,js,jsx,mjs,cjs}, and the strings .md and markdown appear nowhere in the config; (2) --format json over the diff's three files returns 3 results, 0 errors, and one message each whose ruleId is null and whose text is "File ignored because no matching configuration was supplied."; (3) type-aware linting is not enabled anywhere — the five parserOptions occurrences are all { ecmaVersion, sourceType } and none is project, which the config's own line 327 states in prose — so this diff cannot move any verdict on an untouched file. Zero of the changed files are in that gate's population in either direction.

No changeset

This PR releases nothing from any package — a pure skills/** documentation correction, the repo's live convention for which is no changeset. skip-changeset is applied.

Calibration numbers for the anchor

Counting rule: one claim per assertable proposition about platform behaviour — each type row's existence, each config key attributed to a type, each mapping-table cell, each numbered rule, each behavioural sentence, each asserted example output. Front-matter, section headings, the field-type decision tree (navigation over types already inventoried) and pure style prescriptions are out of the inventory by the anchor's rule; that is ~158 of these 1,219 lines.

314 behavioral claims inventoried across PR 1's three filesfield-types.md 166, relationships.md 77, indexing.md 71. 6 FALSE (1.9%), ~34 NOT MEASURABLE (concentrated in indexing.md's performance heuristics — the "Max Indexes by table size" table, the selectivity folklore, the "more than 5 levels" tree-depth warning — which assert database-tuning judgement no repo artifact settles), the remainder VERIFIED, with executed probes wherever the claim is behaviour-bearing.

Flight ①'s ~8x table concentration did NOT reproduce here, and the reading depends on how "table" is defined — which is itself worth the anchor's attention:

  • By markdown rendering: 2 of 6 falsehoods sit in an actual pipe table (both relationships.md capability-matrix cells) — 2/~150 in-table (1.3%) against 4/~164 elsewhere (2.4%). That is a mild inversion of flight ①'s finding, not an 8x concentration.
  • By content class (what flight ① was really pointing at — assertions about what surfaces exist and what the engine does with them): 5 of 6 are surface/mapping claims — the unknown-key posture, vectorConfig, the FK auto-index list, and the two matrix cells. Only the autonumber tokenizer rule is a pure behavioural rule.

The actionable version for flights ③–⑫ is therefore the content class, not the markdown syntax: point execution at every "what is automatic / what keys exist / what is retired" assertion regardless of whether it is rendered as a table, a bullet list, or a callout. Three of this flight's six falsehoods were in bullet lists and callouts that a table-shaped search would have walked straight past.

A second pattern worth carrying forward: two of the six were caught by cross-file contradiction inside the same package (indexing.md vs relationships.md on FK indexing; relationships.md vs SKILL.md on master_detail requiredness). Neither is a document-vs-document verdict — both were then settled against the implementation — but the disagreement is a cheap, high-yield way to find candidates before spending a probe.

What is not here, on purpose

  • No implementation was touched. Every disagreement was the document being wrong about settled code, not code contradicting itself. No implementation defect was found worth filing from these three files.
  • Probes were written in the worktree, run, and deleted — they are evidence, not commits. None earned a place as a permanent pin: the roll-up-over-lookup behaviour is already pinned by packages/objectql/src/summary-rollup.test.ts, and the field-key posture by packages/spec/src/data/field.test.ts.
  • references/_index.md is generator-owned (pnpm --filter @objectstack/spec gen:skill-refs) and is not in this PR's diff. Its node_modules/@objectstack/spec/src/** pointers were nevertheless spot-measured against a real npm pack --dry-run --json: the tarball carries 209 src/ entries and every .zod.ts path this package points at is present, so the flight-① "unshipped-path pointer" flavour does not apply to @objectstack/spec (it applied to @objectstack/formula, which ships dist only).
  • The performance heuristics in indexing.md were left alone. They are advice, not platform claims, and rewriting advice is not a factual correction.

Generated by Claude Code

…ationships, indexing
Six measured-false behavioral claims corrected against the implementation:
- field-types.md: an unknown field key is REFUSED at parse (unrecognized_keys),
not "silently stripped"; same for an authored vectorConfig.
- field-types.md: a wrong-cased date token ({yyyy}) parses as a {field}
reference and renders EMPTY — it is not emitted literally.
- relationships.md: roll-up summary fields DO work over a lookup child.
- relationships.md: master_detail required is forced only under
sharingModel: 'controlled_by_parent'; elsewhere it is a lint warning.
- indexing.md: foreign keys are NOT auto-indexed.
Token ratchet: all three files shrink (-6 / -5 / -1 = -12).
Part of #13675
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de
@os-zhuang
os-zhuang marked this pull request as ready for review August 31, 2026 07:15
@os-zhuang
os-zhuang enabled auto-merge August 31, 2026 07:15
@os-zhuang
os-zhuang added this pull request to the merge queueAug 31, 2026
@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Aug 31, 2026
@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

Queue dequeue root-caused — not this PR's failure; re-queueing once.

Failing check: merge-queue CI, job Temporal Conformance (live PG + MySQL), run 33367664966 on gh-readonly-queue/main/pr-13687-90578117f: MySQL 8.0.46 refused CREATE TABLE os11565_narrow with [MY-011825] record size will exceed the maximum allowed size — the narrow contrast fixture of sql-driver-11565-row-byte-budget.test.ts, i.e. the table that exists to FIT.

Why it is not this PR's: (1) this diff is three markdown files under skills/objectstack-data/** — zero code, and the whole queue batch ahead of it (#13674) is markdown too; a live-driver conformance job cannot read either; (2) the test is a week old (7e8393262, 2026-08-24), not newly landed; (3) the same job is green on main's own CI at 2cce3fd30 (06:55Z), and the only commits between that head and the queue base 90578117f are two lint fixes and a docs correction — none touches driver-sql or MySQL DDL emission; (4) the narrow fixture being refused points at service-container environment variance on the freshly initialized MySQL (row-format/charset dependent effective budget), not at any change in what the driver emits.

Actions: re-queued via auto-merge as the one legitimate re-run. If the queue fails on the same test a second time, that is real evidence the fixture's byte budget sits too close to an environment-dependent limit — I will file it for the driver lane with both run ids and park this PR on that fix.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo added this pull request to the merge queueAug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33367664966 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Temporal Conformance (live PG + MySQL) — 失败步骤: Run driver-sql suite against both live servers

    × converges — a second detect finds neither the index nor an orphan column 6123ms
    FAIL src/sql-driver-13056-orphan-shadow-column-cleanup.test.ts > a retired shadow-carried UNIQUE leaves no column behind (#13056) > converges — a second detect finds neither the index nor an orphan c
    ↳ 失败原因: Error: Test timed out in 5000ms.
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • src/sql-driver-13056-orphan-shadow-column-cleanup.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 3 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

Correction to my standing-down comment above, prompted by the triage bot's extraction: the failing test was sql-driver-13056-orphan-shadow-column-cleanup.test.ts (「converges — a second detect finds neither the index nor an orphan column」, Test timed out in 5000ms) — a timeout, not an assertion. The os11565_narrow MySQL error I quoted from the service-container log is the row-byte-budget test's deliberately provoked refusal (its whole subject), not the failure; I mis-attributed it.

The verdict is unchanged and now stronger: a 5s timeout in a live-driver convergence test, on a markdown-only diff, first occurrence in 24h per the bot's ledger — load/timing class by the bot's own criterion (「超时…多半是负载/时序,不是本 PR 的回归」). The single re-queue was already spent before this comment and the PR is back in the queue; per the bot's guidance I will NOT re-queue again on a second failure — if the same signature fires once more (here or on another PR, which auto-opens the aggregation issue), the timeout budget of that convergence case goes to the driver lane as a card with both run ids.


Generated by Claude Code

Merged via the queue into main with commit 476ec0aAug 31, 2026
34 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-13675-data-skill-sweep branch August 31, 2026 07:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@zhuangjianguo@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' docs(skills): objectstack-data factual sweep (1/3) — field-types, relationships, indexing by zhuangjianguo · Pull Request #13687 · objectstack-ai/objectstack · GitHub
Skip to content

docs(skills): objectstack-data factual sweep (1/3) — field-types, relationships, indexing - #13687

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-13675-data-skill-sweep
Aug 31, 2026
Merged

docs(skills): objectstack-data factual sweep (1/3) — field-types, relationships, indexing#13687
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-13675-data-skill-sweep

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Part of #13675

Program anchor: #13658 — flight ② of the published-skills factual sweep, PR 1 of up to three at whole-file boundaries. The card and the anchor both stay open; only the last PR in this sequence carries a closing keyword.

Method precedent: PR #13577 (the class proof) and PR #13674 (flight ①, the worked example). Session, for durable attribution: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de

Governed surface — human merge, deliberately parked

skills/** is the published, customer-facing surface. This PR is a draft on purpose: auto-merge is not armed, it is not marked ready, no review is requested, and this seat will not arm it. A draft awaiting the maintainer's own merge is the correct terminal state here, not a stuck PR.

File order, and why

skills/objectstack-data is 12 files / 4,935 lines. Flight ①'s calibration says to point execution at surfaces and mapping tables first, so PR 1 takes the three rule files with the highest table density and the most engine-asserting columns: rules/field-types.md (49 type rows, each with a config column), rules/relationships.md (the lookup-vs-master_detail capability matrix), rules/indexing.md (the declaration surface and the "what is automatic" list). SKILL.md follows in PR 2, the hooks reference and the remaining rule files in PR 3.

Non-vacuity control — claims proven TRUE by execution

1. The field-type roster.rules/field-types.md says "49 available options" and enumerates them across ten category tables. Probed against FieldType from @objectstack/spec/data:

FIELD TYPE COUNT = 49
SKILL LISTED COUNT = 49
IN SPEC NOT IN SKILL: []
IN SKILL NOT IN SPEC: []

2. The ADR-0120 unique-scope materialization. The unique-scope table states that unique: 'organization' materializes as (COALESCE(organization_id, '__global__'), …fields). Executed against organizationKeyPartSql from @objectstack/driver-sql:

organizationKeyPartSql => "COALESCE(organization_id, '__global__')"

3. The auto-generated index-name pattern. Stated as idx_{object}_{field1}_{field2}_{...}. Executed against buildIndexName:

buildIndexName(account, [status,created_at], unique=false) => idx_account_status_created_at
buildIndexName(task, [a,b,c], unique=false) => idx_task_a_b_c

4. The autonumber token table, row by row. Executed against parseAutonumberFormat + renderAutonumber at a pinned now of 2026-06-17 UTC:

CASE-{0000} seq=42 => CASE-0042 (table claims CASE-0042)
AD{YYYYMMDD}{0000} seq=1 => AD202606170001 (table claims AD202606170001)
{plan_no}{000} plan_no='PLAN-001' seq=1=> PLAN-001001 (table claims PLAN-001001)
{000} seq=1 / 999 / 1000 => 001 / 999 / 1000 (rule 3: minimum width, grows)
'{a}{b}{000}' scope for (AB,C) => "ABC"
'{a}{b}{000}' scope for (A,BC) => "ABC" (rule 2: shared counter)
'{a}-{b}{000}' scope for (AB,C)/(A,BC) => "AB-C" / "A-BC" (rule 2: delimiter separates)
'CASE-{0000}' scope => "" (fixed prefix = one global counter)

5. Per-type config columns. All 49 rows' config keys were parsed through FieldSchema; every claimed key was accepted and retained on its own type (text → maxLength/minLength/defaultValue, number → min/max/precision/scale, lookup → reference/lookupFilters/multiple/deleteBehavior, slider → min/max/step, vector → dimensions, and so on). Enum columns were pinned by rejection as well as acceptance: returnType refuses 'currency' ("expected one of number|text|boolean|date"), summaryOperations.function refuses 'median', currencyMode refuses anything but dynamic|fixed, and a field-level unique: 'tenant' / 'org' is refused by name with the ADR-0120 message.

The six FALSE claims, and what measured them

落点beforeafter
field-types.md L6-8"an unknown field key is silently stripped at parse (dead metadata), so don't invent config like …""an unknown field key is REFUSED at parse (unrecognized_keys), so don't invent …"
field-types.md L357"an authored vectorConfig is silently stripped (dead metadata)""an authored vectorConfig is refused at parse"
field-types.md L346"Date tokens are case-sensitive and exact ({YYYY}, not {yyyy} or {YYYY-MM}). An unrecognized {...} is emitted literally into the number""Date tokens are case-sensitive{yyyy} parses as a {field} reference and renders empty (rule 1 applies). Only a spelling no field could have — { YYYY }, {YYYY-MM} — is emitted literally."
relationships.md L9lookup row, Roll-ups column — "Not available""Supported via summary"
relationships.md L10master_detail row, Required column — "Always required""Forced only under controlled_by_parent; else lint-warned"
indexing.md L9"- Foreign keys (lookup/master_detail fields)" listed under "ObjectStack automatically creates indexes for" (restated twice more)bullet deleted; "- Field-level uniquenot foreign keys: declare those"

1 and 2. Unknown field keys are refused, not silently stripped

This is the single highest-consequence correction in the PR, because it inverts the cost of a typo for an AI author: "dead metadata" says a misspelled key is harmless, when it actually stops the build. Measured through FieldSchema.safeParse:

REJECT | text + theme (invented) | unrecognized_keys
REJECT | text + rows (invented) | unrecognized_keys
REJECT | file + fileAttachmentConfig (invented) | unrecognized_keys
REJECT | vector + vectorConfig | unrecognized_keys

The error's own text names the retired behaviour the skill was still describing:

Unrecognized key(s) on this field: `theme`. Until this shape was closed these were
dropped silently — the field was still created, minus whatever the key was meant to
constrain, protect or compute.

So the silent-strip sentence was true once and is now exactly backwards. The same pass re-measured the genuinely-retired keys and confirms they are refused with their migration prescription rather than dropped: referenceFilters ("removed in the 16.x line … Use lookupFilters"), encryptionConfig ("pruned in 2026-06 … The real channel is type: 'secret'"), columnName, and conditionalRequired ("removed in @objectstack/spec 17 — use requiredWhen"). Those four sentences in the skill are correct and were left alone.

3. A wrong-cased date token renders EMPTY, not literally

The rule named {yyyy} as an unrecognized spelling and then promised unrecognized spellings appear literally. Both halves cannot hold, and the measurement says the second one fails on exactly the example the first one names:

{ YYYY } tokens=[{"kind":"literal","text":"{ YYYY }"}]
{YYYY-MM} tokens=[{"kind":"literal","text":"{YYYY-MM}"}]
{yyyy} tokens=[{"kind":"field","field":"yyyy"}] <-- a FIELD reference
{YYYY} tokens=[{"kind":"date","pattern":"YYYY"}]
render "{yyyy}-{000}" with no field named yyyy => "-001"
referencedFields("{yyyy}-{000}") => ["yyyy"]
missingFieldValues("{yyyy}-{000}", {}) => ["yyyy"]

{yyyy} is a legal field-name spelling, so the tokenizer reads it as {field_name}; only a spelling no field could carry (a space, a hyphen) falls back to a literal. The practical consequence is worse than a cosmetic difference: the year silently vanishes from the record number, and the format now carries a phantom {field} reference, which puts it under rule 1 of the same list ("Every {field} you interpolate must be required: true" — an empty interpolated field throws at create). The replacement states the tokenizer's actual boundary and points back at rule 1.

4. Roll-up summaries DO work over a lookup child

The capability matrix said roll-ups are "Not available" on lookup and "Supported via summary" only on master_detail. Measured with the engine and the roll-up test's own driver stub — a parent summary over a child whose FK to the parent is a plain lookup:

MEASURED lookup-child rollup => {"opp_total":42,"opp_count":2}
Test Files 1 passed (1)
Tests 1 passed (1)

The spec agrees in its own describe text: summaryOperations.relationshipField is "Auto-detected from the child's lookup/master_detail field referencing this object when omitted". This row was not merely inaccurate, it was pushing authors toward a destructive shape — a reader who wants a roll-up and believes it needs master_detail converts an independent-lifecycle relationship into a cascade-delete one. The two adjacent prose bullets that repeated the claim ("No roll-up aggregations needed" under use lookup, "Need roll-up summaries" under use master_detail) were removed for the same reason; the deletion is also what pays for this PR's additions under the ratchet.

The tree row still reads "Not available" and was left alone: a self-referential FK is neither of the two shapes the auto-detection names, and this flight did not measure it. It is recorded as NOT MEASURABLE rather than corrected on inference.

5. master_detail is required only under controlled_by_parent

"Always required" reads as a platform invariant. Measured through ObjectSchema.create() across sharing models:

ACCEPT | sharingModel=(unset) required=undefined | stored required=false
ACCEPT | sharingModel=private required=false | stored required=false
ACCEPT | sharingModel=controlled_by_parent required=undefined | stored required=true
THROW | sharingModel=controlled_by_parent required=false | "declares `required: false` on a
`master_detail` reference under `sharingModel: 'controlled_by_parent'` — a contradiction
with no honest reading."

The controlled_by_parent rows are the control: they prove the probe reaches real enforcement, and they locate exactly where the invariant does hold. Everywhere else a master_detail defaults to required: false and an explicit false is accepted; the only thing that objects is relationship/master-detail-required, which packages/lint/src/data-model-rules.ts:592 emits at severity: 'warning'. The skill's own lint table (in SKILL.md, PR 2's file) already says "warning", so the two documents disagreed with each other and one of them disagreed with the code.

6. Foreign keys are NOT auto-indexed

indexing.md opened with "ObjectStack automatically creates indexes for: … Foreign keys (lookup/master_detail fields)", and repeated it twice ("Foreign keys — Automatic, but verify" under Always Index; "Index foreign keys — Always (automatic in ObjectStack)" under Best Practices). Measured against the driver's own index oracle, for an object carrying both a lookup and a master_detail and no declarations:

uniqueIndexesFromFields(no unique declared) => []
expectedIndexes(same object) => []
control - the same object with one field-level unique:
uniqueIndexesFromFields(email unique) =>
[{"name":"uniq_task_organization_id_email","columns":["organization_id","email"],
"unique":true,"nullSafeColumns":["organization_id"]}]

The control row is the point: the probe is live, and it returns nothing for foreign keys. SqlDriver.syncTableIndexes (sql-driver.ts:11437) is the single entry point and its body is uniqueIndexesFromFields(...) plus the object's declared indexes — there is no third source, and a repo-wide search for any foreign-key indexing path finds none. relationships.md had it right all along ("Index foreign keys — Always create indexes on lookup/master_detail fields"), so the two files in this very diff contradicted each other; indexing.md was the wrong one.

Written to the token ratchet, not around it

All three files sat at zero headroom, so every correction is paid for inside its own file by genuine deletion — the removed roll-up bullets in relationships.md, the removed foreign-key bullet in indexing.md, and tightened wording in field-types.md. No ceiling was raised; that is a maintainer's call, not a dev's.

readingbeforeafter
rules/field-types.md lines428428
rules/relationships.md lines415413
rules/indexing.md lines376375
objectstack-data/** package lines (all .md)49354932
rules/field-types.md tokens (ceiling 3584)35843578
rules/relationships.md tokens (ceiling 3778)37783773
rules/indexing.md tokens (ceiling 3241)32413240

The ratchet's own verdict lines at head 2d9e2a02c:

✓ check-skills-token-ratchet: skills/objectstack-data/rules/field-types.md is 3578 tokens (ceiling 3584; headroom 6).
✓ check-skills-token-ratchet: skills/objectstack-data/rules/indexing.md is 3240 tokens (ceiling 3241; headroom 1).
✓ check-skills-token-ratchet: skills/objectstack-data/rules/relationships.md is 3773 tokens (ceiling 3778; headroom 5).
✓ check-skills-token-ratchet: 38 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted.

Two nuances were not stuffed into ratcheted text and are recorded for a content/docs/** follow-up instead: (a) the fuller foreign-key indexing guidance the corrected bullet compresses — which FK columns actually earn an index, and the composite shapes that serve filter-plus-sort; (b) user field config — multiple: true and defaultValue: 'current_user' are each legal alone but are refused together, with a located message explaining that the runtime token resolves to one scalar and a multi-value field stores an array. The user row lists both keys and does not claim they compose, so it was left alone.

Gates — derived, not recalled, run locally at head 2d9e2a02c

Family derived from the real diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (13 families; harvested with --commands so both invocation spellings survive). 12 green, 1 NOT MEASURED:

EXIT=0 :: node scripts/check-ci-filter-parity.mjs
EXIT=0 :: node scripts/check-cross-package-test-inputs.mjs
EXIT=0 :: node scripts/check-shard-attestation.mjs
EXIT=0 :: node scripts/check-skills-token-ratchet.mjs
EXIT=3 :: node scripts/check-test-completeness.mjs <-- PREREQUISITE NOT MET
EXIT=0 :: pnpm --filter @objectstack/lint run check:doc-formula-expressions
EXIT=0 :: pnpm check:agent-test-spelling
EXIT=0 :: pnpm check:cross-package-test-inputs
EXIT=0 :: pnpm check:doc-authoring
EXIT=0 :: pnpm check:pm-governed-merges
EXIT=0 :: pnpm check:role-word
EXIT=0 :: pnpm check:skill-compatibility
EXIT=0 :: pnpm check:skill-frame-sync

Exit codes were captured before any pipe (each command redirected to its own file first, then read), so these are the gates' statuses and not a tail's.

check-test-completeness.mjs exits 3 and says why in its own words — "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named". Recorded as NOT MEASURED, not as a red.

check:doc-formula-expressions needed @objectstack/formula and @objectstack/lint built first; built, re-run, and it quotes itself:

✓ check:doc-formula-expressions self-test: 58 cases passed
✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 426 files / 1451 TS blocks judged clean by @objectstack/formula.
✓ check:doc-formula-expressions (spec TSDoc, #6763): 9 @example(s) judged clean across 1092 packages/spec/src files
✓ check:doc-formula-expressions (field-level `*When`, #11407): 14 predicate(s) judged clean; 6 skipped as undeterminable.

check-nul-bytes clean: OK (scanned 7555 text file(s) … no raw ASCII control bytes), plus a direct control-byte scan over the three changed files with no hits.

check:skill-examples is NOT MEASURED for this PR, and vacuous for it either way. It refuses to run until @objectstack/client-react is built ("packages/client-react/dist holds no .d.ts declarations … A verdict now would be computed against a build that no longer matches src") — a prerequisite refusal, not a finding, and the refusal is the gate working as designed. Independently, none of this PR's three files carries an os:check marker (measured: 0, 0, 0), so the gate has nothing to say about this diff. It becomes load-bearing in PR 2 (SKILL.md carries 4) and PR 3 (data-hooks.md carries 1), and will be run there against a real build.

ESLint was not run repo-wide, and the narrowing is measured, not assumed. Three readings: (1) the population read from eslint.config.mjs itself — every files: block names {ts,tsx,mts,cts,js,jsx,mjs,cjs}, and the strings .md and markdown appear nowhere in the config; (2) --format json over the diff's three files returns 3 results, 0 errors, and one message each whose ruleId is null and whose text is "File ignored because no matching configuration was supplied."; (3) type-aware linting is not enabled anywhere — the five parserOptions occurrences are all { ecmaVersion, sourceType } and none is project, which the config's own line 327 states in prose — so this diff cannot move any verdict on an untouched file. Zero of the changed files are in that gate's population in either direction.

No changeset

This PR releases nothing from any package — a pure skills/** documentation correction, the repo's live convention for which is no changeset. skip-changeset is applied.

Calibration numbers for the anchor

Counting rule: one claim per assertable proposition about platform behaviour — each type row's existence, each config key attributed to a type, each mapping-table cell, each numbered rule, each behavioural sentence, each asserted example output. Front-matter, section headings, the field-type decision tree (navigation over types already inventoried) and pure style prescriptions are out of the inventory by the anchor's rule; that is ~158 of these 1,219 lines.

314 behavioral claims inventoried across PR 1's three filesfield-types.md 166, relationships.md 77, indexing.md 71. 6 FALSE (1.9%), ~34 NOT MEASURABLE (concentrated in indexing.md's performance heuristics — the "Max Indexes by table size" table, the selectivity folklore, the "more than 5 levels" tree-depth warning — which assert database-tuning judgement no repo artifact settles), the remainder VERIFIED, with executed probes wherever the claim is behaviour-bearing.

Flight ①'s ~8x table concentration did NOT reproduce here, and the reading depends on how "table" is defined — which is itself worth the anchor's attention:

  • By markdown rendering: 2 of 6 falsehoods sit in an actual pipe table (both relationships.md capability-matrix cells) — 2/~150 in-table (1.3%) against 4/~164 elsewhere (2.4%). That is a mild inversion of flight ①'s finding, not an 8x concentration.
  • By content class (what flight ① was really pointing at — assertions about what surfaces exist and what the engine does with them): 5 of 6 are surface/mapping claims — the unknown-key posture, vectorConfig, the FK auto-index list, and the two matrix cells. Only the autonumber tokenizer rule is a pure behavioural rule.

The actionable version for flights ③–⑫ is therefore the content class, not the markdown syntax: point execution at every "what is automatic / what keys exist / what is retired" assertion regardless of whether it is rendered as a table, a bullet list, or a callout. Three of this flight's six falsehoods were in bullet lists and callouts that a table-shaped search would have walked straight past.

A second pattern worth carrying forward: two of the six were caught by cross-file contradiction inside the same package (indexing.md vs relationships.md on FK indexing; relationships.md vs SKILL.md on master_detail requiredness). Neither is a document-vs-document verdict — both were then settled against the implementation — but the disagreement is a cheap, high-yield way to find candidates before spending a probe.

What is not here, on purpose

  • No implementation was touched. Every disagreement was the document being wrong about settled code, not code contradicting itself. No implementation defect was found worth filing from these three files.
  • Probes were written in the worktree, run, and deleted — they are evidence, not commits. None earned a place as a permanent pin: the roll-up-over-lookup behaviour is already pinned by packages/objectql/src/summary-rollup.test.ts, and the field-key posture by packages/spec/src/data/field.test.ts.
  • references/_index.md is generator-owned (pnpm --filter @objectstack/spec gen:skill-refs) and is not in this PR's diff. Its node_modules/@objectstack/spec/src/** pointers were nevertheless spot-measured against a real npm pack --dry-run --json: the tarball carries 209 src/ entries and every .zod.ts path this package points at is present, so the flight-① "unshipped-path pointer" flavour does not apply to @objectstack/spec (it applied to @objectstack/formula, which ships dist only).
  • The performance heuristics in indexing.md were left alone. They are advice, not platform claims, and rewriting advice is not a factual correction.

Generated by Claude Code

…ationships, indexing
Six measured-false behavioral claims corrected against the implementation:
- field-types.md: an unknown field key is REFUSED at parse (unrecognized_keys),
not "silently stripped"; same for an authored vectorConfig.
- field-types.md: a wrong-cased date token ({yyyy}) parses as a {field}
reference and renders EMPTY — it is not emitted literally.
- relationships.md: roll-up summary fields DO work over a lookup child.
- relationships.md: master_detail required is forced only under
sharingModel: 'controlled_by_parent'; elsewhere it is a lint warning.
- indexing.md: foreign keys are NOT auto-indexed.
Token ratchet: all three files shrink (-6 / -5 / -1 = -12).
Part of #13675
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de
@os-zhuang
os-zhuang marked this pull request as ready for review August 31, 2026 07:15
@os-zhuang
os-zhuang enabled auto-merge August 31, 2026 07:15
@os-zhuang
os-zhuang added this pull request to the merge queueAug 31, 2026
@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Aug 31, 2026
@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

Queue dequeue root-caused — not this PR's failure; re-queueing once.

Failing check: merge-queue CI, job Temporal Conformance (live PG + MySQL), run 33367664966 on gh-readonly-queue/main/pr-13687-90578117f: MySQL 8.0.46 refused CREATE TABLE os11565_narrow with [MY-011825] record size will exceed the maximum allowed size — the narrow contrast fixture of sql-driver-11565-row-byte-budget.test.ts, i.e. the table that exists to FIT.

Why it is not this PR's: (1) this diff is three markdown files under skills/objectstack-data/** — zero code, and the whole queue batch ahead of it (#13674) is markdown too; a live-driver conformance job cannot read either; (2) the test is a week old (7e8393262, 2026-08-24), not newly landed; (3) the same job is green on main's own CI at 2cce3fd30 (06:55Z), and the only commits between that head and the queue base 90578117f are two lint fixes and a docs correction — none touches driver-sql or MySQL DDL emission; (4) the narrow fixture being refused points at service-container environment variance on the freshly initialized MySQL (row-format/charset dependent effective budget), not at any change in what the driver emits.

Actions: re-queued via auto-merge as the one legitimate re-run. If the queue fails on the same test a second time, that is real evidence the fixture's byte budget sits too close to an environment-dependent limit — I will file it for the driver lane with both run ids and park this PR on that fix.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo added this pull request to the merge queueAug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33367664966 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Temporal Conformance (live PG + MySQL) — 失败步骤: Run driver-sql suite against both live servers

    × converges — a second detect finds neither the index nor an orphan column 6123ms
    FAIL src/sql-driver-13056-orphan-shadow-column-cleanup.test.ts > a retired shadow-carried UNIQUE leaves no column behind (#13056) > converges — a second detect finds neither the index nor an orphan c
    ↳ 失败原因: Error: Test timed out in 5000ms.
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • src/sql-driver-13056-orphan-shadow-column-cleanup.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 3 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

Correction to my standing-down comment above, prompted by the triage bot's extraction: the failing test was sql-driver-13056-orphan-shadow-column-cleanup.test.ts (「converges — a second detect finds neither the index nor an orphan column」, Test timed out in 5000ms) — a timeout, not an assertion. The os11565_narrow MySQL error I quoted from the service-container log is the row-byte-budget test's deliberately provoked refusal (its whole subject), not the failure; I mis-attributed it.

The verdict is unchanged and now stronger: a 5s timeout in a live-driver convergence test, on a markdown-only diff, first occurrence in 24h per the bot's ledger — load/timing class by the bot's own criterion (「超时…多半是负载/时序,不是本 PR 的回归」). The single re-queue was already spent before this comment and the PR is back in the queue; per the bot's guidance I will NOT re-queue again on a second failure — if the same signature fires once more (here or on another PR, which auto-opens the aggregation issue), the timeout budget of that convergence case goes to the driver lane as a card with both run ids.


Generated by Claude Code

Merged via the queue into main with commit 476ec0aAug 31, 2026
34 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-13675-data-skill-sweep branch August 31, 2026 07:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@zhuangjianguo@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' docs(skills): objectstack-data factual sweep (1/3) — field-types, relationships, indexing by zhuangjianguo · Pull Request #13687 · objectstack-ai/objectstack · GitHub
Skip to content

docs(skills): objectstack-data factual sweep (1/3) — field-types, relationships, indexing - #13687

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-13675-data-skill-sweep
Aug 31, 2026
Merged

docs(skills): objectstack-data factual sweep (1/3) — field-types, relationships, indexing#13687
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-13675-data-skill-sweep

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Part of #13675

Program anchor: #13658 — flight ② of the published-skills factual sweep, PR 1 of up to three at whole-file boundaries. The card and the anchor both stay open; only the last PR in this sequence carries a closing keyword.

Method precedent: PR #13577 (the class proof) and PR #13674 (flight ①, the worked example). Session, for durable attribution: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de

Governed surface — human merge, deliberately parked

skills/** is the published, customer-facing surface. This PR is a draft on purpose: auto-merge is not armed, it is not marked ready, no review is requested, and this seat will not arm it. A draft awaiting the maintainer's own merge is the correct terminal state here, not a stuck PR.

File order, and why

skills/objectstack-data is 12 files / 4,935 lines. Flight ①'s calibration says to point execution at surfaces and mapping tables first, so PR 1 takes the three rule files with the highest table density and the most engine-asserting columns: rules/field-types.md (49 type rows, each with a config column), rules/relationships.md (the lookup-vs-master_detail capability matrix), rules/indexing.md (the declaration surface and the "what is automatic" list). SKILL.md follows in PR 2, the hooks reference and the remaining rule files in PR 3.

Non-vacuity control — claims proven TRUE by execution

1. The field-type roster.rules/field-types.md says "49 available options" and enumerates them across ten category tables. Probed against FieldType from @objectstack/spec/data:

FIELD TYPE COUNT = 49
SKILL LISTED COUNT = 49
IN SPEC NOT IN SKILL: []
IN SKILL NOT IN SPEC: []

2. The ADR-0120 unique-scope materialization. The unique-scope table states that unique: 'organization' materializes as (COALESCE(organization_id, '__global__'), …fields). Executed against organizationKeyPartSql from @objectstack/driver-sql:

organizationKeyPartSql => "COALESCE(organization_id, '__global__')"

3. The auto-generated index-name pattern. Stated as idx_{object}_{field1}_{field2}_{...}. Executed against buildIndexName:

buildIndexName(account, [status,created_at], unique=false) => idx_account_status_created_at
buildIndexName(task, [a,b,c], unique=false) => idx_task_a_b_c

4. The autonumber token table, row by row. Executed against parseAutonumberFormat + renderAutonumber at a pinned now of 2026-06-17 UTC:

CASE-{0000} seq=42 => CASE-0042 (table claims CASE-0042)
AD{YYYYMMDD}{0000} seq=1 => AD202606170001 (table claims AD202606170001)
{plan_no}{000} plan_no='PLAN-001' seq=1=> PLAN-001001 (table claims PLAN-001001)
{000} seq=1 / 999 / 1000 => 001 / 999 / 1000 (rule 3: minimum width, grows)
'{a}{b}{000}' scope for (AB,C) => "ABC"
'{a}{b}{000}' scope for (A,BC) => "ABC" (rule 2: shared counter)
'{a}-{b}{000}' scope for (AB,C)/(A,BC) => "AB-C" / "A-BC" (rule 2: delimiter separates)
'CASE-{0000}' scope => "" (fixed prefix = one global counter)

5. Per-type config columns. All 49 rows' config keys were parsed through FieldSchema; every claimed key was accepted and retained on its own type (text → maxLength/minLength/defaultValue, number → min/max/precision/scale, lookup → reference/lookupFilters/multiple/deleteBehavior, slider → min/max/step, vector → dimensions, and so on). Enum columns were pinned by rejection as well as acceptance: returnType refuses 'currency' ("expected one of number|text|boolean|date"), summaryOperations.function refuses 'median', currencyMode refuses anything but dynamic|fixed, and a field-level unique: 'tenant' / 'org' is refused by name with the ADR-0120 message.

The six FALSE claims, and what measured them

落点beforeafter
field-types.md L6-8"an unknown field key is silently stripped at parse (dead metadata), so don't invent config like …""an unknown field key is REFUSED at parse (unrecognized_keys), so don't invent …"
field-types.md L357"an authored vectorConfig is silently stripped (dead metadata)""an authored vectorConfig is refused at parse"
field-types.md L346"Date tokens are case-sensitive and exact ({YYYY}, not {yyyy} or {YYYY-MM}). An unrecognized {...} is emitted literally into the number""Date tokens are case-sensitive{yyyy} parses as a {field} reference and renders empty (rule 1 applies). Only a spelling no field could have — { YYYY }, {YYYY-MM} — is emitted literally."
relationships.md L9lookup row, Roll-ups column — "Not available""Supported via summary"
relationships.md L10master_detail row, Required column — "Always required""Forced only under controlled_by_parent; else lint-warned"
indexing.md L9"- Foreign keys (lookup/master_detail fields)" listed under "ObjectStack automatically creates indexes for" (restated twice more)bullet deleted; "- Field-level uniquenot foreign keys: declare those"

1 and 2. Unknown field keys are refused, not silently stripped

This is the single highest-consequence correction in the PR, because it inverts the cost of a typo for an AI author: "dead metadata" says a misspelled key is harmless, when it actually stops the build. Measured through FieldSchema.safeParse:

REJECT | text + theme (invented) | unrecognized_keys
REJECT | text + rows (invented) | unrecognized_keys
REJECT | file + fileAttachmentConfig (invented) | unrecognized_keys
REJECT | vector + vectorConfig | unrecognized_keys

The error's own text names the retired behaviour the skill was still describing:

Unrecognized key(s) on this field: `theme`. Until this shape was closed these were
dropped silently — the field was still created, minus whatever the key was meant to
constrain, protect or compute.

So the silent-strip sentence was true once and is now exactly backwards. The same pass re-measured the genuinely-retired keys and confirms they are refused with their migration prescription rather than dropped: referenceFilters ("removed in the 16.x line … Use lookupFilters"), encryptionConfig ("pruned in 2026-06 … The real channel is type: 'secret'"), columnName, and conditionalRequired ("removed in @objectstack/spec 17 — use requiredWhen"). Those four sentences in the skill are correct and were left alone.

3. A wrong-cased date token renders EMPTY, not literally

The rule named {yyyy} as an unrecognized spelling and then promised unrecognized spellings appear literally. Both halves cannot hold, and the measurement says the second one fails on exactly the example the first one names:

{ YYYY } tokens=[{"kind":"literal","text":"{ YYYY }"}]
{YYYY-MM} tokens=[{"kind":"literal","text":"{YYYY-MM}"}]
{yyyy} tokens=[{"kind":"field","field":"yyyy"}] <-- a FIELD reference
{YYYY} tokens=[{"kind":"date","pattern":"YYYY"}]
render "{yyyy}-{000}" with no field named yyyy => "-001"
referencedFields("{yyyy}-{000}") => ["yyyy"]
missingFieldValues("{yyyy}-{000}", {}) => ["yyyy"]

{yyyy} is a legal field-name spelling, so the tokenizer reads it as {field_name}; only a spelling no field could carry (a space, a hyphen) falls back to a literal. The practical consequence is worse than a cosmetic difference: the year silently vanishes from the record number, and the format now carries a phantom {field} reference, which puts it under rule 1 of the same list ("Every {field} you interpolate must be required: true" — an empty interpolated field throws at create). The replacement states the tokenizer's actual boundary and points back at rule 1.

4. Roll-up summaries DO work over a lookup child

The capability matrix said roll-ups are "Not available" on lookup and "Supported via summary" only on master_detail. Measured with the engine and the roll-up test's own driver stub — a parent summary over a child whose FK to the parent is a plain lookup:

MEASURED lookup-child rollup => {"opp_total":42,"opp_count":2}
Test Files 1 passed (1)
Tests 1 passed (1)

The spec agrees in its own describe text: summaryOperations.relationshipField is "Auto-detected from the child's lookup/master_detail field referencing this object when omitted". This row was not merely inaccurate, it was pushing authors toward a destructive shape — a reader who wants a roll-up and believes it needs master_detail converts an independent-lifecycle relationship into a cascade-delete one. The two adjacent prose bullets that repeated the claim ("No roll-up aggregations needed" under use lookup, "Need roll-up summaries" under use master_detail) were removed for the same reason; the deletion is also what pays for this PR's additions under the ratchet.

The tree row still reads "Not available" and was left alone: a self-referential FK is neither of the two shapes the auto-detection names, and this flight did not measure it. It is recorded as NOT MEASURABLE rather than corrected on inference.

5. master_detail is required only under controlled_by_parent

"Always required" reads as a platform invariant. Measured through ObjectSchema.create() across sharing models:

ACCEPT | sharingModel=(unset) required=undefined | stored required=false
ACCEPT | sharingModel=private required=false | stored required=false
ACCEPT | sharingModel=controlled_by_parent required=undefined | stored required=true
THROW | sharingModel=controlled_by_parent required=false | "declares `required: false` on a
`master_detail` reference under `sharingModel: 'controlled_by_parent'` — a contradiction
with no honest reading."

The controlled_by_parent rows are the control: they prove the probe reaches real enforcement, and they locate exactly where the invariant does hold. Everywhere else a master_detail defaults to required: false and an explicit false is accepted; the only thing that objects is relationship/master-detail-required, which packages/lint/src/data-model-rules.ts:592 emits at severity: 'warning'. The skill's own lint table (in SKILL.md, PR 2's file) already says "warning", so the two documents disagreed with each other and one of them disagreed with the code.

6. Foreign keys are NOT auto-indexed

indexing.md opened with "ObjectStack automatically creates indexes for: … Foreign keys (lookup/master_detail fields)", and repeated it twice ("Foreign keys — Automatic, but verify" under Always Index; "Index foreign keys — Always (automatic in ObjectStack)" under Best Practices). Measured against the driver's own index oracle, for an object carrying both a lookup and a master_detail and no declarations:

uniqueIndexesFromFields(no unique declared) => []
expectedIndexes(same object) => []
control - the same object with one field-level unique:
uniqueIndexesFromFields(email unique) =>
[{"name":"uniq_task_organization_id_email","columns":["organization_id","email"],
"unique":true,"nullSafeColumns":["organization_id"]}]

The control row is the point: the probe is live, and it returns nothing for foreign keys. SqlDriver.syncTableIndexes (sql-driver.ts:11437) is the single entry point and its body is uniqueIndexesFromFields(...) plus the object's declared indexes — there is no third source, and a repo-wide search for any foreign-key indexing path finds none. relationships.md had it right all along ("Index foreign keys — Always create indexes on lookup/master_detail fields"), so the two files in this very diff contradicted each other; indexing.md was the wrong one.

Written to the token ratchet, not around it

All three files sat at zero headroom, so every correction is paid for inside its own file by genuine deletion — the removed roll-up bullets in relationships.md, the removed foreign-key bullet in indexing.md, and tightened wording in field-types.md. No ceiling was raised; that is a maintainer's call, not a dev's.

readingbeforeafter
rules/field-types.md lines428428
rules/relationships.md lines415413
rules/indexing.md lines376375
objectstack-data/** package lines (all .md)49354932
rules/field-types.md tokens (ceiling 3584)35843578
rules/relationships.md tokens (ceiling 3778)37783773
rules/indexing.md tokens (ceiling 3241)32413240

The ratchet's own verdict lines at head 2d9e2a02c:

✓ check-skills-token-ratchet: skills/objectstack-data/rules/field-types.md is 3578 tokens (ceiling 3584; headroom 6).
✓ check-skills-token-ratchet: skills/objectstack-data/rules/indexing.md is 3240 tokens (ceiling 3241; headroom 1).
✓ check-skills-token-ratchet: skills/objectstack-data/rules/relationships.md is 3773 tokens (ceiling 3778; headroom 5).
✓ check-skills-token-ratchet: 38 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted.

Two nuances were not stuffed into ratcheted text and are recorded for a content/docs/** follow-up instead: (a) the fuller foreign-key indexing guidance the corrected bullet compresses — which FK columns actually earn an index, and the composite shapes that serve filter-plus-sort; (b) user field config — multiple: true and defaultValue: 'current_user' are each legal alone but are refused together, with a located message explaining that the runtime token resolves to one scalar and a multi-value field stores an array. The user row lists both keys and does not claim they compose, so it was left alone.

Gates — derived, not recalled, run locally at head 2d9e2a02c

Family derived from the real diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (13 families; harvested with --commands so both invocation spellings survive). 12 green, 1 NOT MEASURED:

EXIT=0 :: node scripts/check-ci-filter-parity.mjs
EXIT=0 :: node scripts/check-cross-package-test-inputs.mjs
EXIT=0 :: node scripts/check-shard-attestation.mjs
EXIT=0 :: node scripts/check-skills-token-ratchet.mjs
EXIT=3 :: node scripts/check-test-completeness.mjs <-- PREREQUISITE NOT MET
EXIT=0 :: pnpm --filter @objectstack/lint run check:doc-formula-expressions
EXIT=0 :: pnpm check:agent-test-spelling
EXIT=0 :: pnpm check:cross-package-test-inputs
EXIT=0 :: pnpm check:doc-authoring
EXIT=0 :: pnpm check:pm-governed-merges
EXIT=0 :: pnpm check:role-word
EXIT=0 :: pnpm check:skill-compatibility
EXIT=0 :: pnpm check:skill-frame-sync

Exit codes were captured before any pipe (each command redirected to its own file first, then read), so these are the gates' statuses and not a tail's.

check-test-completeness.mjs exits 3 and says why in its own words — "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named". Recorded as NOT MEASURED, not as a red.

check:doc-formula-expressions needed @objectstack/formula and @objectstack/lint built first; built, re-run, and it quotes itself:

✓ check:doc-formula-expressions self-test: 58 cases passed
✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 426 files / 1451 TS blocks judged clean by @objectstack/formula.
✓ check:doc-formula-expressions (spec TSDoc, #6763): 9 @example(s) judged clean across 1092 packages/spec/src files
✓ check:doc-formula-expressions (field-level `*When`, #11407): 14 predicate(s) judged clean; 6 skipped as undeterminable.

check-nul-bytes clean: OK (scanned 7555 text file(s) … no raw ASCII control bytes), plus a direct control-byte scan over the three changed files with no hits.

check:skill-examples is NOT MEASURED for this PR, and vacuous for it either way. It refuses to run until @objectstack/client-react is built ("packages/client-react/dist holds no .d.ts declarations … A verdict now would be computed against a build that no longer matches src") — a prerequisite refusal, not a finding, and the refusal is the gate working as designed. Independently, none of this PR's three files carries an os:check marker (measured: 0, 0, 0), so the gate has nothing to say about this diff. It becomes load-bearing in PR 2 (SKILL.md carries 4) and PR 3 (data-hooks.md carries 1), and will be run there against a real build.

ESLint was not run repo-wide, and the narrowing is measured, not assumed. Three readings: (1) the population read from eslint.config.mjs itself — every files: block names {ts,tsx,mts,cts,js,jsx,mjs,cjs}, and the strings .md and markdown appear nowhere in the config; (2) --format json over the diff's three files returns 3 results, 0 errors, and one message each whose ruleId is null and whose text is "File ignored because no matching configuration was supplied."; (3) type-aware linting is not enabled anywhere — the five parserOptions occurrences are all { ecmaVersion, sourceType } and none is project, which the config's own line 327 states in prose — so this diff cannot move any verdict on an untouched file. Zero of the changed files are in that gate's population in either direction.

No changeset

This PR releases nothing from any package — a pure skills/** documentation correction, the repo's live convention for which is no changeset. skip-changeset is applied.

Calibration numbers for the anchor

Counting rule: one claim per assertable proposition about platform behaviour — each type row's existence, each config key attributed to a type, each mapping-table cell, each numbered rule, each behavioural sentence, each asserted example output. Front-matter, section headings, the field-type decision tree (navigation over types already inventoried) and pure style prescriptions are out of the inventory by the anchor's rule; that is ~158 of these 1,219 lines.

314 behavioral claims inventoried across PR 1's three filesfield-types.md 166, relationships.md 77, indexing.md 71. 6 FALSE (1.9%), ~34 NOT MEASURABLE (concentrated in indexing.md's performance heuristics — the "Max Indexes by table size" table, the selectivity folklore, the "more than 5 levels" tree-depth warning — which assert database-tuning judgement no repo artifact settles), the remainder VERIFIED, with executed probes wherever the claim is behaviour-bearing.

Flight ①'s ~8x table concentration did NOT reproduce here, and the reading depends on how "table" is defined — which is itself worth the anchor's attention:

  • By markdown rendering: 2 of 6 falsehoods sit in an actual pipe table (both relationships.md capability-matrix cells) — 2/~150 in-table (1.3%) against 4/~164 elsewhere (2.4%). That is a mild inversion of flight ①'s finding, not an 8x concentration.
  • By content class (what flight ① was really pointing at — assertions about what surfaces exist and what the engine does with them): 5 of 6 are surface/mapping claims — the unknown-key posture, vectorConfig, the FK auto-index list, and the two matrix cells. Only the autonumber tokenizer rule is a pure behavioural rule.

The actionable version for flights ③–⑫ is therefore the content class, not the markdown syntax: point execution at every "what is automatic / what keys exist / what is retired" assertion regardless of whether it is rendered as a table, a bullet list, or a callout. Three of this flight's six falsehoods were in bullet lists and callouts that a table-shaped search would have walked straight past.

A second pattern worth carrying forward: two of the six were caught by cross-file contradiction inside the same package (indexing.md vs relationships.md on FK indexing; relationships.md vs SKILL.md on master_detail requiredness). Neither is a document-vs-document verdict — both were then settled against the implementation — but the disagreement is a cheap, high-yield way to find candidates before spending a probe.

What is not here, on purpose

  • No implementation was touched. Every disagreement was the document being wrong about settled code, not code contradicting itself. No implementation defect was found worth filing from these three files.
  • Probes were written in the worktree, run, and deleted — they are evidence, not commits. None earned a place as a permanent pin: the roll-up-over-lookup behaviour is already pinned by packages/objectql/src/summary-rollup.test.ts, and the field-key posture by packages/spec/src/data/field.test.ts.
  • references/_index.md is generator-owned (pnpm --filter @objectstack/spec gen:skill-refs) and is not in this PR's diff. Its node_modules/@objectstack/spec/src/** pointers were nevertheless spot-measured against a real npm pack --dry-run --json: the tarball carries 209 src/ entries and every .zod.ts path this package points at is present, so the flight-① "unshipped-path pointer" flavour does not apply to @objectstack/spec (it applied to @objectstack/formula, which ships dist only).
  • The performance heuristics in indexing.md were left alone. They are advice, not platform claims, and rewriting advice is not a factual correction.

Generated by Claude Code

…ationships, indexing
Six measured-false behavioral claims corrected against the implementation:
- field-types.md: an unknown field key is REFUSED at parse (unrecognized_keys),
not "silently stripped"; same for an authored vectorConfig.
- field-types.md: a wrong-cased date token ({yyyy}) parses as a {field}
reference and renders EMPTY — it is not emitted literally.
- relationships.md: roll-up summary fields DO work over a lookup child.
- relationships.md: master_detail required is forced only under
sharingModel: 'controlled_by_parent'; elsewhere it is a lint warning.
- indexing.md: foreign keys are NOT auto-indexed.
Token ratchet: all three files shrink (-6 / -5 / -1 = -12).
Part of #13675
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de
@os-zhuang
os-zhuang marked this pull request as ready for review August 31, 2026 07:15
@os-zhuang
os-zhuang enabled auto-merge August 31, 2026 07:15
@os-zhuang
os-zhuang added this pull request to the merge queueAug 31, 2026
@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Aug 31, 2026
@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

Queue dequeue root-caused — not this PR's failure; re-queueing once.

Failing check: merge-queue CI, job Temporal Conformance (live PG + MySQL), run 33367664966 on gh-readonly-queue/main/pr-13687-90578117f: MySQL 8.0.46 refused CREATE TABLE os11565_narrow with [MY-011825] record size will exceed the maximum allowed size — the narrow contrast fixture of sql-driver-11565-row-byte-budget.test.ts, i.e. the table that exists to FIT.

Why it is not this PR's: (1) this diff is three markdown files under skills/objectstack-data/** — zero code, and the whole queue batch ahead of it (#13674) is markdown too; a live-driver conformance job cannot read either; (2) the test is a week old (7e8393262, 2026-08-24), not newly landed; (3) the same job is green on main's own CI at 2cce3fd30 (06:55Z), and the only commits between that head and the queue base 90578117f are two lint fixes and a docs correction — none touches driver-sql or MySQL DDL emission; (4) the narrow fixture being refused points at service-container environment variance on the freshly initialized MySQL (row-format/charset dependent effective budget), not at any change in what the driver emits.

Actions: re-queued via auto-merge as the one legitimate re-run. If the queue fails on the same test a second time, that is real evidence the fixture's byte budget sits too close to an environment-dependent limit — I will file it for the driver lane with both run ids and park this PR on that fix.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo added this pull request to the merge queueAug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33367664966 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Temporal Conformance (live PG + MySQL) — 失败步骤: Run driver-sql suite against both live servers

    × converges — a second detect finds neither the index nor an orphan column 6123ms
    FAIL src/sql-driver-13056-orphan-shadow-column-cleanup.test.ts > a retired shadow-carried UNIQUE leaves no column behind (#13056) > converges — a second detect finds neither the index nor an orphan c
    ↳ 失败原因: Error: Test timed out in 5000ms.
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • src/sql-driver-13056-orphan-shadow-column-cleanup.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 3 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

Correction to my standing-down comment above, prompted by the triage bot's extraction: the failing test was sql-driver-13056-orphan-shadow-column-cleanup.test.ts (「converges — a second detect finds neither the index nor an orphan column」, Test timed out in 5000ms) — a timeout, not an assertion. The os11565_narrow MySQL error I quoted from the service-container log is the row-byte-budget test's deliberately provoked refusal (its whole subject), not the failure; I mis-attributed it.

The verdict is unchanged and now stronger: a 5s timeout in a live-driver convergence test, on a markdown-only diff, first occurrence in 24h per the bot's ledger — load/timing class by the bot's own criterion (「超时…多半是负载/时序,不是本 PR 的回归」). The single re-queue was already spent before this comment and the PR is back in the queue; per the bot's guidance I will NOT re-queue again on a second failure — if the same signature fires once more (here or on another PR, which auto-opens the aggregation issue), the timeout budget of that convergence case goes to the driver lane as a card with both run ids.


Generated by Claude Code

Merged via the queue into main with commit 476ec0aAug 31, 2026
34 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-13675-data-skill-sweep branch August 31, 2026 07:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@zhuangjianguo@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' docs(skills): objectstack-data factual sweep (1/3) — field-types, relationships, indexing by zhuangjianguo · Pull Request #13687 · objectstack-ai/objectstack · GitHub
Skip to content

docs(skills): objectstack-data factual sweep (1/3) — field-types, relationships, indexing - #13687

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-13675-data-skill-sweep
Aug 31, 2026
Merged

docs(skills): objectstack-data factual sweep (1/3) — field-types, relationships, indexing#13687
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-13675-data-skill-sweep

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Part of #13675

Program anchor: #13658 — flight ② of the published-skills factual sweep, PR 1 of up to three at whole-file boundaries. The card and the anchor both stay open; only the last PR in this sequence carries a closing keyword.

Method precedent: PR #13577 (the class proof) and PR #13674 (flight ①, the worked example). Session, for durable attribution: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de

Governed surface — human merge, deliberately parked

skills/** is the published, customer-facing surface. This PR is a draft on purpose: auto-merge is not armed, it is not marked ready, no review is requested, and this seat will not arm it. A draft awaiting the maintainer's own merge is the correct terminal state here, not a stuck PR.

File order, and why

skills/objectstack-data is 12 files / 4,935 lines. Flight ①'s calibration says to point execution at surfaces and mapping tables first, so PR 1 takes the three rule files with the highest table density and the most engine-asserting columns: rules/field-types.md (49 type rows, each with a config column), rules/relationships.md (the lookup-vs-master_detail capability matrix), rules/indexing.md (the declaration surface and the "what is automatic" list). SKILL.md follows in PR 2, the hooks reference and the remaining rule files in PR 3.

Non-vacuity control — claims proven TRUE by execution

1. The field-type roster.rules/field-types.md says "49 available options" and enumerates them across ten category tables. Probed against FieldType from @objectstack/spec/data:

FIELD TYPE COUNT = 49
SKILL LISTED COUNT = 49
IN SPEC NOT IN SKILL: []
IN SKILL NOT IN SPEC: []

2. The ADR-0120 unique-scope materialization. The unique-scope table states that unique: 'organization' materializes as (COALESCE(organization_id, '__global__'), …fields). Executed against organizationKeyPartSql from @objectstack/driver-sql:

organizationKeyPartSql => "COALESCE(organization_id, '__global__')"

3. The auto-generated index-name pattern. Stated as idx_{object}_{field1}_{field2}_{...}. Executed against buildIndexName:

buildIndexName(account, [status,created_at], unique=false) => idx_account_status_created_at
buildIndexName(task, [a,b,c], unique=false) => idx_task_a_b_c

4. The autonumber token table, row by row. Executed against parseAutonumberFormat + renderAutonumber at a pinned now of 2026-06-17 UTC:

CASE-{0000} seq=42 => CASE-0042 (table claims CASE-0042)
AD{YYYYMMDD}{0000} seq=1 => AD202606170001 (table claims AD202606170001)
{plan_no}{000} plan_no='PLAN-001' seq=1=> PLAN-001001 (table claims PLAN-001001)
{000} seq=1 / 999 / 1000 => 001 / 999 / 1000 (rule 3: minimum width, grows)
'{a}{b}{000}' scope for (AB,C) => "ABC"
'{a}{b}{000}' scope for (A,BC) => "ABC" (rule 2: shared counter)
'{a}-{b}{000}' scope for (AB,C)/(A,BC) => "AB-C" / "A-BC" (rule 2: delimiter separates)
'CASE-{0000}' scope => "" (fixed prefix = one global counter)

5. Per-type config columns. All 49 rows' config keys were parsed through FieldSchema; every claimed key was accepted and retained on its own type (text → maxLength/minLength/defaultValue, number → min/max/precision/scale, lookup → reference/lookupFilters/multiple/deleteBehavior, slider → min/max/step, vector → dimensions, and so on). Enum columns were pinned by rejection as well as acceptance: returnType refuses 'currency' ("expected one of number|text|boolean|date"), summaryOperations.function refuses 'median', currencyMode refuses anything but dynamic|fixed, and a field-level unique: 'tenant' / 'org' is refused by name with the ADR-0120 message.

The six FALSE claims, and what measured them

落点beforeafter
field-types.md L6-8"an unknown field key is silently stripped at parse (dead metadata), so don't invent config like …""an unknown field key is REFUSED at parse (unrecognized_keys), so don't invent …"
field-types.md L357"an authored vectorConfig is silently stripped (dead metadata)""an authored vectorConfig is refused at parse"
field-types.md L346"Date tokens are case-sensitive and exact ({YYYY}, not {yyyy} or {YYYY-MM}). An unrecognized {...} is emitted literally into the number""Date tokens are case-sensitive{yyyy} parses as a {field} reference and renders empty (rule 1 applies). Only a spelling no field could have — { YYYY }, {YYYY-MM} — is emitted literally."
relationships.md L9lookup row, Roll-ups column — "Not available""Supported via summary"
relationships.md L10master_detail row, Required column — "Always required""Forced only under controlled_by_parent; else lint-warned"
indexing.md L9"- Foreign keys (lookup/master_detail fields)" listed under "ObjectStack automatically creates indexes for" (restated twice more)bullet deleted; "- Field-level uniquenot foreign keys: declare those"

1 and 2. Unknown field keys are refused, not silently stripped

This is the single highest-consequence correction in the PR, because it inverts the cost of a typo for an AI author: "dead metadata" says a misspelled key is harmless, when it actually stops the build. Measured through FieldSchema.safeParse:

REJECT | text + theme (invented) | unrecognized_keys
REJECT | text + rows (invented) | unrecognized_keys
REJECT | file + fileAttachmentConfig (invented) | unrecognized_keys
REJECT | vector + vectorConfig | unrecognized_keys

The error's own text names the retired behaviour the skill was still describing:

Unrecognized key(s) on this field: `theme`. Until this shape was closed these were
dropped silently — the field was still created, minus whatever the key was meant to
constrain, protect or compute.

So the silent-strip sentence was true once and is now exactly backwards. The same pass re-measured the genuinely-retired keys and confirms they are refused with their migration prescription rather than dropped: referenceFilters ("removed in the 16.x line … Use lookupFilters"), encryptionConfig ("pruned in 2026-06 … The real channel is type: 'secret'"), columnName, and conditionalRequired ("removed in @objectstack/spec 17 — use requiredWhen"). Those four sentences in the skill are correct and were left alone.

3. A wrong-cased date token renders EMPTY, not literally

The rule named {yyyy} as an unrecognized spelling and then promised unrecognized spellings appear literally. Both halves cannot hold, and the measurement says the second one fails on exactly the example the first one names:

{ YYYY } tokens=[{"kind":"literal","text":"{ YYYY }"}]
{YYYY-MM} tokens=[{"kind":"literal","text":"{YYYY-MM}"}]
{yyyy} tokens=[{"kind":"field","field":"yyyy"}] <-- a FIELD reference
{YYYY} tokens=[{"kind":"date","pattern":"YYYY"}]
render "{yyyy}-{000}" with no field named yyyy => "-001"
referencedFields("{yyyy}-{000}") => ["yyyy"]
missingFieldValues("{yyyy}-{000}", {}) => ["yyyy"]

{yyyy} is a legal field-name spelling, so the tokenizer reads it as {field_name}; only a spelling no field could carry (a space, a hyphen) falls back to a literal. The practical consequence is worse than a cosmetic difference: the year silently vanishes from the record number, and the format now carries a phantom {field} reference, which puts it under rule 1 of the same list ("Every {field} you interpolate must be required: true" — an empty interpolated field throws at create). The replacement states the tokenizer's actual boundary and points back at rule 1.

4. Roll-up summaries DO work over a lookup child

The capability matrix said roll-ups are "Not available" on lookup and "Supported via summary" only on master_detail. Measured with the engine and the roll-up test's own driver stub — a parent summary over a child whose FK to the parent is a plain lookup:

MEASURED lookup-child rollup => {"opp_total":42,"opp_count":2}
Test Files 1 passed (1)
Tests 1 passed (1)

The spec agrees in its own describe text: summaryOperations.relationshipField is "Auto-detected from the child's lookup/master_detail field referencing this object when omitted". This row was not merely inaccurate, it was pushing authors toward a destructive shape — a reader who wants a roll-up and believes it needs master_detail converts an independent-lifecycle relationship into a cascade-delete one. The two adjacent prose bullets that repeated the claim ("No roll-up aggregations needed" under use lookup, "Need roll-up summaries" under use master_detail) were removed for the same reason; the deletion is also what pays for this PR's additions under the ratchet.

The tree row still reads "Not available" and was left alone: a self-referential FK is neither of the two shapes the auto-detection names, and this flight did not measure it. It is recorded as NOT MEASURABLE rather than corrected on inference.

5. master_detail is required only under controlled_by_parent

"Always required" reads as a platform invariant. Measured through ObjectSchema.create() across sharing models:

ACCEPT | sharingModel=(unset) required=undefined | stored required=false
ACCEPT | sharingModel=private required=false | stored required=false
ACCEPT | sharingModel=controlled_by_parent required=undefined | stored required=true
THROW | sharingModel=controlled_by_parent required=false | "declares `required: false` on a
`master_detail` reference under `sharingModel: 'controlled_by_parent'` — a contradiction
with no honest reading."

The controlled_by_parent rows are the control: they prove the probe reaches real enforcement, and they locate exactly where the invariant does hold. Everywhere else a master_detail defaults to required: false and an explicit false is accepted; the only thing that objects is relationship/master-detail-required, which packages/lint/src/data-model-rules.ts:592 emits at severity: 'warning'. The skill's own lint table (in SKILL.md, PR 2's file) already says "warning", so the two documents disagreed with each other and one of them disagreed with the code.

6. Foreign keys are NOT auto-indexed

indexing.md opened with "ObjectStack automatically creates indexes for: … Foreign keys (lookup/master_detail fields)", and repeated it twice ("Foreign keys — Automatic, but verify" under Always Index; "Index foreign keys — Always (automatic in ObjectStack)" under Best Practices). Measured against the driver's own index oracle, for an object carrying both a lookup and a master_detail and no declarations:

uniqueIndexesFromFields(no unique declared) => []
expectedIndexes(same object) => []
control - the same object with one field-level unique:
uniqueIndexesFromFields(email unique) =>
[{"name":"uniq_task_organization_id_email","columns":["organization_id","email"],
"unique":true,"nullSafeColumns":["organization_id"]}]

The control row is the point: the probe is live, and it returns nothing for foreign keys. SqlDriver.syncTableIndexes (sql-driver.ts:11437) is the single entry point and its body is uniqueIndexesFromFields(...) plus the object's declared indexes — there is no third source, and a repo-wide search for any foreign-key indexing path finds none. relationships.md had it right all along ("Index foreign keys — Always create indexes on lookup/master_detail fields"), so the two files in this very diff contradicted each other; indexing.md was the wrong one.

Written to the token ratchet, not around it

All three files sat at zero headroom, so every correction is paid for inside its own file by genuine deletion — the removed roll-up bullets in relationships.md, the removed foreign-key bullet in indexing.md, and tightened wording in field-types.md. No ceiling was raised; that is a maintainer's call, not a dev's.

readingbeforeafter
rules/field-types.md lines428428
rules/relationships.md lines415413
rules/indexing.md lines376375
objectstack-data/** package lines (all .md)49354932
rules/field-types.md tokens (ceiling 3584)35843578
rules/relationships.md tokens (ceiling 3778)37783773
rules/indexing.md tokens (ceiling 3241)32413240

The ratchet's own verdict lines at head 2d9e2a02c:

✓ check-skills-token-ratchet: skills/objectstack-data/rules/field-types.md is 3578 tokens (ceiling 3584; headroom 6).
✓ check-skills-token-ratchet: skills/objectstack-data/rules/indexing.md is 3240 tokens (ceiling 3241; headroom 1).
✓ check-skills-token-ratchet: skills/objectstack-data/rules/relationships.md is 3773 tokens (ceiling 3778; headroom 5).
✓ check-skills-token-ratchet: 38 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted.

Two nuances were not stuffed into ratcheted text and are recorded for a content/docs/** follow-up instead: (a) the fuller foreign-key indexing guidance the corrected bullet compresses — which FK columns actually earn an index, and the composite shapes that serve filter-plus-sort; (b) user field config — multiple: true and defaultValue: 'current_user' are each legal alone but are refused together, with a located message explaining that the runtime token resolves to one scalar and a multi-value field stores an array. The user row lists both keys and does not claim they compose, so it was left alone.

Gates — derived, not recalled, run locally at head 2d9e2a02c

Family derived from the real diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (13 families; harvested with --commands so both invocation spellings survive). 12 green, 1 NOT MEASURED:

EXIT=0 :: node scripts/check-ci-filter-parity.mjs
EXIT=0 :: node scripts/check-cross-package-test-inputs.mjs
EXIT=0 :: node scripts/check-shard-attestation.mjs
EXIT=0 :: node scripts/check-skills-token-ratchet.mjs
EXIT=3 :: node scripts/check-test-completeness.mjs <-- PREREQUISITE NOT MET
EXIT=0 :: pnpm --filter @objectstack/lint run check:doc-formula-expressions
EXIT=0 :: pnpm check:agent-test-spelling
EXIT=0 :: pnpm check:cross-package-test-inputs
EXIT=0 :: pnpm check:doc-authoring
EXIT=0 :: pnpm check:pm-governed-merges
EXIT=0 :: pnpm check:role-word
EXIT=0 :: pnpm check:skill-compatibility
EXIT=0 :: pnpm check:skill-frame-sync

Exit codes were captured before any pipe (each command redirected to its own file first, then read), so these are the gates' statuses and not a tail's.

check-test-completeness.mjs exits 3 and says why in its own words — "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named". Recorded as NOT MEASURED, not as a red.

check:doc-formula-expressions needed @objectstack/formula and @objectstack/lint built first; built, re-run, and it quotes itself:

✓ check:doc-formula-expressions self-test: 58 cases passed
✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 426 files / 1451 TS blocks judged clean by @objectstack/formula.
✓ check:doc-formula-expressions (spec TSDoc, #6763): 9 @example(s) judged clean across 1092 packages/spec/src files
✓ check:doc-formula-expressions (field-level `*When`, #11407): 14 predicate(s) judged clean; 6 skipped as undeterminable.

check-nul-bytes clean: OK (scanned 7555 text file(s) … no raw ASCII control bytes), plus a direct control-byte scan over the three changed files with no hits.

check:skill-examples is NOT MEASURED for this PR, and vacuous for it either way. It refuses to run until @objectstack/client-react is built ("packages/client-react/dist holds no .d.ts declarations … A verdict now would be computed against a build that no longer matches src") — a prerequisite refusal, not a finding, and the refusal is the gate working as designed. Independently, none of this PR's three files carries an os:check marker (measured: 0, 0, 0), so the gate has nothing to say about this diff. It becomes load-bearing in PR 2 (SKILL.md carries 4) and PR 3 (data-hooks.md carries 1), and will be run there against a real build.

ESLint was not run repo-wide, and the narrowing is measured, not assumed. Three readings: (1) the population read from eslint.config.mjs itself — every files: block names {ts,tsx,mts,cts,js,jsx,mjs,cjs}, and the strings .md and markdown appear nowhere in the config; (2) --format json over the diff's three files returns 3 results, 0 errors, and one message each whose ruleId is null and whose text is "File ignored because no matching configuration was supplied."; (3) type-aware linting is not enabled anywhere — the five parserOptions occurrences are all { ecmaVersion, sourceType } and none is project, which the config's own line 327 states in prose — so this diff cannot move any verdict on an untouched file. Zero of the changed files are in that gate's population in either direction.

No changeset

This PR releases nothing from any package — a pure skills/** documentation correction, the repo's live convention for which is no changeset. skip-changeset is applied.

Calibration numbers for the anchor

Counting rule: one claim per assertable proposition about platform behaviour — each type row's existence, each config key attributed to a type, each mapping-table cell, each numbered rule, each behavioural sentence, each asserted example output. Front-matter, section headings, the field-type decision tree (navigation over types already inventoried) and pure style prescriptions are out of the inventory by the anchor's rule; that is ~158 of these 1,219 lines.

314 behavioral claims inventoried across PR 1's three filesfield-types.md 166, relationships.md 77, indexing.md 71. 6 FALSE (1.9%), ~34 NOT MEASURABLE (concentrated in indexing.md's performance heuristics — the "Max Indexes by table size" table, the selectivity folklore, the "more than 5 levels" tree-depth warning — which assert database-tuning judgement no repo artifact settles), the remainder VERIFIED, with executed probes wherever the claim is behaviour-bearing.

Flight ①'s ~8x table concentration did NOT reproduce here, and the reading depends on how "table" is defined — which is itself worth the anchor's attention:

  • By markdown rendering: 2 of 6 falsehoods sit in an actual pipe table (both relationships.md capability-matrix cells) — 2/~150 in-table (1.3%) against 4/~164 elsewhere (2.4%). That is a mild inversion of flight ①'s finding, not an 8x concentration.
  • By content class (what flight ① was really pointing at — assertions about what surfaces exist and what the engine does with them): 5 of 6 are surface/mapping claims — the unknown-key posture, vectorConfig, the FK auto-index list, and the two matrix cells. Only the autonumber tokenizer rule is a pure behavioural rule.

The actionable version for flights ③–⑫ is therefore the content class, not the markdown syntax: point execution at every "what is automatic / what keys exist / what is retired" assertion regardless of whether it is rendered as a table, a bullet list, or a callout. Three of this flight's six falsehoods were in bullet lists and callouts that a table-shaped search would have walked straight past.

A second pattern worth carrying forward: two of the six were caught by cross-file contradiction inside the same package (indexing.md vs relationships.md on FK indexing; relationships.md vs SKILL.md on master_detail requiredness). Neither is a document-vs-document verdict — both were then settled against the implementation — but the disagreement is a cheap, high-yield way to find candidates before spending a probe.

What is not here, on purpose

  • No implementation was touched. Every disagreement was the document being wrong about settled code, not code contradicting itself. No implementation defect was found worth filing from these three files.
  • Probes were written in the worktree, run, and deleted — they are evidence, not commits. None earned a place as a permanent pin: the roll-up-over-lookup behaviour is already pinned by packages/objectql/src/summary-rollup.test.ts, and the field-key posture by packages/spec/src/data/field.test.ts.
  • references/_index.md is generator-owned (pnpm --filter @objectstack/spec gen:skill-refs) and is not in this PR's diff. Its node_modules/@objectstack/spec/src/** pointers were nevertheless spot-measured against a real npm pack --dry-run --json: the tarball carries 209 src/ entries and every .zod.ts path this package points at is present, so the flight-① "unshipped-path pointer" flavour does not apply to @objectstack/spec (it applied to @objectstack/formula, which ships dist only).
  • The performance heuristics in indexing.md were left alone. They are advice, not platform claims, and rewriting advice is not a factual correction.

Generated by Claude Code

…ationships, indexing
Six measured-false behavioral claims corrected against the implementation:
- field-types.md: an unknown field key is REFUSED at parse (unrecognized_keys),
not "silently stripped"; same for an authored vectorConfig.
- field-types.md: a wrong-cased date token ({yyyy}) parses as a {field}
reference and renders EMPTY — it is not emitted literally.
- relationships.md: roll-up summary fields DO work over a lookup child.
- relationships.md: master_detail required is forced only under
sharingModel: 'controlled_by_parent'; elsewhere it is a lint warning.
- indexing.md: foreign keys are NOT auto-indexed.
Token ratchet: all three files shrink (-6 / -5 / -1 = -12).
Part of #13675
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de
@os-zhuang
os-zhuang marked this pull request as ready for review August 31, 2026 07:15
@os-zhuang
os-zhuang enabled auto-merge August 31, 2026 07:15
@os-zhuang
os-zhuang added this pull request to the merge queueAug 31, 2026
@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Aug 31, 2026
@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

Queue dequeue root-caused — not this PR's failure; re-queueing once.

Failing check: merge-queue CI, job Temporal Conformance (live PG + MySQL), run 33367664966 on gh-readonly-queue/main/pr-13687-90578117f: MySQL 8.0.46 refused CREATE TABLE os11565_narrow with [MY-011825] record size will exceed the maximum allowed size — the narrow contrast fixture of sql-driver-11565-row-byte-budget.test.ts, i.e. the table that exists to FIT.

Why it is not this PR's: (1) this diff is three markdown files under skills/objectstack-data/** — zero code, and the whole queue batch ahead of it (#13674) is markdown too; a live-driver conformance job cannot read either; (2) the test is a week old (7e8393262, 2026-08-24), not newly landed; (3) the same job is green on main's own CI at 2cce3fd30 (06:55Z), and the only commits between that head and the queue base 90578117f are two lint fixes and a docs correction — none touches driver-sql or MySQL DDL emission; (4) the narrow fixture being refused points at service-container environment variance on the freshly initialized MySQL (row-format/charset dependent effective budget), not at any change in what the driver emits.

Actions: re-queued via auto-merge as the one legitimate re-run. If the queue fails on the same test a second time, that is real evidence the fixture's byte budget sits too close to an environment-dependent limit — I will file it for the driver lane with both run ids and park this PR on that fix.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo added this pull request to the merge queueAug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33367664966 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Temporal Conformance (live PG + MySQL) — 失败步骤: Run driver-sql suite against both live servers

    × converges — a second detect finds neither the index nor an orphan column 6123ms
    FAIL src/sql-driver-13056-orphan-shadow-column-cleanup.test.ts > a retired shadow-carried UNIQUE leaves no column behind (#13056) > converges — a second detect finds neither the index nor an orphan c
    ↳ 失败原因: Error: Test timed out in 5000ms.
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • src/sql-driver-13056-orphan-shadow-column-cleanup.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 3 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

Correction to my standing-down comment above, prompted by the triage bot's extraction: the failing test was sql-driver-13056-orphan-shadow-column-cleanup.test.ts (「converges — a second detect finds neither the index nor an orphan column」, Test timed out in 5000ms) — a timeout, not an assertion. The os11565_narrow MySQL error I quoted from the service-container log is the row-byte-budget test's deliberately provoked refusal (its whole subject), not the failure; I mis-attributed it.

The verdict is unchanged and now stronger: a 5s timeout in a live-driver convergence test, on a markdown-only diff, first occurrence in 24h per the bot's ledger — load/timing class by the bot's own criterion (「超时…多半是负载/时序,不是本 PR 的回归」). The single re-queue was already spent before this comment and the PR is back in the queue; per the bot's guidance I will NOT re-queue again on a second failure — if the same signature fires once more (here or on another PR, which auto-opens the aggregation issue), the timeout budget of that convergence case goes to the driver lane as a card with both run ids.


Generated by Claude Code

Merged via the queue into main with commit 476ec0aAug 31, 2026
34 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-13675-data-skill-sweep branch August 31, 2026 07:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@zhuangjianguo@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); docs(skills): objectstack-data factual sweep (1/3) — field-types, relationships, indexing by zhuangjianguo · Pull Request #13687 · objectstack-ai/objectstack · GitHub
Skip to content

docs(skills): objectstack-data factual sweep (1/3) — field-types, relationships, indexing - #13687

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-13675-data-skill-sweep
Aug 31, 2026
Merged

docs(skills): objectstack-data factual sweep (1/3) — field-types, relationships, indexing#13687
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-13675-data-skill-sweep

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Part of #13675

Program anchor: #13658 — flight ② of the published-skills factual sweep, PR 1 of up to three at whole-file boundaries. The card and the anchor both stay open; only the last PR in this sequence carries a closing keyword.

Method precedent: PR #13577 (the class proof) and PR #13674 (flight ①, the worked example). Session, for durable attribution: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de

Governed surface — human merge, deliberately parked

skills/** is the published, customer-facing surface. This PR is a draft on purpose: auto-merge is not armed, it is not marked ready, no review is requested, and this seat will not arm it. A draft awaiting the maintainer's own merge is the correct terminal state here, not a stuck PR.

File order, and why

skills/objectstack-data is 12 files / 4,935 lines. Flight ①'s calibration says to point execution at surfaces and mapping tables first, so PR 1 takes the three rule files with the highest table density and the most engine-asserting columns: rules/field-types.md (49 type rows, each with a config column), rules/relationships.md (the lookup-vs-master_detail capability matrix), rules/indexing.md (the declaration surface and the "what is automatic" list). SKILL.md follows in PR 2, the hooks reference and the remaining rule files in PR 3.

Non-vacuity control — claims proven TRUE by execution

1. The field-type roster.rules/field-types.md says "49 available options" and enumerates them across ten category tables. Probed against FieldType from @objectstack/spec/data:

FIELD TYPE COUNT = 49
SKILL LISTED COUNT = 49
IN SPEC NOT IN SKILL: []
IN SKILL NOT IN SPEC: []

2. The ADR-0120 unique-scope materialization. The unique-scope table states that unique: 'organization' materializes as (COALESCE(organization_id, '__global__'), …fields). Executed against organizationKeyPartSql from @objectstack/driver-sql:

organizationKeyPartSql => "COALESCE(organization_id, '__global__')"

3. The auto-generated index-name pattern. Stated as idx_{object}_{field1}_{field2}_{...}. Executed against buildIndexName:

buildIndexName(account, [status,created_at], unique=false) => idx_account_status_created_at
buildIndexName(task, [a,b,c], unique=false) => idx_task_a_b_c

4. The autonumber token table, row by row. Executed against parseAutonumberFormat + renderAutonumber at a pinned now of 2026-06-17 UTC:

CASE-{0000} seq=42 => CASE-0042 (table claims CASE-0042)
AD{YYYYMMDD}{0000} seq=1 => AD202606170001 (table claims AD202606170001)
{plan_no}{000} plan_no='PLAN-001' seq=1=> PLAN-001001 (table claims PLAN-001001)
{000} seq=1 / 999 / 1000 => 001 / 999 / 1000 (rule 3: minimum width, grows)
'{a}{b}{000}' scope for (AB,C) => "ABC"
'{a}{b}{000}' scope for (A,BC) => "ABC" (rule 2: shared counter)
'{a}-{b}{000}' scope for (AB,C)/(A,BC) => "AB-C" / "A-BC" (rule 2: delimiter separates)
'CASE-{0000}' scope => "" (fixed prefix = one global counter)

5. Per-type config columns. All 49 rows' config keys were parsed through FieldSchema; every claimed key was accepted and retained on its own type (text → maxLength/minLength/defaultValue, number → min/max/precision/scale, lookup → reference/lookupFilters/multiple/deleteBehavior, slider → min/max/step, vector → dimensions, and so on). Enum columns were pinned by rejection as well as acceptance: returnType refuses 'currency' ("expected one of number|text|boolean|date"), summaryOperations.function refuses 'median', currencyMode refuses anything but dynamic|fixed, and a field-level unique: 'tenant' / 'org' is refused by name with the ADR-0120 message.

The six FALSE claims, and what measured them

落点beforeafter
field-types.md L6-8"an unknown field key is silently stripped at parse (dead metadata), so don't invent config like …""an unknown field key is REFUSED at parse (unrecognized_keys), so don't invent …"
field-types.md L357"an authored vectorConfig is silently stripped (dead metadata)""an authored vectorConfig is refused at parse"
field-types.md L346"Date tokens are case-sensitive and exact ({YYYY}, not {yyyy} or {YYYY-MM}). An unrecognized {...} is emitted literally into the number""Date tokens are case-sensitive{yyyy} parses as a {field} reference and renders empty (rule 1 applies). Only a spelling no field could have — { YYYY }, {YYYY-MM} — is emitted literally."
relationships.md L9lookup row, Roll-ups column — "Not available""Supported via summary"
relationships.md L10master_detail row, Required column — "Always required""Forced only under controlled_by_parent; else lint-warned"
indexing.md L9"- Foreign keys (lookup/master_detail fields)" listed under "ObjectStack automatically creates indexes for" (restated twice more)bullet deleted; "- Field-level uniquenot foreign keys: declare those"

1 and 2. Unknown field keys are refused, not silently stripped

This is the single highest-consequence correction in the PR, because it inverts the cost of a typo for an AI author: "dead metadata" says a misspelled key is harmless, when it actually stops the build. Measured through FieldSchema.safeParse:

REJECT | text + theme (invented) | unrecognized_keys
REJECT | text + rows (invented) | unrecognized_keys
REJECT | file + fileAttachmentConfig (invented) | unrecognized_keys
REJECT | vector + vectorConfig | unrecognized_keys

The error's own text names the retired behaviour the skill was still describing:

Unrecognized key(s) on this field: `theme`. Until this shape was closed these were
dropped silently — the field was still created, minus whatever the key was meant to
constrain, protect or compute.

So the silent-strip sentence was true once and is now exactly backwards. The same pass re-measured the genuinely-retired keys and confirms they are refused with their migration prescription rather than dropped: referenceFilters ("removed in the 16.x line … Use lookupFilters"), encryptionConfig ("pruned in 2026-06 … The real channel is type: 'secret'"), columnName, and conditionalRequired ("removed in @objectstack/spec 17 — use requiredWhen"). Those four sentences in the skill are correct and were left alone.

3. A wrong-cased date token renders EMPTY, not literally

The rule named {yyyy} as an unrecognized spelling and then promised unrecognized spellings appear literally. Both halves cannot hold, and the measurement says the second one fails on exactly the example the first one names:

{ YYYY } tokens=[{"kind":"literal","text":"{ YYYY }"}]
{YYYY-MM} tokens=[{"kind":"literal","text":"{YYYY-MM}"}]
{yyyy} tokens=[{"kind":"field","field":"yyyy"}] <-- a FIELD reference
{YYYY} tokens=[{"kind":"date","pattern":"YYYY"}]
render "{yyyy}-{000}" with no field named yyyy => "-001"
referencedFields("{yyyy}-{000}") => ["yyyy"]
missingFieldValues("{yyyy}-{000}", {}) => ["yyyy"]

{yyyy} is a legal field-name spelling, so the tokenizer reads it as {field_name}; only a spelling no field could carry (a space, a hyphen) falls back to a literal. The practical consequence is worse than a cosmetic difference: the year silently vanishes from the record number, and the format now carries a phantom {field} reference, which puts it under rule 1 of the same list ("Every {field} you interpolate must be required: true" — an empty interpolated field throws at create). The replacement states the tokenizer's actual boundary and points back at rule 1.

4. Roll-up summaries DO work over a lookup child

The capability matrix said roll-ups are "Not available" on lookup and "Supported via summary" only on master_detail. Measured with the engine and the roll-up test's own driver stub — a parent summary over a child whose FK to the parent is a plain lookup:

MEASURED lookup-child rollup => {"opp_total":42,"opp_count":2}
Test Files 1 passed (1)
Tests 1 passed (1)

The spec agrees in its own describe text: summaryOperations.relationshipField is "Auto-detected from the child's lookup/master_detail field referencing this object when omitted". This row was not merely inaccurate, it was pushing authors toward a destructive shape — a reader who wants a roll-up and believes it needs master_detail converts an independent-lifecycle relationship into a cascade-delete one. The two adjacent prose bullets that repeated the claim ("No roll-up aggregations needed" under use lookup, "Need roll-up summaries" under use master_detail) were removed for the same reason; the deletion is also what pays for this PR's additions under the ratchet.

The tree row still reads "Not available" and was left alone: a self-referential FK is neither of the two shapes the auto-detection names, and this flight did not measure it. It is recorded as NOT MEASURABLE rather than corrected on inference.

5. master_detail is required only under controlled_by_parent

"Always required" reads as a platform invariant. Measured through ObjectSchema.create() across sharing models:

ACCEPT | sharingModel=(unset) required=undefined | stored required=false
ACCEPT | sharingModel=private required=false | stored required=false
ACCEPT | sharingModel=controlled_by_parent required=undefined | stored required=true
THROW | sharingModel=controlled_by_parent required=false | "declares `required: false` on a
`master_detail` reference under `sharingModel: 'controlled_by_parent'` — a contradiction
with no honest reading."

The controlled_by_parent rows are the control: they prove the probe reaches real enforcement, and they locate exactly where the invariant does hold. Everywhere else a master_detail defaults to required: false and an explicit false is accepted; the only thing that objects is relationship/master-detail-required, which packages/lint/src/data-model-rules.ts:592 emits at severity: 'warning'. The skill's own lint table (in SKILL.md, PR 2's file) already says "warning", so the two documents disagreed with each other and one of them disagreed with the code.

6. Foreign keys are NOT auto-indexed

indexing.md opened with "ObjectStack automatically creates indexes for: … Foreign keys (lookup/master_detail fields)", and repeated it twice ("Foreign keys — Automatic, but verify" under Always Index; "Index foreign keys — Always (automatic in ObjectStack)" under Best Practices). Measured against the driver's own index oracle, for an object carrying both a lookup and a master_detail and no declarations:

uniqueIndexesFromFields(no unique declared) => []
expectedIndexes(same object) => []
control - the same object with one field-level unique:
uniqueIndexesFromFields(email unique) =>
[{"name":"uniq_task_organization_id_email","columns":["organization_id","email"],
"unique":true,"nullSafeColumns":["organization_id"]}]

The control row is the point: the probe is live, and it returns nothing for foreign keys. SqlDriver.syncTableIndexes (sql-driver.ts:11437) is the single entry point and its body is uniqueIndexesFromFields(...) plus the object's declared indexes — there is no third source, and a repo-wide search for any foreign-key indexing path finds none. relationships.md had it right all along ("Index foreign keys — Always create indexes on lookup/master_detail fields"), so the two files in this very diff contradicted each other; indexing.md was the wrong one.

Written to the token ratchet, not around it

All three files sat at zero headroom, so every correction is paid for inside its own file by genuine deletion — the removed roll-up bullets in relationships.md, the removed foreign-key bullet in indexing.md, and tightened wording in field-types.md. No ceiling was raised; that is a maintainer's call, not a dev's.

readingbeforeafter
rules/field-types.md lines428428
rules/relationships.md lines415413
rules/indexing.md lines376375
objectstack-data/** package lines (all .md)49354932
rules/field-types.md tokens (ceiling 3584)35843578
rules/relationships.md tokens (ceiling 3778)37783773
rules/indexing.md tokens (ceiling 3241)32413240

The ratchet's own verdict lines at head 2d9e2a02c:

✓ check-skills-token-ratchet: skills/objectstack-data/rules/field-types.md is 3578 tokens (ceiling 3584; headroom 6).
✓ check-skills-token-ratchet: skills/objectstack-data/rules/indexing.md is 3240 tokens (ceiling 3241; headroom 1).
✓ check-skills-token-ratchet: skills/objectstack-data/rules/relationships.md is 3773 tokens (ceiling 3778; headroom 5).
✓ check-skills-token-ratchet: 38 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted.

Two nuances were not stuffed into ratcheted text and are recorded for a content/docs/** follow-up instead: (a) the fuller foreign-key indexing guidance the corrected bullet compresses — which FK columns actually earn an index, and the composite shapes that serve filter-plus-sort; (b) user field config — multiple: true and defaultValue: 'current_user' are each legal alone but are refused together, with a located message explaining that the runtime token resolves to one scalar and a multi-value field stores an array. The user row lists both keys and does not claim they compose, so it was left alone.

Gates — derived, not recalled, run locally at head 2d9e2a02c

Family derived from the real diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (13 families; harvested with --commands so both invocation spellings survive). 12 green, 1 NOT MEASURED:

EXIT=0 :: node scripts/check-ci-filter-parity.mjs
EXIT=0 :: node scripts/check-cross-package-test-inputs.mjs
EXIT=0 :: node scripts/check-shard-attestation.mjs
EXIT=0 :: node scripts/check-skills-token-ratchet.mjs
EXIT=3 :: node scripts/check-test-completeness.mjs <-- PREREQUISITE NOT MET
EXIT=0 :: pnpm --filter @objectstack/lint run check:doc-formula-expressions
EXIT=0 :: pnpm check:agent-test-spelling
EXIT=0 :: pnpm check:cross-package-test-inputs
EXIT=0 :: pnpm check:doc-authoring
EXIT=0 :: pnpm check:pm-governed-merges
EXIT=0 :: pnpm check:role-word
EXIT=0 :: pnpm check:skill-compatibility
EXIT=0 :: pnpm check:skill-frame-sync

Exit codes were captured before any pipe (each command redirected to its own file first, then read), so these are the gates' statuses and not a tail's.

check-test-completeness.mjs exits 3 and says why in its own words — "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named". Recorded as NOT MEASURED, not as a red.

check:doc-formula-expressions needed @objectstack/formula and @objectstack/lint built first; built, re-run, and it quotes itself:

✓ check:doc-formula-expressions self-test: 58 cases passed
✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 426 files / 1451 TS blocks judged clean by @objectstack/formula.
✓ check:doc-formula-expressions (spec TSDoc, #6763): 9 @example(s) judged clean across 1092 packages/spec/src files
✓ check:doc-formula-expressions (field-level `*When`, #11407): 14 predicate(s) judged clean; 6 skipped as undeterminable.

check-nul-bytes clean: OK (scanned 7555 text file(s) … no raw ASCII control bytes), plus a direct control-byte scan over the three changed files with no hits.

check:skill-examples is NOT MEASURED for this PR, and vacuous for it either way. It refuses to run until @objectstack/client-react is built ("packages/client-react/dist holds no .d.ts declarations … A verdict now would be computed against a build that no longer matches src") — a prerequisite refusal, not a finding, and the refusal is the gate working as designed. Independently, none of this PR's three files carries an os:check marker (measured: 0, 0, 0), so the gate has nothing to say about this diff. It becomes load-bearing in PR 2 (SKILL.md carries 4) and PR 3 (data-hooks.md carries 1), and will be run there against a real build.

ESLint was not run repo-wide, and the narrowing is measured, not assumed. Three readings: (1) the population read from eslint.config.mjs itself — every files: block names {ts,tsx,mts,cts,js,jsx,mjs,cjs}, and the strings .md and markdown appear nowhere in the config; (2) --format json over the diff's three files returns 3 results, 0 errors, and one message each whose ruleId is null and whose text is "File ignored because no matching configuration was supplied."; (3) type-aware linting is not enabled anywhere — the five parserOptions occurrences are all { ecmaVersion, sourceType } and none is project, which the config's own line 327 states in prose — so this diff cannot move any verdict on an untouched file. Zero of the changed files are in that gate's population in either direction.

No changeset

This PR releases nothing from any package — a pure skills/** documentation correction, the repo's live convention for which is no changeset. skip-changeset is applied.

Calibration numbers for the anchor

Counting rule: one claim per assertable proposition about platform behaviour — each type row's existence, each config key attributed to a type, each mapping-table cell, each numbered rule, each behavioural sentence, each asserted example output. Front-matter, section headings, the field-type decision tree (navigation over types already inventoried) and pure style prescriptions are out of the inventory by the anchor's rule; that is ~158 of these 1,219 lines.

314 behavioral claims inventoried across PR 1's three filesfield-types.md 166, relationships.md 77, indexing.md 71. 6 FALSE (1.9%), ~34 NOT MEASURABLE (concentrated in indexing.md's performance heuristics — the "Max Indexes by table size" table, the selectivity folklore, the "more than 5 levels" tree-depth warning — which assert database-tuning judgement no repo artifact settles), the remainder VERIFIED, with executed probes wherever the claim is behaviour-bearing.

Flight ①'s ~8x table concentration did NOT reproduce here, and the reading depends on how "table" is defined — which is itself worth the anchor's attention:

  • By markdown rendering: 2 of 6 falsehoods sit in an actual pipe table (both relationships.md capability-matrix cells) — 2/~150 in-table (1.3%) against 4/~164 elsewhere (2.4%). That is a mild inversion of flight ①'s finding, not an 8x concentration.
  • By content class (what flight ① was really pointing at — assertions about what surfaces exist and what the engine does with them): 5 of 6 are surface/mapping claims — the unknown-key posture, vectorConfig, the FK auto-index list, and the two matrix cells. Only the autonumber tokenizer rule is a pure behavioural rule.

The actionable version for flights ③–⑫ is therefore the content class, not the markdown syntax: point execution at every "what is automatic / what keys exist / what is retired" assertion regardless of whether it is rendered as a table, a bullet list, or a callout. Three of this flight's six falsehoods were in bullet lists and callouts that a table-shaped search would have walked straight past.

A second pattern worth carrying forward: two of the six were caught by cross-file contradiction inside the same package (indexing.md vs relationships.md on FK indexing; relationships.md vs SKILL.md on master_detail requiredness). Neither is a document-vs-document verdict — both were then settled against the implementation — but the disagreement is a cheap, high-yield way to find candidates before spending a probe.

What is not here, on purpose

  • No implementation was touched. Every disagreement was the document being wrong about settled code, not code contradicting itself. No implementation defect was found worth filing from these three files.
  • Probes were written in the worktree, run, and deleted — they are evidence, not commits. None earned a place as a permanent pin: the roll-up-over-lookup behaviour is already pinned by packages/objectql/src/summary-rollup.test.ts, and the field-key posture by packages/spec/src/data/field.test.ts.
  • references/_index.md is generator-owned (pnpm --filter @objectstack/spec gen:skill-refs) and is not in this PR's diff. Its node_modules/@objectstack/spec/src/** pointers were nevertheless spot-measured against a real npm pack --dry-run --json: the tarball carries 209 src/ entries and every .zod.ts path this package points at is present, so the flight-① "unshipped-path pointer" flavour does not apply to @objectstack/spec (it applied to @objectstack/formula, which ships dist only).
  • The performance heuristics in indexing.md were left alone. They are advice, not platform claims, and rewriting advice is not a factual correction.

Generated by Claude Code

…ationships, indexing
Six measured-false behavioral claims corrected against the implementation:
- field-types.md: an unknown field key is REFUSED at parse (unrecognized_keys),
not "silently stripped"; same for an authored vectorConfig.
- field-types.md: a wrong-cased date token ({yyyy}) parses as a {field}
reference and renders EMPTY — it is not emitted literally.
- relationships.md: roll-up summary fields DO work over a lookup child.
- relationships.md: master_detail required is forced only under
sharingModel: 'controlled_by_parent'; elsewhere it is a lint warning.
- indexing.md: foreign keys are NOT auto-indexed.
Token ratchet: all three files shrink (-6 / -5 / -1 = -12).
Part of #13675
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de
@os-zhuang
os-zhuang marked this pull request as ready for review August 31, 2026 07:15
@os-zhuang
os-zhuang enabled auto-merge August 31, 2026 07:15
@os-zhuang
os-zhuang added this pull request to the merge queueAug 31, 2026
@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Aug 31, 2026
@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

Queue dequeue root-caused — not this PR's failure; re-queueing once.

Failing check: merge-queue CI, job Temporal Conformance (live PG + MySQL), run 33367664966 on gh-readonly-queue/main/pr-13687-90578117f: MySQL 8.0.46 refused CREATE TABLE os11565_narrow with [MY-011825] record size will exceed the maximum allowed size — the narrow contrast fixture of sql-driver-11565-row-byte-budget.test.ts, i.e. the table that exists to FIT.

Why it is not this PR's: (1) this diff is three markdown files under skills/objectstack-data/** — zero code, and the whole queue batch ahead of it (#13674) is markdown too; a live-driver conformance job cannot read either; (2) the test is a week old (7e8393262, 2026-08-24), not newly landed; (3) the same job is green on main's own CI at 2cce3fd30 (06:55Z), and the only commits between that head and the queue base 90578117f are two lint fixes and a docs correction — none touches driver-sql or MySQL DDL emission; (4) the narrow fixture being refused points at service-container environment variance on the freshly initialized MySQL (row-format/charset dependent effective budget), not at any change in what the driver emits.

Actions: re-queued via auto-merge as the one legitimate re-run. If the queue fails on the same test a second time, that is real evidence the fixture's byte budget sits too close to an environment-dependent limit — I will file it for the driver lane with both run ids and park this PR on that fix.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo added this pull request to the merge queueAug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33367664966 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Temporal Conformance (live PG + MySQL) — 失败步骤: Run driver-sql suite against both live servers

    × converges — a second detect finds neither the index nor an orphan column 6123ms
    FAIL src/sql-driver-13056-orphan-shadow-column-cleanup.test.ts > a retired shadow-carried UNIQUE leaves no column behind (#13056) > converges — a second detect finds neither the index nor an orphan c
    ↳ 失败原因: Error: Test timed out in 5000ms.
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • src/sql-driver-13056-orphan-shadow-column-cleanup.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 3 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

Correction to my standing-down comment above, prompted by the triage bot's extraction: the failing test was sql-driver-13056-orphan-shadow-column-cleanup.test.ts (「converges — a second detect finds neither the index nor an orphan column」, Test timed out in 5000ms) — a timeout, not an assertion. The os11565_narrow MySQL error I quoted from the service-container log is the row-byte-budget test's deliberately provoked refusal (its whole subject), not the failure; I mis-attributed it.

The verdict is unchanged and now stronger: a 5s timeout in a live-driver convergence test, on a markdown-only diff, first occurrence in 24h per the bot's ledger — load/timing class by the bot's own criterion (「超时…多半是负载/时序,不是本 PR 的回归」). The single re-queue was already spent before this comment and the PR is back in the queue; per the bot's guidance I will NOT re-queue again on a second failure — if the same signature fires once more (here or on another PR, which auto-opens the aggregation issue), the timeout budget of that convergence case goes to the driver lane as a card with both run ids.


Generated by Claude Code

Merged via the queue into main with commit 476ec0aAug 31, 2026
34 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-13675-data-skill-sweep branch August 31, 2026 07:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@zhuangjianguo@os-zhuang@claude