Skip to content

Commit 7362ce1

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-22340-s3-governed-text-rename
2 parents 5963d2c + 6a7eca6 commit 7362ce1

22 files changed

Lines changed: 1001 additions & 143 deletions
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
fix(lint): 18 more author-time rule ids print a `fix:` line of one remedy sentence, and `os explain RULE_ID` carries what those lines used to explain
6+
7+
Clause-②: no
8+
9+
- **Shorter fix lines.** Each finding of these 18 rule ids now carries a `hint` (the CLI's `fix:` line, the runtime issue's `hint`) of one remedy sentence of at most 200 characters on every variant the lint suite fires (the lengths here are the `hint` alone; the CLI prints it after `fix: `). The longest of each, before → after:
10+
- flow credentials: `flow-credential-literal` (646 → 177);
11+
- flow writes: `flow-update-readonly-when-field` (549 → 174) and `flow-update-readonly-field` (400 → 143);
12+
- list-view bulk wiring: `action-dispatch-contract-mismatch` (514 → 157);
13+
- security posture: `security-controlled-by-parent-ambiguous-relation` (496 → 174), `security-master-detail-ungranted` (314 → 153), `security-controlled-by-parent-no-relation` (294 → 194), `security-fls-unknown-field` (293 → 132) and `security-wildcard-vama` (218 → 103);
14+
- sharing rules: `sharing-rule-object-not-shareable` (482 → 144) and `sharing-rule-object-controlled-by-parent` (395 → 169);
15+
- interface pages: `page/visualization-without-binding` (480 → 183);
16+
- action bodies: `action-api-update-readonly-when-field` (470 → 188);
17+
- flow triggers: `flow-api-trigger-secret-missing` (461 → 171), `flow-time-relative-descriptor-unroutable` (441 → 166), `flow-trigger-unroutable` (295 → 163), `flow-trigger-unknown-event` (268 → 101) and `flow-trigger-unknown-object` (210 → 156).
18+
- **One line, one remedy.** Where a fix line explained its remedy, the explanation moved: why `runAs: 'system'` is the channel for a static readonly field and why a `beforeInsert` stamp survives the strip; why a hook-derived value lands on a `readonlyWhen`-locked record, the drop-the-field alternative and that the warning never blocks a build; how to pick between the two bulk contracts; the controlled-by-parent tier precedence and what each sharing baseline means; how to tell which half of a sharing rule on a public object is wrong; why `sourceView` is an interface page's one binding channel; the descriptor's optional keys and the schedule's default; the api trigger's signature format; the connector's `provider`, the Basic auth variant, where `credentialRef` resolves and that a `{variable}` template draws nothing.
19+
- **A list becomes a nearest name.** `security-fls-unknown-field` no longer lists up to twelve of the object's fields; it names the nearest declared or registry-injected column when one is close (`'crm_account.description' is the nearest`), and none otherwise. `page/visualization-without-binding` names the field TYPE the derivation looks for and no longer repeats the name pattern it also tries.
20+
- **`os explain RULE_ID` carries the rest.** What the fix lines stopped saying is each id's `RULE_EXPLANATIONS` entry in `@objectstack/lint`, which `os explain RULE_ID` prints. Two ids had no entry and gain one, so their `rule:` line now ends with the `os explain` pointer: `security-wildcard-vama` ("why the '*' entry cannot carry View All / Modify All") and `flow-trigger-unknown-object` ("why an unknown trigger object only warns"). The other 16 entries gain paragraphs. The auth variants and the descriptor keys the lines and entries spell out are held to the spec's `ConnectorInstanceAuthSchema` and `TimeRelativeTriggerSchema` by the rules' tests.
21+
- **At the runtime publish gate** (Studio, REST `PUT /api/v1/meta`, and the MCP and AI authors that write through that door), 13 of the 18 fire; the `hint` FROM → TO, measured at the door:
22+
- a `flow` write: `flow-credential-literal` (2xx `advisories[].hint`, header / query / connector input 646 / 528 / 488 → 173 / 177 / 141), `flow-update-readonly-field` (422 `issues[].hint`, update / create 239 / 396 → 116 / 139), `flow-update-readonly-when-field` (2xx advisory, 527 → 161, under `runAs: 'user'` and `'system'` alike), `flow-trigger-unknown-object` (2xx advisory, record / time-relative 210 / 179 → 156 / 125), `flow-trigger-unknown-event` (422, token / array 191 / 268 → 101 / 96), `flow-time-relative-descriptor-unroutable` (422, 441 → 166), `flow-trigger-unroutable` (422, 295 → 163) and `flow-api-trigger-secret-missing` (422, 461 → 171);
23+
- an `object` write: `security-controlled-by-parent-no-relation` (422, 294 → 194), `security-controlled-by-parent-ambiguous-relation` (422, 496 → 167) and `security-master-detail-ungranted` (2xx advisory, 304 → 147);
24+
- a `permission` write: `security-fls-unknown-field` (422, 324 → 132), `security-wildcard-vama` (422, 218 → 103) and `security-master-detail-ungranted` (2xx advisory, 304 → 147).
25+
26+
Studio's save-advisory toast prints an advisory's `hint` after its message, so the credential, readonlyWhen, unknown-object and ungranted-detail toasts shorten, and Studio's draft-changes panel shows the `hint` in place of the message for the two controlled-by-parent errors its in-browser security check finds on object drafts. No Studio, REST or MCP reader in this repository or in objectui matches on `hint` text; a tool that did should match on `rule` and `path` instead.
27+
- **Never at the runtime gate:** `action-dispatch-contract-mismatch`, `page/visualization-without-binding`, `action-api-update-readonly-when-field` and the two `sharing-rule-object-*` ids run on the CLI only.
28+
- **Unchanged:** every `message` (the verdict), rule id, severity and `path`, and what each rule accepts or refuses.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: `defineStack` refuses two entries of one stack's `functions` array that author the same `name`, naming the name and every index (#22902)
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: registered stack-functions-array-duplicate-name-refused -->
10+
11+
**BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for breaking changes.
12+
13+
Functions are one flat namespace resolved by name: a hook's `handler`, an action's `target` and a flow `script` node's `config.function` all look a function up by its name. The `functions` **array** form could author one name twice, and nothing refused it. Measured on `main` at this change's base with `functions: [{ name: 'kc_a', handler: first }, { name: 'kc_a', handler: second }]`:
14+
15+
- `objectstack validate` exited 0 (`valid: true`);
16+
- `objectstack build` exited 0 and shipped `[{ name: 'kc_a' }, { name: 'kc_a__2' }]`: the second entry went out under a suffixed internal ref the author never wrote and nothing references, and `kc_a` ran `first`;
17+
- a source boot (`objectstack serve`, no build) registered `kc_a` as `second` and dropped `first`, with no notice.
18+
19+
So the same stack ran different code under `kc_a` depending on whether it was built. `composeStacks` already refused one name defined by two stacks (`STACK_COMPOSE_FUNCTION_CONFLICT`); this is the one-stack case.
20+
21+
**What is refused.** `defineStack`'s cross-reference pass refuses the stack with its existing envelope (`STACK_CROSS_REFERENCE_INVALID`, status 422, `defineStack cross-reference validation failed (N issues):`), one finding per colliding name, naming the name and every index:
22+
23+
```text
24+
✗ Function 'kc_a' is declared twice in the `functions` array: stack.functions[0] and stack.functions[1]. …
25+
```
26+
27+
It runs in an object-less stack too, and every entry counts, byte-identical twins included. `objectstack validate`, `objectstack build` and a source boot all load the config through `defineStack`, so all three refuse it at load, printing that line: `objectstack validate --json` answers `valid: false` with `code: 'STACK_CROSS_REFERENCE_INVALID'` (exit 1), `objectstack build` exits 2, and `objectstack serve` exits 1 before the kernel boots. Like every cross-reference check it is skipped under `defineStack(…, { strict: false })`.
28+
29+
**Unchanged.** The map form, which cannot author one name twice; an array whose names are distinct; the suffixed refs a build emits for an internal slot (`kc_a__2` beside `kc_a` is two names); and the cross-stack refusal in `composeStacks`.
30+
31+
## FROM → TO
32+
33+
| you wrote | write instead |
34+
|:--|:--|
35+
| `functions: [{ name: 'sync', handler: a }, { name: 'sync', handler: b }]` | `functions: [{ name: 'sync', handler: a }, { name: 'sync_backup', handler: b }]`, and point each hook, action or script node that meant `b` at `sync_backup` |
36+
| two entries that are the same function under one name | one entry: delete the other |
37+
38+
**The one-line fix: give each `functions` array entry its own `name`, or merge the duplicates into one entry.**
39+
40+
No D2 conversion rewrites it: which entry keeps the name, and what every caller of the other one should name instead, is the author's call. Its D3 record is the semantic entry `stack-functions-array-duplicate-name-refused` (protocol 18).
41+
42+
**Who is affected, measured.** On `main` at this change's base, no example app, fixture or test authors one name twice in a `functions` array: the four example apps author the map form or none, and every array-form fixture under `packages/` names distinct entries. `objectstack validate --json` on `examples/app-crm`, `app-todo`, `app-showcase` and `app-multi-package` answers byte-for-byte as it did without the check, apart from the `duration` value. Deployed metadata and other repositories were not measured.

