diff --git a/.changeset/20749-lint-strings-stage2-state-the-decision.md b/.changeset/20749-lint-strings-stage2-state-the-decision.md new file mode 100644 index 00000000000..0b1a4637f65 --- /dev/null +++ b/.changeset/20749-lint-strings-stage2-state-the-decision.md @@ -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. diff --git a/content/docs/data-modeling/schema-design.mdx b/content/docs/data-modeling/schema-design.mdx index e0b2bbdc8e4..5a729b8e6a7 100644 --- a/content/docs/data-modeling/schema-design.mdx +++ b/content/docs/data-modeling/schema-design.mdx @@ -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`: diff --git a/packages/cli/test/data-model-rules.test.ts b/packages/cli/test/data-model-rules.test.ts index d8ecfcd2615..3f38496037e 100644 --- a/packages/cli/test/data-model-rules.test.ts +++ b/packages/cli/test/data-model-rules.test.ts @@ -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 diff --git a/packages/lint/src/authoring-rules.ts b/packages/lint/src/authoring-rules.ts index 06b6a0f5052..b3ce5aa402d 100644 --- a/packages/lint/src/authoring-rules.ts +++ b/packages/lint/src/authoring-rules.ts @@ -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.'; /** @@ -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 @@ -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), }, @@ -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.', @@ -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), }, diff --git a/packages/lint/src/data-model-rules.ts b/packages/lint/src/data-model-rules.ts index 02b300d1bd6..04cde0f6295 100644 --- a/packages/lint/src/data-model-rules.ts +++ b/packages/lint/src/data-model-rules.ts @@ -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 ` + @@ -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: diff --git a/packages/lint/src/lint-liveness-properties.ts b/packages/lint/src/lint-liveness-properties.ts index 85d077c140b..c63e2763cc2 100644 --- a/packages/lint/src/lint-liveness-properties.ts +++ b/packages/lint/src/lint-liveness-properties.ts @@ -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`.', ); } diff --git a/packages/lint/src/validate-component-props.ts b/packages/lint/src/validate-component-props.ts index 3f6203e6e19..4250cd84694 100644 --- a/packages/lint/src/validate-component-props.ts +++ b/packages/lint/src/validate-component-props.ts @@ -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.', }); } } diff --git a/packages/lint/src/validate-dashboard-action-refs.ts b/packages/lint/src/validate-dashboard-action-refs.ts index b1754bb75e6..cd686c0dbc0 100644 --- a/packages/lint/src/validate-dashboard-action-refs.ts +++ b/packages/lint/src/validate-dashboard-action-refs.ts @@ -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. ` + diff --git a/packages/lint/src/validate-empty-combinators.ts b/packages/lint/src/validate-empty-combinators.ts index 7e5af870019..e5b811b47c7 100644 --- a/packages/lint/src/validate-empty-combinators.ts +++ b/packages/lint/src/validate-empty-combinators.ts @@ -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.`; @@ -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.', @@ -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: @@ -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; } diff --git a/packages/lint/src/validate-nav-object-servability.ts b/packages/lint/src/validate-nav-object-servability.ts index c26e337fb66..f48a755c23d 100644 --- a/packages/lint/src/validate-nav-object-servability.ts +++ b/packages/lint/src/validate-nav-object-servability.ts @@ -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\` ` diff --git a/packages/lint/src/validate-null-guards.ts b/packages/lint/src/validate-null-guards.ts index d35b5e617c1..64638b82d14 100644 --- a/packages/lint/src/validate-null-guards.ts +++ b/packages/lint/src/validate-null-guards.ts @@ -594,11 +594,12 @@ export type NullGuardOutcome = const OUTCOME_CLAUSE: Record = { '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', }; /** diff --git a/packages/lint/src/validate-predicate-path-refs.ts b/packages/lint/src/validate-predicate-path-refs.ts index b3b154b2156..b5c4e314b0f 100644 --- a/packages/lint/src/validate-predicate-path-refs.ts +++ b/packages/lint/src/validate-predicate-path-refs.ts @@ -649,7 +649,8 @@ function checkPredicate( `predicate references \`${full}\`, which the target schema does not declare — ` + `\`${segment}\` is not a key of \`${container}\`. The reference resolves to nothing, ` + `so the predicate can never evaluate and the console falls OPEN: the element renders ` - + `unconditionally and looks exactly like one carrying no predicate at all (#5149).`, + + `unconditionally and looks exactly like one carrying no predicate at all (failing open is the ` + + `console's settled behaviour).`, hint: `${formatSuggestion(findClosestMatches(segment, step.declared)) || `\`${container}\` declares: ${step.declared.slice(0, 12).sort().join(', ')}`}` @@ -691,7 +692,7 @@ function checkPredicate( + `this form edits — the binding root was dropped. Values are bound under \`${ROOT}\` and are ` + `never flattened to top level, so \`${id}\` resolves to nothing, the predicate can never ` + `evaluate and the console falls OPEN: the element renders unconditionally and looks exactly ` - + `like one carrying no predicate at all (#5149, #6254).`, + + `like one carrying no predicate at all (failing open is the console's settled behaviour).`, hint: `Write \`${ROOT}.${id}\` instead of \`${id}\`. A metadata-editing form binds the row under ` + `edit as \`${ROOT}\` at every depth — inside a repeater \`${ROOT}\` is the ROW, but it is ` @@ -728,15 +729,16 @@ function checkPredicate( + `literal string "${text}". The verdict therefore does not depend on the right-hand path ` + `at all: \`a == ${text}\` is FALSE even when both sides hold the same value, and ` + `\`a != ${text}\` is correspondingly TRUE. An \`==\` written this way hides the element ` - + `on every row, and nothing in the console says why (objectui#4049).` + + `on every row: the form evaluator keeps its right-hand side a literal by design, and says ` + + `why only in a development build.` : `predicate compares against the unquoted word \`${text}\` on the RIGHT of \`${op}\`. The ` + `right-hand side of \`${op}\` is a literal, never a reference, so this is read as the ` + `literal string "${text}" — which is probably what you meant, and is why it appears to ` + `work. It is outside the declared subset all the same (\`path == 'literal'\`), and it ` + `stops working when this surface moves to the real CEL evaluator, where a bare ` - + `\`${text}\` resolves to nothing (objectui#4049). The token also reads as a \`${ROOT}.\` ` + + `\`${text}\` resolves to nothing. The token also reads as a \`${ROOT}.\` ` + `root someone dropped, so this one finding carries BOTH readings: which one you meant ` - + `is the thing no linter can know, and it changes the fix (#7696).`, + + `is the thing no linter can know, and it changes the fix.`, hint: dotted ? `Two sanctioned spellings. (1) If you meant the TEXT, quote it: \`${op} '${text}'\`. ` + `(2) If you meant the PATH, restructure so the path is on the LEFT and a literal is on ` diff --git a/packages/lint/src/validate-react-page-props.test.ts b/packages/lint/src/validate-react-page-props.test.ts index 8c2745b110f..1923d9e102f 100644 --- a/packages/lint/src/validate-react-page-props.test.ts +++ b/packages/lint/src/validate-react-page-props.test.ts @@ -1199,7 +1199,7 @@ describe('validateReactPageProps — PARSED (#5020)', () expect(hit.length).toBe(1); expect(hit[0].severity).toBe('warning'); expect(hit[0].message).toContain('aggregate.groupBy is not set'); - expect(hit[0].hint).toContain('5583'); + expect(hit[0].hint).toContain('groupBy stays required, and a single number belongs in an object-metric block instead'); expect(hit[0].hint, 'the hint must carry the RULING, not an open question').toContain('NOT a supported'); expect( validateReactPageProps(agg(`{ function: 'count' }`)).filter((x) => x.severity === 'error'), diff --git a/packages/lint/src/validate-react-page-props.ts b/packages/lint/src/validate-react-page-props.ts index 55de4782567..c4bd5a4bb03 100644 --- a/packages/lint/src/validate-react-page-props.ts +++ b/packages/lint/src/validate-react-page-props.ts @@ -455,7 +455,7 @@ function checkChartAggregate( REACT_CHART_AGGREGATE_INVALID, 'aggregate.groupBy is not set, so the aggregate returns ONE ungrouped row and the chart plots a single point.', 'Add aggregate.groupBy (a field name, or { field, dateGranularity } to bucket dates) to give the chart a category axis. ' + - 'objectstack#5583 ruled that an ungrouped single-value chart is NOT a supported shape — groupBy stays required, and a single number belongs in an object-metric block instead. ' + + 'An ungrouped single-value chart is NOT a supported shape — groupBy stays required, and a single number belongs in an object-metric block instead. ' + 'This stays a warning rather than an error only because promoting it is its own step.', ); } diff --git a/packages/lint/src/validate-rule-schema-formats.ts b/packages/lint/src/validate-rule-schema-formats.ts index 42fe3a358f5..64192d5e4f3 100644 --- a/packages/lint/src/validate-rule-schema-formats.ts +++ b/packages/lint/src/validate-rule-schema-formats.ts @@ -323,7 +323,8 @@ export function validateRuleSchemaFormats(stack: unknown): RuleSchemaFormatFindi hint: (suggestion ? `Did you mean \`format: '${suggestion}'\`? ` : '') + `The registered names are: ${registered.join(', ')} — the default \`ajv-formats\` set, the one ` + - `\`rule-validator.ts\` registers (#5029). Names are case-sensitive and hyphenated ` + + `\`rule-validator.ts\` registers so that a \`format\` is enforced on every write. Names are ` + + `case-sensitive and hyphenated ` + `(\`date-time\`, not \`datetime\`). If you meant a constraint ajv has no format for, express it ` + `with \`pattern\` instead — a regex is enforced, an unknown format name is not.`, }); diff --git a/packages/lint/src/validate-searchable-fields.test.ts b/packages/lint/src/validate-searchable-fields.test.ts index c5554ba3281..f4c92259e14 100644 --- a/packages/lint/src/validate-searchable-fields.test.ts +++ b/packages/lint/src/validate-searchable-fields.test.ts @@ -757,7 +757,7 @@ describe('validateSearchableFields — objectstack-ui SKILL.md parity (#6675)', + 'declared searchableFields (subject, case_number, description) — the set \'search\' ' + 'scans. Clients echo this declaration verbatim as the \'$searchFields\' override, ' + 'and the runtime refuses an entry outside the allowed set: every toolbar search on ' - + 'this list returns 400 INVALID_FIELD (#4254).', + + 'this list returns 400 INVALID_FIELD.', ); }); diff --git a/packages/lint/src/validate-searchable-fields.ts b/packages/lint/src/validate-searchable-fields.ts index 965210ecbc7..771c57d6d82 100644 --- a/packages/lint/src/validate-searchable-fields.ts +++ b/packages/lint/src/validate-searchable-fields.ts @@ -353,7 +353,7 @@ export function checkSearchableFieldList( : `Fix the name, or add "${name}" to ${objectName}.fields. `) + `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.` + + `search, not just a quietly narrowed one.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(', ')}.` : ''), }); continue; @@ -411,12 +411,12 @@ export function checkSearchableFieldList( `'${vtype}' field: its value is computed on read and never stored, so no ` + `driver materializes a column for 'search' to scan and the entry can never ` + `match. It reads as search coverage and delivers none — the runtime used to ` + - `admit it verbatim because the declaration named it (#6674).`, + `admit it verbatim because the declaration named it.`, hint: `Mirror the computed value onto a stored text field on "${objectName}" and ` + `declare that instead, or drop "${name}". At runtime the ingress gate now ` + `refuses this entry with 400 INVALID_FIELD, the same answer a stale entry ` + - `gets (#4254).`, + `gets.`, }); continue; } @@ -440,7 +440,7 @@ export function checkSearchableFieldList( `searchableFields (${resolution.declaredList.join(', ')}) — the set 'search' ` + `scans. Clients echo this declaration verbatim as the '$searchFields' ` + `override, and the runtime refuses an entry outside the allowed set: every ` + - `toolbar search on this list returns 400 INVALID_FIELD (#4254).`, + `toolbar search on this list returns 400 INVALID_FIELD.`, hint: `Add "${name}" to ${objectName}.searchableFields, or drop it from this ` + `view — a view narrows the object's searchable set, never widens it ` + @@ -473,7 +473,7 @@ export function checkSearchableFieldList( `columns (${[...SEARCHABLE_TEXTUAL_TYPES, ...SEARCHABLE_ENUM_TYPES].join(' / ')}). ` + `Clients echo this declaration verbatim as the '$searchFields' override, and ` + `the runtime refuses it: every toolbar search on this list returns ` + - `400 INVALID_FIELD (#4254).`, + `400 INVALID_FIELD.`, hint: (isReference ? `A ${meta?.type} column stores only the referenced record's id, so it ` + diff --git a/packages/lint/src/validate-security-posture.ts b/packages/lint/src/validate-security-posture.ts index 0835e8c2071..3da798498d2 100644 --- a/packages/lint/src/validate-security-posture.ts +++ b/packages/lint/src/validate-security-posture.ts @@ -461,7 +461,8 @@ export function validateSecurityPosture(stack: AnyRec, opts?: { nowMs?: number } message: `custom object "${objName}" declares no sharingModel (OWD). The runtime fails ` + `CLOSED to 'private' (ADR-0090 D1), but the baseline must be an authored decision, ` + - `not an accident — this is the exact shape of the leave_request incident (objectui#2348).`, + `not an accident — this is the exact shape of the leave_request incident, where an object ` + + `with no sharingModel let an ordinary read/write grant read and edit every other user's records.`, hint: `Declare sharingModel explicitly: 'private' (owner + shares; recommended default), ` + `'public_read', 'public_read_write', or 'controlled_by_parent' (master-detail children).`, @@ -523,7 +524,8 @@ export function validateSecurityPosture(stack: AnyRec, opts?: { nowMs?: number } `can derive access from. ADR-0055 resolves the master through a required master_detail, then ` + `any master_detail, then a required lookup — each of which must also name a reference target — ` + `and this object matches none of the three. At runtime every read is DENIED and every write is ` + - `refused with 422 INVALID_METADATA (#7474), so the object is unusable rather than merely locked down.`, + `refused with 422 INVALID_METADATA, as a metadata defect rather than a permission denial, so the ` + + `object is unusable rather than merely locked down.`, hint: `Add the master relation this object is derived from, e.g. fields.parent: ` + `{ type: 'master_detail', reference: '', required: true }. If the object has no ` + diff --git a/packages/lint/src/validate-seed-state-machine.ts b/packages/lint/src/validate-seed-state-machine.ts index b762833dab1..d68198a105c 100644 --- a/packages/lint/src/validate-seed-state-machine.ts +++ b/packages/lint/src/validate-seed-state-machine.ts @@ -143,7 +143,8 @@ export function validateSeedStateMachine(stack: AnyRec): SeedStateMachineFinding message: `seeds '${rule.field}=${value}', which the '${objectName}' state machine does not declare ` + `(known states: ${[...rule.states].sort().join(', ')}). Seed writes are exempt from the ` + - 'state_machine rule (#3433), so this is NOT rejected at write time — a typo lands silently.', + 'state_machine rule (a seed records established facts rather than walking the lifecycle), so ' + + 'this is NOT rejected at write time — a typo lands silently.', hint: `If '${value}' is a real state, add it to the state machine (as an initial state or a ` + `transition endpoint). If it is a typo, correct it to a declared state. The exemption lets ` + diff --git a/packages/lint/src/validate-sortable-fields.ts b/packages/lint/src/validate-sortable-fields.ts index dbc4b24a935..7ab9de708bd 100644 --- a/packages/lint/src/validate-sortable-fields.ts +++ b/packages/lint/src/validate-sortable-fields.ts @@ -412,8 +412,8 @@ export function checkSortDeclaration( `Measured on a federated object over a real remote table, 'asc' and ` + `'desc' return BYTE-IDENTICAL row order under a 200 while the same ` + `query on a real column reverses. Unlike this rule's other two ` + - `verdicts nothing refuses it: the REST ingress (#6994) and the engine ` + - `(#7095) both judge only 'formula', so the view's FIRST fetch — and ` + + `verdicts nothing refuses it: the REST ingress and the engine ` + + `both judge only 'formula', so the view's FIRST fetch — and ` + `every fetch after it — silently answers in an arbitrary order, which ` + `'limit'/'offset' then slice into an arbitrary page.`, hint: unprovisionedAnchorHint(objectName, name), @@ -432,7 +432,7 @@ export function checkSortDeclaration( message: `${subject} orders by "${name}", which is not a field on object ` + `"${objectName}". The runtime refuses the sort rather than dropping it: ` + - `every load of this view answers 400 INVALID_SORT (#6994), because a sort ` + + `every load of this view answers 400 INVALID_SORT, because a sort ` + `is the view's FIRST fetch and not an optional interaction.` + (dotted ? '' : suggestName(head, known)), hint: @@ -468,7 +468,7 @@ export function checkSortDeclaration( `Denormalise the value onto "${objectName}" (a stored field, written when ` + `the source changes) and sort by that, or drop "${name}" from this sort. ` + `At runtime both doors now refuse it with 400 INVALID_SORT — the REST ` + - `ingress (#6994) and the engine itself (#7095) — so the declaration ` + + `ingress and the engine itself — so the declaration ` + `breaks the view's first fetch, and every fetch after it.`, }); } diff --git a/packages/lint/src/validate-view-containers.ts b/packages/lint/src/validate-view-containers.ts index 2094da1e80d..8936a4c09d4 100644 --- a/packages/lint/src/validate-view-containers.ts +++ b/packages/lint/src/validate-view-containers.ts @@ -130,7 +130,8 @@ export function validateViewContainers(stack: Record): ViewCont message: 'A ViewItem record is not a view container: the stack `views:` collection carries ' + 'containers only — `viewKind` belongs to a single VIEW, not to the container. The ' - + 'registration loop refuses this entry (#5320).', + + 'registration loop refuses this entry too: the stack schema, this rule and the loop hold ' + + '`views:` to one container-only contract.', hint: 'Wrap it in a defineView container: defineView({ list: { type, data, columns, ... }, ' + 'listViews: { ... } }) — or author the standalone view through the metadata door ' + '(Studio / `PUT /api/v1/meta/view`). Machine-assembled manifests carry it under ' diff --git a/packages/lint/src/validate-visibility-predicates.test.ts b/packages/lint/src/validate-visibility-predicates.test.ts index 39018f45197..bef19d004c4 100644 --- a/packages/lint/src/validate-visibility-predicates.test.ts +++ b/packages/lint/src/validate-visibility-predicates.test.ts @@ -965,7 +965,7 @@ describe('visibility-predicate-syntax (#6253)', () => { expect(findings[0].message).toContain('Unexpected character: ='); expect(findings[0].message).toContain('country === "USA"'); // The consequence is stated, because on screen it is invisible. - expect(findings[0].message).toContain('#5149'); + expect(findings[0].message).toContain("failing open is the console's settled behaviour"); }); it('the CEL spelling of the SAME predicate is clean — paired so it cannot pass vacuously', () => { @@ -1415,7 +1415,7 @@ describe('visibility-predicate-over-budget (#7217)', () => { expect(findings[0].message).toContain('Exceeded maxAstNodes (256)'); expect(findings[0].message).toContain('`maxAstNodes` budget (platform limit 256)'); // The consequence is unchanged: it still falls OPEN on screen. - expect(findings[0].message).toContain('#5149'); + expect(findings[0].message).toContain("failing open is the console's settled behaviour"); // ⛔ The defect itself: the dialect prescription must not reach this class. expect(findings[0].hint).not.toMatch(/bare CEL/); diff --git a/packages/lint/src/validate-visibility-predicates.ts b/packages/lint/src/validate-visibility-predicates.ts index 52d7bec3b9d..b70a784bd08 100644 --- a/packages/lint/src/validate-visibility-predicates.ts +++ b/packages/lint/src/validate-visibility-predicates.ts @@ -998,7 +998,8 @@ function checkElement( `visibility predicate is syntactically valid CEL but overruns ${bound} ` + `(${refusal.overrun.summary}) (predicate: \`${quoteSource(source)}\`). The canonical front ` + `end refuses it, so it can never evaluate, and the console falls OPEN: the element renders ` + - `unconditionally and looks exactly like one with no predicate at all (#5149).`, + `unconditionally and looks exactly like one with no predicate at all (failing open is the ` + + `console's settled behaviour).`, hint: `There is no syntax or dialect error to correct here — this is a SIZE fault, not a dialect ` + `mistake, so re-spelling the predicate will not fix it. Make it smaller, or move the work ` + @@ -1020,7 +1021,7 @@ function checkElement( `visibility predicate is not valid CEL — ${refusal.detail} ` + `(predicate: \`${quoteSource(source)}\`). A predicate that does not parse can never ` + `evaluate, and the console falls OPEN: the element renders unconditionally and looks ` + - `exactly like one with no predicate at all (#5149).`, + `exactly like one with no predicate at all (failing open is the console's settled behaviour).`, hint: refusal.token ? `\`${refusal.token.wrote}\` is not a CEL operator — CEL spells it ` + `\`${refusal.token.cel}\`. Replace \`${refusal.token.wrote}\` with ` + @@ -1123,7 +1124,8 @@ function checkElement( `Values are bound under a namespace on this surface — they are never ` + `flattened to top level — so \`${bare}\` resolves to nothing, the predicate ` + `can never evaluate, and the console falls OPEN: the element renders ` + - `unconditionally and looks exactly like one with no predicate at all (#5149).`, + `unconditionally and looks exactly like one with no predicate at all (failing open is the ` + + `console's settled behaviour).`, hint: `Write \`${root}.${bare}\` instead of \`${bare}\`` + (layer === 'runtime' diff --git a/packages/lint/src/validate-widget-bindings.ts b/packages/lint/src/validate-widget-bindings.ts index 2733671627d..29871c26cdf 100644 --- a/packages/lint/src/validate-widget-bindings.ts +++ b/packages/lint/src/validate-widget-bindings.ts @@ -979,7 +979,7 @@ export function validateWidgetBindings(stack: AnyRec): WidgetBindingFinding[] { rule: DASHBOARD_FILTER_FIELD_UNPROVISIONED, message: `${provenance}${unprovisionedAnchorCause(leafObject, leafField)}. The filter is ANDed ` + - `into this widget's analytics query (#2501), so it can never match a real value — ` + + `into this widget's analytics query, so it can never match a real value — ` + `on SQLite it silently degrades to constant-false and the widget renders empty ` + `(HTTP 200, zero rows, no error).`, hint: diff --git a/scripts/doc-authoring-prose-id.baseline.json b/scripts/doc-authoring-prose-id.baseline.json index e765765f0ad..8776c095746 100644 --- a/scripts/doc-authoring-prose-id.baseline.json +++ b/scripts/doc-authoring-prose-id.baseline.json @@ -1,75 +1,4 @@ { - "packages/lint/src/authoring-rules.ts": { - "#4463": 3, - "#4716": 2, - "#4717": 1, - "#7220": 1, - "#8309": 1, - "#9698": 1 - }, - "packages/lint/src/data-model-rules.ts": { - "#5030": 1, - "#5082": 1 - }, - "packages/lint/src/lint-liveness-properties.ts": { - "#11384": 1 - }, - "packages/lint/src/validate-component-props.ts": { - "#5068": 1 - }, - "packages/lint/src/validate-dashboard-action-refs.ts": { - "#6739": 1 - }, - "packages/lint/src/validate-empty-combinators.ts": { - "#5134": 1, - "#5297": 1, - "#5322": 3, - "#5330": 1 - }, - "packages/lint/src/validate-nav-object-servability.ts": { - "#7912": 1 - }, - "packages/lint/src/validate-null-guards.ts": { - "#4649": 2, - "#4763": 1, - "#4811": 1 - }, - "packages/lint/src/validate-predicate-path-refs.ts": { - "#4049": 2, - "#5149": 2, - "#6254": 1, - "#7696": 1 - }, - "packages/lint/src/validate-react-page-props.ts": { - "#5583": 1 - }, - "packages/lint/src/validate-rule-schema-formats.ts": { - "#5029": 1 - }, - "packages/lint/src/validate-searchable-fields.ts": { - "#4254": 4, - "#6674": 1 - }, - "packages/lint/src/validate-security-posture.ts": { - "#2348": 1, - "#7474": 1 - }, - "packages/lint/src/validate-seed-state-machine.ts": { - "#3433": 1 - }, - "packages/lint/src/validate-sortable-fields.ts": { - "#6994": 3, - "#7095": 2 - }, - "packages/lint/src/validate-view-containers.ts": { - "#5320": 1 - }, - "packages/lint/src/validate-visibility-predicates.ts": { - "#5149": 3 - }, - "packages/lint/src/validate-widget-bindings.ts": { - "#2501": 1 - }, "packages/plugins/plugin-audit/src/audit-writers.ts": { "#5226": 1 },