Uh oh!
There was an error while loading. Please reload this page.
docs(skills): objectstack-data factual sweep (1/3) — field-types, relationships, indexing - #13687
Conversation
…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_01EXxTW8mvPBhoHxmyPZ63deUh oh!
There was an error while loading. Please reload this page.
zhuangjianguo
commented
Aug 31, 2026
Queue dequeue root-caused — not this PR's failure; re-queueing once. Failing check: merge-queue CI, job Why it is not this PR's: (1) this diff is three markdown files under 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 |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 33367664966 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
zhuangjianguo
commented
Aug 31, 2026
Correction to my standing-down comment above, prompted by the triage bot's extraction: the failing test was 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 |
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_01EXxTW8mvPBhoHxmyPZ63deGoverned 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-datais 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.mdfollows 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.mdsays "49 available options" and enumerates them across ten category tables. Probed againstFieldTypefrom@objectstack/spec/data:2. The ADR-0120 unique-scope materialization. The unique-scope table states that
unique: 'organization'materializes as(COALESCE(organization_id, '__global__'), …fields). Executed againstorganizationKeyPartSqlfrom@objectstack/driver-sql:3. The auto-generated index-name pattern. Stated as
idx_{object}_{field1}_{field2}_{...}. Executed againstbuildIndexName:4. The autonumber token table, row by row. Executed against
parseAutonumberFormat+renderAutonumberat a pinnednowof 2026-06-17 UTC: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:returnTyperefuses'currency'("expected one of number|text|boolean|date"),summaryOperations.functionrefuses'median',currencyModerefuses anything butdynamic|fixed, and a field-levelunique: 'tenant'/'org'is refused by name with the ADR-0120 message.The six FALSE claims, and what measured them
field-types.mdL6-8unrecognized_keys), so don't invent …"field-types.mdL357vectorConfigis silently stripped (dead metadata)"vectorConfigis refused at parse"field-types.mdL346{YYYY}, not{yyyy}or{YYYY-MM}). An unrecognized{...}is emitted literally into the number"{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.mdL9lookuprow, Roll-ups column — "Not available"summary"relationships.mdL10master_detailrow, Required column — "Always required"controlled_by_parent; else lint-warned"indexing.mdL9unique— not 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:The error's own text names the retired behaviour the skill was still describing:
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 … UselookupFilters"),encryptionConfig("pruned in 2026-06 … The real channel istype: 'secret'"),columnName, andconditionalRequired("removed in @objectstack/spec 17 — userequiredWhen"). 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}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 berequired: 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
lookupchildThe capability matrix said roll-ups are "Not available" on
lookupand "Supported viasummary" only onmaster_detail. Measured with the engine and the roll-up test's own driver stub — a parentsummaryover a child whose FK to the parent is a plainlookup:The spec agrees in its own describe text:
summaryOperations.relationshipFieldis "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 needsmaster_detailconverts 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
treerow 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_detailis required only undercontrolled_by_parent"Always required" reads as a platform invariant. Measured through
ObjectSchema.create()across sharing models:The
controlled_by_parentrows are the control: they prove the probe reaches real enforcement, and they locate exactly where the invariant does hold. Everywhere else amaster_detaildefaults torequired: falseand an explicitfalseis accepted; the only thing that objects isrelationship/master-detail-required, whichpackages/lint/src/data-model-rules.ts:592emits atseverity: 'warning'. The skill's own lint table (inSKILL.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.mdopened 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 alookupand amaster_detailand no declarations: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 isuniqueIndexesFromFields(...)plus the object's declaredindexes— there is no third source, and a repo-wide search for any foreign-key indexing path finds none.relationships.mdhad 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.mdwas 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 inindexing.md, and tightened wording infield-types.md. No ceiling was raised; that is a maintainer's call, not a dev's.rules/field-types.mdlinesrules/relationships.mdlinesrules/indexing.mdlinesobjectstack-data/**package lines (all.md)rules/field-types.mdtokens (ceiling 3584)rules/relationships.mdtokens (ceiling 3778)rules/indexing.mdtokens (ceiling 3241)The ratchet's own verdict lines at head
2d9e2a02c: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)userfield config —multiple: trueanddefaultValue: '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. Theuserrow lists both keys and does not claim they compose, so it was left alone.Gates — derived, not recalled, run locally at head
2d9e2a02cFamily derived from the real diff with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(13 families; harvested with--commandsso both invocation spellings survive). 12 green, 1 NOT MEASURED: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.mjsexits 3 and says why in its own words — "PREREQUISITE NOT MET — this gate grades a savedturbo run testlog, and no log was named". Recorded as NOT MEASURED, not as a red.check:doc-formula-expressionsneeded@objectstack/formulaand@objectstack/lintbuilt first; built, re-run, and it quotes itself:check-nul-bytesclean: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-examplesis NOT MEASURED for this PR, and vacuous for it either way. It refuses to run until@objectstack/client-reactis built ("packages/client-react/distholds no.d.tsdeclarations … 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 anos:checkmarker (measured: 0, 0, 0), so the gate has nothing to say about this diff. It becomes load-bearing in PR 2 (SKILL.mdcarries 4) and PR 3 (data-hooks.mdcarries 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.mjsitself — everyfiles:block names{ts,tsx,mts,cts,js,jsx,mjs,cjs}, and the strings.mdandmarkdownappear nowhere in the config; (2)--format jsonover the diff's three files returns 3 results, 0 errors, and one message each whoseruleIdisnulland whose text is "File ignored because no matching configuration was supplied."; (3) type-aware linting is not enabled anywhere — the fiveparserOptionsoccurrences are all{ ecmaVersion, sourceType }and none isproject, 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-changesetis 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 files —
field-types.md166,relationships.md77,indexing.md71. 6 FALSE (1.9%), ~34 NOT MEASURABLE (concentrated inindexing.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:
relationships.mdcapability-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.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.mdvsrelationships.mdon FK indexing;relationships.mdvsSKILL.mdon 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
packages/objectql/src/summary-rollup.test.ts, and the field-key posture bypackages/spec/src/data/field.test.ts.references/_index.mdis generator-owned (pnpm --filter @objectstack/spec gen:skill-refs) and is not in this PR's diff. Itsnode_modules/@objectstack/spec/src/**pointers were nevertheless spot-measured against a realnpm pack --dry-run --json: the tarball carries 209src/entries and every.zod.tspath 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 shipsdistonly).indexing.mdwere left alone. They are advice, not platform claims, and rewriting advice is not a factual correction.Generated by Claude Code