‎packages/lint/src/lint-flow-credential-literals.test.ts‎

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import {
3131
FLOW_CREDENTIAL_LITERAL,
3232
lintFlowCredentialLiterals as lintFlowCredentialLiteralsUnrecorded,
3333
} from './lint-flow-credential-literals.js';
34+
import { ConnectorInstanceAuthSchema } from '@objectstack/spec/integration';
35+
3436
import { explainRule } from './rule-explanations.js';
3537
import { runRuntimeAuthoringRules, runtimeAuthoringRulesFor } from './runtime-gate.js';
3638
import * as barrel from './index.js';
@@ -41,7 +43,7 @@ import * as barrel from './index.js';
4143
// verdict to one line of at most 200 characters — every firing variant this
4244
// suite exercises, not a chosen few. Run the whole file: those cases read what
4345
// the cases above fired.
44-
const fired: Array<{ rule: string; message: string }> = [];
46+
const fired: Array<{ rule: string; message: string; hint: string }> = [];
4547
const lintFlowCredentialLiterals: typeof lintFlowCredentialLiteralsUnrecorded = (...args) => {
4648
const findings = lintFlowCredentialLiteralsUnrecorded(...args);
4749
fired.push(...findings);
@@ -474,3 +476,55 @@ describe('[#22161] one-line verdicts — flow-credential-literal', () => {
474476
}
475477
});
476478
});
479+
480+
// [#22161] Slice 13: the `fix:` line. Each position's hint is one remedy line:
481+
// the auth variant that holds the credential and the node that calls the
482+
// connector. Where `credentialRef` resolves, the connector's `provider`, the
483+
// Basic variant, that a template draws nothing and that nothing is refused are
484+
// the explanation's.
485+
describe('[#22161] one-line fixes — flow-credential-literal', () => {
486+
it('every `fix:` line the cases in this file fired is one line of at most 200 characters', () => {
487+
const converted = fired.filter((f) => f.rule === FLOW_CREDENTIAL_LITERAL);
488+
// Coverage control: all three positions fired, each with its own route.
489+
for (const opening of ['header ', 'url query parameter ', 'connector input ']) {
490+
expect(converted.some((f) => f.message.startsWith(opening)), opening).toBe(true);
491+
}
492+
expect(new Set(converted.map((f) => f.hint)).size).toBe(3);
493+
for (const f of converted) {
494+
expect(f.hint, f.rule).not.toContain('\n');
495+
expect(f.hint.length, `${f.rule}: ${f.hint}`).toBeLessThanOrEqual(200);
496+
}
497+
});
498+
499+
it('`os explain` carries what the `fix:` line no longer says', () => {
500+
const text = explainRule(FLOW_CREDENTIAL_LITERAL)!.paragraphs.join('\n');
501+
for (const fact of [
502+
'resolved at boot (an environment variable in the open tier)',
503+
"names a `provider` ('rest' for a plain HTTP API)",
504+
"`auth: { type: 'basic', username, credentialRef }`",
505+
'A `{variable}` template is not a literal and draws nothing',
506+
'nothing is refused',
507+
'content/docs/automation/connectors.mdx',
508+
]) {
509+
expect(text, `explanation names ${fact}`).toContain(fact);
510+
}
511+
});
512+
513+
it('every auth variant the `fix:` lines and the explanation name is one the connector auth schema accepts', () => {
514+
// The explanation module imports nothing, so the variants it and the
515+
// `fix:` lines spell out are held to the spec's schema here.
516+
for (const auth of [
517+
{ type: 'bearer', credentialRef: 'PROBE_REF' },
518+
{ type: 'api-key', headerName: 'X-Api-Key', credentialRef: 'PROBE_REF' },
519+
{ type: 'api-key', paramName: 'api_key', credentialRef: 'PROBE_REF' },
520+
{ type: 'basic', username: 'probe', credentialRef: 'PROBE_REF' },
521+
]) {
522+
expect(ConnectorInstanceAuthSchema.safeParse(auth).success, JSON.stringify(auth)).toBe(true);
523+
}
524+
// The connector-input line offers every secret-bearing variant by name.
525+
const input = lintFlowCredentialLiteralsUnrecorded(controlStack()).find((f) =>
526+
f.message.startsWith('connector input '),
527+
)!;
528+
expect(input.hint).toContain("type: 'bearer' | 'api-key' | 'basic', credentialRef");
529+
});
530+
});

