Repository navigation
Commit 9b7a0ef
docs(spec): the liveness README's author-warning section states the verdict model the lint ships (#21450)
Fixes #21135
Clause-②: no
## What this changes
`packages/spec/liveness/README.md`, section "Author warnings — closing
the loop", now describes the model
`packages/lint/src/lint-liveness-properties.ts` ships on `main`:
- A `dead`, `live-elsewhere` or `experimental` verdict warns on its own.
`authorWarn` opts in a `planned` row, and nothing else.
- Every warning shows the row's `authorHint`, or else the verdict's
default hint. The ledger `note` is never shown.
- Rule 1 is about the verdict, not the marker. Rule 2 (booleans) covers
any materialized default.
- The coverage paragraph states the walk's real reach.
Two sentences outside the section restated the old opt-in model, so they
are fixed too (dispatch Zone 2, item 4). There is also one
`@objectstack/spec` `patch` changeset.
⛔ No ledger row changes. ⛔ No lint code change. The paragraph "And one
the gate enforces for you: never on a `live` row" was re-checked against
the code and is unchanged (see Premise check, item 1).
## Premise check (dispatch Zone 2, measured at `535d1d25ab`)
1. **The hint chain had moved again since the card's table. The section
is written to the code, not to the card or the triage.**
- `shouldWarn` (`lint-liveness-properties.ts:173-176`) admits a row
whose status is `dead`, `live-elsewhere` or `experimental`, or whose
`authorWarn` is `true`.
- `checkItem` (`:407-410`) builds the hint as `entry.authorHint ??
defaultHint`, for every row. Commit `327391c3af` (PR #21169, landed for
#21096) removed the `note` fallback that opted-in and `experimental`
rows still had. So triage's chain ("`note` only for opted-in and
`experimental` rows") no longer holds. The README now says the `note` is
never shown, on any row.
- `live` + `authorWarn`: `describe()` throws its integrity sentinel
(`:283`). `check:liveness` refuses the combination at every depth
(`scanAuthorWarnRows` in `check-liveness.mts`, from commit `9bdc6d3faa`,
PR #21176, landed for #21127). The README paragraph #21176 added says
exactly that, so it stays as it is.
- **Probe** (tsx over `src`, against the shipped ledgers):
- A view container `name` / `label` → `liveness-dead-property`, hint
"Remove it — it is declared in the spec but not consumed at runtime."
- `rowLevelSecurity[].label` / `.description` → the same.
- `object.externalSharingModel` (`planned` + `authorWarn`, no
`authorHint`) → `liveness-planned-property`, default "Keep it — …" hint.
- `action.outcomeMessages` (`planned` + `authorWarn` + `authorHint`) →
its `authorHint`.
- `agent.lifecycle` / `tool.outputSchema` (`experimental`) → the default
hint, not the `note`.
- `manifest.runtime` (`live-elsewhere`) is admitted to the warn map, but
`lintLivenessProperties({ manifest })` gives 0 findings: no walk visits
`manifest`.
- `dashboard`'s warn map holds no depth-2 key (`widgets.chartConfig.*`
are `dead`), so the lint reads one level of `children`.
- Synthetic: a `dead` row gives byte-identical findings with and without
`authorWarn`. A `live` + `authorWarn` row throws.
2. **Stale sentences re-located.** At `535d1d25ab` they sit at
`README.md:560` ("opt-in per entry"), `:564` (the `experimental`-only
default), `:565` ("falls back to `note`"), `:569-572` (rule 1),
`:573-577` (rule 2) and `:587-591` (coverage). The same section also had
two false statements the card did not list:
- "(see `enable.searchable`'s `_authorWarnSkipped`)": no ledger row at
any depth carries `_authorWarnSkipped` (census over all 41 ledgers, 940
rows), and `enable.searchable` is `live`.
- "It covers **every governed type**": 11 governed types with warning
rows are not walked (`manifest`, `connector`, `analytics_cube`, `query`,
`realtime_subscription`, `api` and the five `RestServerConfig`
families).
3. **No generated file lifts this section.** `pnpm --filter
@objectstack/spec run check:generated` passes with "All 15 generated
artifacts are up to date", `check:docs` included. The README's only
programmatic readers are `check-liveness.mts`, `build-state-counts.mts`
and `readme-table.mts`, and all three read the *Current state* table
only. `git grep` for the section's phrases over `content/`, `apps/` and
`packages/spec/scripts` gives 0 hits.
4. **The old model elsewhere.** `git grep -n "opt-in per
entry\|authorWarn"` over `packages/spec/liveness/` and `content/docs/`:
- `README.md:379` ("the honest status is `dead` + `authorWarn`") and
`:966-969` ("every misleading entry carries `authorWarn`", plus "must
also be registered in `TYPE_COLLECTIONS`") state the old model as
current. Both are fixed here.
- `:348` / `:360` (the dated 2026-07 tally), `:434`, and the *Current
state* Notes cells are dated history, not the current model. They are
unchanged.
- `content/docs/`: the only hits are `releases/v14.mdx:245` and
`releases/v17/index.mdx:175`. Both are release-owned history, so there
is no owner to notify. `permissions/authorization.mdx:524` and
`ui/translations.mdx:390` describe warnings in terms that still hold.
## Every sentence changed, before → after
**Section heading**
- Before: `## Author warnings — closing the loop (`authorWarn`)`
- After: `## Author warnings — closing the loop (verdicts, and
`authorWarn`)`
**Intro paragraph**
- Before: "Classification is also fed back to the *author* at build
time. The CLI `compile` lint
(`packages/lint/src/lint-liveness-properties.ts`) reads these ledgers
and emits an advisory **warning** when an authored object/field sets a
property that is misleading — "you set this expecting it to do
something; at runtime it does nothing / isn't enforced" — with a
corrective hint. Never fails the build."
- After: "Classification is also fed back to the *author*. The liveness
lint (`packages/lint/src/lint-liveness-properties.ts`) reads these
ledgers and emits a **warning** when an authored item sets a property
whose row warns — "you set this expecting it to do something; at runtime
it does nothing / isn't enforced / isn't read yet" — under a rule id per
verdict, with a corrective hint. `os lint`, `os validate` and `os build`
/ `os compile` run it, and so does the runtime metadata write door for
`email_template`, `mapping` and `datasource` items. It is never an error
and never fails `os build`, but `os lint --strict` and `os validate
--strict` turn every warning into exit 1."
- Sources: `AUTHORING_COMMANDS` and `runtimeTypes` in
`authoring-rules.ts`, and `failing = errors.length + (strict ?
warnings.length : 0)` in `lint.ts`. #21092 measured the `--strict` 0 → 1
flip at both doors.
**The opt-in line**
- Before: "Signal over noise is the whole point, so warnings are
**opt-in per entry**:"
- After: "**The verdict decides which rows warn.** `authorWarn` only
opts in a row whose verdict does not warn on its own:" It is followed by
a new five-row table: `dead`, `live-elsewhere` and `experimental` always
warn, `planned` only with `authorWarn`, and `live` never. Each row names
its rule id.
**`authorWarn` field row**
- Before: "warn when this property is authored (in addition, any
`experimental` entry warns by default — it's a declared-but-unenforced
guarantee)."
- After: "makes a `planned` row warn. On a `dead`, `live-elsewhere` or
`experimental` row it changes nothing, because the verdict already
warns. So leaving it off never keeps such a row quiet."
**`authorHint` field row**
- Before: "the corrective one-liner shown under the warning (falls back
to `note`)."
- After: "the corrective one-liner shown under the warning, on every row
that warns. Without one, the verdict's default hint is shown: `dead`'s
says "Remove it", `planned`'s and `live-elsewhere`'s say "Keep it", and
`experimental`'s says the guarantee is not yet enforced. The row's
`note` is never shown to an author, on any row: it is written for this
ledger's maintainers."
**Rules intro**
- Before: "Two rules keep it false-positive-free, **both of which the
marker author must respect**:"
- After: "Two rules keep the warnings truthful. **Both bind whoever
grades a row, not only whoever marks one**:"
**Rule 1**
- Before: "**Only mark genuinely *misleading* dead props** — ones that
imply a capability/behavior that doesn't exist (`versioning`,
`field.columnName`, `softDelete`). Benign display/doc metadata that's
"dead" (no runtime reader) — `description`, `tags`, `icon` — must NOT be
marked; an author isn't misled by them."
- After: "**Grading a row `dead`, `live-elsewhere` or `experimental` is
an author-facing act.** The verdict warns every author who sets the key,
and fails their `--strict` run. No marker keeps it quiet. That includes
benign display or doc metadata with no runtime reader (`description`,
`tags`, `icon`): graded `dead`, it warns "Remove it" like any other dead
key." It is followed by three bullets:
- Measure a display key against *Designer previews count as consumers*
before writing `dead`. A key that something shows to a person is `live`,
and that measurement, not a missing marker, is what keeps it quiet.
- A display key still `dead` after that warns, and the warning is the
measurement speaking. ⛔ Never grade a row `live` or `planned` to silence
it.
- When the verdict's default hint is the wrong corrective, give the row
an `authorHint`. The `note` cannot do that job.
**Rule 2**
- Before: "**Booleans: only mark `default(false)` flags.** The lint
warns on a boolean only when set `true`, and it can't tell
author-set-`true` from a schema default. A `default(true)` flag
(`enable.searchable`) would then warn on *every* object that has an
`enable` block — so leave those unmarked (see `enable.searchable`'s
`_authorWarnSkipped`). Object/string/array props warn when merely
present, so this caveat is boolean-only."
- After: "**Booleans and materialized defaults.** The lint warns on a
boolean only when it is `true`, and on any other value when it is
present at all. Where the stack it reads has been parsed (`os validate`,
`os build`, any `defineStack` config), schema defaults have already
filled in, so it cannot tell an authored value from a default. A
`default(true)` flag, or any key whose default materializes, would warn
on every item that carries the default, whoever wrote it. So
`authorWarn` stays off such a row, and a warning verdict on one warns
every such item. `enable.searchable` (`default(true)`) is the shape; it
is graded `live`, so the question does not arise for it.
`mapping.errorPolicy` and `batchSize` were dead keys of this kind, and
no warning could reach their authors truthfully, so they were retired
instead (the `mapping` row below)."
- The old "boolean-only" sentence contradicted the README's own
`mapping` row, which calls those two keys "the non-boolean instance of
the default(true) rule". On parsing: the rule's input tier is `parsed`,
and `defineStack` runs `ObjectStackDefinitionSchema.safeParse` by
default. `os lint`'s own comment says its `parsed` tier fills no
defaults for a raw config, which is why the sentence names the doors
where defaults do fill.
**Coverage paragraph**
- Before: "The lint is ledger-driven: coverage grows by marking more
entries `authorWarn`, not by touching the lint code. It covers **every
governed type**: objects (incl. `enable.*`) and their fields walk
bespoke nesting;
flows/actions/agents/tools/skills/datasets/permissions/hooks/pages are
checked as flat stack collections, and container properties fan out over
arrays (each flow node, each dataset measure)."
- After: "The lint is ledger-driven: coverage grows with the ledger's
verdicts (grading a row `dead`, `live-elsewhere` or `experimental`) and
with `authorWarn` opt-ins on `planned` rows, not by touching the lint
code. That holds only inside the types its walk visits: objects (incl.
`enable.*`) and their fields walk bespoke nesting, translation bundles
walk their locale entries, and every type listed in the lint's
`TYPE_COLLECTIONS` is checked as a flat stack collection, with container
properties fanning out over arrays (each flow node, each dataset
measure). It reads a row and its direct `children`, no deeper. A
governed type the walk does not visit (`manifest`, `connector` and
`realtime_subscription` are three) warns no author through this lint,
whatever its rows say."
**Outside the section, `README.md:379`**
- Before: "When in doubt, the honest status is `dead` + `authorWarn`: an
author who gets a"
- After: "When in doubt, the honest status is `dead`, which warns its
authors with no `authorWarn` marker (see *Author warnings* below): an
author who gets a"
**Outside the section, below the *Current state* table**
- Before: "The `dead` set across types is the enforce-or-remove worklist
(ADR-0049); every misleading entry carries `authorWarn` so authors hear
about it at compile time (governed types with warn entries must also be
registered in the CLI lint's `TYPE_COLLECTIONS` — see
lint-liveness-properties.ts)."
- After: "The `dead` set across types is the enforce-or-remove worklist
(ADR-0049); a `dead` row warns its authors at compile time by its
verdict alone, with no `authorWarn` marker (see *Author warnings*
above). That reaches an author only in a type the lint walks: a governed
type's warning rows are heard only once the lint visits the type, which
for a flat stack collection means registering it in the lint's
`TYPE_COLLECTIONS` (see lint-liveness-properties.ts)."
- Measured: 0 `dead` rows carry `authorWarn` today. The only
`authorWarn` rows are 3 `planned` ones: `action.outcomeMessages`,
`object.externalSharingModel` and `translation.flows`. The old "must
also be registered" did not hold either: `manifest` is deliberately
unwalked, and #21092 pinned that.
## Changeset: measured, not assumed
- `npm pack --dry-run --json --ignore-scripts` in `packages/spec` lists
2077 files, and `liveness/README.md` is one of them (control:
`liveness/view.json`, also listed). The README ships, so it gets a
`patch` changeset for `@objectstack/spec`:
`.changeset/21135-liveness-readme-author-warnings.md`, carrying
`Clause-②: no`.
- The `Check Changeset` job (`pr-automation.yml`) has no path exemption.
It is skipped only by the `skip-changeset` label or the release PR.
## Verification (all at head `a3aed7da52`)
| What | Command | Result |
|---|---|---|
| spec build (empty closure) | `pnpm --filter @objectstack/spec build`
(under the verify lock) | exit 0 |
| generated artifacts | `pnpm --filter @objectstack/spec run
check:generated` | exit 0, "All 15 generated artifacts are up to date" |
| spec tests (the card's done-when) | `pnpm --filter @objectstack/spec
exec vitest run --project local --maxWorkers=2` (lock) | exit 0: 600
files, 17684 passed, 1 todo |
| the lint the section documents (not touched; evidence that the model
described is the pinned one) | `pnpm --filter @objectstack/lint exec
vitest run --maxWorkers=2 src/lint-liveness-properties.test.ts` (lock) |
exit 0, 95 passed |
| derived gates | `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` (no paths; 2 paths vs merge base
`535d1d25a`), each command run, exit captured before any pipe | 60
derived: 58 exit 0, 2 exit 3 (below) |
| reconciliation | `dispatch-gates.mjs --ran ran.list` | "60 derived, 58
run, 2 NOT-MEASURED, 0 UNRUN" |
| roster gates whose roster sits in a directory this diff touches |
`check-changeset-fixed`, `check:spec-changes`, `check:authz-resolver`,
`check:error-code-casing`, `check:filter-alias-parity` | all exit 0 |
| liveness gates | `check:liveness`, `check:empty-state`,
`check:strictness-ledger`, `check:variant-docs` (in the derived set) |
all exit 0 |
| nul bytes, plus a hand scan for control bytes | `pnpm
check:nul-bytes`, and a `grep -P` control-byte scan over both files |
exit 0; 0 hits |
**NOT MEASURED (declared to CI).** Both report `PREREQUISITE NOT MET`:
they load built `dist/` entry points this worktree does not have.
Neither reads anything this diff changes, since a markdown file and a
changeset are emitted into no `dist/`. The `Lint & Repo Gates` job
measures both on a fresh full build.
- `pnpm check:dual-build-cjs-loads` needs every package's `dist`.
- `pnpm check:lean-entry-closure` needs the `@objectstack/objectql`
closure's `dist`.
**Typecheck not run (declared).** The diff has no TypeScript, and
neither `.md` file is in any tsc program.
**eslint, narrowed and measured.** `pnpm exec eslint --no-inline-config
--format json` was run over both changed paths.
- Population, from eslint's own answer: 2 files, and both report "File
ignored because no matching configuration was supplied". The config's
file globs are code extensions only
(`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`).
- Count, from the JSON output: 2 results, 0 errors, 0 lint messages.
- Invariance: `eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project` / `projectService`), so this diff cannot move
the verdict on any file it does not touch.
## Acceptance notes
These are observations, not filed.
- **Stale `_authorWarnSkipped` mentions elsewhere.** No ledger carries
the field any more. It is still named in spec source comments
(`data/mapping.zod.ts:30`, `ui/app.zod.ts:1153`,
`conversions/registry.ts:3013`) and in two dated *Current state* Notes
cells (`mapping`, `qa`). These are comments and history, with no runtime
or author-facing effect. Carrier: none.
- **Ledger notes on `view.label`, `rowLevelSecurity.label` and
`.description`** end with "Not authorWarn'd" / "Benign, not
authorWarn'd". That is true of the marker, but no longer means silence:
the verdict warns. This card touches no ledger JSON, so they are
unchanged. The rows themselves were measured under the designer-previews
ruling (objectui `db11afd4967`), so the restated rule 1 implies no
re-grade. Carrier: none.
- **Stale reasoning in code comments.**
`kernel/metadata-plugin.zod.ts:947` reasons from "`datasource.json`
carries 0 `authorWarn` rows". Its conclusion still holds, because
`datasource` has no warning-verdict row, but the reason is the old
model. The lint's own docblock (`lint-liveness-properties.ts:720`) says
"Covers every governed metadata type", which the coverage measurement
above contradicts. Both are comments, and lint code is out of this
card's scope. Carrier: none.
- **Upstream since `535d1d25ab`.** `origin/main` moved 2 commits,
touching `liveness/analytics_cube.json` and `liveness/dataset.json`
only. Neither touches a sentence here or a file in this diff.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent b94a2a7 commit 9b7a0ef
2 files changed
Lines changed: 82 additions & 31 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
0 commit comments