Skip to content

Migrate from npm to pnpm - #6

Merged
huangyiirene merged 3 commits into
mainfrom
copilot/update-project-to-pnpm
Jan 18, 2026
Merged

huangyiirene merged 3 commits into
mainfrom
copilot/update-project-to-pnpm

Conversation

Copilot AI commented Jan 18, 2026 •

Copy link
Copy Markdown
Contributor

Converts the project to use pnpm as the package manager.

Changes

  • Package manager: Replaced package-lock.json with pnpm-lock.yaml, added packageManager field to package.json
  • Scripts: Updated prepublishOnly to use pnpm commands
  • CI/CD: Modified workflows to use pnpm/action-setup@v4 with proper store caching and pnpm install --frozen-lockfile
  • Documentation: Updated all installation and build commands in README to use pnpm

Install example

pnpm install @objectstack/spec

Development

pnpm install
pnpm run build
Original prompt

改为pnpm 项目


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits January 18, 2026 10:12
Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
Copilot AI changed the title [WIP] Update project to use pnpm Migrate from npm to pnpm Jan 18, 2026
Copilot AI requested a review from huangyiirene January 18, 2026 10:16
@huangyiirene
huangyiirene marked this pull request as ready for review January 18, 2026 10:19
@huangyiirene
huangyiirene merged commit 380e398 into main Jan 18, 2026
1 check failed
Copilot AI added a commit that referenced this pull request Feb 16, 2026
…fineInterface

- Export defineInterface from root index.ts (reviewer suggestion)
- Mark ROADMAP Phase A items as complete
- Update ROADMAP metrics (199 test files, 5468 tests, 177 schema files)
- Update Phase 1 deliverables with Interface/content elements
- Add defineInterface() to DX helper list
- Update design doc status from "Proposal" to "Phase A Implemented"
- Mark design doc Phase A checklist items complete
- Add decisions #6-8 to design doc decision log (PageSchema merge, SortItemSchema, branding extend)

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
xuyushun441-sys pushed a commit that referenced this pull request May 22, 2026
Introduces an opt-in path in ObjectStackProtocolImplementation.saveMetaItem
that writes overlay metadata through SysMetadataRepository.put instead of
the raw engine, so writes append to the change-log and emit HMR seq events.

Behavioural changes (all behind options.useRepositoryWritePath /
OBJECTSTACK_USE_REPOSITORY_WRITE_PATH=1):
- saveMetaItem request gained optional parentVersion (If-Match) and
  actor fields. ConflictError -> 409 metadata_conflict.
- Plural type aliases (views, dashboards, ...) normalized to singular
  before the repo's overlay-allowlist gate (rubber-duck #5).
- Object-registry mutation moved AFTER successful put() so a conflict
  does not leave the in-memory registry stale (rubber-duck #3 invariant
  test added).

Repo/test-fake fixes uncovered by rubber-duck review:
- SysMetadataRepository.put/delete now update/delete by row id because
  the engine's strict .update requires id or multi:true (rubber-duck #1).