‎packages/lint/src/lint-flow-credential-literals.ts‎

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -90,32 +90,24 @@ const SERVED = 'readable by every member who can read flows';
9090
const STEER_CONNECTOR = "move it to a connector's `auth.credentialRef`";
9191
const STEER_INPUT = 'drop it: the connector authenticates through `auth.credentialRef`';
9292

93-
/** Where `credentialRef` resolves — stated once, in every hint. */
94-
const RESOLVES =
95-
' `credentialRef` names a secrets-layer reference that is resolved at boot (an environment variable in ' +
96-
'the open tier), so the secret never lands in metadata.';
97-
93+
/**
94+
* [#22161] One remedy line per position: the auth variant that holds the
95+
* credential and the node that calls the connector. Where `credentialRef`
96+
* resolves, the connector's `provider`, the Basic variant, that a `{variable}`
97+
* template draws nothing, that nothing is refused and the connector guide are
98+
* `os explain flow-credential-literal`.
99+
*/
98100
const HEADER_ROUTE =
99-
"Declare a `connectors:` entry with a `provider` (`'rest'` for a plain HTTP API) and " +
100-
"`auth: { type: 'bearer', credentialRef }` for a bearer token, or " +
101-
"`auth: { type: 'api-key', headerName, credentialRef }` for a key in a named header (a Basic pair is " +
102-
"`auth: { type: 'basic', username, credentialRef }`), and call it from a `connector_action` node." +
103-
RESOLVES;
101+
"Declare a `connectors:` entry with `auth: { type: 'bearer', credentialRef }` (or " +
102+
"`{ type: 'api-key', headerName, credentialRef }`) and call it from a `connector_action` node";
104103

