@ultimat3/core 22.9.0 → 22.10.0
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/package.json +2 -2
- package/src/config-site.ts +41 -0
- package/src/errors.ts +51 -1
- package/src/exports/error-contract.ts +3 -0
- package/src/index.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/core",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.10.0",
|
|
4
4
|
"description": "Ultimate's foundation: errors, context, env, config, clock, ids, logging, telemetry, lifecycle",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -40,6 +40,6 @@
|
|
|
40
40
|
"test": "bun test"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@ultimat3/schema": "22.
|
|
43
|
+
"@ultimat3/schema": "22.10.0"
|
|
44
44
|
}
|
|
45
45
|
}
|
package/src/config-site.ts
CHANGED
|
@@ -19,13 +19,36 @@ export interface SeoRobotsConfig {
|
|
|
19
19
|
readonly disallow: readonly string[];
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Where each sitemap `<lastmod>` comes from. `'none'` (the default): no `<lastmod>`, as before.
|
|
24
|
+
* `'git'`: the last commit that touched the route's source file, read with `git log` — and the
|
|
25
|
+
* file's mtime where the process has no work tree (a container image). `'mtime'`: the file's mtime.
|
|
26
|
+
* `'build'`: one timestamp for every URL, the moment the process built its sitemap.
|
|
27
|
+
*/
|
|
28
|
+
export type SitemapLastmod = 'none' | 'git' | 'mtime' | 'build';
|
|
29
|
+
|
|
30
|
+
export const SITEMAP_LASTMOD_SOURCES: readonly SitemapLastmod[] = ['none', 'git', 'mtime', 'build'];
|
|
31
|
+
|
|
32
|
+
export interface SeoSitemapConfig {
|
|
33
|
+
/**
|
|
34
|
+
* Public pages OUTSIDE `site/` to list — an `app/` route anyone may open, e.g. `['/verificar']`.
|
|
35
|
+
* Each must match a registered route that declares no policy (checked when the sitemap is built,
|
|
36
|
+
* `X_SITEMAP_EXTRA_INVALID`), and is listed per routed locale with hreflang alternates exactly
|
|
37
|
+
* as a `site/` page is.
|
|
38
|
+
*/
|
|
39
|
+
readonly extra: readonly string[];
|
|
40
|
+
readonly lastmod: SitemapLastmod;
|
|
41
|
+
}
|
|
42
|
+
|
|
22
43
|
export interface SeoConfig {
|
|
23
44
|
readonly robots: SeoRobotsConfig;
|
|
45
|
+
readonly sitemap: SeoSitemapConfig;
|
|
24
46
|
}
|
|
25
47
|
|
|
26
48
|
/** `robots` is NESTED for `AiConfigInput`'s reason: `section` applies a patch one level deep. */
|
|
27
49
|
export interface SeoConfigInput {
|
|
28
50
|
readonly robots?: Input<SeoRobotsConfig> | undefined;
|
|
51
|
+
readonly sitemap?: Input<SeoSitemapConfig> | undefined;
|
|
29
52
|
}
|
|
30
53
|
|
|
31
54
|
export interface SiteSections {
|
|
@@ -50,6 +73,10 @@ export function mergeSite(layers: readonly SiteSectionsInput[]): SiteSections {
|
|
|
50
73
|
{ disallow: [] },
|
|
51
74
|
layers.map((layer) => layer.seo?.robots),
|
|
52
75
|
),
|
|
76
|
+
sitemap: layered<SeoSitemapConfig>(
|
|
77
|
+
{ extra: [], lastmod: 'none' },
|
|
78
|
+
layers.map((layer) => layer.seo?.sitemap),
|
|
79
|
+
),
|
|
53
80
|
},
|
|
54
81
|
};
|
|
55
82
|
}
|
|
@@ -84,4 +111,18 @@ export function siteIssues(config: SiteSections, issues: string[]): void {
|
|
|
84
111
|
for (const path of config.seo.robots.disallow) {
|
|
85
112
|
if (!path.startsWith('/')) issues.push(`seo.robots.disallow entry "${path}" must start with /`);
|
|
86
113
|
}
|
|
114
|
+
for (const path of config.seo.sitemap.extra) {
|
|
115
|
+
// A PATH, never a URL: every `<loc>` is built against the one declared origin, and a query or
|
|
116
|
+
// a fragment names a variant of a page, which a sitemap lists by its canonical URL alone.
|
|
117
|
+
if (!path.startsWith('/') || path.startsWith('//') || /[?#]/.test(path)) {
|
|
118
|
+
issues.push(
|
|
119
|
+
`seo.sitemap.extra entry "${path}" must be a path starting with / — no origin, query or fragment`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!SITEMAP_LASTMOD_SOURCES.includes(config.seo.sitemap.lastmod)) {
|
|
124
|
+
issues.push(
|
|
125
|
+
`seo.sitemap.lastmod "${String(config.seo.sitemap.lastmod)}" must be one of ${SITEMAP_LASTMOD_SOURCES.join(', ')}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
87
128
|
}
|
package/src/errors.ts
CHANGED
|
@@ -26,6 +26,18 @@ export interface UltimateErrorInit {
|
|
|
26
26
|
readonly cause: string;
|
|
27
27
|
/** The exact command or edit that fixes it. */
|
|
28
28
|
readonly fix: string;
|
|
29
|
+
/**
|
|
30
|
+
* The fix for a REMOTE caller — an agent over MCP, a client reading a problem document — when
|
|
31
|
+
* `fix` names something only the app's developer can do (`x policy explain`, an edit to a
|
|
32
|
+
* declaration). Omitted, every surface reads `fix`. The developer's `fix` is never replaced: the
|
|
33
|
+
* terminal, the log line and `--json` keep it, and a dev-mode problem document does too.
|
|
34
|
+
*/
|
|
35
|
+
readonly callerFix?: string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Where the reader goes next. Defaults to the code's registered page. An app may point it at its
|
|
38
|
+
* own guide — `docs://recipes/refund-a-payment` — and `@ultimat3/mcp` then prints it as a fourth
|
|
39
|
+
* `docs:` line in the tool result, the moment the agent is stuck.
|
|
40
|
+
*/
|
|
29
41
|
readonly docs?: string | undefined;
|
|
30
42
|
readonly meta?: Readonly<Record<string, unknown>> | undefined;
|
|
31
43
|
/**
|
|
@@ -43,6 +55,8 @@ export interface UltimateErrorJSON {
|
|
|
43
55
|
readonly title: string;
|
|
44
56
|
readonly cause: string;
|
|
45
57
|
readonly fix: string;
|
|
58
|
+
/** Present only when the error declared one. See `UltimateErrorInit.callerFix`. */
|
|
59
|
+
readonly callerFix?: string | undefined;
|
|
46
60
|
readonly docs: string;
|
|
47
61
|
/** Whether a client may try again. Always present — a client never has to infer it. */
|
|
48
62
|
readonly retry: ErrorRetry;
|
|
@@ -50,9 +64,26 @@ export interface UltimateErrorJSON {
|
|
|
50
64
|
readonly stack?: string | undefined;
|
|
51
65
|
}
|
|
52
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Who reads a rendering. `'developer'` (the default) is the terminal, a log line, `--json`: the
|
|
69
|
+
* `fix` the author wrote. `'caller'` is a remote client that cannot run the app's CLI — an MCP
|
|
70
|
+
* tool result, a production problem document: `callerFix` when the error declared one, else `fix`.
|
|
71
|
+
*/
|
|
72
|
+
export type ErrorAudience = 'developer' | 'caller';
|
|
73
|
+
|
|
53
74
|
export interface FormatErrorOptions {
|
|
54
75
|
/** Append a 4th `docs:` line. Off by default — the contract's rendering is 3 lines. */
|
|
55
76
|
readonly docs?: boolean | undefined;
|
|
77
|
+
/** Whose fix the `fix:` line carries. See `ErrorAudience`. */
|
|
78
|
+
readonly audience?: ErrorAudience | undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The fix line a reader of `audience` is given: `callerFix` for a caller when one exists. */
|
|
82
|
+
export function fixFor(
|
|
83
|
+
error: { readonly fix: string; readonly callerFix?: string | undefined },
|
|
84
|
+
audience: ErrorAudience | undefined,
|
|
85
|
+
): string {
|
|
86
|
+
return audience === 'caller' && error.callerFix !== undefined ? error.callerFix : error.fix;
|
|
56
87
|
}
|
|
57
88
|
|
|
58
89
|
export class UltimateError extends Error {
|
|
@@ -63,6 +94,7 @@ export class UltimateError extends Error {
|
|
|
63
94
|
/** Set by `Error`'s `cause` option; always a human-readable string in Ultimate. */
|
|
64
95
|
declare readonly cause: string;
|
|
65
96
|
readonly fix: string;
|
|
97
|
+
readonly callerFix: string | undefined;
|
|
66
98
|
readonly docs: string;
|
|
67
99
|
readonly retry: ErrorRetry;
|
|
68
100
|
readonly meta: Readonly<Record<string, unknown>> | undefined;
|
|
@@ -90,6 +122,7 @@ export class UltimateError extends Error {
|
|
|
90
122
|
this.code = code;
|
|
91
123
|
this.title = title;
|
|
92
124
|
this.fix = singleLine(init.fix);
|
|
125
|
+
this.callerFix = init.callerFix === undefined ? undefined : singleLine(init.callerFix);
|
|
93
126
|
this.docs = singleLine(init.docs ?? described.docs);
|
|
94
127
|
this.retry = init.retry ?? retryFor(init.code);
|
|
95
128
|
this.meta = init.meta;
|
|
@@ -110,7 +143,8 @@ export class UltimateError extends Error {
|
|
|
110
143
|
// would be a second place that has to be right — and the one that gets forgotten. This method
|
|
111
144
|
// is line-oriented and stays exactly 3 lines (4 with `docs`) because the fields cannot carry
|
|
112
145
|
// a line break, not because this joiner removes them.
|
|
113
|
-
const
|
|
146
|
+
const fix = fixFor(this, options?.audience);
|
|
147
|
+
const lines = [`${this.code}: ${this.title}`, ` cause: ${this.cause}`, ` fix: ${fix}`];
|
|
114
148
|
if (options?.docs === true) lines.push(` docs: ${this.docs}`);
|
|
115
149
|
return lines.join('\n');
|
|
116
150
|
}
|
|
@@ -128,6 +162,7 @@ export class UltimateError extends Error {
|
|
|
128
162
|
title: this.title,
|
|
129
163
|
cause: this.cause,
|
|
130
164
|
fix: this.fix,
|
|
165
|
+
...(this.callerFix === undefined ? {} : { callerFix: this.callerFix }),
|
|
131
166
|
docs: this.docs,
|
|
132
167
|
retry: this.retry,
|
|
133
168
|
meta: renderMetaRecord(this.meta),
|
|
@@ -147,6 +182,21 @@ export function isUltimateError(value: unknown): value is UltimateError {
|
|
|
147
182
|
}
|
|
148
183
|
}
|
|
149
184
|
|
|
185
|
+
/**
|
|
186
|
+
* The `callerFix` of an authorization denial (`X_FORBIDDEN`), ONE sentence for the five packages
|
|
187
|
+
* that refuse one — policy, action, query, auth, http — so a remote caller reads the same
|
|
188
|
+
* instruction whichever layer said no. A permission is granted by whoever administers the account,
|
|
189
|
+
* never by retrying, and saying so is what stops an agent looping on a refusal. `permission`, when
|
|
190
|
+
* the denial names a bare `resource:verb`, is what to ask for.
|
|
191
|
+
*/
|
|
192
|
+
export function deniedCallerFix(permission?: string): string {
|
|
193
|
+
const ask =
|
|
194
|
+
permission === undefined
|
|
195
|
+
? 'this caller is not permitted to do this: ask the account owner or an administrator for access'
|
|
196
|
+
: `this caller lacks the permission ${permission}: ask the account owner or an administrator to grant it`;
|
|
197
|
+
return `${ask}, or call with credentials that have it — retrying the same call is refused the same way`;
|
|
198
|
+
}
|
|
199
|
+
|
|
150
200
|
/** Init for a subclass that owns its code. */
|
|
151
201
|
export type CodedErrorInit = Omit<UltimateErrorInit, 'code'>;
|
|
152
202
|
|
|
@@ -46,14 +46,17 @@ export {
|
|
|
46
46
|
} from '../error-retry';
|
|
47
47
|
export type {
|
|
48
48
|
CodedErrorInit,
|
|
49
|
+
ErrorAudience,
|
|
49
50
|
FormatErrorOptions,
|
|
50
51
|
UltimateErrorInit,
|
|
51
52
|
UltimateErrorJSON,
|
|
52
53
|
} from '../errors';
|
|
53
54
|
export {
|
|
54
55
|
ConfigInvalidError,
|
|
56
|
+
deniedCallerFix,
|
|
55
57
|
EnvMissingError,
|
|
56
58
|
errorRetry,
|
|
59
|
+
fixFor,
|
|
57
60
|
formatError,
|
|
58
61
|
InternalError,
|
|
59
62
|
isUltimateError,
|
package/src/index.ts
CHANGED
|
@@ -135,10 +135,13 @@ export type {
|
|
|
135
135
|
SeoConfig,
|
|
136
136
|
SeoConfigInput,
|
|
137
137
|
SeoRobotsConfig,
|
|
138
|
+
SeoSitemapConfig,
|
|
138
139
|
SiteConfig,
|
|
140
|
+
SitemapLastmod,
|
|
139
141
|
SiteSections,
|
|
140
142
|
SiteSectionsInput,
|
|
141
143
|
} from './config-site';
|
|
144
|
+
export { SITEMAP_LASTMOD_SOURCES } from './config-site';
|
|
142
145
|
export type { ConflictPolicy, ResolveConflictOptions, Row } from './conflict-policy';
|
|
143
146
|
export { resolveConflict } from './conflict-policy';
|
|
144
147
|
export type { Ctx, CtxFacts, CtxInit, CtxPatch, CtxServices, ServiceBag } from './context';
|
|
@@ -206,6 +209,7 @@ export {
|
|
|
206
209
|
export type {
|
|
207
210
|
CodedErrorInit,
|
|
208
211
|
CoreErrorCode,
|
|
212
|
+
ErrorAudience,
|
|
209
213
|
ErrorCodeDeclaration,
|
|
210
214
|
ErrorCodeDescriptor,
|
|
211
215
|
ErrorCodeEntry,
|
|
@@ -220,6 +224,7 @@ export {
|
|
|
220
224
|
classifyThrown,
|
|
221
225
|
DEFAULT_ERROR_RETRY,
|
|
222
226
|
declaredErrorRetry,
|
|
227
|
+
deniedCallerFix,
|
|
223
228
|
describeErrorCode,
|
|
224
229
|
describeValue,
|
|
225
230
|
EnvMissingError,
|
|
@@ -227,6 +232,7 @@ export {
|
|
|
227
232
|
ERROR_RETRY_KINDS,
|
|
228
233
|
errorCodeSnapshot,
|
|
229
234
|
errorRetry,
|
|
235
|
+
fixFor,
|
|
230
236
|
formatError,
|
|
231
237
|
hasErrorCode,
|
|
232
238
|
InternalError,
|