Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/20749-lint-strings-stage2-state-the-decision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
'@objectstack/lint': patch
---

Data-model, filter, predicate, search, sort, security, seed, view, widget and registry findings no longer cite tracker numbers; each one states the decision behind it in words

Clause-②: no

The remaining `@objectstack/lint` findings that `os validate`, `os lint` and `os build` show to authors, plus the `surfaceReason` texts of the exported `AUTHORING_RULES` registry and one integrity error, pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does.

- Data model: the bare declared `unique: true` warning says that protocol 18 rejects the spelling and that stored metadata still carrying it converts to `unique: 'global'`, which builds the same physical index.
- Empty filter combinators: the `$and: []`, `$or: []` and empty-node messages say every backend reduces an empty combinator to its boolean identity; the `$or: []` message says an empty disjunction never opens a read scope to the whole table.
- Null guards: the fail-closed outcome says a predicate that cannot evaluate refuses the write rather than being skipped.
- Visibility and metadata-form predicates: the fall-open consequence says failing open is the console's settled behaviour; the dotted right-hand-side message says the form evaluator keeps its right-hand side a literal by design and says why only in a development build.
- Component props: the advisory hint says props are judged at the authoring door as a warning before they become an error.
- Rule schema formats: the format hint says `rule-validator.ts` registers the default `ajv-formats` set so that a `format` is enforced on every write.
- Security posture: the unset-OWD message describes the leave_request incident (an object with no `sharingModel` let an ordinary read/write grant read and edit every other user's records); the `controlled_by_parent` message says the write is refused as a metadata defect rather than a permission denial.
- Seeds and views: the seed state-machine message says a seed records established facts rather than walking the lifecycle; the `views:` container message says the stack schema, the rule and the registration loop hold `views:` to one container-only contract.
- React pages: the absent-`groupBy` hint states the ruling directly.
- Liveness: the unrecognised-status integrity error says such a status fails loudly rather than being graded `dead`.
- `AUTHORING_RULES` `surfaceReason` texts: the full-snapshot, capability-reference and sharing-rule reasons name the runtime publish gate (the Studio, REST and MCP door that runs this registry) in place of a tracker number; the advisory-volume reason says the object door opened to the gating object rules alone; the component-types reason names the crossing discipline the gating object rules went through.
- The other findings (search fields, sort fields, nav servability, dashboard actions, widget bindings and the remaining predicate and combinator messages) drop a citation the sentence already explained.

Text only: no rule id, severity, condition, finding or registry field moves. A tool or test that matches the old text (for example a tracker-number suffix) needs the new spelling.
2 changes: 1 addition & 1 deletion content/docs/data-modeling/schema-design.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ hint: 'search' scans this object's own columns, so a related record's column
cannot be a search target — expand the relation and search the related object,
or copy the value onto a stored text field here. Clients echo this declaration
verbatim as the '$searchFields' override, so a stale entry becomes a 400
INVALID_FIELD on list search (#4254), not just a quietly narrowed one.
INVALID_FIELD on list search, not just a quietly narrowed one.
```

A request that sends the dotted path is `400 INVALID_FIELD`:
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/data-model-rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,7 @@ describe('lintLegacyOrganizationComposites — S6 respelling nudge (ADR-0120 D5c
expect(issues[0].rule).toBe(RULE);
expect(issues[0].severity).toBe('warning'); // advisory forever — zero forced drift
expect(issues[0].path).toBe('objects[0].indexes[0]');
expect(issues[0].message).toContain('#5030');
expect(issues[0].message).toContain("on every row whose 'organization_id' is NULL it enforces nothing");
expect(issues[0].message).toContain('NULL-distinct');
expect(issues[0].fix).toContain("unique: 'organization'");
// The respelling keeps `fields` — the driver makes the LISTED column
Expand Down
27 changes: 17 additions & 10 deletions packages/lint/src/authoring-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,8 @@ const CLI_AND_RUNTIME: readonly AuthoringSurface[] = ['cli', 'runtime-publish'];
* collection the sentence above stands.
*/
const RUNTIME_NEEDS_FULL_SNAPSHOT =
'P2 (#4463): reads a stack-wide collection the per-write snapshot does not carry, so running it ' +
'P2 of the runtime publish gate (the Studio, REST and MCP door that runs this registry): reads a ' +
'stack-wide collection the per-write snapshot does not carry, so running it ' +
'now would report the rest of the tenant\'s metadata as missing rather than judging this write.';

/**
Expand Down Expand Up @@ -503,8 +504,9 @@ const RUNTIME_HTML_SOURCE_COMPILED_AT_SAVE =
*/
const RUNTIME_OBJECT_ADVISORY_VOLUME =
'Advisory-tier object rule: it cannot refuse a write, and it is held off the runtime door for ' +
'advisory VOLUME (~8 findings per object write measured on unswept metadata, rendered in Studio ' +
'since #4717), not refusal risk. Crossing it is a UX decision with its own card (#4716).';
'advisory VOLUME (~8 findings per object write measured on unswept metadata, each carried back in ' +
'the save response and rendered by Studio\'s designer), not refusal risk. The object door opened to ' +
'the gating object rules alone; crossing an advisory one is a separate UX decision.';

/**
* `ExprIssue` is the one rule finding that carries no rule id of its own — it
Expand Down Expand Up @@ -1108,8 +1110,9 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
// the only door that tenant has. Crossing is its own rollout decision with
// that replay as its evidence, not a bare `runtimeTypes` edit.
surfaceReason:
'Gating rule held off the runtime door pending the #4716 crossing discipline: a measured ' +
'false-refusal budget over stored tenant page rows (the in-repo 0-finding measurement covers ' +
'Gating rule held off the runtime door pending the crossing discipline the gating object rules ' +
'went through: a measured false-refusal budget, here over stored tenant page rows (the in-repo ' +
'0-finding measurement covers ' +
'authored config-file metadata only). Crossing is its own rollout card.',
run: (stack) => validateComponentTypes(stack),
},
Expand Down Expand Up @@ -1174,7 +1177,8 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
commands: ALL,
source: 'packages/lint/src/validate-capability-references.ts',
surfaces: CLI_ONLY,
surfaceReason: 'P2 (#4463): the ONE rule the runtime universe makes strictly stronger — the advisory hedge ("another '
surfaceReason: 'P2 of the runtime publish gate (the Studio, REST and MCP door that runs this registry): '
+ 'the ONE rule the runtime universe makes strictly stronger — the advisory hedge ("another '
+ 'installed package may provide it") is decidable against the live capability registry, so it '
+ 'graduates from advisory to gating there rather than merely being ported. That promotion is a '
+ 'severity change on a published rule id and belongs in its own PR, not riding a wiring change.',
Expand Down Expand Up @@ -1971,15 +1975,18 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
source: 'packages/lint/src/validate-sharing-rule-enforceability.ts',
surfaces: CLI_ONLY,
surfaceReason:
'P2 (#4463): a sharing rule is not a `flow`, and P1 gates `flow` alone. This entry used to add '
'P2 of the runtime publish gate (the Studio, REST and MCP door that runs this registry): a sharing '
+ 'rule is not a `flow`, and P1 gates `flow` alone. This entry used to add '
+ 'that the rule reads ONLY `stack.sharingRules[].condition` and needs no other collection, so '
+ 'crossing was a lone `runtimeTypes` edit. #9698 FALSIFIED that: the anchor arm resolves '
+ 'crossing was a lone `runtimeTypes` edit. The anchor arm, which refuses a rule anchored on a '
+ 'public-OWD object or a master-detail detail (no share row could widen either), FALSIFIED that: '
+ 'it resolves '
+ '`sharingRules[].object` against `stack.objects` to read the anchor\'s OWD, so the rule is now '
+ 'cross-collection. `objects` IS carried by the per-write snapshot (`CONTEXT_STACK_KEYS`, #8309), '
+ 'cross-collection. `objects` IS carried by the per-write snapshot (`CONTEXT_STACK_KEYS`), '
+ 'so the remaining gap is unchanged in SHAPE — the gate must accept a `sharing_rule` type and the '
+ 'snapshot must carry `sharingRules`, which it does not — but it is now TWO collections, not one. '
+ 'Crossing with `sharingRules` uncarried would enforce this id for zero of its inputs while the '
+ 'entry claimed the door (#7220). Recorded as pending rather than done, because a rule that has '
+ 'entry claimed the door. Recorded as pending rather than done, because a rule that has '
+ 'never run at a door should not claim it.',
run: (stack) => validateSharingRuleEnforceability(stack),
},
Expand Down
5 changes: 3 additions & 2 deletions packages/lint/src/data-model-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,8 @@ export function lintUnscopedDeclaredIndexes(objects: any[]): LocatedLintIssue[]
`"${obj.name}" declares index${indexLabel} [${cols}] with bare \`unique: true\` — a unique index whose scope is ` +
`unstated (ADR-0120). Today the bare spelling materializes over exactly its \`fields\`, i.e. installation-wide; ` +
`an author who meant "unique per organization" gets no per-organization constraint and no error. ` +
`Protocol 18 rejects this spelling (#5082).`,
`Protocol 18 rejects this spelling, and stored metadata that still carries it converts to ` +
`\`unique: 'global'\`, which builds the same physical index.`,
path: `objects[${i}].indexes[${j}]`,
fix:
`State the scope: \`unique: 'global'\` (installation-wide — exactly today's behavior) or ` +
Expand Down Expand Up @@ -518,7 +519,7 @@ export function lintLegacyOrganizationComposites(objects: any[]): LocatedLintIss
`"${obj.name}" declares index${indexLabel} [${cols.join(', ')}] with ${spelling} and lists the organization ` +
`column '${tenantColumn}' itself — the hand-written per-organization composite that predates the scope ` +
`vocabulary (ADR-0120 S6). It reads as "unique per organization" but materializes as a plain composite, and ` +
`SQL UNIQUE is NULL-distinct: on every row whose '${tenantColumn}' is NULL it enforces nothing (#5030) — which ` +
`SQL UNIQUE is NULL-distinct: on every row whose '${tenantColumn}' is NULL it enforces nothing — which ` +
`on a single-organization deployment is every row.`,
path: `objects[${i}].indexes[${j}]`,
fix:
Expand Down
3 changes: 2 additions & 1 deletion packages/lint/src/lint-liveness-properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,8 @@ function describe(entry: LedgerEntry): { kind: string; rule: string; defaultHint
"describe() only knows 'experimental' | 'planned' | 'dead' | 'live-elsewhere'. This is a " +
'shipped-ledger integrity bug, not an authoring error: either the ledger JSON has a typo, or a ' +
'new status was added to the vocabulary without teaching describe() in ' +
'lint-liveness-properties.ts about it (#11384).',
'lint-liveness-properties.ts about it. An unrecognised status fails loudly here rather than being ' +
'graded `dead`.',
);
}

Expand Down
5 changes: 3 additions & 2 deletions packages/lint/src/validate-component-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,8 +322,9 @@ export function validateComponentProps(stack: AnyRec): ComponentPropsFinding[] {
message: `${at.slice(base.length + 1) || 'properties'}: ${describeIssue(issue, props)}`,
hint:
`\`${type}\`'s props are declared by ComponentPropsMap (@objectstack/spec/ui) — the ` +
'rejection above carries the fix. Advisory for now: the props bag is not parsed on the ' +
'storage path either, so nothing rejects this today (objectstack#5068).',
'rejection above carries the fix. Advisory for now: props are judged here, at the authoring ' +
'door, as a warning before they become an error, and the props bag is not parsed on the ' +
'storage path either, so nothing rejects this today.',
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/lint/src/validate-dashboard-action-refs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ export function validateDashboardActionRefs(stack: AnyRec): DashboardActionRefFi
message:
actionType === 'modal'
? `modal action target "${target}" names no declared page — a modal target ` +
`names a PAGE, only (objectstack#6739). The button renders but the runtime ` +
`names a PAGE, only. The button renders but the runtime ` +
`refuses the dispatch when clicked — a dangling reference ` +
`(ADR-0049: a declared reference must resolve).`
: `script action target "${target}" resolves to no defined action. ` +
Expand Down
17 changes: 10 additions & 7 deletions packages/lint/src/validate-empty-combinators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,13 +228,16 @@ function emitEmptyCombinator(key: '$and' | '$or' | '$not', path: string, ctx: Ct

const message =
key === '$and'
? '`$and: []` is a conjunction of ZERO conditions. Under the #5322 identity ruling it ' +
? '`$and: []` is a conjunction of ZERO conditions. Every backend reduces an empty combinator to ' +
'its boolean identity, so it ' +
`${rows(VERDICT_OF.$and)} — the key is authored, and it constrains nothing, so this surface ` +
'reads as filtered and is not.'
: key === '$or'
? '`$or: []` is a disjunction of ZERO branches. Under the #5322 identity ruling it ' +
? '`$or: []` is a disjunction of ZERO branches. Every backend reduces an empty combinator to ' +
'its boolean identity, so it ' +
`${rows(VERDICT_OF.$or)}: this surface renders permanently empty, and on a read scope it hides ` +
'every row (fail-closed by design — #5134).'
'every row (fail-closed by design: an empty disjunction never opens a read scope to the whole ' +
'table).'
: '`$not: {}` negates an EMPTY node. An empty node is TRUE and NOT TRUE is FALSE, so it ' +
`${rows(VERDICT_OF.$not)} — the opposite of the "no filter" an empty operand looks like.`;

Expand All @@ -253,7 +256,7 @@ function emitEmptyCombinator(key: '$and' | '$or' | '$not', path: string, ctx: Ct
rule: FILTER_EMPTY_COMBINATOR,
where: ctx.where,
path,
message: `${message} A literal ${spelling} is not an authoring surface (#5330).`,
message: `${message} A literal ${spelling} is not an authoring surface.`,
hint:
`${hint} A PROGRAMMATIC producer that loops to zero operands keeps the runtime identity ` +
'unchanged — this rule judges only what is written in the metadata.',
Expand All @@ -268,7 +271,8 @@ function emitEmptyNode(position: EmptyNodePosition, path: string, ctx: Ctx): voi
where: ctx.where,
path,
message:
'An EMPTY filter node (`{}`) is authored here. Under the #5322 identity ruling an empty node is ' +
'An EMPTY filter node (`{}`) is authored here. Every backend reduces an empty node to its boolean ' +
'identity, so it is ' +
`TRUE — it ${rows(VERDICT_OF.node)}, exactly as if the key were absent — so a filter is declared ` +
'and enforces nothing.',
hint:
Expand All @@ -291,8 +295,7 @@ function emitEmptyNode(position: EmptyNodePosition, path: string, ctx: Ctx): voi
hint:
'Delete the empty branch — the `$or` then means what it looks like. If it was meant to carry a ' +
'condition, write it. (A compiler that DROPPED the empty branch instead would silently NARROW ' +
'the scope to the surviving branches, which is why the runtime absorbs rather than filters — ' +
'#5297.)',
'the scope to the surviving branches, which is why the runtime absorbs rather than filters.)',
});
return;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/lint/src/validate-nav-object-servability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export function validateNavObjectServability(stack: unknown): NavObjectServabili
+ `platform administrators included, since that gate reads only the object's \`enable\` `
+ `block and never the caller. The entry cannot be rescued with `
+ `\`requiredPermissions\`: they are independent conditions. The server prunes this `
+ `entry from the served \`/meta\` payload (#7912), so publishing it ships a menu row `
+ `entry from the served \`/meta\` payload, so publishing it ships a menu row `
+ `that silently is not there.`,
hint:
`Remove the nav entry, or make "${target}" listable by setting \`enable.apiEnabled: true\` `
Expand Down
5 changes: 3 additions & 2 deletions packages/lint/src/validate-null-guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,11 +594,12 @@ export type NullGuardOutcome =

const OUTCOME_CLAUSE: Record<NullGuardOutcome, string> = {
'fail-closed':
'so the rule enforces nothing and the write is rejected fail-closed (#4649/#4763)',
'so the rule enforces nothing and the write is rejected fail-closed (a predicate that cannot ' +
'evaluate refuses the write rather than being skipped)',
'fail-open':
'so the predicate is SKIPPED fail-open — the field is never actually required, the write ' +
'proceeds unchecked, and the only trace is a `requiredWhen … failed to evaluate — skipped` ' +
'log line (#4649/#4811)',
'log line',
};

/**
Expand Down
Loading
Loading