105104
const QUERY_ROUTE =
106-
"Declare a `connectors:` entry with a `provider` (`'rest'` for a plain HTTP API) and " +
107-
"`auth: { type: 'api-key', paramName, credentialRef }`, which sends the key as that query parameter, and " +
108-
'call it from a `connector_action` node.' +
109-
RESOLVES;
105+
"Declare a `connectors:` entry with `auth: { type: 'api-key', paramName, credentialRef }`, which sends the " +
106+
'key as that query parameter, and call it from a `connector_action` node';
110107

111108
const INPUT_ROUTE =
112-
'The connector authenticates through its own declaration: give its `connectors:` entry ' +
113-
"`auth: { type: 'bearer' | 'api-key' | 'basic', credentialRef }` and drop the credential from `input`." +
114-
RESOLVES;
115-
116-
const NOT_A_GATE =
117-
' A `{variable}` template is not a literal and draws nothing. This is an advisory: nothing is ' +
118-
'refused. See content/docs/automation/connectors.mdx.';
109+
"Give the connector's `connectors:` entry `auth: { type: 'bearer' | 'api-key' | 'basic', credentialRef }` " +
110+
'and drop the credential from `input`';
119111

120112
/** A config-path segment for a map key: `.key` when it is an identifier, `["x-key"]` otherwise. */
121113
function keySegment(key: string): string {
@@ -173,7 +165,7 @@ export function lintFlowCredentialLiterals(stack: AnyRec): FlowCredentialLiteral
173165
// auth variant and why it only warns are
174166
// `os explain flow-credential-literal`.
175167
message: `${what} holds a credential-shaped literal, ${SERVED}; ${steer}`,
176-
hint: `${route}${NOT_A_GATE}`,
168+
hint: route,
177169
});
178170
};
179171

0 commit comments

Comments
 (0)