- sys_metadata.checksum column widened from 64 -> 71 chars to hold the
  sha256: prefix produced by hashSpec() (rubber-duck #2).
- Three test fake engines extended to support both overlay-tuple and
  id-based where lookups.

333/333 objectql tests pass.

Deferred to PR-10d.4: REST plumbing for parentVersion/actor
(rubber-duck #6), race-window retry for omitted parentVersion
(rubber-duck #4), default flag flip + legacy path removal.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
xuyushun441-sys pushed a commit that referenced this pull request May 23, 2026
Walking through Studio as a low-code developer surfaced a fundamental
gap: it is a beautiful metadata BROWSER but offers no authoring
affordances. The #1 reflex of every Airtable / Power Apps user — add a
field — has no entry point in our UI.

This change adds two authoring touchpoints to the Object Hub > Fields
panel that respect Prime Directive #6 (no temporary workarounds) and
stay true to metadata-as-code:

1. + Add field button
   A primary CTA in the toolbar opens a guided dialog (AddFieldDialog)
   with a type picker (18 supported field types, each with icon +
   one-line semantics), a derived snake_case machine-name preview, and a
   live snippet preview. Two actions:
     • Copy snippet — pastes a defineField-style literal into the
       clipboard, ready to drop into the fields: { … } block.
     • Open .object.ts in VS Code — vscode:// deep-link via the
       existing vscode-objectstack extension.

   Filesystem writes from the browser are intentionally avoided. When
   the runtime overlay write-path matures (ADR-0005), the dialog can
   swap the snippet flow for a real persist call without changing its
   contract.

2. Click any field row to open a detail drawer
   Rows are now cursor-pointer and trigger a side Sheet
   (FieldDetailDrawer) showing the full normalised field spec — all
   properties, options enumerated, references, formula, validation —
   plus the same VS Code deep-link and a per-field Copy snippet that
   emits just this field's literal. The drawer is read-only; users who
   want to edit follow the VS Code link.

   The previous behaviour (clicking a row did nothing) was the single
   biggest dead-end during the persona walkthrough. The drawer is the
   minimum viable acknowledgement that a field is an interactive object,
   not a static row of text.

Plumbing changes
- ObjectSchemaInspector preserves every property of the field spec
  (spread over the cherry-picked subset) so the drawer has access to
  schema properties beyond the table columns.
- Added a ChevronRight column on the right edge of every row,
  group-hover translate-x for the same drill-in affordance used on
  MetadataListPage compact rows.
- CopyButton stops propagation so the row click does not fire when
  copying the field name.

Build / tests
pnpm --filter @objectstack/studio build — clean.
pnpm --filter @objectstack/studio test — 69/69 tests pass; same 2
pre-existing @object-ui/core/dist/evaluator/ExpressionEvaluator module
resolution failures in playground-plugins / plugin-system suites,
unrelated to this work.

Files
- apps/studio/src/components/FieldDetailDrawer.tsx (new, ~160 lines)
- apps/studio/src/components/AddFieldDialog.tsx (new, ~280 lines)
- apps/studio/src/components/ObjectSchemaInspector.tsx
  · Imports FieldDetailDrawer, AddFieldDialog, Plus, ChevronRight
  · State for selectedField + addOpen
  · Preserves full field spec via spread in fieldEntries
  · Toolbar: + Add field primary CTA
  · TableRow: cursor-pointer, onClick → setSelectedField
  · New chevron column on right; colSpan bumped to 7
  · Drawer + dialog mounted at end of component
  · CopyButton stops click propagation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
os-zhuang added a commit that referenced this pull request Jun 19, 2026
…antics

Full hardening of the remaining items from review:

- #5 MySQL/length: key _objectstack_sequences by a single key_hash
  (SHA-256 of object,tenant_id,field,scope) instead of a 4-column natural
  PK. The natural PK exceeded MySQL's utf8mb4 index-length limit (a certain
  CREATE TABLE failure) and bounded how long a {field} scope could be. The
  hash PK keys every dialect uniformly and lets scope be a generous
  non-indexed column. Legacy 3-column and interim {scope}-column tables are
  migrated in place; migration fails safe (fixed-prefix keeps working, a
  per-scope write errors actionably).

- #1 scope ambiguity: confirmed NOT fixable by separating adjacent token
  boundaries — when two records render the same prefix they render the same
  visible number, so they MUST share a counter to stay unique (a separator
  would mint duplicates). Documented the semantics + the remedy (delimiter
  literal in the format), backed by tests. The compile lint already nudges
  authors toward unambiguous formats.

- #6 width overflow: confirmed by-design — the pad width is a MINIMUM, the
  counter grows past it and never wraps (mainstream autonumber semantics).
  Documented + regression test, no throw (throwing would break legitimate
  high-count sequences).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jun 19, 2026
…ber formats (#2043)

* feat(autonumber): date, {field} and per-scope counter reset for formats

Tokenize autonumberFormat via a shared pure renderer in @objectstack/spec
(parseAutonumberFormat / renderAutonumber) that both the engine fallback and
the SQL driver call, so they emit byte-identical numbers (#1603 parity):

- date tokens {YYYY}{YY}{MM}{DD}{YYYYMMDD} resolve the calendar day in the
  request's business timezone (ExecutionContext.timezone, ADR-0053; UTC
  fallback), threaded through new DriverOptions.timezone
- {field} interpolation substitutes record values into the prefix
- counter scope = rendered prefix before the sequence slot, so AD{YYYYMMDD}{0000}
  resets daily, {section}{island_zone}{000} numbers per group, {plan_no}{000}
  numbers per parent — one mechanism, no separate reset config

Fixed-prefix formats (CASE-{0000}) render an empty scope and keep their single
global counter. _objectstack_sequences gains a scope column (PK widened to
object,tenant_id,field,scope); legacy 3-column tables migrate in place on first
use, carrying existing counters to scope=''.

* fix(autonumber): drop backtracking lookahead in seed scan (ReDoS)

The empty-prefix legacy branch used /(\d+)(?!.*\d)/ to grab the last digit
run, whose negative lookahead is a polynomial-ReDoS sink on stored values
with many repeated zeros (CodeQL js/polynomial-redos, high). Replace both
branches with the linear /\d+/g, preserving the last-digit-run semantics.

* fix(autonumber): guard {field} interpolation footguns

Add three guardrails on top of the {field}/date/per-scope autonumber work:

- Empty interpolated {field} now throws (shared missingFieldValues helper)
  in both the SQL driver and the engine fallback, instead of silently
  collapsing the record into the wrong counter scope.
- Build-time lint (objectstack compile): unknown / self-referencing {field}
  fails the build; an optional {field} warns to mark it required.
- Legacy _objectstack_sequences PK-widen failure fails safe — fixed-prefix
  sequences keep working and a per-scope write raises an actionable error
  rather than an opaque DB primary-key violation at insert time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(autonumber): hash-keyed sequence table; clarify scope & width semantics

Full hardening of the remaining items from review:

- #5 MySQL/length: key _objectstack_sequences by a single key_hash
  (SHA-256 of object,tenant_id,field,scope) instead of a 4-column natural
  PK. The natural PK exceeded MySQL's utf8mb4 index-length limit (a certain
  CREATE TABLE failure) and bounded how long a {field} scope could be. The
  hash PK keys every dialect uniformly and lets scope be a generous
  non-indexed column. Legacy 3-column and interim {scope}-column tables are
  migrated in place; migration fails safe (fixed-prefix keeps working, a
  per-scope write errors actionably).

- #1 scope ambiguity: confirmed NOT fixable by separating adjacent token
  boundaries — when two records render the same prefix they render the same
  visible number, so they MUST share a counter to stay unique (a separator
  would mint duplicates). Documented the semantics + the remedy (delimiter
  literal in the format), backed by tests. The compile lint already nudges
  authors toward unambiguous formats.

- #6 width overflow: confirmed by-design — the pad width is a MINIMUM, the
  counter grows past it and never wraps (mainstream autonumber semantics).
  Documented + regression test, no throw (throwing would break legitimate
  high-count sequences).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: os-zhuang <jack@objectstack.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
os-litant pushed a commit that referenced this pull request Sep 2, 2026
…02, B-03, C-01)

Nine findings, all the same shape: a rule the file already states, or prose
re-teaching what the CLI prints for itself.

- E-04/E-05 §3.5 becomes a 3-row outcome -> evidence table; the re-quoted error
  block was already available verbatim from the install. The new probe rule is
  the E-05 fix: `os validate` loads WITHOUT authoredSource (validate.ts:159;
  config.ts:224 "Set by `os migrate meta` only"), so a probe calling `define*`
  throws inside the load and never reaches the gate the recipe is proving.
- D-03 the data-gate table is printed by the run itself (meta.ts:106-141,
  measured: the two `unlocks` strings, the deployment scoping and the dry-run
  sentence are all emitted). Only the non-CLI half survives -- carry each
  printed gate into the report as pending, by name.
- D-07 the three-layers preamble and Layer table; the section headings and the
  pipeline line already are the construct.
- B-02 the Guardrails section: 4 of 6 restated the boundary verbatim and #6
  restated the data-gate rule. The one non-duplicate (one conversionId per
  commit) moves into the boundary; #6 now lives where the gates are printed.
- D-04 the AST-is-lossy rationale, D-05 the 2nd of five statements of
  "replay from the target major", D-06 two bullets restating the paragraph
  above them, D-08 a justification of a template row already in the template,
  B-03 the 3rd copy of the [REMOVED] grep, C-01 routing owned by README and by
  the domain skills' own frontmatter.

B-03 left the .upgrade/ workspace listing a file nothing writes any more; that
list now names retired-names.txt, which 2.6 actually produces.

Ratchet: 7799 -> 6944 (ceiling 8333).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…ibeMessageSchema declares (objectstack-ai#17747)

Part of objectstack-ai#17184 — the **second half**. The first half (the four
undeclared message types) landed as objectstack-ai#17700; this PR does not revisit it.

- **Clause-②: no** — this PR puts no new key on any published payload.

`packages/spec/src/api/websocket.zod.ts` is **byte-unchanged**: blob
`3348dd76745b61e83801e2e8bae48c6756c5a69c` on both sides (`git rev-parse
HEAD:<path>` vs `git hash-object <path>`, and an empty `git diff HEAD --
<path>`). Prose only, one file.

## The defect

Six subscribe/unsubscribe sites carried every subscription field at the
**top level**, in snake_case, where the declared contract nests them and
spells them camelCase. `SubscribeMessageSchema`
(`websocket.zod.ts:278`–`:281`) declares exactly `type: 'subscribe'`
plus a `subscription` object, on top of the three `BaseWebSocketMessage`
fields; `EventSubscriptionSchema` (`:102`–`:117`) declares
`subscriptionId` / `events` / `objects` / `filters` / `channels`. A
fence copied verbatim parses against no schema in the file.

The mismatch is **not one thing**, and each axis was verified
separately:

| page (top level) | declared | axes |
|:--|:--|:--|
| `subscription_id` | `subscription.subscriptionId` | nesting +
snake/camel + **`z.string().uuid()`, so `"sub_1"` does not parse** |
| `object`, a string | `subscription.objects`, an array | nesting +
singular/plural + scalar/array |
| `filter` | `subscription.filters` | nesting + singular/plural |
| `events: ["created", …]` | `z.array(EventPatternSchema)` | bare verbs
vs patterns — `DataEventType` publishes `data.record.created` |
| *absent* | `messageId`, `timestamp` | **both required by
`BaseWebSocketMessage`** — an axis not in the card |

### Two page keys map to nothing at all

`record_id` and `query` are declared **nowhere** in the protocol. The
narrowest scope the contract can express is object + event pattern, so
those two sections now say so and move the narrowing to the client
rather than showing fields no schema carries.

### Second, independent defect in the same bullet list

The page described `filter` as *"Optional filter (same syntax as HTTP
API filters)"*. The schema's docblock (`:106`–`:115`) says the opposite
in as many words — *"⚠️ **NOT YET ENFORCED** — no runtime evaluates a
payload filter. `matchesSubscription` matches on object name and event
type only … a subscription carrying `filters` receives every event its
patterns match."* The page promised enforcement the contract explicitly
disclaims; it now carries that disclaimer.

## Fence census — the round's first deliverable

Enumerated on this tree, **not** inherited from the dispatch's page-wide
counts.

| # | line (pre) | section | type | top-level keys |
|--:|--:|:--|:--|:--|
| 1 | 256–266 | Subscribe to Object Events | `subscribe` | `type`,
`subscription_id`, `object`, `events`, `filter` |
| 2 | 379–384 | **Unsubscribe** | `unsubscribe` | `type`,
`subscription_id` |
| 3 | 404–412 | Subscribe to Specific Record | `subscribe` | `type`,
`subscription_id`, `object`, `record_id`, `events` |
| 4 | 421–435 | Subscribe to Query Results | `subscribe` | `type`,
`subscription_id`, `object`, `query`, `events` |
| 5 | 655–662 | Permission Enforcement | `subscribe` | `type`,
`subscription_id`, `object`, `events` |
| 6 | 679–689 | **Client Reconnection (JS)** | `subscribe` | `type`,
`subscription_id` (+ `sub_${Date.now()}`) |

⭐ The card named **four** sections. The census found **six** sites:
**Unsubscribe** (objectstack-ai#2) and the **JS client snippet** (objectstack-ai#6) were not on the
card's list. objectstack-ai#6 was also missed by a first pass that matched only JSON
`"type": "subscribe"` — it uses the single-quoted JS form, and the
census was widened to catch it.

After the edit all six carry exactly `messageId` / `type` / `timestamp`
/ `subscription` (or `request` for unsubscribe).

## Fork verdict: **(a) the page is wrong** — every fence

Producer evidence on this tree, all six fences landing on the same
verdict:

- `SubscribeMessageSchema`, `UnsubscribeMessageSchema` and
`EventSubscriptionSchema` have **zero** consumers outside
`packages/spec`'s own declaration, its unit test and the type-alias pin.
- `IRealtimeService.handleUpgrade` is deliberately unimplemented
platform-wide — `service-realtime/src/no-channel-route.pin.test.ts:49`
asserts it **is not a function**.
- `subscription_id` → **0** across **6120** tracked `packages/**` source
files (no `dist/`), with four lit controls on the same corpus and the
same call: `subscriptionId` **40**, `eventTypes` **27**, `handleUpgrade`
**18**, `EventSubscriptionSchema` **13**.
- The 660 `record_id` hits are the generic DB-column convention:
**zero** files carry both `record_id` and `WebSocket`.

Nothing produces or consumes a WebSocket subscribe message, so no
top-level field on one can be real. Reading (b) — the schema is
incomplete — would need a producer, and there is none.

**objectui: reported, not counted.** All WS-subscription probes return 0
there, but the schema-side control is also dark (`subscriptionId` 0,
`EventSubscription` 0) — only the generic `useState` (2262) lights,
which proves the corpus is real source and nothing more. A zero whose
control does not light is not a reading. `cloud` is not checked out
here: **NOT MEASURED**.

## Verification

- **40/40** derived gates green, on a clean full re-run after the
builds. Gate family derived mechanically via `node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (it reads the change set from the merge base
itself), not hand-listed.
- Five gates first returned `PREREQUISITE NOT MET` / "build first" (exit
3 and exit 1) — **NOT MEASURED, not red**. After building
`@objectstack/lint...`, `@objectstack/spec` and
`@objectstack/client-react...` all five are green. Exit codes captured
**before** any pipe.
- MDX compiles: `pnpm exec fumadocs-mdx` exit 0.
- No generated or tracked artifact moved —
`authorable-surface.base.json` included. `git status --porcelain` shows
exactly one file.
- No control characters: `check:nul-bytes` exit 0, plus a direct `grep
-naP` sweep of the edited file.

## `skip-changeset`, and a conflict with the dispatch to flag

⚠️ **The dispatch asked for a patch changeset; this PR carries none, and
the label instead.** Reporting rather than silently choosing:

- `content/docs/**` ships in **no** published package's `files[]` — 0 of
70 publishable packages mention `content/` or `docs/` there, with all 70
shipping `dist` as the positive control.
- `@objectstack/docs`, which owns the page, is `private: true` — there
is no publishable package to bump. Naming any other package would ship a
release note for a package this PR does not touch.
- **Precedent on the identical surface**: objectstack-ai#17700, the first half of this
very card, merged with no changeset.

By the standing criterion — nothing published moves — this is the
textbook `skip-changeset` case, which `lint.yml` states in those words.
Happy to add a changeset if the maintainer reads it the other way.

## Scope

Confined to the subscribe/unsubscribe fences and their Parameters bullet
lists. ⛔ Not `websocket.zod.ts`, ⛔ not the message-type sections objectstack-ai#17700
corrected, ⛔ not `content/docs/releases/`, ⛔ no other page.

One finding deliberately left out and filed separately: the `type:
"event"` fences on this same page carry top-level `subscription_id` /
`event` / `data` / `changes` / `reason` where `EventMessageSchema`
declares `subscriptionId` / `eventName` / `payload` / `userId` and no
`changes` or `reason`. Same defect class, different message family — it
is not this round's declared face, and it is not silently widened into.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH

---
_Generated by [Claude
Code](https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
… throwing (objectstack-ai#18103)

Closes objectstack-ai#16910

Clause-②: no

The diff is `packages/lint/src/**` plus one changeset. It touches no
path under `packages/spec/src/**` — the contract surface
`SUSPECT_TIER_GLOBS` declares — adds no schema, no published error code
and no new finding id, so the PR-body carrier agrees with the card's.
Self-read with `readClause2Line()` against `CLAUSE2_KEY_LINE` taken
fresh from `scripts/pm/check-clause2-carriers.mjs` (three capture
groups; the value is the last): `{ kind: 'declared', value: 'no' }`.

## What was wrong

`lintFlowPatterns` read `.label` off each member of a flow's `edges`
list behind nothing but an `Array.isArray` check, which proves the LIST
and never its MEMBERS. A YAML `edges:` item left empty deserialises to
`null`, so hand-written metadata turned `objectstack validate` into an
uncaught `TypeError` out of a function contractually typed `(stack) =>
FlowLintFinding[]`.

Reproduced on `origin/main` at `66e34d14d9` before any edit — the same
frame the card names:

```
edges:[null, valid]        threw=YES  TypeError: Cannot read properties of null (reading 'label')
                                      at scanErrorLabelledEdges (lint-flow-patterns.ts:691:28)
edges:[undefined, valid]   threw=YES  TypeError: Cannot read properties of undefined (reading 'label')
edges:['a string', valid]  threw=NO   findings=1
edges:[42, valid]          threw=NO   findings=1
CONTROL edges:[valid]      findings=1  rules=flow-error-label-not-fault
```

A linter that throws instead of reporting takes the whole gate down on
exactly the malformed document it exists to catch, and the author gets a
stack trace where a diagnostic belongs.

## The semantics chosen, and why it matches objectstack-ai#16751

**The junk member is DROPPED, silently**, through `recordsOf`'s `isRec`
filter — the same coercion, from the same one home (`object-graph.ts`),
that objectstack-ai#16751 chose for the seven flow-NODE-list readers. Two sibling
lists on one flow member now cannot disagree about what a malformed
member means. Dropping is not reporting: inventing a finding about an
entry no author wrote is the phantom half of this same defect class,
which is why every assertion below is an equality against a control and
not a lower bound.

## Positive control — the valid edge is still judged

Not throwing is half a contract; a guard that abandoned the list would
satisfy it and would have traded the crash for silence. Pinned by
IDENTITY (rule id and `where`), not by "did not throw", at both
addressing depths and for all four bad-member kinds:

```
edges:[null, valid]       findings=1  flow-error-label-not-fault  where="flow 'crm_flow' · edge 'act' → 'done'"
edges:[undefined, valid]  findings=1  (identical)
edges:['a string', valid] findings=1  (identical)
edges:[42, valid]         findings=1  (identical)
CONTROL edges:[valid]     findings=1  (identical)
```

and the whole `lintFlowPatterns(...)` result is asserted `toEqual` the
control's, so nothing is invented either. The family sweep adds the same
pin over the WHOLE rule table (`AUTHORING_RULES`), for `null` /
`undefined` / a string / a number / an array, at both depths —
`RESIDUAL_THROWS` and `RESIDUAL_INVENTED` both stay empty.

## The census — list-shaped record reads in `lint-flow-patterns.ts`

Triage asked how many exist and how many go through `recordsOf`. Read
off the file, not off memory. **Nine sites; four went through
`recordsOf` before this PR, six do now, three never did and do not need
to.** Zeros reported.

| # | Site | Before | After |
|---|---|---|---|
| 1 | `:1430` `recordsOf(stack.flows)` | `recordsOf` | unchanged |
| 2 | `:1441` `recordsOf(flow.nodes)` | `recordsOf` | unchanged |
| 3 | `:462` `recordsOf(graph.nodes)` — `findDataNodeAnywhere` |
`recordsOf` | unchanged |
| 4 | `:1541` `recordsOf(graph.nodes)` — main walk | `recordsOf` |
unchanged |
| 5 | `:1442` `flow.edges` via `Array.isArray` cast | ⛔ member-blind —
**THREW** | **`recordsOf`** |
| 6 | `:1542` `graph.edges as unknown as AnyRec[]` | ⛔ member-blind —
**THREW** | **`recordsOf`** |
| 7 | `:1276` `regionNodesOf` — `Array.isArray(nodes)` cast | not
`recordsOf`; **member-safe** | unchanged |
| 8 | `:866` `cfg.conditions` via `Array.isArray` cast | not
`recordsOf`; **member-safe** | unchanged |
| 9 | `:1359` `cfg[slot]` region `branches` list | not `recordsOf`;
**member-safe** | unchanged |

**The eighth triage predicted is real, and it is objectstack-ai#6.** It is not
reachable through the call-site coercion: a nested region's edge list is
read out of a container's open `z.record` config by `collectFlowGraphs`
behind only `Array.isArray`. Measured on a tree carrying the top-level
repair alone, a nested `body.edges` holding `null` still threw from the
identical `scanErrorLabelledEdges` frame.

Sites 7–9 stay as they are, deliberately, and each was checked rather
than assumed: `regionNodesOf`'s only consumer guards with `if (!child ||
typeof child !== 'object') continue`, `cfg.conditions` is read with
optional chaining (`c?.label`), and the `branches` list hands every
member to `regionNodesOf`, which refuses a non-object. Re-pointing them
at `recordsOf` would change no behaviour and would add copies for
`collection-coercion-single-copy.test.ts` to count.

⭐ **A tenth and eleventh site, one file over, and they are in this PR.**
`os validate` runs the rule TABLE, so one throwing reader takes every
other rule's verdict down with it. The moment `lintFlowPatterns` stopped
throwing, the new sweep arm went red on `validateStackExpressions`
(`validate-expressions.ts`), which read the same list through the same
double cast at `:1405` and handed `flow.edges` on raw out of a `{
...flow }` spread. Both are re-pointed here. Fixing only the filed rule
would have satisfied the card's letter and left the gate down on the
same document.

## Ablation — which reader actually carried the crash

Each reader reverted one at a time on top of the final commit, the
mutation proved on disk (`grep -cF` on both texts plus a `git
hash-object` blob-changed check), then restored. ⛔ Not settled by an
exit code and ⛔ not by `trap` alone: every restore is settled by `git
hash-object` equal to the HEAD blob AND an empty `git diff HEAD`, both
printed.

| Ablation | Result |
|---|---|
| A · `lint-flow-patterns.ts` `flow.edges` back to the cast | **519
passed, 0 failed** — green |
| B · `lint-flow-patterns.ts` `graph.edges` back to the cast | **10
failed** / 509 passed |
| C · `validate-expressions.ts` `graph.edges` back to the cast | **6
failed** / 513 passed |
| D · `validate-expressions.ts` `flow.edges` to a member-blind cast |
**519 passed, 0 failed** — green |

⭐ **Reported as measured, not as predicted.** Both edge walks read
`graph.edges`, because `collectFlowGraphs` re-exposes whatever array it
is handed — so B and C are the load-bearing repairs and A and D are
**defence in depth, not the fix**. A and D are kept deliberately: they
hand the COERCED array to `collectFlowGraphs` instead of the raw one,
which is the discipline the node lists already follow, and they keep two
sibling lists on one flow member reading the same way. ⛔ Read them as
belt and braces, not as one repair written twice.

⚠️ D's first run was a no-op — the anchor arguments were inverted, the
replacement asserted zero occurrences, and a hole in the landing check
(it could not tell "swapped" from "never had the new text") reported it
as landed. That reading was **discarded as void**, the check was
strengthened with the blob-changed assertion above, and the row in the
table is the re-run.

## Reverse-read — which existing sentence does this make false

**One sentence changed truth value, and it changed in the direction that
is easy to miss.** `findDataNodeAnywhere`'s docblock
(`lint-flow-patterns.ts:450-453`) says the arrays handed in "are the
ones the caller already coerced through `recordsOf`, so a malformed
member cannot make this throw". Its call site at `:1505` passes `nodes,
edges` — and before this PR `edges` was NOT coerced, so that sentence
was **already false about half its subject**. This change makes it true.
It is left unedited because it is now accurate as written.

**Nothing became false.** The comments at `:1432-1440` and `:1537-1540`
are scoped to the node lists and stay true; `:1531-1533` ("a non-array
`nodes` still cannot throw") stays true. The
`non-record-object-entry.test.ts` narrative sentences carrying issue
numbers and dates are history and were not touched.

**A bare present-tense count DID rot, and its ratchet caught it rather
than my reading it.** `validate-expressions.test.ts`' read-surface table
pinned `flow` as reading exactly `['name', 'nodes']`; the new literal
`flow.edges` read makes that `['edges', 'name', 'nodes']`. That table
exists to force a deliberate visit when a read is added, so the visit is
the update, with the reason recorded beside it. `edges` is declared by
`ObjectStackSchema.flows[]`, so the guard's second half (every key read
is a key the spec declares) still holds and `TRACKED_UNDECLARED_READS`
stays empty.

**Tests pinning today's throwing behaviour: ZERO.** Searched `toThrow`
across `packages/lint/src/*.test.ts` — no case anywhere asserted that a
flow edge list throws, in either file. Nothing was re-judged and nothing
was deleted.

## Verification

- `pnpm --filter @objectstack/lint test` — **103 files, 3817 passed, 5
skipped, 0 failed** (`VERDICT command-exit 0` under the shared verify
lock).
- `pnpm --filter @objectstack/lint typecheck` — exit 0, including
`check:test-typecheck`.
- `pnpm --filter '@objectstack/lint^...' build` — dependency closure
built before anything was judged (`VERDICT command-exit 0`).
- **Repo-wide lint, not narrowed**: `node --stack-size=4000 eslint .
--no-inline-config --format json` ran to completion in the foreground at
`4b70081eb8` — **6743 files linted, 0 errors, 0 warnings, exit 0**. The
population is read from eslint's own config and the count from its
`--format json` output; no narrowing was claimed or needed.
- **Derived gate family**, from `scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` over the real change set
(it takes the merge base itself), reconciled with `--ran` carrying exit
codes: **59 derived, 59 accounted, 56 green, 0 UNRUN.** The 3 non-green
are `check:dual-build-cjs-loads`, `check:lean-entry-closure` and
`check:type-check-debt`, each `exit 3` = the gate's own `PREREQUISITE
NOT MET` (they read a whole-repo `dist/`). ⛔ Those are NOT MEASURED —
neither a pass nor a failure — and CI builds the repo before running
them.

## Changeset — measured, not defaulted

**A changeset, ⛔ not `skip-changeset`.** `@objectstack/lint` is
published (no `private`), its `files[]` is `["dist", "README.md",
"CHANGELOG.md"]`, and after `pnpm --filter @objectstack/lint build` the
changed modules are present in `dist/index.js` and `dist/runtime.js`
with a positive control (`flow-error-label-not-fault` found) and a
negative control (the test-only `underNestedRegionEdges` absent).
Behaviour on malformed input changes on a shipped path, so `patch`. No
`skip-changeset` label applied.

**No new published finding id.** No `FLOW_*` rule constant is added and
no new diagnostic is emitted; on every well-formed document the output
is byte-identical. The only behaviour that changes is on input that
previously crashed.

## 验收备注

- **filed as objectstack-ai#18102** — `collectFlowGraphs` (`packages/spec`) filters
the NODE members it hands out (objectstack-ai#16752) and forwards `edges` untouched,
so `FlowGraph.edges` is declared `readonly FlowEdgeParsed[]` and can
hold a non-record. Latent, ⛔ not a live crash: both `graph.edges`
consumers are coerced by this PR and the producer never dereferences an
edge member. Filed rather than fixed here because it is a different
package and would widen this PR's verification surface past the card's
declared landing point. Deduped first — the REST search endpoint is 403
by proxy design and the MCP search quota was exhausted, so the
documented fallback was used (REST list of `domain:devx` issues,
`state=all`, 198 read, local grep) with a positive control:
`collectFlowGraphs` hit 2 cards including the known objectstack-ai#16751, while
`FlowGraph.edges` and `isRegionDict` hit zero.
- **noted, not filed** — `validate-expressions.ts:1200`'s
`collectFlowGraphs({ ...flow, nodes, edges })` still spreads `flow`,
though the callee reads only `nodes` and `edges`. The spread is now
inert. Successor: none — no PR or person is heading for that line, and
removing it would be an unmeasured behaviour change for no gain.
- ⚠️ **Scope declaration**: `validate-expressions.ts` and
`validate-expressions.test.ts` are edited beyond the card's named file.
Taken in place under the bounded-repair exemption — identical defect
class, mechanical repair whose shape is already pinned by objectstack-ai#15793 at
`:1279` in that same file, no open PR touches `packages/lint` (5 open
PRs checked), and it adds no verification surface: the sweep arm this
card required is what caught it and what pins it. The claim comment is
the PM's and carries no file list, so this paragraph is the file-surface
declaration.

---
_Generated by [Claude
Code](https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ferences, TOC the seven long reference files, trim data's description (objectstack-ai#19738)

Fixes objectstack-ai#19715
Clause-②: no

## 维护者速读(草稿)

**改了什么** — 四个超过 500 行的已发布 `SKILL.md`(platform 1223 → 487 · automation
961 → 448 · data 854 → 469 · upgrade 600 →
488)按标题边界切分:每个被移走的段落与代码块**逐字节**落到该技能自己的 `references/` 下(新增 9
个参考文件),原位只留一行指针;七个超过 300 行的参考文件顶部各加 5 行目录(锚到已有标题);`objectstack-data` 的
`description` 压到 689 字符,触发短语与排除句原样保留、只删括号枚举。零删内容:20 个移动块中 17 个字节相同,3
个仅改了 10 处相对链接路径(逐条列在下文)。

**为什么改** — 决策批次 objectstack-ai#213 第 1 项,维护者裁决「1 2 4 同意」:超长 SKILL.md 会被 `head`
预读截断,示例与运维尾段读起来像第二个技能;切成「路由 + 规则」后入口文件只装规则、决策表与指针,长示例按需跟指针再读。

**风险与代价(含回滚)** — ① 直接读 `SKILL.md` 单文件时,105 个 eval 断言词里有 18 个(从 103 降到
85)现在只经指针可达;整棵技能树(SKILL.md + rules + references)的读数 105/105
前后不变,方法与局限见下文。② 为了到 500 行,platform 的第二部分(插件开发)与 automation
的「状态机与审批」整段下沉,其中含少量决策表——它们仍在树里,入口页保留指向 `rules/*.md` 的路由行。③
三个门禁脚本的数据行跟着文本走(token 天花板行、role-word 基线两行搬家、scaffold 政策的载体路径),没有新增门禁。④
令牌总量 +2058(指针行 + 目录行),四个入口行已按落地值下锁,七个目录文件中 4 行按裁决上调、3 行有余量按落地值下锁。回滚:整个
PR 一次 revert 即可,没有生成物或状态迁移。

**席位意见** — **建议批准并合并,但先看一眼「Sections that left SKILL.md」。**
裁决的八条形状逐条核过:零删内容(20 个移动块 17 个字节相同、3 个只改了 8 处相对链接;席位另做了四个技能的整行多重集比对,除链接与
description 外没有任何一行离开树)、四个入口文件全部 ≤ 500、data 的 description 689
字且触发短语与排除句原文保留、七个长参考文件各 5 行目录且锚点全部可达、净 +77 ≤ +80、无新门禁、changeset 按 objectstack-ai#19721
惯例。evals 读数按机械法前后树列 105 / 105 不变;入口页单读 103 → 85,离开的 18 个词都在指针可达处 ——
这是切分的代价,不是丢失。token 棘轮四行入口下锁、九个新文件零余量钉住、四行目录文件按裁决引文上调。第 0 轮 CI 红是本 PR
的:`packages/rest` 的仓级测试钉住 automation 入口页必须写出状态内省路由,切分把唯一拼写搬走了;第 1
轮把拼写放回指针行(净 0 行,不改测试),席位在补丁头上重跑该测试 8 / 8 绿。留给你的一个判断:为了到 500 行,platform
的插件开发与运维两大部分、automation 的状态机与审批、data 的 seeds / 字段组 / lint 整段下沉,其中含少量决策表
—— 它们仍在树里、入口页留有路由行;如认为某段必须留在入口页,点名即可,dev 按行预算换一段下沉。复核记录:PR 评论
5786766593(PASS,在席按服务档渲染);ACCEPT 在卡 objectstack-ai#19715。CI 读数:32 latest-per-name
check runs — 25 success, 7 skipped, 0 in progress, 0 other。

**你要做的** — 这是 Tier H(`skills/**`)受管面:确认「哪些规则段可以离开入口页」符合你的意图(见「Sections
that left SKILL.md」),然后手工合并;如认为某段必须留在入口页,请点名,我按行预算再换一段下沉。

---

## What this does

The four published `SKILL.md` files over 500 lines are split at heading
boundaries into routing + rules. Every moved paragraph and code block
lands **verbatim** under that skill's `references/` (nine new files)
with a one-line pointer left where it was; the seven reference files
over 300 lines gain a 5-line table of contents anchored to their
existing headings; `objectstack-data`'s `description` shrinks to 689
characters with the same trigger phrases and exclusion sentence. The
token ratchet re-locks the four entry rows at their landed counts, pins
the nine new files at theirs, and raises the four TOC'd rows that
carried no headroom by exactly the added tokens under the ruling.
`skills/README.md` and `content/docs/ai/skills-reference.mdx` are
regenerated from the trimmed frontmatter by `gen:skill-docs`.

Premise re-measured on `origin/main` `106d4c8dd` before editing: the
four line counts (1223 · 961 · 854 · 600) hold; the seven files carried
no TOC; data's `description` measured **1003** characters by a YAML
folded-scalar parse (the card's 1,023 was a different counting; either
way it sat against the 1,024 cap). `premise_still_valid: true`.

## Readings after (head `011dfd603`)

| File | Before | After |
|:--|--:|--:|
| `skills/objectstack-platform/SKILL.md` | 1223 | **487** |
| `skills/objectstack-automation/SKILL.md` | 961 | **448** |
| `skills/objectstack-data/SKILL.md` | 854 | **469** |
| `skills/objectstack-upgrade/SKILL.md` | 600 | **488** |
| data `description` (YAML folded, trailing newline excluded) | 1003
chars | **689 chars** |
| net lines across `skills/**` (tracked +77 / −1781, new files +1781) |
— | **+77** (budget ≤ +80) |

## Diff proof — every moved block (source lines at `106d4c8dd` →
destination), verified byte-for-byte against the base blob

| # | Source (lines, count) | Heading(s) | Destination (lines) | Bytes |
|:-:|:--|:--|:--|:--|
| 1 | platform `SKILL.md` 39–68 (30) | `### Minimal Example` |
`references/bootstrap.md` 3–32 | verbatim |
| 2 | platform 116–157 (42) | `### Map Format (Key → Name)` · `###
Barrel Import Pattern` | `references/bootstrap.md` 34–75 | verbatim |
| 3 | platform 272–280 (9) | `### Scaffolding Command` |
`references/bootstrap.md` 77–85 | verbatim |
| 4 | platform 498–527 (30) | `### Plugin Loading Order Matters` · `###
Programmatic Bootstrap (Without CLI)` | `references/bootstrap.md` 87–116
| verbatim |
| 5 | platform 531–542 (12) | `## Multi-App Composition` |
`references/bootstrap.md` 118–129 | verbatim |
| 6 | platform 585–644 (60) | `## Complete Working Example` |
`references/bootstrap.md` 131–190 | verbatim |
| 7 | platform 649–1007 (359) | `# Part 2 — Plugin Development & Kernel
Extension` (whole) | `references/plugin-development.md` 1–359 | 6 link
paths re-pathed, else verbatim |
| 8 | platform 1012–1213 (202) | `# Part 3 — Operations: CLI, Testing,
Deployment` (whole) | `references/operations.md` 1–202 | verbatim |
| 9 | automation 143–209 (67) | `### Flow Example — Auto-Escalate
Overdue Cases` | `references/examples-flows.md` 3–69 | verbatim |
| 10 | automation 378–760 (383) | `## State Machines & Approvals`
(whole) | `references/state-machines-and-approvals.md` 1–383 | verbatim
|
| 11 | automation 851–916 (66) | `### Time-relative triggers — scheduled
per-record date sweep` | `references/examples-flows.md` 71–136 |
verbatim |
| 12 | data 184–230 (47) | `## Field Groups (MVP)` |
`references/examples-objects.md` 3–49 | verbatim |
| 13 | data 300–359 (60) | `## Quick-Start Template` |
`references/examples-objects.md` 51–110 | 1 link path re-pathed |
| 14 | data 499–525 (27) | `### Lifecycle Hooks` |
`references/examples-objects.md` 112–138 | 1 link path re-pathed |
| 15 | data 565–622 (58) | `## Metadata Protection (\`protection\`)` |
`references/examples-objects.md` 140–197 | verbatim |
| 16 | data 626–767 (142) | `## Seed Data & Fixtures (\`defineSeed()\`)`
(whole) | `references/seeds.md` 1–142 | verbatim |
| 17 | data 771–823 (53) | `## Linting & Generation Quality` |
`references/lint-rules.md` 1–53 | verbatim |
| 18 | upgrade 269–301 (33) | `### 2.3 A worked R1 — the retired
field-mapping \`transform\`` | `references/examples-upgrade.md` 3–35 |
verbatim |
| 19 | upgrade 449–496 (48) | `### 3.4 The report — the human half` |
`references/examples-upgrade.md` 37–84 | verbatim |
| 20 | upgrade 540–573 (34) | `## The v17-canonical shapes, compiled` |
`references/examples-upgrade.md` 86–119 | verbatim |

Method: for each row the base bytes (`git show 106d4c8:path`, the
listed lines) were hashed against the destination's listed lines — 17
rows identical, 3 rows differ only on the link-path lines below. The
upgrade `a`-tag anchors (`decide-alone-or-ask`, `reverse-check`) stayed
in `SKILL.md` because they sit just outside the moved ranges. The four
new multi-block files carry a one-line H1; the five single-block files
begin with the moved heading itself.

**The only non-verbatim bytes — 10 link-path retargets** (a relative
link crossing a move boundary would otherwise dangle):

- platform `SKILL.md` (staying text): `#verify-your-work`,
`#ports--networking`, `#part-3--operations-cli-testing-deployment` →
`./references/operations.md#…` (3)
- `references/plugin-development.md` (moved text, lines
656/657/658/738/760/791 at base): `./rules/plugin-lifecycle.md` →
`../rules/…` (2), `./rules/service-registry.md` → `../rules/…` (2),
`./references/plugin-hooks.md` → `./plugin-hooks.md` (2),
`../objectstack-data/SKILL.md` → `../../objectstack-data/SKILL.md` (1)
- data `SKILL.md` (staying text): `#field-groups-mvp` →
`./references/examples-objects.md#field-groups-mvp` (1)
- `references/examples-objects.md` (moved text, base lines 359 and 523):
`./rules/indexing.md` → `../rules/indexing.md` (1),
`./references/data-hooks.md` → `./data-hooks.md` (1)

Every intra-file anchor in the 15 touched markdown files resolves
(github-slugger, the repo's slug authority; 100 anchors, 0 unresolved)
and all 104 relative links under `skills/**` resolve to existing files
(the two `./crm_index.md` / `./crm_user_guide.md` spellings in
`objectstack-ui/rules/pages.md` are illustrative example text on the
base, untouched).

## Sections that left SKILL.md, and why the route had to widen

The card's route (platform's ops tail + the two 60-line complete
examples) reaches 894 lines on platform — the fenced code in the four
files totals 462 / 310 / 223 / 162 lines against the 723 / 461 / 354 /
100 that had to go, so examples alone cannot reach 500 on three of the
four. The cut therefore also moves whole self-contained parts: platform
Part 2 (plugin development, which already has its own `rules/*.md` and
`references/plugin-hooks.md` — the entry keeps a routing line to them),
automation's State Machines & Approvals, data's Seed Data & Fixtures,
Field Groups and Linting. Decision tables that now live behind a
pointer: platform's ObjectKernel vs LiteKernel, Plugin Loading Order,
Well-Known Plugin Names, MetadataPlugin boundary, Feature Flags, the ops
tables; automation's Approver Types, Node Config, Branching, Best
Practices and the state-machine rule; data's seed tables and the
lint-rule table. Every pointer names the destination and the moved
headings (heading names only — no paraphrase). If the maintainer wants a
named section back in the entry, it costs its line count against the 500
ceiling; say which and I swap.

## Evals reading (card item 5) — method, before / after, limit

There is no executable runner in this repo: the fixtures
`skills/*/evals/*.json` are read by
`scripts/check-skills-token-ratchet.mjs` for pricing only (no script
consumes `must_contain`). The reading is therefore the mechanical one
the dispatch names: for every eval of the four skills, each
`must_contain` term's presence (substring, case as written) in that
skill's `SKILL.md` alone and in the skill tree (`SKILL.md` + `rules/**`
+ `references/**`), on `106d4c8dd` and on this head. Script:
`eval-reading.mjs` (in the scratchpad, not committed).

| Skill | SKILL.md alone, before | SKILL.md alone, after | Tree, before
| Tree, after |
|:--|--:|--:|--:|--:|
| objectstack-platform | 24/24 | 18/24 | 24/24 | 24/24 |
| objectstack-automation | 26/26 | 20/26 | 26/26 | 26/26 |
| objectstack-data | 35/37 | 31/37 | 37/37 | 37/37 |
| objectstack-upgrade | 18/18 | 16/18 | 18/18 | 18/18 |
| **total** | **103/105** | **85/105** | **105/105** | **105/105** |

The tree column — the reading of record for a skill whose entry routes
to references — is unchanged for all 105 terms. The 18 terms that left
`SKILL.md` alone, each with the pointer on the eval prompt's path (the
two data terms already absent on the base, `schemaMode` and `type:
'secret'`, live in `rules/datasources.md` and `rules/security.md` and
are untouched):

| Eval | Term | Now in | Pointer in SKILL.md |
|:--|:--|:--|:--|
| platform objectstack-ai#2 (audit plugin) | `registerService`, `ctx.hook(`,
`metadata:reloaded`, `init(`, `destroy` |
`references/plugin-development.md` (+ `rules/plugin-lifecycle.md`,
`references/plugin-hooks.md`) | the Part 2 pointer, which also names
`rules/plugin-lifecycle.md` · `rules/service-registry.md` ·
`references/plugin-hooks.md` |
| platform objectstack-ai#6 (production deploy) | `OS_TRUSTED_ORIGINS` |
`references/operations.md` | the Part 3 pointer (names Ports &
networking · Deployment targets) |
| automation objectstack-ai#1 (nightly sweep) | `defineFlow`, `type: 'schedule'` |
`references/examples-flows.md` | the Flow Example pointer; the Flow
Types table row `schedule` stays in the entry |
| automation objectstack-ai#3 (approval + revise) | `type: 'approval'`, `type:
'back'`, `maxRevisions` | `references/state-machines-and-approvals.md` |
the State Machines & Approvals pointer (names Send-back for revision ·
Node Config); `approval_revise` and `revise` stay in the entry |
| automation objectstack-ai#5 (function step) | `timeoutMs` |
`references/examples-flows.md` | the Flow Example pointer |
| data objectstack-ai#4 (seeds) | `env:`, `daysFromNow` | `references/seeds.md` | the
Seed Data pointer (names Dynamic values (CEL)); `defineSeed`,
`externalId`, `upsert` stay in the entry |
| data objectstack-ai#2 (invoice lines) | `deleteBehavior`, `inlineEdit` |
`rules/relationships.md`, `rules/field-types.md`,
`references/lint-rules.md` | the Relationship Patterns table's link to
`rules/relationships.md` (unchanged) |
| upgrade objectstack-ai#2 (conditionalRequired) | `requiredWhen` |
`references/examples-upgrade.md` | the v17-canonical shapes pointer |
| upgrade objectstack-ai#3 (transform residue) | `never executed` |
`references/examples-upgrade.md` | the worked R1 pointer |

Limit of the method: substring presence is not a graded run — it cannot
say whether an agent given the prompt would follow the pointer. It is
the reading the card can have today; no runner was built (item 6). No
assertion was tuned.

## Token ratchet (`scripts/check-skills-token-ratchet.mjs`, convention
ceil(utf8 bytes / 4))

Bundle total 152,338 → **154,396** (+2,058: the 20 pointer lines, seven
5-line TOCs, four new-file titles, minus the description trim);
ratcheted ceiling sum 157,621 → 154,915 (the four entry re-locks bank
their shrink). Gate and `--self-test` green on this head (54 authored
files within ceilings, 65 self-test cases).

| Row | Ceiling before → after | Kind |
|:--|:--|:--|
| `objectstack-platform/SKILL.md` | 12984 → 5833 | re-lock at landed
count |
| `objectstack-automation/SKILL.md` | 12768 → 5762 | re-lock |
| `objectstack-data/SKILL.md` | 10009 → 6128 | re-lock |
| `objectstack-upgrade/SKILL.md` | 8333 → 6193 | re-lock |
| `objectstack-data/rules/field-types.md` | 3032 → 3158 | **raise** +126
(TOC; zero headroom) — ruling cited in the row |
| `objectstack-ui/rules/dashboards.md` | 6090 → 6252 | **raise** +162
(TOC; zero headroom) |
| `objectstack-ui/rules/list-views.md` | 3011 → 3141 | **raise** +130
(TOC +132, 2 absorbed) |
| `objectstack-ui/rules/pages.md` | 5501 → 5692 | **raise** +191 (TOC;
zero headroom) |
| `objectstack-data/references/data-hooks.md` | 12611 → 10066 | re-lock
(TOC +182 landed inside 2,727 headroom) |
| `objectstack-data/rules/relationships.md` | 3778 → 3676 | re-lock (TOC
+154 inside 256 headroom) |
| `objectstack-data/rules/validation.md` | 3109 → 2756 | re-lock (TOC
+155 inside 508 headroom) |
| 9 new rows | `examples-flows` 1436 · `state-machines-and-approvals`
5569 · `examples-objects` 1779 · `lint-rules` 970 · `seeds` 1299 ·
`bootstrap` 1351 · `operations` 3125 · `plugin-development` 3137 ·
`examples-upgrade` 1197 | pinned AT landed count, zero headroom |

The dispatch's assumption that all seven TOC'd rows sat at zero headroom
was false for four of them; those four are re-locked (a lowering, always
legitimate) rather than raised.

## Other gate data that followed the moved text (no new gate, ratchet or
check script)

- `scripts/role-word-baseline.json`: the automation entry's one `role`
occurrence (the Approver Types row) and the upgrade entry's one (`role:`
in the v17 agent shape) moved with their sections; the two ledger rows
moved with them. 44 files / 123 occurrences before and after — a
relocation, not an expansion; `check:role-word` green.
- `scripts/sync-scaffold-emission-policy.mjs`: the published-restatement
row that locates the Complete Working Example's `package.json` fence by
heading now points at `references/bootstrap.md`, where the fence lives;
`check:scaffold-emission-policy` green (2 carriers).
- Generator-owned files: `check:skill-refs` stays green without
regeneration — `build-skill-references.ts` owns only `_index.md`,
`*.zod.ts` and subfolders under `references/`, so the new top-level
hand-written files are inputs it tolerates; `_index.md` lists Zod
schemas only. `gen:skill-docs` regenerated `skills/README.md` and
`content/docs/ai/skills-reference.mdx` (the one file outside `skills/**`
and the gate scripts).
- `check-skill-identifier-liveness` leg-2 bindings (`## Seed Data` on
platform, `### Access depth …` on data) stayed in their entries.

## Gates (captured exit before any pipe; union derived by `node
scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` on `011dfd603`, 70 families, `--ran` reconciles 70/70 with
exit codes)

All 70 derived families exit 0 on `011dfd603`, plus `pnpm --filter
@objectstack/spec check:generated` (15 artifacts up to date after the
`origin/main` merge). The ones that read a build were run after `pnpm
--filter '@objectstack/lint...' build` and `pnpm --filter
'@objectstack/client-react...' build` under the shared verify lock:
`check:skill-examples` — "258 prose examples type-check across 3
surface(s)" (the moved os:check blocks included); `check:docs`;
`check:docs-transcript-drift`; the two `@objectstack/lint` doc checks.
Highlights: `check:skills-token-ratchet` + self-test ✓ ·
`check:skill-docs` ✓ · `check:skill-refs` ✓ ·
`check:skill-identifier-liveness` ✓ · `check:skill-top-level-keys` ✓ ·
`check:skill-compatibility` ✓ · `check:skill-frame-sync` ✓ ·
`check:doc-authoring` ✓ · `check:role-word` ✓ ·
`check:scaffold-emission-policy` ✓ · `check:nul-bytes` ✓ ·
`check:pm-dispatch-gates` (1905 cases) ✓ · `bare-root-worklist
--self-test` ✓ (the `skills/*/references/**` liveness/precision pin
holds over the enlarged population). `pnpm lint` (repo-wide eslint) is
CI's run: the diff touches no `.ts`/`.js` source, only markdown, one
JSON ledger and two `.mjs` gate scripts whose own self-tests ran green.

Branch merged `origin/main` `170fd836a` (three spec commits, no overlap
with this diff) before opening; `packages/spec` was rebuilt after the
merge.

## Changeset

`skip-changeset`, per the last merged `skills/**` PR (objectstack-ai#19721:
`documentation` · `skip-changeset`, Clause-②: no) and measured: no
workspace package's `files[]` ships `skills/` (positive control:
`packages/cli` ships `bin`). `Clause-②: no`. Tier H — draft, the
maintainer merges by hand.

## Acceptance notes

- Two data eval terms were already reachable only through `rules/` on
the base (`schemaMode`, `type: 'secret'`) — noted, not a card.
- `objectstack-ui/rules/pages.md` lines 356/448 carry
`./crm_index.md`-style example links that resolve to nothing; they are
illustrative package-docs text inside the Docs section, untouched here —
noted, not a card.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Wnstp2kTth7sGXfr8fXypc)_

---
_Generated by [Claude Code](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants