chore: version packages - #10
Merged
Merged
Conversation
This was referenced May 25, 2026
xuyushun441-sys
added a commit
that referenced
this pull request
Jun 1, 2026
…ility coverage (#1478) Add Prime Directive #10: when you encounter a bug/gap/unenforced capability that's unrelated to the task or too large to fix in scope, open a GitHub issue and link it — don't silently expand scope or leave it buried. Corollary: never advertise or demo a capability the runtime doesn't deliver (declared ≠ enforced). Cites the validation-rule enforcement gap (#1475) as the example. Co-authored-by: Jack Zhuang <[email protected]> Co-authored-by: Claude Opus 4.8 <[email protected]>
This was referenced Jun 28, 2026
This was referenced Jul 10, 2026
os-zhuang
added a commit
that referenced
this pull request
Jul 16, 2026
…estore PageTabsProps (#2978) (#3012) * fix(spec): stop gen:schema silently unpublishing transform-bearing schemas (#2978) PageTabsProps vanished from json-schema/ when #2967 added an ExpressionInputSchema (.transform) field — zod's toJSONSchema cannot represent transforms in the default output mode, and build-schemas.ts silently skipped it, so the next gen:docs run would have deleted the published PageTabsProps reference section. Two-part fix: 1. io:'input' fallback — when output-mode conversion fails on a transform, retry with io:'input'. These JSON Schemas describe what authors WRITE, and the input side of a transform pipe is plain data, so it is representable (for PageTabsProps.visibleWhen it emits the correct `anyOf: [string, expression envelope]` authoring shape). Rescued schemas are marked `x-io: "input"`. This restores PageTabsProps and 149 other transform-blocked public contracts (ObjectSchema, FieldSchema, FlowSchema, PageSchema, ActionSchema, …); only 18 truly unrepresentable schemas (function/Date/BigInt/custom) remain skipped. 2. Disappearance ratchet — json-schema/ is gitignored, so the committed json-schema.manifest.json records every schema key ever emitted. A key present in the manifest but absent from a build now fails gen:schema loudly with remediation steps; deliberate retirements must remove the key in the same PR. Silent skip remains only for types that have never been representable. Also escape literal `|` in the description cell of generated property tables (build-docs.ts) — rescued schemas surfaced descriptions with pipes that split GFM table rows. Closes #2978 Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Fn6qMKtJeVbs2KzouWDHhB * docs(references): regen from restored JSON Schemas (#2978) gen:docs over the post-fix json-schema/ output. PageTabsProps keeps its section (now including the visibleWhen items shape from #2967), and the 149 schemas rescued by the io:'input' fallback gain reference sections — previously delivered-but-undeclared contracts (Prime Directive #10). Existing table rows with literal pipes in descriptions are re-emitted with GFM escaping. Verified: `pnpm docs:build` compiles all regenerated MDX. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Fn6qMKtJeVbs2KzouWDHhB * fix(spec): address CodeQL alerts in gen:schema/gen:docs scripts (#3012) - build-schemas.ts: read the ratchet manifest directly and treat ENOENT as first-run bootstrap instead of existsSync-then-read (TOCTOU). - build-docs.ts: escape backslashes before pipes in table-cell descriptions — an existing `\|` would otherwise decay into an escaped backslash followed by a live pipe, splitting the GFM cell. No output changes: regenerated json-schema/ and references/ are byte-identical. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Fn6qMKtJeVbs2KzouWDHhB --------- Co-authored-by: Claude <[email protected]>
This was referenced Jul 16, 2026
xuyushun441-sys
pushed a commit
that referenced
this pull request
Jul 17, 2026
…rule Adversarial verification of my own rewrite turned up the irony: "the runtime validator enforces all 6" is itself advertising a capability the runtime doesn't fully deliver — exactly what this directive prohibits. evaluateValidationRules() has two call sites (engine.ts:2265 insert, engine.ts:2440 single-id update). The options.multi/updateMany branch never calls it, so on a bulk update all 6 rule types — plus field-level requiredWhen and per-option visibleWhen authorization, which live inside the same evaluator — are silent no-ops. The guard warning there is gated on needsPriorRecord(), which returns false for format/json_schema, so those two fail with no signal at all. #1475's 9-vs-3 gap is genuinely closed; the switch does handle all 6. But "declared == enforced" was true of the switch, not of the system. Reworded to say what's true, and filed #3106 for the call-site hole rather than expanding this docs PR into an engine fix (PD #10). The example now carries the transferable lesson: a case label is not enforcement — check the call site. Co-Authored-By: Claude Fable 5 <[email protected]>
os-zhuang
added a commit
that referenced
this pull request
Jul 17, 2026
…rective #10 (#3105) The example cited "the spec declares 9 validation-rule types but the write-path validator enforces only 3" as a live gap. #1475 closed that: the types that could never be enforced (unique/async/custom) were trimmed and the rest implemented, so ValidationRuleSchema now declares 6 and rule-validator.ts handles all 6. Rewritten as a closed loop, which can't drift the way the old claim did — and which demonstrates all three verbs the directive prescribes ("fix it, trim it, or file an issue") in one case: filed #1475, trimmed 3, implemented 3. The example turned out to still be true one layer down, so the rewrite says so rather than claiming victory: evaluateValidationRules() is wired into insert and single-id update only, so a bulk updateMany skips every rule silently — and for format/json_schema without even the warning, since it's gated on needsPriorRecord(). Filed as #3106 (bug+security) rather than fixed here, per this directive. Added lesson: a case label is not enforcement — check the call site. A stale example is itself the failure mode the directive warns about — documentation advertising a state the code no longer has. Co-Authored-By: Claude Fable 5 <[email protected]>
This was referenced Sep 11, 2026
This was referenced Sep 14, 2026
Merged
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…ion guidance (objectstack-ai#17401) `PageSchema`'s `visibleWhen` and `permissions` wrong-layer prescriptions both ended by pointing the author at `assignedProfiles` — "or gate the page with `assignedProfiles`" and "reach it through `assignedProfiles`". Measured on 2026-09-10, `assignedProfiles` has zero readers in this repo and zero readers in objectui at `3fbdd4a2d`, so both sentences advertised a capability the runtime does not deliver, at parse time, in the error whose job is to teach the correct spelling. That is Prime Directive objectstack-ai#10's exact prohibition. The key is also named for the concept ADR-0090 D2 removed. Both prescriptions now state only what the platform actually does. The `assignedProfiles` declaration and the `profiles:` / `assignedTo:` alias entries are deliberately untouched: the key's disposition needs a ruling and stays open on objectstack-ai#16929. Both edited channels fire only from the `unrecognized_keys` path, so the accept set is unchanged. Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH Co-authored-by: Claude <[email protected]>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…ver an ambient NODE_ENV says (objectstack-ai#17927) Fixes objectstack-ai#12271 Clause-②: no Dispatched by the `domain:cli` execution seat (objectstack-ai#6024) under triage's re-grade [`5650901283`](objectstack-ai#12271 (comment)) (p3 → **p2**, `Task` → **Bug**, file face unlocked to `start.ts`). Branch base `5741ff10c`, the parked worktree merged forward and **fully rebuilt** before any reading was taken — freshness proven by content, not mtime: a symbol introduced by merged commit `eadcde6d8` (`KNOWN_UNSUPPORTED_JSON_SCHEMA_PATTERNS`) occurs twice in `packages/cli/dist/commands/generate.js` and twice in its source. --- ## 1. What was actually broken — and it is not what the card's title says The card is titled around the `os dev` **compile child** inheriting an ambient `NODE_ENV=development`. That inheritance is real, but it is a symptom. Reading the failure output rather than the card: ``` [MODULE_NOT_FOUND] Warning: ModuleLoadError task: findCommand (doctor) plugin: @objectstack/cli message: [MODULE_NOT_FOUND] import() failed to load …/packages/cli/src/commands/doctor.ts: Cannot find module './registry' Require stack: - …/packages/formula/src/index.ts ``` ⭐ The module that failed to load is **`packages/cli/src/commands/doctor.ts`** — one of the CLI's *own* command modules. The casualty is the command table, not the user's config. `@oclif/[email protected]`'s `lib/config/ts-path.js` skips its TypeScript path lookup only when `isProd()`, defined in `lib/util/util.js` as a negated `['development', 'test'].includes(process.env.NODE_ENV ?? '')`. Under either value it (a) rewrites the command target from the declared `./dist/commands` to `src/commands` and (b) calls `registerTsx()`. tsx honours the tsconfig of the **current working directory**, so an application that maps a CommonJS workspace package to its TypeScript source for *type* resolution steers this CLI's *runtime* module graph into `.ts` files — and Node's CJS resolver then walks their extensionless siblings and knows nothing about `.ts`. Three consequences the card did not have: - It is **not specific to `compile`**, or to any command. `os --version` reproduces it. - It reaches invocations with **no parent at all** — `os serve --dev`, `os start` — so ⛔ no child-environment scrub can fix it. This is the reason triage measured shape B as not fixing the card. - `NODE_ENV=test` is the **second** value oclif treats as non-production, and it had never been measured on this card. vitest exports it on its own worker. ## 2. The spawn census (acceptance item 3) — measured here, with controls ⛔ Not taken from the card, the previous dev's report, or triage's comment. Read through **TypeScript's own parser** (`ts.createSourceFile`), resolving `child_process` bindings through named / aliased / namespace / default imports, `require()` destructuring and `await import()` destructuring, then matching call expressions against those bindings. Script: `census.mjs`, run at `149135743`. **Controls, both required to pass before the census prints:** | control | expectation | result | |---|---|---| | positive fixture — 7 spawn calls across every import spelling, plus one look-alike identifier that must NOT match | 7 | **7 — PASS** (`spawn, spawnSync, execFileSync, exec, execFile, execSync, fork`) | | negative fixture — a local binding *named* `spawn`, and the word inside a string literal | 0 | **0 — PASS** | **Census — 6 child-process spawn sites, not 5:** | site | API | `env` handed to the child | |---|---|---| | `dev.ts:349` | `spawnSync` | ⛔ no `env` property — inherits `process.env` whole | | `dev.ts:586` | `spawn` | `env: localEnv` (a spread of `process.env`) | | **`dev.ts:724`** | **`execSync`** | ⛔ **no `env` property — inherits `process.env` whole** | | `dev.ts:821` | `spawnSync` | `env: process.env` | | `start.ts:238` | `spawnSync` | `env: process.env` | | `start.ts:435` | `spawn` | `env: localEnv` | Dark-instrument control: the API names occur as bare words 13 times in `dev.ts` and 6 times in `start.ts`; the site counts are 4 and 2, and the gap is imports and prose.⚠️ **`dev.ts:724` is the disagreement, and it is a real site all four previous numbers missed.** The card, the previous dev's report, triage's own count and the seat's re-count all say **5**. They agree because they all counted `spawn` / `spawnSync`. `dev.ts:724` is an `execSync` reached through `const { execSync } = await import('child_process')` — a dynamic import inside a function body, invisible to a scan looking for the two static names. It is the workspace-root branch of `os dev`, which delegates to `pnpm [--filter X] dev`. ⛔ **It is deliberately not touched**, and that is a classification rather than an omission: it spawns a **user's own workspace script**, not the `os` CLI. `NODE_ENV=development` is meaningful and expected there, and scrubbing it would change the behaviour of arbitrary user dev scripts. It is in the census because the census was asked for; it is outside the defect class because the defect class is *this CLI resolving its own commands*. ## 3. Which sites were scrubbed, and why that set is **empty** **None.** The fix is one declaration in `packages/cli/bin/run.js`: ```js settings.enableAutoTranspile = false; ``` oclif checks `settings.enableAutoTranspile ?? settings.tsnodeEnabled` **ahead of** `isProd()`, so `false` skips both the source redirect and the tsx registration under every value of `NODE_ENV`. The argument for the entry over the six sites, point by point: - **A child-env scrub cannot satisfy acceptance item 1.** `os serve --dev` and `os start` are top-level processes. There is no parent. - **It is the contract this repo already wrote down.** `bin/run.js` is the BUILT entry (`bin.objectstack` / `bin.os`; `package.json` declares its command table over `./dist/commands`); `bin/run-dev.js` is the SOURCE entry. `scripts/check-cli-test-child-env.mjs` rule 3 enforces exactly that division on every *test* that spawns the CLI, with no baseline. The entry had simply never asserted it about itself. - **Every spawn site hands the child `process.argv[1]`**, i.e. this same entry — so fixing the entry fixes all of them, plus every future one, with no per-site convention to keep true. - **A per-site scrub changes what the child DOES.** `NODE_ENV` is read by product code (`start.ts`'s production default, the crypto posture, plugin-auth's origin gate). Rewriting it at six spawns to work around a module-resolution bug is the lenient-consumer shape Prime Directive objectstack-ai#12 refuses. The entry-point declaration changes only *which of two already-declared code paths* oclif loads. - ⛔ **Not a `TSX_TSCONFIG_PATH` pin either** (what `bin/run-dev.js` carries). That shim genuinely executes TypeScript, so all it can do is aim the transpiler; it cannot even do that in-process and pays a full re-exec. This entry executes no TypeScript, and a published install has no `packages/cli/tsconfig.json` to aim at — `files` names `dist` only. `dev.ts` and `start.ts` carry comment-only changes, both required by Prime Directive objectstack-ai#10 because **this change** moved what they claim. See §6. ## 4. Two-leg ablation with the `production` control (acceptance items 1 and 2) Run from the committed state. `exit 124` = compiled, booted, and **still serving** when the 60 s timeout killed it; `exit 1` = died. `NODE_ENV` is the only variable; `OS_SECRET_KEY` supplied so the production arm is not refused on crypto policy. Exit codes captured before any pipe. **End to end, `examples/app-crm` and `examples/app-showcase` (both carry sibling-src `paths`):** | invocation | leg | `NODE_ENV=development` | `NODE_ENV=production` (control) | |---|---|---|---| | `os dev --compile --fresh` | fix REVERTED | **exit 1** · 9 hits of `Cannot find module './registry'` | exit 124 · 0 hits | | `os dev --compile --fresh` | fix IN PLACE | **exit 124** · 0 hits | exit 124 · 0 hits | | `os serve --dev` | fix REVERTED | **exit 1** · 9 hits | exit 124 · 0 hits | | `os serve --dev` | fix IN PLACE | **exit 124** · 0 hits | exit 124 · 0 hits | Identical at both apps, 8 legs per side. **`os compile`, all four example apps, published entry `bin/run.js`:** | leg | showcase | crm | todo | multi-package | |---|---|---|---|---| | fix REVERTED, `NODE_ENV=development` | **1** | **1** | 0 | **2** | | fix REVERTED, `NODE_ENV=production` (control) | 0 | 0 | 0 | 0 | | fix IN PLACE, `development` / `test` / `production` | 0 | 0 | 0 | 0 | `examples/app-todo` — the one app whose tsconfig carries no `paths` block — is the only armed pass, so the failures map 1:1 onto the sibling-src population.⚠️ **The entry the card's own probe used.** `bin/run-dev.js`, invoked correctly (under `tsx`, which its shebang requires), is **0/0/0/0 under both `development` and `production`** — its `TSX_TSCONFIG_PATH` re-exec mitigation works. That is why the card read "latent" for weeks: the probe went through the mitigated entry while users run the unmitigated one. ⛔ Re-measured here rather than carried over, and a note for the next reader: running `run-dev.js` under plain `node` answers 1/1/0/2 in **both** `NODE_ENV`s — that is the shim failing to load its own `.ts` import, not this defect, and it is an easy false reading to take. **The ablation legs are one-off and left nothing behind.** Every mutation was proved to have reached disk before its run (injected marker counted, removed line counted — ⛔ never a bare `git diff --stat`), every restore leg is `git checkout HEAD -- packages/cli/bin/run.js` (⛔ never a bare `git checkout --`, which restores from the polluted index), every restore was proved by `git hash-object` against the HEAD blob rather than by an exit code, and every mutating script carried `trap … EXIT INT TERM`. ## 5. The regression pin, and why it needs a control `packages/cli/test/published-entry-node-env-source-reroute.test.ts` — 5 cases, 5 passing. Every assertion in it is an **absence** (no reroute, no signature, exit 0), and an absence passes just as well over a fixture that arms nothing. So one leg defeats the declaration **inside the child** — `test/fixtures/published-entry-auto-transpile-neutraliser.mjs`, an `--import` preload installing an accessor whose setter swallows the entry's assignment (a plain write loses to it; a non-writable property makes ESM strict mode throw) — and asserts the card reproduces **verbatim**. A second control leg runs the neutralised child under `production` and is green, pinning that `NODE_ENV` is the variable. ⛔ Nothing touches `bin/run.js` on disk: a crashed or timed-out run must not leave the entry point neutralised for the next reader.⚠️ The fixture is built in the test's own temp dir rather than pointed at `examples/app-crm`, so the suite reads nothing outside `packages/cli` — pointing at the example app would be a cross-package test input, needing a declaration in `scripts/cross-package-test-inputs.mjs` and a mirrored `turbo.json` entry that widens this package's test cache key over another package's whole source tree. Recorded because it cost a measurement: the **first** fixture attempted mapped the specifier onto an ESM `.ts` source inside `packages/cli` and stayed green in all six legs — the failure needs Node's **CJS** resolver walking a `.ts` file's siblings, so the trap has to be CommonJS with an extensionless relative `require`. A fixture that arms nothing is the exact vacuity the control exists to refuse. ## 6. `start.ts:419-421` (acceptance item 5) — the comment is true again, ⛔ no card needed ``` // NODE_ENV is only forced to production when the user has not set it. // Allows `NODE_ENV=development objectstack start` to work for debugging. if (!localEnv.NODE_ENV) localEnv.NODE_ENV = 'production'; ``` Measured at `examples/app-crm` on unmodified `5741ff10c`: `NODE_ENV=development objectstack start` → **exit 1**, 9 hits of the card's signature. `NODE_ENV=production` → exit 124. So the sentence advertised a debugging mode the runtime did not deliver (Prime Directive objectstack-ai#10). ⭐ Which half was wrong matters: the **line** was always correct — the operator's value does reach the child. What failed was the invocation the sentence names, and it failed *before `localEnv` was ever built*, in this process. So the fix makes the sentence true rather than needing a behaviour change, and the comment now says so and points at the pin instead of asserting it on its own authority. ⛔ No separate card is filed, because there is no separable behaviour change left to file. `dev.ts:328-348`'s NOTE is corrected for the same reason: it stated the consequence ("`os dev` dies before the server starts") as a property of *writing* `NODE_ENV` on the child, and this change makes that consequence unreachable. The rule itself stays — a source that asserts a loader-activating value is a different claim from an entry that refuses to act on one, and it is the half `child-env-source-loader.pin.test.ts` can see. ## 7. Fence: ⛔ no general `paths`-resolution gate None is added. Nothing here parses a tsconfig, and the pin's fixture is a tsconfig the test **writes**, never one it reads. The objectstack-ai#8020 / objectstack-ai#8108 class is untouched. ## 8. The gate interaction this PR could not avoid, stated rather than buried The new pin spawns `bin/run.js` with a `development` / `test` child — exactly what `check:cli-test-child-env` rule 3 refuses. Three `DELIBERATE_REROUTE` entries are added, which is the mechanism that gate designs for a site whose `NODE_ENV` is its independent variable.⚠️ They are the **inverse** of the two entries already there, and the registry now says so: the existing pair needs the reroute, this file asserts the reroute does **not** happen. The gate's header gains a paragraph recording that the published entry now refuses it, what rule 3 therefore still buys (it is what would notice the declaration being dropped), and ⛔ that these entries are not precedent for a spawner that wants `src/` — that is still `bin/run-dev.js`. The gate's own oclif table is untouched and still correct: it was measured against `Config.load()` with **default** settings, which is what every other built oclif entry still gets. Its self-test census pin moves from five files to six, with the reason beside the new member. ## 9. Docs — hand-read, because the drift tool declared it could not cover this file The docs-drift advisory reports `packages/cli/bin/run.js` as yielding **no anchor**, so the pages documenting the published entry's behaviour are outside its run. Hand-read instead: every hand-written page naming `NODE_ENV` (8, excluding the 6 release-owned pages, which are ⛔ read-only and were not edited), plus every page naming the CLI entry, `tsx`, or auto-transpilation. ⭐ **Nothing is falsified. Two pages document the exact invocation that was broken, and this change is what makes them deliverable:** - `content/docs/protocol/kernel/http-protocol.mdx:206-209` — *"Anything that boots the runtime without `os dev` — a bare `os serve`, an embedded host, a hand-written container entry point — must now set `NODE_ENV=development` explicitly to keep being advertised as such."* Following that instruction in a `paths`-carrying project exited 1 before this change. - `content/docs/deployment/environment-variables.mdx:33-36` — *"In dev (`os dev`, or `NODE_ENV=development`) a busy port auto-hops…"*. Same: the documented way to opt into dev behaviour was the thing that broke. The other six (`deployment/cli.mdx`, `deployment/self-hosting.mdx`, `permissions/authentication.mdx`, `plugins/packages.mdx`, `protocol/kernel/config-resolution.mdx`, `upgrading.mdx`) read `NODE_ENV` for auto-reconcile posture, the dev seed gate, sample app code, plugin-dev's production refusal, config-file selection and migration policy — none touches module resolution, and none moves. ⭐ **One page changed this PR's own claim.** `content/docs/plugins/index.mdx:399-408` documents that `os plugins …` is **not a registered command**: `@oclif/plugin-plugins` sits in `devDependencies` and oclif's core-plugin loader only matches names under `dependencies`. Verified here rather than taken on trust — `os --help` lists **34** topics and **zero** of them is `plugins`, the topic count being the control that makes the zero a reading. ⇒ the linked-TypeScript-plugin cost this PR originally stated is **unreachable today**; the docblock and the changeset were corrected to say so, with the condition under which it would have to be revisited. ⛔ No docs page needs an edit, and none was made. ## 10. Verification Everything below at HEAD `c6ea03075`, exit codes captured **before any pipe**, heavy runs serialised through `scripts/pm/os-verify-lock.sh` (slot `dev-12271`). | run | result | |---|---| | `pnpm build` (full, post-merge rebuild) | `VERDICT command-exit 0` — 73/73 tasks | | `pnpm --filter @objectstack/cli build && … typecheck` | `VERDICT command-exit 0` | | `vitest run --project unit` | `VERDICT command-exit 0` — **204 files / 2935 tests passed** | | `vitest run --project integration` | `VERDICT command-exit 0` — **47 files / 409 tests passed** | | `pnpm lint` (full repo union, ⛔ not narrowed) | **exit 0** | | the 81 families from `dispatch-gates --commands --repo objectstack-ai/objectstack` | **81 of 81 exit 0** | `node scripts/pm/dispatch-gates.mjs --ran … --repo objectstack-ai/objectstack` reconciles: *"81 derived famil(ies) accounted for — 81 run, 0 NOT-MEASURED (a DERIVED zero — all 81 recorded an exit code and none of them is 3)"*, with `pnpm lint` recorded as one run beyond the union. Re-derived after `git fetch origin main` advanced it to `dbea1756d`: the family set is **identical**, 81 before and after, zero added and zero removed.⚠️ Two readings stated rather than smoothed over: - `pnpm check:pm-dispatch-gates` first returned **124** — that was *my* 420 s per-command timeout, ⛔ not a gate verdict. Re-run with a longer budget: **exit 0**, 1678 self-test cases pass. The record carries the 0, and this note carries the first reading. - The tree is **4 commits behind `origin/main`** (`dbea1756d`), and `dispatch-gates` flags one derived-from file as stale across that range: `scripts/engine-double-contract.pinned.json`. Its diff and mine share **zero files** — the four commits touch `spec`, `lint`, `plugin-auth`, `client`, `runtime`, `metadata-protocol` and `examples`, and no `packages/cli` path — so nothing is re-scoped; the merge queue rebuilds onto current `main` regardless. ## Acceptance notes - **noted, not filed:** `dev.ts:724`'s `execSync` hands `pnpm [--filter X] dev` the parent environment whole, with no `env` property. Not a defect — that child is a user workspace script, for which `NODE_ENV=development` is correct — but it is the sixth spawn site, and the four previous counts of "5" are all explained by it being reached through `await import('child_process')` rather than the static import. Carrier: the census table above; any future card on `os dev`'s workspace-root branch inherits it. - **noted, not filed:** `check:cli-test-child-env` rule 3's *specific* harm — a `bin/run.js` spawn silently executing `src/` — is now closed at the source for this repo's entry, so the rule can no longer fire for the reason its header gives. It is not dead: it keeps a built-entrypoint spawn readable about the `NODE_ENV` it means, and it is the instrument that would catch the declaration being dropped. Recorded in the gate's own header by this PR rather than left for the next reader to discover while deleting it. - **noted, not filed:** `examples/app-showcase`'s tsconfig now carries **three** sibling-src `paths` entries (`formula`, `plugin-email`, `lint`), against the two the card tabulates, and `examples/app-multi-package` — a fourth example app the card predates — redirects `@objectstack/spec` itself. Population growth only; objectstack-ai#8249 owns it. - **noted, not filed:** `examples/app-multi-package` fails with a *different* surface under the armed environment (`The requested module '@objectstack/spec/api' does not provide an export named 'ErrorCode'`, then `command compile not found`, exit 2) rather than the `./registry` signature. Same cause, different first casualty; green after this change like the rest. Authored by the `os-dev` executor in session `session_01TSf4DV7ziu4V5j73e46b7c`, dispatched by the `domain:cli` seat (objectstack-ai#6024). (Recorded here as prose: this PR body has been edited, and the edit channel appends its own attribution block regardless of what is sent — measured twice on this PR.) --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <[email protected]>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…ld-type table — all 74 refused pairs answer DATASET_INVALID at one compile door (objectstack-ai#17560) (objectstack-ai#18011) Fixes objectstack-ai#17560 Executes the director-seat ruling on this card (decision batch objectstack-ai#127 item 3, comment 5651572190) in **one pass, not per field class**: `min` and `max` are judged by `AGGREGATE_FIELD_TYPE_COMPATIBILITY` like every other aggregate, and all 74 pairs that were refused-by-the-table-and-enforced-by-nothing now answer `DATASET_INVALID` / **400** at the compile door. ## What the tree said before this Four declarations, three answers, one pair: | declaration | `min` x `text` | |---|---| | `AGGREGATE_FIELD_TYPE_COMPATIBILITY` (spec, objectstack-ai#16353) | refused | | `dataset-compiler`'s compile leg | never judged — `if (!DERIVING_AGGREGATES.has(aggregate)) return;` | | `measureResultType` (service-analytics, objectstack-ai#15768) | a supported `'string'` result | | two shipped test files, in prose | "ruled C — the table is to be AMENDED to accept it, tracked as objectstack-ai#17513" | The fourth row had nothing behind it: objectstack-ai#17513 is closed as a duplicate of this card carrying zero rulings, and the one recorded ruling on this table — decision batch objectstack-ai#59 on objectstack-ai#16099 — refuses those rows. The ruling settled all three sub-questions together because one shared fixture drove members of both halves. ## The ruling's Execution list, line by line - **`dataset-compiler.ts`** — the `DERIVING_AGGREGATES` scope condition is gone; `assertAggregateFieldTypeCompatible` judges all six aggregates through the same `DATASET_INVALID` / 400 door. The refusal message now names the divergence each aggregate class really has (`min`/`max` SELECT a stored value and diverge on ORDER — collation-dependent for text, absent altogether for `jsonb`; `sum`/`avg` DERIVE a number and diverge on arithmetic) and prescribes accordingly. The `sum`/`avg` sentence is byte-identical to what shipped, so objectstack-ai#16099's and objectstack-ai#16778's message pins are untouched. - **`measureResultType`** — asks `isAggregateCompatibleWithFieldType` before it answers, so the rule and the table agree **by construction** rather than by review. `STRING_SOURCE_FIELD_TYPES` and the `formula` branch are retired; `min`/`max` over the temporal class still answers `'time'`. - **Tests** — the shared fixture in `measure-result-type.test.ts` is re-aimed off refused pairs following the objectstack-ai#16737 precedent in the same file; the **two conditional pins are FLIPPED, never deleted** (`aggregate-nontemporal-measure-refusal.test.ts` "min / max are NOT judged by this gate", relocated by text rather than by the ruling's line number — see Deviations; `aggregate-datetime-measure-refusal.test.ts` "a min over a TEXT field still compiles here"), each naming batch objectstack-ai#59 and this ruling; the negative control that accepted pairs still compile (`min` x `number`) is **kept** and joined by four more; the eight `objectstack-ai#17513` citations are rewritten to this card. - **The table's TSDoc "override" paragraph** is rewritten as settled ground — the overridden opinion is retired, not standing beside it. - **Breaking** — `minor` under the launch-window convention with the BREAKING banner, plus a new ADR-0087 **semantic migration entry** (`dataset-measure-selecting-aggregate-field-type-refused`, protocol major 18) writing the structured TODO that names the measure and the field type. No lossless conversion exists, which is why it is a semantic TODO and not a D2 conversion. ## One consequence the ruling implies and did not name Retiring the `formula` branch left `measureResultType`'s third input (`formulaReturnType`, objectstack-ai#16236) with no reader, and `AnalyticsServiceConfig.sourceFieldMeta`'s `returnType` key with no consumer. Both are **removed**: a declared input nobody reads is the declared-not-enforced shape Prime Directive objectstack-ai#10 refuses. `FieldSchema.returnType` itself is untouched — display formatting and validation are its other declared consumers. ## Verification Every exit code captured **on the command** (redirect first, `EXIT=$?`, then read), never after a pipe. All figures below are from the FINAL tree, `035a41c7e` — the second merge of `origin/main` (`8261ff717`) that this branch carries, built whole. They were re-taken in full on this tree after the patch round; the pre-patch tree `399d12244` read the same shape. Build and tests, through `scripts/pm/os-verify-lock.sh` (`OS_VERIFY_LOCK_SLOT=issue-17560`; four holds across both rounds, VERDICT command-exit 0 on every one — 590s / 21s / 795s / 890s held): ``` pnpm install --frozen-lockfile :: exit 0 pnpm build (whole repo) :: exit 0 pnpm --filter @objectstack/spec check:generated :: exit 0 all 15 generated artifacts up to date pnpm --filter @objectstack/service-analytics exec vitest run :: exit 0 111 files / 2392 tests passed pnpm --filter @objectstack/spec test :: exit 0 476 files / 13556 tests passed pnpm --filter @objectstack/service-analytics typecheck :: exit 0 pnpm --filter @objectstack/spec typecheck :: exit 0 pnpm lint (whole repo) :: exit 0 ``` Gate families derived from the **actual** changed paths, not from a list: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` — 15 paths, **87 commands**, no STALE TREE warning on this tree. All 87 re-run on `035a41c7e` after the patch round, each recorded as `COMMAND :: exit CODE`, and reconciled: ``` dispatch-gates --ran :: exit 0 87 derived famil(ies) accounted for — 87 run, 0 NOT-MEASURED (a DERIVED zero — all 87 recorded an exit code and none of them is 3) ```⚠️ On the pre-merge tree four of them answered **exit 3 — PREREQUISITE NOT MET** (`check:doc-formula-expressions`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:type-check-debt`): each reads built output and the closure was not built yet. That is each gate's own "nothing was measured" code, neither a pass nor a finding. All four were re-run after the whole-repo build and all four exit 0. ⛔ Not a complete account of what CI runs: the 48 artifact-roster families, the 11 declared wide-population families, the 5 path-scheduled CI jobs and the always-runs tail are each outside the derived 87, as `dispatch-gates` prints. ### Reverse verification — direction predicted BEFORE running, and it held Ablation: the deleted scope condition put back as `if (aggregate !== 'sum' && aggregate !== 'avg') return;` in `dataset-compiler.ts`, on the committed fix, under the same verify lock. ``` HEAD blob 9062405 mutated blob b2935944651c57ba3cd8ec759a3303fb33ff105c (differs; an equal or empty hash was coded to abort) on-disk proof anchor occurrences 1 before / 1 after · injected marker 0 before / 1 after (occurrence counts on the mutated text — never an editor's exit code, never a --stat) MUTATED :: exit 1 4 files failed · 13 of 209 tests red RESTORED :: exit 0 4 files passed · 209 of 209 green restore proof blob back to 9062405… · marker count 0 · git diff HEAD clean · WHOLE-TREE git status --porcelain empty (trap '<restore>' EXIT INT TERM, paths absolute from git rev-parse --show-toplevel) ``` Predicted: red in the ordinary direction — with the scope back, the compile SUCCEEDS, so every refused-pair case fails on "expected a refusal, none was thrown" rather than passing vacuously on an empty result. Observed: exactly that, spread across all four files — the two flipped pins, the re-aimed section E, and the formula end-to-end section. `node scripts/ablation-dist-preflight.mjs @objectstack/service-analytics ABLATION-17560-SCOPE-RESTORED` **:: exit 1**, reported rather than worked around: the package has no `dist/` in this tree at all. Its prerequisite is inapplicable here rather than unmet — these suites import their subject relatively (`../analytics-service.js`), so they resolve to `src/`, and the 13 reds on the mutated tree beside 209 greens on the restored one are the direct evidence that the edit reached the subject. ## Acceptance notes — noted, not filed - The table's module TSDoc still calls itself "the contract both consumer legs execute — the compile-time refusal in the dataset compiler and the authoring-time lint rule". The authoring-time leg still does not exist; `packages/lint` never calls the predicate. Carrier: **objectstack-ai#16354**, open and labelled `pm:blocked`, and the ruling leaves it there ("The lint leg is unchanged"). Not filed. - A **ninth** `objectstack-ai#17513` citation exists outside `packages/`, in the still-pending changeset `.changeset/deriving-aggregate-nonnumeric-field-refused.md` (objectstack-ai#16099's). It ships in the same release as this one and its "min / max are still not judged here" section would contradict this entry in one compiled CHANGELOG, so it carries a superseded-within-the-same-release-window note and its tracker pointer is repointed. Reported rather than assumed in scope. - `packages/spec/src/migrations/entries/semantic/18.dataset-measure-aggregate-field-type-refused.ts` (objectstack-ai#16778's entry) carried two scope sentences this change makes false — "the compile leg is scoped to that class and to nothing else" and "a measure over a field of any OTHER class is neither refused nor certified here" — plus a pointer to objectstack-ai#16785, an issue number that does not resolve. objectstack-ai#16099's own changeset recorded that widening it "is a `packages/spec` edit this card is fenced out of and is reported to the `domain:spec` seat rather than done here"; this is that seat and this card is the carrier, so the two sentences are corrected in place and now name all three entries. ## Patch round — the at-tier contract review's three FAIL grounds, closed The review (comment `5653288459`) passed the behaviour in full — one door for all six aggregates, 74 re-derived off the raw enum, table unamended, `formula` refused on the storage ground, both pins flipped not deleted, controls kept, `Clause-②: no` correct — and upheld the `returnType` removal as compelled by the ruling's own words. What failed was the truth of the contract TEXT shipping beside it. All three fixes are text-only; ⛔ no behaviour, pin, control or the `returnType` decision was touched. **T1 — a same-release changeset said the opposite, twice.** `.changeset/16236-formula-return-type-measure-column.md` (still pending, so it compiles into the same CHANGELOG block as this entry) promised a typed formula measure column and a `returnType?: string` fourth member on `sourceFieldMeta`. It now carries the same superseded-within-the-release-window treatment already given to objectstack-ai#16099's, at the head and again on the `sourceFieldMeta` paragraph. This PR's own changeset now states that the removed key **was never released** and gives the host its one line. > Reading: released `packages/services/service-analytics/CHANGELOG.md` (17.4.0) — `16236` 0 hits, "fourth member" 0 hits. Lit controls on the same file: `15768` 1, `measureResultType` 2, `sourceFieldMeta` 4. Dark control `qzwxrt4419` 0. Stronger still, the one `returnType` hit in that released text says in as many words that the key "is not on `AnalyticsServiceConfig.sourceFieldMeta`'s return shape". And `git grep "fourth member" origin/main -- .changeset/16236-…` is 1 — the adding changeset is still pending on `main`, so no tarball ever carried the key. **T2 — a dead tracker pointer the ruling itself named.** `.changeset/dataset-measure-aggregate-field-type-refused.md` (objectstack-ai#16778's, pending) still said the string rows were "under objectstack-ai#16785, ruled C — the table itself is to be amended". Corrected where it stands: `16785` resolves to nothing, batch objectstack-ai#127 found no ruling C behind the citation, and the table is not amended. The file also gains the superseded banner, and its two other now-false sentences — `sum` over a `percent` "compiles exactly as it did before", and `avg`/`sum` over temporal being "the only pairs whose behaviour changes in this release" — are marked where they stand. > Reading: `git grep 16785` over the whole repo was **1** hit, all of it in that file — the ruling's own claim reproduced. It is now 2 in the same file, both naming it as the retired pointer; repo-wide it appears nowhere else. Dark control `qzwxrt4419` 0. **T3 — the corrected objectstack-ai#16778 registry entry miscounted itself.** It claimed to be "the FIRST of three" and pointed at "the two entries that widened it — objectstack-ai#16099 …". objectstack-ai#16099 registered **no entry**: its changeset declares `not-required (already-registered dataset-measure-aggregate-field-type-refused)`, so its widening rides this id. The `surface` now reads "ONE OF TWO", names that `not-required` relationship explicitly, and says there is no third; `acceptanceCriteria` keeps its one true sentence (every refused pair is refused at the compile door at major 18) and drops the phantom entry. ⛔ Its scope criterion itself is **not** widened — that is objectstack-ai#16099's open ask and the seat is tracking it separately. `registry.ts` regenerated. > Reading: semantic entries whose id contains `aggregate-field-type` = **2** (lit control: 211 entries in the directory; dark control `qzwxrt4419` 0). No entry file is named for objectstack-ai#16099 (`git grep -l deriving-aggregate|nonnumeric` over `entries/` exits 1). In the regenerated `registry.ts`: "ONE OF TWO" 1, "no third entry to look for" 1, "FIRST of three" 0, "the two entries that widened it" 0 — and repo-wide both stale phrases are 0.⚠️ Also taken, declared rather than smuggled: the review's **F5 nit** — the changeset's opening list read as exhaustive while naming 10 of 37 types. It now says "any of the **37** field types outside the numeric, temporal and boolean classes — for example …", and points at the ADR-0087 entry for the full list. One phrase, in a file T1 already reopens. ⛔ Not done, deliberately: widening objectstack-ai#16778's `acceptanceCriteria` to `sum`/`avg` over every class (objectstack-ai#16099's open ask, fenced out by the dispatch), and the four items the review listed as "not this PR's to fix". ## Landing ⛔ **Draft, and it stays that way until the seat's contract review clears it.** The claim grades this `Clause-②: no` — nothing starts being accepted, this pulls code back to the declared contract — but the **path** limb of the clause-② enqueue gate fires on `packages/spec/src/**` regardless of the declaration, so `needs:contract-review` is carried on both the PR and the card. ⛔ Not flipped ready, ⛔ auto-merge not armed, ⛔ not queued. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <[email protected]>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…sert identity (objectstack-ai#18381) Fixes objectstack-ai#17135 Clause-②: yes The rule's first run on a real application reported **12 fields, and all 12 were on screen or load-bearing that day**. Triage ruled the direction (`objectstack-ai#17135` comment `5620725440`): *"⇒ Teach the rule about both consumer paths"*, with the guardrail *"⛔ Do not fix it by exempting `hidden` fields wholesale — a `hidden` field with genuinely no consumer is still a finding; the point is that a seeder-only identity column **has** one."* Both consumers are now read off the spec, the way the rule's existing two exemptions (`injectedColumnsFor`, `resolveDisplayField`) already are. ## 1 · The synthesized layout is a display consumer `deriveFieldGroupLayout` (`@objectstack/spec/data`, ADR-0085 §5) is the single derivation every renderer applies — form, detail, drawer, designer. It places a field by its `group` membership in the object's declared `fieldGroups`, so **no `fields: [...]` array anywhere names it**. A field the derivation puts in a **keyed** section is drawn, and is now credited as a `display` site. This follows the **previews-count-as-consumers ruling** (2026-08-10, [objectstack-ai#7131 comment 5237224148](objectstack-ai#7131 (comment))), which objectstack-ai#17135 pointed at through objectstack-ai#7427: *"A designer preview that renders a key to a human is a runtime consumer."* **The trailing untitled bucket is deliberately NOT credited**, and this is the load-bearing line in the change. `deriveFieldGroupLayout` collects every visible field that named no declared group into a trailing keyless section, and returns `null` — "callers fall back to their existing flat/auto layout" — for an object declaring no groups at all. So *every visible field renders somewhere*. Crediting "renders on the synthesized layout" in full would leave `field-no-consumers` able to report `hidden` fields and nothing else, which is switching the gate off by another name. What is credited is the author's **placement**; what is not is the fallback that would draw anything.⚠️ **Reach, measured, for the maintainer.** On `hotcrm@965933b`, **326 of 331 declared fields** (98%) sit in a declared group. The 12 findings were 11 of them, so the acceptance set widens by 11 fields on this application — but on an app that groups everything, the display half of this rule now rests on the author's grouping decision. That is the price of the ruled direction and it is stated rather than buried. ## 2 · An upsert identity inside a carrier root is a read `bucketFor` returned `carrier` for a carrier root before anything else could ask. But a seed's `externalId` (`SeedSchema`: *"Field (or composite list of fields) matched for the uniqueness check"*) and an import mapping's `upsertKey` name the column the loader **matches on** — it reads that column on every row to decide insert from update. A seeded *value* is still a carrier; the *identity* is the one read a carrier root holds. `crm_forecast.seed_key` is exactly the shape triage described: `hidden: true`, `readonly: true`, *"Written only by the seed loader; empty on every real snapshot"*, and `src/data/forecast.seed.ts` declares `externalId: 'seed_key'`. It is consumed **by being an identity**. ⛔ **Nothing exempts `hidden` as a category.** The derivation never draws a hidden field, so path 1 gives it nothing; path 2 rescues it only when an upsert actually matches on it. The matched-pair control below is the proof. ## Acceptance evidence — the false-positive rate on a real app ⭐ Triage: *"The acceptance evidence is the false-positive rate on a real app, ⛔ not that the unit tests pass — the unit tests passed when it shipped."* **Instrument.** `hotcrm` has no checkout here, so one was cloned (`objectstack-ai/hotcrm`, public, read-only) and its authored stack dumped to JSON, with functions replaced by their source text — which is exactly what the rule does with a function node (`Function.prototype.toString.call(node)` → `scanText`), so the dump is behaviourally equivalent for this rule. **The instrument is calibrated, not assumed**: at `hotcrm@965933b` the app's own `npx objectstack lint --json` (its pinned `@objectstack/[email protected]`) answers `errors: 0 · warnings: 13 · suggestions: 12` — the card's reading byte for byte — with 12 `field-no-consumers` rows, and the harness over the same tree reproduces **the same 12, same objects, same declaration paths**. **Tree.** `objectstack-ai/hotcrm@965933b` — `feat(platform): migrate onto the @objectstack/* 17.4.0 line (hotcrm#1814)`, the commit the 12 were reported on. (Not `v3.0.0`: the app removed `crm_product.tax_rate` and later gave four fields consumers and the contact form its mailing block, so neither the tag nor `main` is the tree the card measured.) ### 12 → 0, per field | # | object · field | before | cleared by | the consumer it really has | |---|---|---|---|---| | 1 | `crm_account.logo` | carrier-only, 4 | layout | `group: 'branding'` | | 2 | `crm_article_feedback.comment` | carrier-only, 4 | layout | `group: 'basic'` | | 3 | `crm_campaign.description` | carrier-only, 11 | layout | `group: 'basic'` | | 4 | `crm_campaign_member.added_date` | carrier-only, **57** | layout | `group: 'basic'` | | 5 | `crm_contact.mailing_street` | carrier-only, 5 | layout | `group: 'mailing_address'` | | 6 | `crm_contact.mailing_city` | carrier-only, 5 | layout | `group: 'mailing_address'` | | 7 | `crm_contact.mailing_state` | carrier-only, 5 | layout | `group: 'mailing_address'` | | 8 | `crm_contact.mailing_postal_code` | carrier-only, 5 | layout | `group: 'mailing_address'` | | 9 | `crm_contact.mailing_country` | carrier-only, 5 | layout | `group: 'mailing_address'` | | 10 | `crm_contract.description` | carrier-only, 9 | **either** | `group: 'basic'`, and the seed matches on it (`externalId: 'description'`) | | 11 | `crm_forecast.seed_key` | carrier-only, 12 | **identity only** | `data` seed `externalId: 'seed_key'`; `hidden`, so the layout gives it nothing | | 12 | `crm_quote_line_item.line_number` | carrier-only, 23 | layout | `group: 'basic'` | **Nothing remains: 12 → 0.** Every one of the 12 carries a `group` naming a declared `fieldGroups` entry, which is what the app's own CHANGELOG says about them — *"every one of them still renders to users through its `fieldGroup` on the synthesized detail layout, which the rule does not resolve."* The per-field column is not asserted, it is **ablated**. Each path was removed from the committed file by anchored replacement (absent or non-unique anchor exits non-zero), the mutation was proven on disk by literal counts before reading any verdict, and each leg restored with `git checkout HEAD -- PATH` proven by blob identity against the `HEAD` blob plus an empty `git diff HEAD`: | leg | live paths | findings | |---|---|---| | `main` | neither | **12** | | ABLATED-A | identity only | **10** (the layout accounts for 11, overlapping on objectstack-ai#10) | | ABLATED-B | layout only | **1** — `crm_forecast.seed_key` (the identity accounts for 2) | | HEAD | both | **0** | ### ⭐ The other half: does a real defect still get reported? ⛔ Showing the 12 disappear proves nothing on its own — switching the gate off does that too. Six deliberately unconsumed fields were injected into **the same real hotcrm stack**, nothing reading, displaying, seeding, translating or granting any of them: | control | shape | reported? | |---|---|---| | `crm_contact.ctl_ungrouped` | visible, no `group` → trailing flat bucket | ✅ reported | | `crm_contact.ctl_undeclared_group` | `group` naming a group the object never declared | ✅ reported | | `crm_contact.ctl_hidden_grouped` | `hidden`, in a **declared** group | ✅ reported | | `crm_contact.ctl_hidden_readonly` | `hidden` + `readonly`, in a declared group | ✅ reported | | `ctl_plain.ctl_orphan` | visible, on an object declaring **no** `fieldGroups` | ✅ reported | | `ctl_seeded.ctl_not_seed_key` | `hidden` + `readonly`, **no upsert matches on it** | ✅ reported | | `ctl_seeded.ctl_seed_key` | byte-identical to the row above, **matched by a seed `externalId`** | silent — correctly | The last two are a **matched pair** on the same object: identical declarations, one variable between them. Only the one an upsert matches on goes quiet. `hidden` is exempt nowhere. ## Tests, gates - `pnpm --filter @objectstack/lint test` — **103 files, 3831 passed, 5 skipped**, including 11 new pins on both paths and on each boundary that keeps the rule judging (undeclared group, flat bucket, hidden field, an object with no groups, array-shaped field maps, composite `externalId`, mapping `upsertKey`, the matched pair). - `pnpm --filter @objectstack/lint typecheck` — exit 0. - `pnpm --filter '@objectstack/lint^...' build` — exit 0 (dependency closure). - Changeset: `patch` on `@objectstack/lint`. Measured, not assumed — the new message text is present in all four published bundles named by `files[]` (`dist/index.js`, `dist/index.cjs`, `dist/runtime.js`, `dist/runtime.cjs`) alongside a pre-existing-text positive control. ## Acceptance notes Observed while measuring, **not filed** — no PR or author is heading for either file: - `SeedSchema` aliases `key` / `externalkey` / `naturalkey` onto `externalId`, and `MappingSchema` aliases `key` / `matchOn` / `externalId` / `externalIdField` onto `upsertKey`. On a **pre-parse** stack an author using an alias gets no credit from path 2. `key` in particular is in `LITERAL_KEYS` and is skipped outright, for good reasons unrelated to this rule. Post-parse — which is what `os lint` feeds — aliases are already normalized, so the live surface is covered. - `SeedSchema.externalId` defaults to `'name'`. On a post-parse stack every seeded object therefore credits its `name` column, which is usually already exempt as the ADR-0079 title field and is in any case genuinely matched by the loader. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk --- _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ --------- Co-authored-by: Claude <[email protected]>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
… record what the kernel actually reads (objectstack-ai#18415) `Part of objectstack-ai#18188` — the PM decides closure. ## 1. The load-bearing reading: does the kernel resolve a manifest `dependencies` entry at plugin load? **No. The kernel never parses the version range.** Measured two ways, call site first (read-only, as the card's acceptance prefers), then executed. ### 1a. Call sites — every consumer of `manifest.dependencies` in this repo `ManifestSchema.dependencies` (`packages/spec/src/kernel/manifest.zod.ts:439`) is `z.record(z.string(), z.string())` — a map of package id to version range, where the value is typed only as "some string". It has **two** consumers outside tests, and **both read `Object.keys(...)` only**: | consumer | line | what it takes | | --- | --- | --- | | `resolveArtifactPackageOrder` — `packages/core/src/artifact-packages.ts` | 282 | `Object.keys(manifest.dependencies ?? {})`, handed to `resolvePluginOrder` as that node's `optionalDependencies` | | `resolveWritePackageScope` — `packages/metadata-protocol/src/protocol.ts` | 5346 | `Object.keys(declared)`, walked transitively for the write-scope closure | Neither ever reads a value. `artifact-packages.ts` says so in its own header (lines 105–124): "Why declared dependencies enter as `optionalDependencies`" — an id that names a package inside the artifact is a real edge, one that does not is simply not an edge here, "hoisted ahead when composed, silently skipped when absent". The machinery the page's *Version Constraints* section describes — `SemanticVersionManager.satisfies()` and `DependencyResolver.detectConflicts()` in `packages/core/src/dependency-resolver.ts` — **has no production call site at all**: ``` SUBJECT grep -rn 'dependency-resolver\.js' --include=*.ts packages/ apps/ -> packages/core/src/index.ts:126 (barrel re-export) -> packages/core/src/dependency-resolver.test.ts:2 (its own unit test) = 0 production importers FIRING CTRL grep -rn 'plugin-order\.js' (same directory, same barrel, same export form) -> index.ts:12, kernel-base.ts:11, artifact-packages.ts:132, kernel.ts:15, plugin-registration.ts:41 = 4 production importers -> the instrument fires DARK CTRL grep -rn 'dependency-resolver-NOSUCH\.js' = 0 -> the instrument is not just printing hits ``` The kernel's own load-order surface is a *different* field: `OrderablePlugin.dependencies` (`packages/core/src/plugin-order.ts:50`) is `string[]` — plugin names, no version ranges in it at all. ### 1b. Executed, with a firing control on the value and one on the key Ran against the built `@objectstack/core` (`packages/core/dist/index.js`), calling `resolveArtifactPackageOrder` directly: ``` S1 caret ^2.0.0 on @objectstack/core -> OK [com.acme.a] S2 caret ^17.0.0 on @objectstack/core -> OK [com.acme.a] C-VALUE 'NOT-A-VERSION-RANGE-@@@' on the same key -> OK [com.acme.a] C-KEY the SAME garbage value, but the key names a sibling package IN the artifact -> OK [com.acme.b, com.acme.a] (declared order was a, b) C-DARK no `dependencies` at all -> OK [com.acme.a, com.acme.b] (declared order preserved) ``` - **C-VALUE** is the firing control on the value: a string that is not a version range in any grammar resolves exactly like `^2.0.0` does. Nothing parses the value. - **C-KEY** is the firing control on the key, and it discriminates: the same unparseable value on a key that *is* in the artifact flips the order from the declared `[a, b]` to `[b, a]`. So the probe can observe `dependencies` having an effect — it has one, through its **keys**, and none through its **values**. - **C-DARK** confirms the flip in C-KEY came from the dependency edge and not from something else in the call. Also already pinned in the existing suite: `packages/objectql/src/artifact-load-path.test.ts:235` — *"leaves a dependency on a package OUTSIDE the artifact to the installer"* — which declares `'@steedos/plugin-auth': '^2.0.0'` and asserts the artifact resolves. Run: `pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/artifact-load-path.test.ts` :: **exit 0**, 14/14 passed. ### 1c. What that means for the card's grading ⇒ **This is an inert wrong number, not a load-time failure.** Copying the example does not fail at plugin load. It is a documentation-accuracy defect, not class (a). The card's own text says exactly this branch belongs "in a PR's acceptance notes rather than in a card" — that judgment is the PM's, not mine; the reading is recorded here either way. ## 2. Every number changed, and the reading behind it⚠️ Because the kernel accepts **any** string here, the kernel reading cannot justify a *particular* number. The justifying reading for the numbers is the **npm registry plus this checkout**, and the page now says which of the two governs. | site | before | after | reading | | --- | --- | --- | --- | | `:60` manifest example 1, `dependencies` | `'^2.0.0'` | `'^17.0.0'` | `packages/core/package.json` version = **17.4.0**; npm `latest` for `@objectstack/core` = **17.4.0** (157 versions published). `^17.0.0` is the spelling PR objectstack-ai#18186 already put on the npm half of this same page at `:703`, so both halves now agree. | | `:436` *Dependency Types* §1, `dependencies` | `'^2.0.0'` | `'^17.0.0'` | same | | `:942` the pin-vs-caret worked example | `'2.0.0', // Not '^2.0.0'` | `'17.4.0', // Not '^17.0.0'` | same; see §3 | Registry control: the same two-legged query returned a full packument for `@objectstack/core` (firing) and `Not found` for `@objectstack/ui` (see §4) — the probe discriminates. ## 3. What I decided about line 942, and why **Changed the number, kept the argument, and added one paragraph so the reason is one the runtime actually delivers.** The section is *Best Practices → 3. Pin Core Dependencies*. Its argument is **exact pin beats caret range**; the number is the vehicle, not the point. Changing `'2.0.0'` to `'17.4.0'` and the comment `// Not '^2.0.0'` to `// Not '^17.0.0'` preserves that argument *form for form* — an exact version on the left, the caret form named in the comment as the thing not to write — while removing the fifteen-major-stale number a reader would copy. Acceptance criterion 3 holds: the prose still argues "pin exactly", and the demonstration still demonstrates an exact pin against a caret. But the old reason — "to avoid surprises" — is one §1 shows the kernel does not deliver: the range is never checked at load, so pinning it prevents no surprise *there*. Leaving that sentence untouched would have left the page advertising an enforcement that does not exist (AGENTS.md Prime Directive objectstack-ai#10's corollary). So the reason now points where it is true: the installer, and the same dependency in `package.json`, which npm really does resolve. The added paragraph and the new `Callout` under *Version Constraints* are the same idiom the page already uses five times over (`optionalDependencies` / `peerDependencies` are "proposal-only"; there is no `definePlugin()`; no `onBoot`, `onUpgrade` or `onUninstall` hook). Nothing was invented for this PR. ## 4. The `@objectstack/ui` sites at `:71` and `:462` — **unchanged**, and here is what I found Both sit in **`peerDependencies`**, which this page's own callout at `:425-429` already declares proposal-only: "the schema declares neither, so nothing resolves them." Two readings on the package itself: - **Not in this checkout.** No `package.json` under `packages/`, `apps/` declares the name `@objectstack/ui`. Firing control: the identical query for `@objectstack/core` returns `packages/core/package.json`. Dark control: a nonsense name returns 0. - **Not on npm.** `GET https://registry.npmjs.org/@objectstack%2fui` -> `{"error":"Not found"}`. Firing control on the same endpoint: `@objectstack/core` -> `latest: 17.4.0`, 157 versions. So `^2.0.0` for `@objectstack/ui` is a range on a package that exists **neither here nor on the public registry**, in a block the page itself says nothing resolves. There is no reading that says what the right number would be, so per the dispatch fence I changed nothing and report it instead. It is a live question for whoever owns that name. ## 5. Changeset: `skip-changeset`, measured against `files[]` Diff is one file: `content/docs/protocol/kernel/plugin-spec.mdx`. ``` package.json scanned: 77 non-private (publishable): 70 publishable packages with NO files[] field: 0 (so npm's ship-everything default is nowhere in play) SUBJECT files[] arrays mentioning "content": 0 FIRING CONTROL files[] arrays mentioning "dist": 70 (70/70 -> the reader is reading the arrays) ``` `content/docs/**` is rendered by `apps/docs`, whose `package.json` is `"private": true` and declares no `files[]` — it publishes nothing. **Zero published bytes move.** Asserted from the `files[]` readings above, not from the path name. ## 6. Gates Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack content/docs/protocol/kernel/plugin-spec.mdx` from this worktree. **39 derived, 39 run, 0 NOT-MEASURED, 0 UNRUN**, reconciled with `--ran` carrying every exit code: ``` ✓ dispatch-gates --ran: 39 derived famil(ies) accounted for — 39 run, 0 NOT-MEASURED (a DERIVED zero — all 39 recorded an exit code and none of them is 3). ``` **38 of 39 exit 0.** Repo-wide `pnpm lint` (`eslint . --no-inline-config`, no narrowing) :: **exit 0** at `7cbd43373`, whose tree `bf6b9c0f5c708647871a55046be254192c1a8b8d` is byte-identical to this PR's head `d4f2919c2` (the amend was message-only). ### The one red, and why it is not this diff `pnpm check:cross-package-test-inputs` :: **exit 1**, flagging `packages/cli/test/init-created-files-summary.e2e.test.ts` descending into `packages/spec/dist/`. Neither path is in this diff. Proven by ablation, not by argument: 1. `git checkout 7358c1c -- content/docs/protocol/kernel/plugin-spec.mdx` — ablation verified on disk: the added marker went to **0** occurrences and the base `'@objectstack/core': '^2.0.0',` came back to **2**. 2. Re-ran the gate with the diff removed :: **exit 1**, same `FAIL:` line. The red survives the ablation ⇒ it is not caused by this change. 3. Restored with `git checkout HEAD -- …`, verified two ways: `git diff HEAD` empty for that path, and `git hash-object` = `f948028b39a1dd0e1f3959bf1c1c628674f3c8ae` = the HEAD blob. CI corroborates: **"Lint & Repo Gates" completed `success`** on `7358c1c5b`, this branch's merge base. The red is a property of a locally-built tree (the gate walks `packages/spec/dist/`, which only exists here because I built it for the §1b probe), not of `main`. `pnpm --filter @objectstack/spec run check:skill-examples` first exited 1 with a **PREREQUISITE NOT MET** refusal — `packages/client-react/dist` held no declarations. That is not a verdict. After `pnpm --filter '@objectstack/client-react...' build` it exits **0**: 258 prose examples type-check across 3 surfaces. ## Acceptance notes (⛔ not filed, noted here) - **`content/docs/protocol/kernel/plugin-spec.mdx:470-471`** still introduces the version-constraint grammar through `SemanticVersionManager.satisfies()`, a function with no production call site. The new callout directly above it now says so, so the page no longer misleads — but the sentence itself is a candidate for a rewrite that stops leading with a dead symbol. Successor: whoever next edits this page's *Dependency Management* section. - **`pnpm check:cross-package-test-inputs` reds on any tree where `packages/spec/dist/` is built.** Green on CI at the same commit, so this is a local-run-only asymmetry, not a defect in the code. Successor: any developer who builds `packages/spec` and then runs the derived gate list locally — which is every dev on a docs card like this one. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk --- _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ Co-authored-by: Claude <[email protected]>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…e first of objectstack-ai#18582's three debts (objectstack-ai#18587) Part of objectstack-ai#18582 Clause-②: no Seeds `packages/spec/liveness/sharing_rule.json` and moves `sharing_rule` out of `PENDING_GOVERNANCE` into `GOVERNED`. This is **one of the three debts** on that card; `connector` and `analytics_cube` stay on it, which is why the first line is `Part of` and not a closing keyword. ## What the coverage line says, before and after PR objectstack-ai#18581 made this line print on **every** run precisely so a change like this is visible. Measured on this branch: ``` before (origin/main @ bb9794a) governance denominator: 30 authorable type(s) — 26 registered kind(s) + 4 unregistered-kind stack collection(s) (analytics_cube, connector, sharing_rule, webhook); 27 governed, 3 awaiting a ledger (analytics_cube, connector, sharing_rule) — a worklist, not a merge gate. (+ 9 type(s) governed from OUTSIDE the denominator via SPEC_ONLY_SCHEMAS … 36 governed in total.) after (this branch) governance denominator: 30 authorable type(s) — 26 registered kind(s) + 4 unregistered-kind stack collection(s) (analytics_cube, connector, sharing_rule, webhook); 28 governed, 2 awaiting a ledger (analytics_cube, connector) — a worklist, not a merge gate. (+ 9 type(s) governed from OUTSIDE the denominator via SPEC_ONLY_SCHEMAS … 37 governed in total.) ``` The other counters move the way a seeded type should move them, and one of them deliberately does not: | counter | before | after | |---|---|---| | classified properties | (no `sharing_rule` row) | `sharing_rule 17 classified (live 16, planned 1)` | | repo-local evidence paths resolved | 505 / 505 | 524 / 524 | | `path#symbol` anchors resolved | 583 / 583 | 617 / 617 | | key-mention pairs anchored | 504 of 505 asked, 1 exempt | 523 of 524 asked, 1 exempt | | live entries citing a PRODUCER | 39 | 48 | | **container coverage (undrilled child keys)** | **113 containers / 578 keys** | **113 containers / 578 keys** | The last row is the point of drilling `sharedWith`: the new type adds **zero** rows to `undrilled-containers.baseline.json`. ## The classification, and the one row that is not `live` Ten authored keys (plus the seven ADR-0010 envelope fields the gate auto-classifies). Nine are `live`; `type` is `planned`. **Every row carries a `producer`, and that is the substance of the work.** The authoring shape is not the enforced shape: ADR-0057 D6 makes the `sys_sharing_rule` row canonical and `bootstrapDeclaredSharingRules` translates each authored key into it at boot — nothing re-parses `SharingRuleSchema` at enforcement time. So a consumer pointer alone would prove only that a **column** is read, never that the authored value reaches it. That is the `seed.env` shape (objectstack-ai#4837) applied to a whole type rather than to one key. **`type` is `planned`, deliberately neither `live` nor `dead`.** `SharingRuleType` has exactly one member and `CriteriaSharingRuleSchema` pins the key as `z.literal('criteria')`, so every value an author can write is the same value. Its only reader in this repo is a defensive `if (r.type === 'owner')` in the seeder, unreachable for anything the schema admits (`owner` rules left the authoring surface with ADR-0078). - not `live` — nothing dispatches on it; a comparison against a value the schema rejects is Prime Directive objectstack-ai#10's `case` label with no reachable call site; - not `dead` — the key is **required**, so it is not a silent no-op an author can get wrong, and the schema header records the intent (kept as the discriminant so a future enforced rule type re-joins as a union member). Marking it `dead` would put a required literal on the ADR-0049 enforce-or-remove worklist, where removing it breaks every authored rule to delete nothing. - precedent: `action.operation`, the same shape — a one-member discriminator held `planned` until a runtime half dispatched on it, then flipped to `live` (objectstack-ai#15080). The negative rests on a census with a lit control, not a bare grep: the population that reads a declared rule **item** is the seeder, four `packages/lint` validators and objectui's create-door client validation; searching that population for `r.type` / `rule.type` returns the one defensive comparison and nothing else, while the same search for `r.condition` / `rule.condition` returns hits in three of them. **Preview read points enumerated, per the objectstack-ai#7131 mechanical rule** — `registerBuiltinPreviews()` (objectui @dda8f381) registers twenty types and `sharing_rule` is **not** one of them. Recorded in the ledger rather than skipped, because "the type has no registered preview" is the sentence a later sweep needs. What objectui does consume is the whole shape, on the CREATE door only (`AUTHOR_SHAPE_ONLY_TYPES`). **One decoy named so the next census does not trip on it:** objectui's own `SharingRuleConfig` (`packages/types/src/permissions.ts`) is a different shape entirely, re-exported twice and read by nothing. It matches this type by name only. ## File surface — three deviations, all mechanically forced, none discretionary The dispatch scoped this to the ledger, the two `check-liveness.mts` rows and a changeset, with `liveness/README.md` **read-only**. Three files outside that surface had to move, because the gates read them off `GOVERNED` and off the ledger set: 1. **`packages/spec/liveness/README.md`** — `reconcileReadmeTable` fails on a `GOVERNED` type with no row in the "Current state" table, and the heading's `N governed types` is checked three ways against the rows and against `GOVERNED.length`. A `check:liveness` that exits 0 and a read-only README are not both reachable. Row written by measurement; heading 36 → 37; the tail paragraph that described the map as holding three debts now says which one is paid. 2. **`packages/spec/liveness/state-counts.md`** — generated, `merge=os-regen`, proved fresh by the same gate. Regenerated with `pnpm --filter @objectstack/spec gen:liveness-counts`, never hand-edited. 3. **`docs/qa/platform-checklist/coverage.json`** — this ratchet **derives its universe from `packages/spec/liveness/*.json`**, so seeding a ledger is exactly what makes the kind `UNCLASSIFIED` there (`check:platform-checklist` exit 1, reproduced before and after). Mapped to the three items that already exercise the surface — `access-security.sharing-rules-widen`, `access-security.sharing-rule-authoring-ui`, `access-security.record-share-grant-revoke` — rather than waived. **No checklist item is authored here**, and a waiver would have been false. A changeset **is** owed and is not `skip-changeset`: `packages/spec`'s `files[]` includes `liveness`, so the new ledger ships inside the tarball. `patch`. ## Gates Derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`, run with each exit code landed on disk before being read, then reconciled with `--ran`: ``` dispatch-gates --ran: 65 derived famil(ies) accounted for — 63 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3), 0 UNRUN. ``` - **`check:liveness` exits 0**, with the coverage line above. - `check:generated` — **exit 0 on a built tree** (15 of 15 artifacts current). It exits 1 on an unbuilt one and says so itself; `packages/spec` was built under the shared verify lock before the verdict was read. - `check:platform-checklist` — exit 1 before the coverage entry, exit 0 after: `36 kinds mapped, 1 waived`. - `check:pm-dispatch-gates` exit 0 (1746 self-test cases; the battery takes ~520s on this box). - `packages/spec` — `typecheck` exit 0, `test` exit 0 (483 files / 13780 tests). The liveness script suites the edited gate owns, plus every test whose text names `check-liveness`, run as their own slice: 13 files / 378 tests, exit 0. - **NOT MEASURED (2)** — `check:dual-build-cjs-loads` and `check:lean-entry-closure`, both `exit 3 · PREREQUISITE NOT MET`: they read a whole-repo `dist/` this container has not built. Neither pass nor finding. CI builds the closure. - **Known and already filed, not a finding here** — `check:cross-package-test-inputs` exits 1 once `packages/spec` has been built and 0 when it has not (objectstack-ai#18353 / objectstack-ai#18440). Recorded in both states; its message names `packages/spec/dist/` and `packages/cli/test/init-created-files-summary.e2e.test.ts`, neither of which this diff touches. - **`pnpm lint` is CI's whole-repo run.** The narrowing here is a measurement, not a skip: eslint's own `files` declaration is `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`, so five of the six changed files (`.md`, `.json`) are outside its universe by that declaration; the sixth, `packages/spec/scripts/liveness/check-liveness.mts`, linted clean — `--format json` reports **1 file, 0 errors, 0 warnings**. `eslint.config.mjs` states in its own prose that this repo "never enables type-aware linting (no `parserOptions.project`, no typed `@typescript-eslint` rules) for ANY file", so nothing in this diff can move the verdict on a file it does not touch. Gate numbers are quoted from runs at `2f932b09`. ## Acceptance notes Noted, not filed, and one to file: - **To file** — the four sharing-related entries in `packages/spec/scripts/liveness/proof-registry.mts` record a `blockedReason` whose premise this PR falsifies: `bu-hierarchy-sharing`, `sharing-rule-criteria-required`, `declarative-rbac-seeding` and `sharing-rule-org-scoped-listing` each say sharing rules are "authored at STACK level, which is not a governed metadata type … so there is no ledger entry to ratchet". There is one now. `showcase-declarative-rbac-seeding` in particular authors `sharingRules[]` on the showcase stack and asserts the seeded row's `object_name`, `recipient_type`, `recipient_id` and translated `criteria_json` — i.e. it exercises five of these keys end to end and is a genuine ADR-0054 binding candidate. ⛔ Deliberately **not** done here: binding a high-risk class is a separate ADR-0054 §3 act, one class at a time, and `sharing-rule-criteria-required` is the counter-example that makes it a judgement rather than a sweep — it POSTs the runtime body to `/sharing/rules` and never authors the spec key, so it must **not** bind `condition`. - Noted, not filed — `packages/spec/liveness/README.md`'s "Adding a type" recipe lists four steps and none of them is the README row, the heading count, the generated `state-counts.md` or the platform-checklist coverage entry, all four of which the gates now require. The recipe predates those three checks. Whoever seeds `connector` or `analytics_cube` next will walk into the same three red gates this PR did; that seat is the one that will carry it. --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ --------- Co-authored-by: Claude <[email protected]>
This was referenced Sep 17, 2026
This was referenced Sep 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
@objectstack/[email protected]
Patch Changes