Skip to content

docs(skills): objectstack-data factual sweep (3/3) — hooks reference, datasources, and the remaining rule files - #13698

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

docs(skills): objectstack-data factual sweep (3/3) — hooks reference, datasources, and the remaining rule files#13698
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-13675-data-skill-sweep-3

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Fixes #13675

Program anchor: #13658 — flight ② of the published-skills factual sweep. This is the final PR of three at whole-file boundaries, so it is the one carrying the closing keyword; the anchor #13658 stays open and keeps the roster. The earlier two are #13687 (rules/field-types.md, rules/relationships.md, rules/indexing.md) and #13694 (SKILL.md). All three are based on origin/main and touch disjoint files — no stacking.

With this PR the package is fully swept: 12 of 12 files, 4,935 lines.

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.

Scope: eight files, two of them edited

This PR's scope is everything the first two did not take: references/data-hooks.md (1,446 lines), rules/validation.md (432), rules/hooks.md (209), rules/lifecycle.md (156), rules/naming.md (107), rules/datasources.md (80), evals/README.md (11) and references/_index.md (65, generator-owned — measured, never hand-edited). All eight were inventoried and verified; four falsehoods were found, all of them in two files. The other six carry no edit because nothing in them measured false — which is a result, not a skip, and the evidence is below.

Non-vacuity control — claims proven TRUE by execution

1. The hook capability vocabulary is exactly five tokens, and the sixth is gone by name.

HookBodyCapability options => ["api.read","api.write","api.transaction","crypto.uuid","log"]
REJECT | capability 'http.fetch'  | Invalid option: expected one of "api.read"|"api.write"|"api.transaction"|"crypto.uuid"|"log"
REJECT | capability 'crypto.hash' | `crypto.hash` was removed from `HookBodyCapability` in @objectstack/spec 17
                                    (ADR-0049 enforce-or-remove) — the sandbox never implemented it.

Both rules/hooks.md ("the five legal tokens … There is no hashing capability: crypto.hash was removed in spec 17 because the sandbox never implemented it") and data-hooks.md ("There is no http.fetch capability by design") are exactly right, down to the reason.

2. Exactly eight lifecycle events, and the four the docs say do not exist really do not.

ACCEPT | beforeFind afterFind beforeInsert afterInsert beforeUpdate afterUpdate beforeDelete afterDelete
REJECT | beforeFindOne | beforeCount | beforeAggregate | beforeUpdateMany
         Invalid option: expected one of "beforeFind"|"afterFind"|"beforeInsert"|"afterInsert"|
         "beforeUpdate"|"afterUpdate"|"beforeDelete"|"afterDelete"

3. The onError: 'abort' default really is unconditional — the reference's most emphasized claim. It states the default is 'abort' "unconditionally … for after* hooks too". Parsed both:

before* hook defaults => {"onError":"abort","priority":100,"async":false}
after*  hook defaults => {"onError":"abort","priority":100,"async":false}

Same run confirms priority default 100 and async default false, both as documented, and onError: 'ignore' is refused ("expected one of abort|log").

4. The body envelope's numeric bounds, exactly as written.

body defaults          => {"language":"js","source":"return {}","capabilities":[]}
ACCEPT timeoutMs=30000 | REJECT timeoutMs=30001 (Too big: expected number to be <=30000)
ACCEPT memoryMb=256    | REJECT memoryMb=257    (Too big: expected number to be <=256)
ACCEPT language='js' 'expression' | REJECT 'ts' 'python'

capabilities really does default to [], language really is exactly the two documented dialects, and the documented ceilings are the real ones. The hook/action timeout defaults the doc quotes are also real: quickjs-runner.ts:51-52 declares DEFAULT_HOOK_TIMEOUT_MS = 250 and DEFAULT_ACTION_TIMEOUT_MS = 5000.

5. rules/validation.md is exactly right about the rule vocabulary — all six discriminators and all three refusals.

