proteum 2.5.16 → 2.5.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/agents/project/AGENTS.md
CHANGED
|
@@ -23,6 +23,7 @@ Managed compact root routers must use trigger -> canonical instruction file refe
|
|
|
23
23
|
- For every non-trivial coding task, load and follow root-level `DOCUMENTATION.md` before coding.
|
|
24
24
|
- For bug fixes, regressions, incidents, broken public routes, auth/OAuth failures, integration failures, or production behavior fixes, load and follow root-level `DOCUMENTATION.md` before coding so the relevant fix note, regression-test docs, ADR, or explicit skip reason is handled in the same change.
|
|
25
25
|
- If the user reports an issue, or you encounter one during exploration, implementation, verification, or runtime reproduction, load and follow root-level `diagnostics.md`.
|
|
26
|
+
- If the task adds or changes client data fetching, polling, or retry behavior, read the `Client Request Discipline` section in `client/AGENTS.md` before editing.
|
|
26
27
|
- If the task touches client-side files, especially `client/**` and page files, load and apply root-level `optimizations.md` only after implementation for post-implementation checking and optimization. Skip it at task start and skip it for server-only, test-only, doc-only, and non-client refactor tasks unless the user explicitly asks for optimization work.
|
|
27
28
|
- If the task changes UX, copy, onboarding, pricing, product semantics, or commercial positioning, use root-level `DOCUMENTATION.md` to choose the smallest relevant `./docs/` pack before editing. If a dev server is already running, print the live dev server URL as a clickable Markdown link.
|
|
28
29
|
- If the task needs new app or artifact boilerplate, prefer `npx proteum init ...` and `npx proteum create ...` before creating files by hand. Use `--dry-run --json` when an agent needs a machine-readable plan before writing files.
|
|
@@ -31,6 +31,17 @@ Coding style source of truth: root-level `CODING_STYLE.md`.
|
|
|
31
31
|
- Toasts and form errors are local feedback only; use `setError(context.app.handleError(error, fallbackMessage))` or rethrow the caught error.
|
|
32
32
|
- `console.*(error)` is not error handling and must not be the last stop for a caught error.
|
|
33
33
|
|
|
34
|
+
## Client Request Discipline
|
|
35
|
+
|
|
36
|
+
- A page or layout must not accumulate one mount-time API call per component or store. Before adding a request that fires on page load, extend an existing load-time payload (SSR `data`, the surface's bootstrap endpoint, or an already-fetched store) and hydrate from it; a new load-time call requires an explicit reason why no existing read can carry the data.
|
|
37
|
+
- When an endpoint accepts a set (for example an array of ids), request the whole needed set in one call and coalesce triggers that land in the same tick. Never loop single-item requests over a known set.
|
|
38
|
+
- Any effect or reactive block that can issue a request must key on explicit values (ids, tokens, statuses), must guard with sequence tokens so state identity churn cannot re-fire it, and must treat every terminal status, including `'error'`, as ineligible for automatic re-request.
|
|
39
|
+
- Automatic retries require exponential backoff with jitter, a hard attempt cap, and respect for server pacing (`retryAfterMs`, `Retry-After`). A transport failure or a server failed status ends in a terminal error state with a manual retry affordance, never in an immediate re-request from a render or effect cycle.
|
|
40
|
+
- Treat HTTP 429 as a stop signal for the whole surface, not as a retriable error.
|
|
41
|
+
- Long polls honor the server-provided delay, run as a single chain per logical resource, and are cancelled when superseded or unmounted.
|
|
42
|
+
- Identical concurrent requests must share one in-flight promise, usually a module-level cache with in-flight dedupe.
|
|
43
|
+
- After changing load-time fetching, polling, or retry behavior, measure the surface's executed request count (browser network log or a request-count contract test) and report the before and after numbers with the change.
|
|
44
|
+
|
|
34
45
|
## Design
|
|
35
46
|
|
|
36
47
|
- Follow the existing design language of the touched area.
|
package/eslint.js
CHANGED
|
@@ -183,6 +183,8 @@ const hasOptionalCallBoundary = (callExpression, ancestors = []) => {
|
|
|
183
183
|
return hasOptional || ancestors.some(({ node }) => node.type === 'ChainExpression');
|
|
184
184
|
};
|
|
185
185
|
|
|
186
|
+
// A loop body is iteration, not a condition. `for (const item of batch) item.reject(error)`
|
|
187
|
+
// preserves the error for every item there is, so it is not a swallow.
|
|
186
188
|
const isUnderConditionalControlFlow = (ancestors = []) =>
|
|
187
189
|
ancestors.some(({ node, childKey }) => {
|
|
188
190
|
if (node.type === 'IfStatement') return childKey === 'consequent' || childKey === 'alternate';
|
|
@@ -190,13 +192,33 @@ const isUnderConditionalControlFlow = (ancestors = []) =>
|
|
|
190
192
|
if (node.type === 'LogicalExpression') return childKey === 'right';
|
|
191
193
|
if (node.type === 'SwitchCase') return childKey === 'consequent';
|
|
192
194
|
|
|
193
|
-
return
|
|
194
|
-
['ForInStatement', 'ForOfStatement', 'ForStatement', 'WhileStatement', 'DoWhileStatement'].includes(
|
|
195
|
-
node.type,
|
|
196
|
-
) && childKey === 'body'
|
|
197
|
-
);
|
|
195
|
+
return false;
|
|
198
196
|
});
|
|
199
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Does the handler throw, whatever it throws?
|
|
200
|
+
*
|
|
201
|
+
* `catch { throw new Error('must be an absolute URL') }` translates a failure
|
|
202
|
+
* into the domain's own vocabulary. The original object is dropped, but the
|
|
203
|
+
* failure still propagates and nothing continues silently, which is what this
|
|
204
|
+
* rule exists to prevent. Attaching the original as `cause` is better practice,
|
|
205
|
+
* not a separate correctness question for this rule.
|
|
206
|
+
*/
|
|
207
|
+
const handlerThrows = (node) => {
|
|
208
|
+
let throws = false;
|
|
209
|
+
|
|
210
|
+
traverseNode(node, (child, _parent, _parentKey, ancestors) => {
|
|
211
|
+
if (child.type === 'ThrowStatement' && !isUnderConditionalControlFlow(ancestors)) throws = true;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return throws;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// A returned fallback is deliberately NOT accepted. `catch { return null }` is
|
|
218
|
+
// the textbook swallow, and no structural signal separates it from
|
|
219
|
+
// `.catch(() => [])`. Where a fallback really is correct, the call site says so
|
|
220
|
+
// with a disable comment and a reason, which stays greppable.
|
|
221
|
+
|
|
200
222
|
const defaultErrorReporters = ['app.reportError', 'app.handleError'];
|
|
201
223
|
|
|
202
224
|
/**
|
|
@@ -363,6 +385,11 @@ const createSwallowedErrorRule = () => ({
|
|
|
363
385
|
const reportHandler = (node, params, body) => {
|
|
364
386
|
const names = params.flatMap((param) => collectPatternNames(param));
|
|
365
387
|
if (names.length === 0) {
|
|
388
|
+
// Nothing was bound, so nothing can be routed. Still accepted when
|
|
389
|
+
// the handler translates the failure into a throw, because the
|
|
390
|
+
// failure keeps propagating rather than being continued past.
|
|
391
|
+
if (handlerThrows(body)) return;
|
|
392
|
+
|
|
366
393
|
context.report({ node, messageId: 'missingParam' });
|
|
367
394
|
return;
|
|
368
395
|
}
|
|
@@ -373,6 +400,8 @@ const createSwallowedErrorRule = () => ({
|
|
|
373
400
|
return;
|
|
374
401
|
}
|
|
375
402
|
|
|
403
|
+
if (handlerThrows(body)) return;
|
|
404
|
+
|
|
376
405
|
if (!handlerPreservesCaughtError(body, collectDerivedErrorNames(body, names), side, reporters)) {
|
|
377
406
|
context.report({ node, messageId: 'unpreserved', data: { name: referencedName } });
|
|
378
407
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proteum",
|
|
3
3
|
"description": "LLM-first Opinionated Typescript Framework for web applications.",
|
|
4
|
-
"version": "2.5.
|
|
4
|
+
"version": "2.5.18",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/proteum.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -240,6 +240,46 @@ test('an error pushed into a result collection counts as preservation', () => {
|
|
|
240
240
|
assert.equal(count(messages, swallowedRuleId), 0);
|
|
241
241
|
});
|
|
242
242
|
|
|
243
|
+
test('translating a failure into a new throw counts, even without the original', () => {
|
|
244
|
+
const messages = lint(`
|
|
245
|
+
export const assertUrl = (raw) => {
|
|
246
|
+
try {
|
|
247
|
+
return new URL(raw);
|
|
248
|
+
} catch {
|
|
249
|
+
throw new Error('must be an absolute URL');
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
`);
|
|
253
|
+
|
|
254
|
+
assert.equal(count(messages, swallowedRuleId), 0);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('preservation inside a loop body counts, because iteration is not a condition', () => {
|
|
258
|
+
const messages = lint(`
|
|
259
|
+
export const run = (batch) => {
|
|
260
|
+
load().catch((error) => {
|
|
261
|
+
for (const item of batch) {
|
|
262
|
+
item.reject(error);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
`);
|
|
267
|
+
|
|
268
|
+
assert.equal(count(messages, swallowedRuleId), 0);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('a returned fallback is still a swallow, however it is spelled', () => {
|
|
272
|
+
const nullFallback = lint(`
|
|
273
|
+
export const run = () => {
|
|
274
|
+
try { risky(); } catch { return null; }
|
|
275
|
+
};
|
|
276
|
+
`);
|
|
277
|
+
assert.equal(count(nullFallback, swallowedRuleId), 1);
|
|
278
|
+
|
|
279
|
+
const emptyList = lint(`export const run = (d) => dns.resolveMx(d).catch(() => []);`);
|
|
280
|
+
assert.equal(count(emptyList, swallowedRuleId), 1);
|
|
281
|
+
});
|
|
282
|
+
|
|
243
283
|
test('a console-only catch is still reported', () => {
|
|
244
284
|
const messages = lint(`
|
|
245
285
|
export const run = async () => {
|