REJECT | type='unique'  | Invalid discriminator value. Expected 'script' | 'state_machine' | 'format' |
REJECT | type='async'   |   'cross_field' | 'json_schema' | 'conditional'
REJECT | type='custom'  |
defaults => {"active":true,"events":["insert","update"],"priority":100,"severity":"error",...}
ACCEPT severity error|warning|info      REJECT 'fatal'
ACCEPT events insert|update             REJECT 'delete'
ACCEPT format email|url|phone|json      REJECT 'uuid'
ACCEPT format + regex                   REJECT format + pattern
         → "Did you mean `pattern` → `regex`?"
REJECT | conditional + validations[]    → the doc's "There is no `validations: []` array", confirmed

The priority: 100 default, the events: ['insert','update'] default, the "no delete event", the "the key is regex, not pattern", the four built-in formats, and the conditional's single-nested-rule shape are all measured true.

6. rules/lifecycle.md is right about all five classes AND all four of its "rejected at parse time" examples — each with a passing control.

ACCEPT | class='record' bare
REJECT | class='audit'|'telemetry'|'transient'|'event' bare
         lifecycle.class '<c>' requires at least one bounding policy: retention, ttl, or storage (rotation) — ADR-0057 §3.5
REJECT | class='archive' → Invalid option: expected one of "record"|"audit"|"telemetry"|"transient"|"event"
REJECT | record + retention → lifecycle.class 'record' is permanent business truth — policies are not allowed on it (ADR-0057 §3.1)
REJECT | audit archive.after '30d' vs maxAge '90d'
         → lifecycle.archive.after ('30d') must equal retention.maxAge ('90d') — the hot window ends where the archive begins
ACCEPT | audit archive.after == maxAge                      ← control
REJECT | maxAge '2 weeks' → must be a duration literal like '6h', '14d', '12w' or '7y'
ACCEPT | maxAge '2w'                                        ← control
REJECT | onlyWhen + rotation → the Rotator DROPs whole shards and would destroy rows the filter protects
ACCEPT | onlyWhen alone                                     ← control
ACCEPT | '6h' '14d' '12w' '7y'   REJECT | '30m' '5s'        ← the h/d/w/y literal set, exactly

Every one of that file's parse-time refusals fires for the stated reason, and every control passes. lifecycle.md needed no correction at all.

7. ctx.title's null contract, which looked wrong and is not. The table types it Promise<string | null> and says "null when there is no title — it never falls back to the id", while the host seam resolves undefined. The implementation settles it in favour of the doc (script-runner.ts):

Resolves undefined — never the record's id — when there is no title to give. … Inside the VM that absence arrives as null, which is what every host value crossing this boundary becomes when it is not JSON-representable; ctx.title() ?? '…' reads the same either way, and === undefined does not, so the VM-facing contract is stated as null.

The skill documents the VM-facing contract, which is the one a body author sees. VERIFIED, not corrected.

The four FALSE claims, and what measured them

落点 before after
data-hooks.md L372 (sandbox ctx table) "ctx.log{ info, warn, error }" "ctx.log{ debug, info, warn, error }"
data-hooks.md L450 (capabilities table) "logctx.log.info / warn / error(msg, data?)" "logctx.log.debug / info / warn / error(msg, data?)"
data-hooks.md L391-397 (ctx.api.object(n) method table) seven methods: find findOne count insert update upsert delete plus a row for aggregate · updateMany · deleteMany — "also installed; same where shape"
datasources.md L37 "field.columnName on managed objects is unaffected" "field.columnName does not exist — on ANY object … authoring it is a parse error everywhere, not only on a federated object"
datasources.md L21 "external settings are required iff schemaMode !== 'managed' (and forbidden otherwise)" "external settings are required when schemaMode !== 'managed'."

1. ctx.log has four levels, not three

Documented as { info, warn, error } in the sandbox ctx table and again as ctx.log.info / warn / error in the capabilities table. The runtime wires four, and its own comment says so in as many words (packages/runtime/src/sandbox/quickjs-runner.ts:639):

// [#7661] FOUR levels, not three. `debug` was granted by the CLI's
// capability extractor (`ctx\.log\.(?:info|warn|error|debug)` → `log`) and
...
for (const level of ['debug', 'info', 'warn', 'error'] as const) {

The host seam agrees — script-runner.ts types log?: { debug, info, warn, error }. The history here is the sharpest part: the same docblock records that debug "was the crypto.hash shape one member over — inferred by the extractor and taught by the docs table with nothing installed behind it, so the one call it typed threw inside the VM. Unlike hashing it was ENFORCED rather than removed (ADR-0049)". So the docs once over-claimed debug, the platform closed the gap by implementing it, and the docs then over-corrected past the new truth. A body calling ctx.log.debug(...) works today and has its capability inferred; the table said it did not exist.

2. The sandbox repo table omits three installed methods

### ctx.api.object(name) — the cross-object repo introduces its table with "Methods:", which reads as the surface. Measured against what installCtx actually installs (quickjs-runner.ts:527-530):

const READ  = ['find', 'findOne', 'count', 'aggregate'] as const;
const WRITE = ['insert', 'update', 'delete', 'updateMany', 'deleteMany', 'upsert'] as const;
for (const m of READ)  installApiMethod(vm, wrap, m, objectName, ctx, caps, 'api.read',  );
for (const m of WRITE) installApiMethod(vm, wrap, m, objectName, ctx, caps, 'api.write', );

Ten methods are installed; the table listed seven. aggregate is gated by api.read, updateMany and deleteMany by api.write — so the two capability rows were incomplete in the same way and were corrected too. The added row is deliberately one line rather than three, because the file is at its ratchet ceiling.

Deliberately NOT changed, and recorded instead: pattern 8 carries the comment "There is NO updateMany — bulk updates use update(data, { where, multi: true })", inside a handler: (in-process) example. The measurement above is of the sandbox wrap; this flight did not measure the in-process ctx.api repo's own surface, and the file elsewhere claims the two forms are "the same repository". Correcting that comment on inference would be exactly the document-vs-document reasoning the sweep forbids, so it is left alone and reported as NOT MEASURABLE with the tension named.

3. field.columnName is gone everywhere, not just on external objects

The Column-mapping section framed columnName as a federated-object mistake and then reassured the reader that "(field.columnName on managed objects is unaffected.)". Measured on a plain managed object — no datasource, no external — with a control:

REJECT | MANAGED object + field.columnName | Unrecognized key(s) on this field: `columnName`.
  • `columnName` was removed in the 16.x line — the SQL driver hardcodes the physical column to the
    field key, so a custom name was ignored. External/federated objects map physical columns with
    `external.columnMap` (ADR-0062 D7).
ACCEPT | CONTROL: the same managed object without columnName

The reassurance was the whole problem: it told an author that a key which is a hard parse error is fine in the common case. The corrected bullet keeps the external.columnMap prescription (which is right) and states the real scope.

4. The "forbidden otherwise" half of the external rule does not hold

external settings are required iff schemaMode !== 'managed' (and forbidden otherwise). The required half is real and the refusal is exact; the forbidden half is not:

REJECT | schemaMode='external', no external block      | schemaMode='external' requires 'external' settings.
REJECT | schemaMode='validate-only', no external block | schemaMode='validate-only' requires 'external' settings.
ACCEPT | schemaMode='external' + external block                       ← control
ACCEPT | schemaMode='managed' EXPLICIT + an external block            ← the claim says this is forbidden

A managed datasource carrying an external block parses cleanly. Corrected to state only the half that holds — a smaller claim, and the one the platform actually enforces.

The six files with no edit, and why that is a result

file lines verdict
rules/validation.md 432 Fully verified — the six discriminators, the three refusals, all defaults, the regex-not-pattern key, the four built-in formats, the conditional shape. 0 false.
rules/lifecycle.md 156 Fully verified — five classes, all four parse-time refusals with their exact reasons, the h/d/w/y literal set, each with a control. 0 false.
rules/hooks.md 209 Verified — five capabilities, crypto.hash removal and its reason, the eight events, the body shape. Its abbreviated ctx.api list is marked in-file as "Sandbox essentials (full contract in references/data-hooks.md)", so it reads as a pointer rather than an enumeration; the authoritative table is the one corrected above. 0 false.
rules/naming.md 107 Verified against the snake_case identifier regexes and the option-value rule. 0 false.
evals/README.md 11 Self-describing placeholder ("⚠️ Not yet implemented"); no behavioral claim to falsify.
references/_index.md 65 Generator-owned (pnpm --filter @objectstack/spec gen:skill-refs) — measured, never hand-edited. Its node_modules/@objectstack/spec/src/** pointers all resolve: a real npm pack --dry-run --json reports 209 src/ entries in the tarball and every .zod.ts path this package names is present. check:skill-refs is green on this diff.

Written to the token ratchet, not around it

Both edited files sat at zero headroom and both shrink. The data-hooks.md payment is a genuine deletion: a ctx.log warning duplicated verbatim ~180 lines apart (the sandbox ctx table already says "ctx.log is an object, not callable as ctx.log(msg)", and the Troubleshooting section repeated it in a parenthetical), plus a session.roles rationale stated three times in one file, trimmed to once-in-full plus two short references. No ceiling was raised.

reading before after
references/data-hooks.md lines 1446 1444
rules/datasources.md lines 80 80
objectstack-data/** package lines (all .md) 4935 4933
references/data-hooks.md tokens (ceiling 12611) 12611 12584
rules/datasources.md tokens (ceiling 911) 911 903

The ratchet's own verdict lines at head d66ac1010:

✓ check-skills-token-ratchet: skills/objectstack-data/references/data-hooks.md is 12584 tokens (ceiling 12611; headroom 27).
✓ check-skills-token-ratchet: skills/objectstack-data/rules/datasources.md is 903 tokens (ceiling 911; headroom 8).
✓ check-skills-token-ratchet: 38 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted.

Gates — derived, not recalled, run locally at head d66ac1010

Family derived from the real diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, harvested with --commands. 14 families this time, not 13 — touching references/** pulls in check:skill-refs, which the earlier two PRs' paths did not. 13 green, 1 NOT MEASURED; exit codes captured before any pipe:

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 --filter @objectstack/spec run check:skill-refs
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

check:skill-refs quotes itself: ✅ 9 generated files in sync with packages/spec — the reading that matters most here, because references/_index.md is generator-owned and this PR edits its sibling.

Two more beyond the derived family, because they read this corpus:

EXIT=0 :: pnpm --filter @objectstack/spec check:skill-docs      ✅ Skill docs in sync
EXIT=0 :: pnpm --filter @objectstack/spec check:skill-examples
   ✅ 260 prose examples type-check across 3 surface(s) — every marked block parsed,
      so tsc ran the SEMANTIC pass on all of them

check:skill-examples is load-bearing here: data-hooks.md carries an os:check block (the copy-paste afterUpdate hook), and this PR edits that file.

check-test-completeness.mjs exits 3 with its own text "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-nul-bytes clean: OK (scanned 7561 text file(s) … no raw ASCII control bytes), plus a direct control-byte scan over both changed files with no hits.

ESLint was not run repo-wide, and the narrowing is measured. Same three readings: (1) no files: block in eslint.config.mjs names .md, and the strings .md/markdown appear nowhere in it; (2) --format json over both changed files returns 2 results, 0 total errors, each with one ruleId: null message reading "File ignored because no matching configuration was supplied."; (3) type-aware linting is not enabled anywhere (no parserOptions.project), so this diff cannot move a verdict on an untouched file.

No changeset

Pure skills/** documentation correction, releasing nothing from any package. skip-changeset is applied.

Calibration — the flight's closing numbers

Same counting rule throughout: one claim per assertable proposition about platform behaviour; front-matter, navigation, routing tables and pure style prescriptions excluded.

PR files lines claims FALSE density
1 (#13687) 3 1,219 314 6 1.9%
2 (#13694) 1 1,210 281 3 1.1%
3 (this) 8 2,506 288 4 1.4%
flight ② total 12 4,935 883 13 1.5%

~92 NOT MEASURABLE across the package, recorded rather than skipped; the rest VERIFIED, with executed probes wherever the claim is behaviour-bearing.

Against flight ①'s 3.0% and its ~8x table concentration, the two findings that should re-size flights ③–⑫:

  • Density halved (1.5% vs 3.0%) over a corpus four times larger. A ~1.5–3% floor across a 186k-token published catalog is the number to plan around.
  • The table concentration does not reproduce by markdown shape, and does reproduce by content class. Of 13 falsehoods, only 4 sit in a pipe table; the other 9 are bullet lists, callouts, numbered rules, code comments and a parenthetical. But 11 of 13 are surface/liveness claims — "does this key exist, is it retired, is it automatic, what does the engine install". A table-shaped search would have found 4 of 13. Point flights ③–⑫ at the content class, not the syntax.
  • The direction split is new. Flight ① found documents advertising DEAD surfaces as live. Flight ② found both directions, and the second is now the majority: 7 of 13 declare a surface DEADER or NARROWER than it is (maskingRule "pruned" but live, titleFormat "retired" but deprecated, ctx.log three levels of four, the repo table seven methods of ten, columnName "unaffected on managed" but universally removed, roll-ups "not available" on lookup, external "forbidden otherwise" when it is not). That direction hides shipped capability from an AI author, and no "grep for retired things" pass finds it. Both directions collapse to one cheap probe: for every surface the document names, parse it and read what the schema says back.
  • Cross-file contradiction is the best free candidate-finder. 4 of the 13 were found that way (indexing.md vs relationships.md on FK indexing; relationships.md vs SKILL.md on master_detail requiredness; SKILL.md's tenancy example vs its own RLS sections on organization_id; data-hooks.md's capability rows vs its own method table). None was decided document-vs-document — every one was then settled against the implementation — but the disagreement costs nothing to spot and pays for the probe.
  • Flavour match against flight ①'s four: retired-surface rows outliving removals — reproduced. Slot-semantics overclaims — reproduced twice (the autonumber tokenizer, the tenancy default). Phantom fields with zero repo-wide hits — did not appear; every key named across all 12 files exists. Unshipped-path pointers — did not apply; @objectstack/spec really does publish src/**/*.zod.ts (209 src/ entries measured in a real npm pack --dry-run --json), unlike @objectstack/formula.

What is not here, on purpose

  • No implementation was touched, in any of the three PRs. Every disagreement was the document being wrong about settled code.
  • Probes were written in the worktree, run, and deleted — evidence, not commits.
  • Two implementation-side observations are reported on the card for PM triage rather than filed unilaterally, because each is a contract judgement rather than a defect: (a) relationship/master-detail-required is a lint warning while object.zod.ts argues the unguarded shape "arms the worst measured failure shape", with hard enforcement only under sharingModel: 'controlled_by_parent'; (b) SEARCHABLE_ENUM_TYPES contains 'status', which is not a member of the 49-value FieldType enum — a dead vocabulary entry of the ADR-0049 shape. The skill reproduces that list faithfully, so its sentence is VERIFIED; the dead entry belongs to the implementation.
  • data-hooks.md's hook-overhead table (0 hooks ~1ms, 5 ~5ms, 20 ~20ms) and indexing.md's "Max Indexes by table size" are recorded NOT MEASURABLE and left alone: they read as measurements and no repo artifact settles them, but rewriting unverifiable advice is not a factual correction and would spend ratchet budget the corrections need.

Generated by Claude Code

… datasources, and the remaining rule files

Four measured-false behavioral claims corrected against the implementation:

- data-hooks.md: ctx.log is documented as { info, warn, error } in two places.
  It has FOUR levels — quickjs-runner.ts wires ['debug','info','warn','error']
  and its own comment reads '[#7661] FOUR levels, not three'.
- data-hooks.md: the sandbox ctx.api.object(n) method table omits three real
  methods — aggregate (api.read), updateMany and deleteMany (api.write).
- datasources.md: 'field.columnName on managed objects is unaffected' — the key
  was removed in the 16.x line and is a parse error on ANY object.
- datasources.md: 'external settings are ... forbidden otherwise' — a managed
  datasource carrying an external block parses fine. The required half holds.

The other six files in this PR's scope (validation.md, hooks.md, lifecycle.md,
naming.md, evals/README.md, references/_index.md) were inventoried and verified
with no falsehood found, so they carry no edit.

Token ratchet: both edited files shrink (-27 / -8).

Fixes #13675

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de
@zhuangjianguo
zhuangjianguo marked this pull request as ready for review August 31, 2026 07:17
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Aug 31, 2026
Merged via the queue into main with commit a7fb23c Aug 31, 2026
34 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-13675-data-skill-sweep-3 branch August 31, 2026 07:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants