@softize/opus 15.2.1 → 16.0.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/CHANGELOG.md +59 -16
- package/bin/lib/check.mjs +98 -15
- package/bin/lib/copy.mjs +811 -314
- package/bin/lib/gen-manifest.mjs +24 -23
- package/bin/lib/gen-runner.mjs +198 -149
- package/docs/adr/0010-page-header-owns-page-chrome.md +3 -4
- package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +5 -3
- package/docs/adr/0012-data-products-are-first-class-declarations.md +4 -2
- package/docs/adr/0012-modal-header-only-names-the-surface.md +45 -0
- package/docs/adr/0013-presentation-is-a-portable-action-oriented-artifact.md +92 -0
- package/docs/code-style.md +24 -19
- package/docs/data-products.md +7 -1
- package/package.json +18 -15
- package/registry/skills/build-opus-ui/references/ui-patterns.md +43 -26
- package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +0 -0
- package/src/core/data-product.ts +51 -7
- package/src/core/index.ts +3 -0
- package/src/core/presentation.ts +512 -0
- package/src/presentation/index.ts +1 -0
- package/src/ui/components/patterns/action-list-dialog.tsx +26 -9
- package/src/ui/components/patterns/confirm.tsx +34 -29
- package/src/ui/components/patterns/form-dialog.tsx +20 -8
- package/src/ui/components/patterns/list.tsx +26 -9
- package/src/ui/components/patterns/page.tsx +99 -139
- package/src/ui/components/patterns/presentation.tsx +316 -0
- package/src/ui/components/patterns/sidebar.tsx +3 -3
- package/src/ui/components/patterns/trigger.tsx +38 -17
- package/src/ui/components/primitives/button-group.tsx +53 -43
- package/src/ui/components/primitives/command.tsx +30 -72
- package/src/ui/components/primitives/dialog.tsx +23 -89
- package/src/ui/components/primitives/drawer.tsx +8 -34
- package/src/ui/docs/content/action-form-dialog.md +12 -10
- package/src/ui/docs/content/action-list-dialog.md +22 -17
- package/src/ui/docs/content/button.md +52 -35
- package/src/ui/docs/content/communication.md +26 -26
- package/src/ui/docs/content/dialog.md +173 -154
- package/src/ui/docs/content/drawer.md +12 -11
- package/src/ui/docs/content/page.md +72 -91
- package/src/ui/docs/content/presentation.md +158 -0
- package/src/ui/docs/registry.tsx +6 -0
- package/src/ui/meta.ts +8 -2
- package/src/ui/react.tsx +10 -3
package/bin/lib/gen-runner.mjs
CHANGED
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
* sourceConfigDir: string, // dirname absoluto do opus.config.ts
|
|
26
26
|
* output: string, // pasta de saída declarada no config
|
|
27
27
|
* domains: SerializedDomain[],
|
|
28
|
-
* seeds: SerializedSeed[]
|
|
28
|
+
* seeds: SerializedSeed[],
|
|
29
|
+
* presentations: PresentationDefinition[]
|
|
29
30
|
* }
|
|
30
31
|
*
|
|
31
32
|
* SerializedDomain:
|
|
@@ -61,92 +62,113 @@
|
|
|
61
62
|
* stubs poderem indicar que existe lógica.
|
|
62
63
|
*/
|
|
63
64
|
|
|
64
|
-
import path from
|
|
65
|
-
import { pathToFileURL, fileURLToPath } from
|
|
65
|
+
import path from "node:path";
|
|
66
|
+
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
66
67
|
|
|
67
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
68
|
-
const __dirname = path.dirname(__filename)
|
|
69
|
-
const PACKAGE_ROOT = path.resolve(__dirname,
|
|
68
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
69
|
+
const __dirname = path.dirname(__filename);
|
|
70
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
|
|
70
71
|
|
|
71
72
|
// =============================================================================
|
|
72
73
|
// Entrada
|
|
73
74
|
// =============================================================================
|
|
74
75
|
|
|
75
76
|
async function main() {
|
|
76
|
-
const configPath = process.argv[2]
|
|
77
|
-
if (typeof configPath !==
|
|
78
|
-
return emitError(
|
|
77
|
+
const configPath = process.argv[2];
|
|
78
|
+
if (typeof configPath !== "string" || configPath.length === 0) {
|
|
79
|
+
return emitError("Caminho do config ausente (argv[2])");
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
const configAbs = path.resolve(configPath)
|
|
82
|
-
const configUrl = pathToFileURL(configAbs).href
|
|
82
|
+
const configAbs = path.resolve(configPath);
|
|
83
|
+
const configUrl = pathToFileURL(configAbs).href;
|
|
83
84
|
|
|
84
85
|
// Importa opus (a partir do PACKAGE_ROOT, não do consumer) — precisamos
|
|
85
86
|
// do `flattenDomain` + helpers internos pra serializar.
|
|
86
87
|
const opusCoreUrl = pathToFileURL(
|
|
87
|
-
path.join(PACKAGE_ROOT,
|
|
88
|
-
).href
|
|
89
|
-
const { flattenDomain } = await import(opusCoreUrl)
|
|
88
|
+
path.join(PACKAGE_ROOT, "src", "core", "index.ts"),
|
|
89
|
+
).href;
|
|
90
|
+
const { flattenDomain } = await import(opusCoreUrl);
|
|
90
91
|
|
|
91
92
|
const opusSeedUrl = pathToFileURL(
|
|
92
|
-
path.join(PACKAGE_ROOT,
|
|
93
|
-
).href
|
|
93
|
+
path.join(PACKAGE_ROOT, "src", "seed", "index.ts"),
|
|
94
|
+
).href;
|
|
94
95
|
const { checkSeedRegistry, isSeedDefinition, publicSeedDefinition } =
|
|
95
|
-
await import(opusSeedUrl)
|
|
96
|
+
await import(opusSeedUrl);
|
|
97
|
+
|
|
98
|
+
const opusPresentationUrl = pathToFileURL(
|
|
99
|
+
path.join(PACKAGE_ROOT, "src", "presentation", "index.ts"),
|
|
100
|
+
).href;
|
|
101
|
+
const { validatePresentations } = await import(opusPresentationUrl);
|
|
96
102
|
|
|
97
103
|
// Acessar logical type meta dos dicts.
|
|
98
104
|
const opusSchemaUrl = pathToFileURL(
|
|
99
|
-
path.join(PACKAGE_ROOT,
|
|
100
|
-
).href
|
|
101
|
-
const opusSchema = await import(opusSchemaUrl)
|
|
105
|
+
path.join(PACKAGE_ROOT, "src", "schema", "index.ts"),
|
|
106
|
+
).href;
|
|
107
|
+
const opusSchema = await import(opusSchemaUrl);
|
|
102
108
|
|
|
103
109
|
// Pra serializar Zod → JSON Schema reusa o caminho que `toOpenAPISpec`
|
|
104
110
|
// usa. Importação direta do npm pq é dep regular da opus.
|
|
105
|
-
const { zodToJsonSchema } = await import(
|
|
111
|
+
const { zodToJsonSchema } = await import("zod-to-json-schema");
|
|
106
112
|
|
|
107
|
-
const pkgUrl = pathToFileURL(path.join(PACKAGE_ROOT,
|
|
108
|
-
const pkg = await import(pkgUrl, { with: { type:
|
|
113
|
+
const pkgUrl = pathToFileURL(path.join(PACKAGE_ROOT, "package.json")).href;
|
|
114
|
+
const pkg = await import(pkgUrl, { with: { type: "json" } }).then(
|
|
109
115
|
(m) => m.default,
|
|
110
|
-
)
|
|
116
|
+
);
|
|
111
117
|
|
|
112
118
|
// Import dinâmico do config do consumer.
|
|
113
|
-
const mod = await import(configUrl)
|
|
114
|
-
const cfg = mod.default ?? mod.config ?? mod
|
|
115
|
-
if (cfg === undefined || cfg === null || typeof cfg !==
|
|
119
|
+
const mod = await import(configUrl);
|
|
120
|
+
const cfg = mod.default ?? mod.config ?? mod;
|
|
121
|
+
if (cfg === undefined || cfg === null || typeof cfg !== "object") {
|
|
116
122
|
return emitError(
|
|
117
123
|
`Config em ${configAbs} não exporta um objeto default ou named "config"`,
|
|
118
|
-
)
|
|
124
|
+
);
|
|
119
125
|
}
|
|
120
126
|
|
|
121
127
|
if (!Array.isArray(cfg.domains)) {
|
|
122
128
|
return emitError(
|
|
123
129
|
`Config em ${configAbs} precisa de \`domains: DomainConfig[]\` no export default`,
|
|
124
|
-
)
|
|
130
|
+
);
|
|
125
131
|
}
|
|
126
132
|
|
|
127
|
-
const outputDir = typeof cfg.output ===
|
|
133
|
+
const outputDir = typeof cfg.output === "string" ? cfg.output : ".gen";
|
|
128
134
|
|
|
129
135
|
const ctx = {
|
|
130
136
|
flattenDomain,
|
|
131
137
|
toJsonSchema: (schema) =>
|
|
132
|
-
zodToJsonSchema(schema, { target:
|
|
138
|
+
zodToJsonSchema(schema, { target: "openApi3", $refStrategy: "none" }),
|
|
133
139
|
sourceConfigDir: path.dirname(configAbs),
|
|
134
|
-
}
|
|
140
|
+
};
|
|
135
141
|
|
|
136
|
-
const domains = cfg.domains.map((d) => serializeDomain(d, ctx))
|
|
142
|
+
const domains = cfg.domains.map((d) => serializeDomain(d, ctx));
|
|
143
|
+
if (cfg.presentations !== undefined && !Array.isArray(cfg.presentations)) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
"config.presentations inválido — use um array de declarações registradas",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const actionRegistry = Object.fromEntries(
|
|
149
|
+
cfg.domains
|
|
150
|
+
.flatMap((domain) => collectDomainActions(domain))
|
|
151
|
+
.map((action) => [action.name, action]),
|
|
152
|
+
);
|
|
153
|
+
const presentations = validatePresentations(
|
|
154
|
+
cfg.presentations ?? [],
|
|
155
|
+
actionRegistry,
|
|
156
|
+
);
|
|
137
157
|
if (cfg.seeds !== undefined && !Array.isArray(cfg.seeds)) {
|
|
138
|
-
throw new Error(
|
|
158
|
+
throw new Error(
|
|
159
|
+
"config.seeds inválido — use um array de declarações registradas",
|
|
160
|
+
);
|
|
139
161
|
}
|
|
140
|
-
const registeredSeeds = cfg.seeds ?? []
|
|
141
|
-
const seedCheck = checkSeedRegistry(registeredSeeds)
|
|
162
|
+
const registeredSeeds = cfg.seeds ?? [];
|
|
163
|
+
const seedCheck = checkSeedRegistry(registeredSeeds);
|
|
142
164
|
if (!seedCheck.ok) {
|
|
143
165
|
throw new Error(
|
|
144
|
-
`config.seeds inválido — ${seedCheck.diagnostics.map((diagnostic) => diagnostic.message).join(
|
|
145
|
-
)
|
|
166
|
+
`config.seeds inválido — ${seedCheck.diagnostics.map((diagnostic) => diagnostic.message).join("; ")}`,
|
|
167
|
+
);
|
|
146
168
|
}
|
|
147
169
|
const seeds = registeredSeeds.map((seed) =>
|
|
148
170
|
serializeSeed(seed, isSeedDefinition, publicSeedDefinition),
|
|
149
|
-
)
|
|
171
|
+
);
|
|
150
172
|
|
|
151
173
|
const payload = {
|
|
152
174
|
opusVersion: pkg.version,
|
|
@@ -154,16 +176,27 @@ async function main() {
|
|
|
154
176
|
output: outputDir,
|
|
155
177
|
domains,
|
|
156
178
|
seeds,
|
|
157
|
-
|
|
179
|
+
presentations,
|
|
180
|
+
};
|
|
158
181
|
|
|
159
|
-
emit({ ok: true, payload })
|
|
182
|
+
emit({ ok: true, payload });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function collectDomainActions(domain) {
|
|
186
|
+
const actions = Array.from(iterateActions(domain.actions));
|
|
187
|
+
for (const subdomain of Array.isArray(domain.subdomains)
|
|
188
|
+
? domain.subdomains
|
|
189
|
+
: []) {
|
|
190
|
+
actions.push(...collectDomainActions(subdomain));
|
|
191
|
+
}
|
|
192
|
+
return actions;
|
|
160
193
|
}
|
|
161
194
|
|
|
162
195
|
function serializeSeed(seed, isSeedDefinition, publicSeedDefinition) {
|
|
163
196
|
if (!isSeedDefinition(seed)) {
|
|
164
|
-
throw new Error(
|
|
197
|
+
throw new Error("config.seeds contém uma declaração inválida");
|
|
165
198
|
}
|
|
166
|
-
return publicSeedDefinition(seed)
|
|
199
|
+
return publicSeedDefinition(seed);
|
|
167
200
|
}
|
|
168
201
|
|
|
169
202
|
// =============================================================================
|
|
@@ -171,7 +204,7 @@ function serializeSeed(seed, isSeedDefinition, publicSeedDefinition) {
|
|
|
171
204
|
// =============================================================================
|
|
172
205
|
|
|
173
206
|
function serializeDomain(domain, ctx) {
|
|
174
|
-
const entityList = serializeEntities(domain.entities)
|
|
207
|
+
const entityList = serializeEntities(domain.entities);
|
|
175
208
|
return {
|
|
176
209
|
name: domain.name,
|
|
177
210
|
description: nullable(domain.description),
|
|
@@ -192,7 +225,7 @@ function serializeDomain(domain, ctx) {
|
|
|
192
225
|
subdomains: Array.isArray(domain.subdomains)
|
|
193
226
|
? domain.subdomains.map((s) => serializeDomain(s, ctx))
|
|
194
227
|
: [],
|
|
195
|
-
}
|
|
228
|
+
};
|
|
196
229
|
}
|
|
197
230
|
|
|
198
231
|
function serializeDataProducts(source) {
|
|
@@ -220,7 +253,16 @@ function serializeDataProducts(source) {
|
|
|
220
253
|
: [],
|
|
221
254
|
entities: Array.isArray(product.entities) ? product.entities : [],
|
|
222
255
|
access: {
|
|
223
|
-
|
|
256
|
+
permissionContexts: Array.isArray(product.access?.permissionContexts)
|
|
257
|
+
? product.access.permissionContexts
|
|
258
|
+
: Array.isArray(product.access?.contexts)
|
|
259
|
+
? product.access.contexts
|
|
260
|
+
: [],
|
|
261
|
+
contexts: Array.isArray(product.access?.permissionContexts)
|
|
262
|
+
? product.access.permissionContexts
|
|
263
|
+
: Array.isArray(product.access?.contexts)
|
|
264
|
+
? product.access.contexts
|
|
265
|
+
: [],
|
|
224
266
|
organizationalScopes: Array.isArray(product.access?.organizationalScopes)
|
|
225
267
|
? product.access.organizationalScopes
|
|
226
268
|
: [],
|
|
@@ -234,13 +276,18 @@ function serializeDataProducts(source) {
|
|
|
234
276
|
/** Serializa as entidades (`defineEntity`) com campos + docs — a fonte que vai pro
|
|
235
277
|
* manifest/lente. Ignora valores que não parecem EntityConfig (name+fields). */
|
|
236
278
|
function serializeEntities(src) {
|
|
237
|
-
if (src === undefined || src === null || typeof src !==
|
|
238
|
-
const out = []
|
|
279
|
+
if (src === undefined || src === null || typeof src !== "object") return [];
|
|
280
|
+
const out = [];
|
|
239
281
|
for (const [key, ent] of Object.entries(src)) {
|
|
240
|
-
if (
|
|
241
|
-
|
|
282
|
+
if (
|
|
283
|
+
ent === null ||
|
|
284
|
+
typeof ent !== "object" ||
|
|
285
|
+
typeof ent.fields !== "object"
|
|
286
|
+
)
|
|
287
|
+
continue;
|
|
288
|
+
const fields = [];
|
|
242
289
|
for (const [fname, f] of Object.entries(ent.fields)) {
|
|
243
|
-
const col = f?.column ?? {}
|
|
290
|
+
const col = f?.column ?? {};
|
|
244
291
|
fields.push({
|
|
245
292
|
name: fname,
|
|
246
293
|
logicalType: f?.meta?.logicalType ?? null,
|
|
@@ -248,70 +295,68 @@ function serializeEntities(src) {
|
|
|
248
295
|
pk: col.pk === true,
|
|
249
296
|
references: col.references ?? null,
|
|
250
297
|
doc: col.doc ?? null,
|
|
251
|
-
})
|
|
298
|
+
});
|
|
252
299
|
}
|
|
253
300
|
out.push({
|
|
254
|
-
name: typeof ent.name ===
|
|
255
|
-
description: typeof ent.description ===
|
|
256
|
-
table: typeof ent.table ===
|
|
301
|
+
name: typeof ent.name === "string" ? ent.name : key,
|
|
302
|
+
description: typeof ent.description === "string" ? ent.description : null,
|
|
303
|
+
table: typeof ent.table === "string" ? ent.table : null,
|
|
257
304
|
fields,
|
|
258
305
|
relations: ent.relations ?? null,
|
|
259
|
-
})
|
|
306
|
+
});
|
|
260
307
|
}
|
|
261
|
-
return out
|
|
308
|
+
return out;
|
|
262
309
|
}
|
|
263
310
|
|
|
264
311
|
function serializeDicts(dicts) {
|
|
265
|
-
if (dicts === undefined || dicts === null || typeof dicts !==
|
|
266
|
-
return {}
|
|
312
|
+
if (dicts === undefined || dicts === null || typeof dicts !== "object") {
|
|
313
|
+
return {};
|
|
267
314
|
}
|
|
268
|
-
const out = {}
|
|
315
|
+
const out = {};
|
|
269
316
|
for (const [name, dict] of Object.entries(dicts)) {
|
|
270
|
-
if (dict === null || typeof dict !==
|
|
317
|
+
if (dict === null || typeof dict !== "object") continue;
|
|
271
318
|
// DictType expõe `meta.params.entries`. Fallback: tenta `.keys()` +
|
|
272
319
|
// `.metaFor(k)` caso seja shape custom.
|
|
273
|
-
const meta = dict.meta
|
|
320
|
+
const meta = dict.meta;
|
|
274
321
|
if (
|
|
275
322
|
meta !== undefined &&
|
|
276
323
|
meta !== null &&
|
|
277
|
-
meta.logicalType ===
|
|
324
|
+
meta.logicalType === "dict" &&
|
|
278
325
|
meta.params !== undefined
|
|
279
326
|
) {
|
|
280
|
-
const params = meta.params
|
|
281
|
-
const keys = Array.isArray(params.keys) ? params.keys : []
|
|
327
|
+
const params = meta.params;
|
|
328
|
+
const keys = Array.isArray(params.keys) ? params.keys : [];
|
|
282
329
|
const entries =
|
|
283
330
|
params.entries !== undefined && params.entries !== null
|
|
284
331
|
? params.entries
|
|
285
|
-
: {}
|
|
286
|
-
const values = {}
|
|
332
|
+
: {};
|
|
333
|
+
const values = {};
|
|
287
334
|
for (const k of keys) {
|
|
288
|
-
values[k] = entries[k] ?? null
|
|
335
|
+
values[k] = entries[k] ?? null;
|
|
289
336
|
}
|
|
290
337
|
// `doc` = entendimento do vocabulário inteiro (o que esse dict representa);
|
|
291
338
|
// `presentation` = papel de apresentação declarado (ADR 0003) — null quando ausente.
|
|
292
339
|
out[name] = {
|
|
293
340
|
keys,
|
|
294
341
|
values,
|
|
295
|
-
doc: typeof params.doc ===
|
|
296
|
-
presentation:
|
|
297
|
-
|
|
298
|
-
|
|
342
|
+
doc: typeof params.doc === "string" ? params.doc : null,
|
|
343
|
+
presentation:
|
|
344
|
+
typeof params.presentation === "string" ? params.presentation : null,
|
|
345
|
+
};
|
|
346
|
+
continue;
|
|
299
347
|
}
|
|
300
348
|
// Fallback: dict-like com `keys()` + `metaFor()`.
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
for (const k of keys) values[k] = dict.metaFor(k)
|
|
308
|
-
out[name] = { keys, values }
|
|
309
|
-
continue
|
|
349
|
+
if (typeof dict.keys === "function" && typeof dict.metaFor === "function") {
|
|
350
|
+
const keys = dict.keys();
|
|
351
|
+
const values = {};
|
|
352
|
+
for (const k of keys) values[k] = dict.metaFor(k);
|
|
353
|
+
out[name] = { keys, values };
|
|
354
|
+
continue;
|
|
310
355
|
}
|
|
311
356
|
// Não-reconhecível: registra placeholder pra não silenciar.
|
|
312
|
-
out[name] = { keys: [], values: {}, unknownShape: true }
|
|
357
|
+
out[name] = { keys: [], values: {}, unknownShape: true };
|
|
313
358
|
}
|
|
314
|
-
return out
|
|
359
|
+
return out;
|
|
315
360
|
}
|
|
316
361
|
|
|
317
362
|
function serializeAction(action, ctx) {
|
|
@@ -342,43 +387,43 @@ function serializeAction(action, ctx) {
|
|
|
342
387
|
examples: Array.isArray(action.examples) ? action.examples : [],
|
|
343
388
|
errors: Array.isArray(action.errors) ? action.errors : [],
|
|
344
389
|
sourceModule: lookupSourceModule(action, ctx),
|
|
345
|
-
}
|
|
390
|
+
};
|
|
346
391
|
|
|
347
|
-
if (action.kind ===
|
|
348
|
-
base.emits = Array.isArray(action.emits) ? action.emits : []
|
|
392
|
+
if (action.kind === "simple" || action.kind === "form") {
|
|
393
|
+
base.emits = Array.isArray(action.emits) ? action.emits : [];
|
|
349
394
|
base.background =
|
|
350
395
|
action.background !== undefined &&
|
|
351
396
|
action.background !== null &&
|
|
352
|
-
action.background.enabled === true
|
|
397
|
+
action.background.enabled === true;
|
|
353
398
|
}
|
|
354
399
|
|
|
355
|
-
if (action.kind ===
|
|
356
|
-
base.fields = serializeFields(action.fields)
|
|
400
|
+
if (action.kind === "form") {
|
|
401
|
+
base.fields = serializeFields(action.fields);
|
|
357
402
|
}
|
|
358
403
|
|
|
359
|
-
if (action.kind ===
|
|
360
|
-
base.filters = serializeFilters(action.filters)
|
|
361
|
-
base.sort = action.sort ?? null
|
|
362
|
-
base.paginate = action.paginate ?? null
|
|
363
|
-
base.text = action.text ?? null
|
|
364
|
-
base.periods = Array.isArray(action.periods) ? action.periods : []
|
|
404
|
+
if (action.kind === "list") {
|
|
405
|
+
base.filters = serializeFilters(action.filters);
|
|
406
|
+
base.sort = action.sort ?? null;
|
|
407
|
+
base.paginate = action.paginate ?? null;
|
|
408
|
+
base.text = action.text ?? null;
|
|
409
|
+
base.periods = Array.isArray(action.periods) ? action.periods : [];
|
|
365
410
|
}
|
|
366
411
|
|
|
367
|
-
if (action.kind ===
|
|
368
|
-
base.projection = Array.isArray(action.projection) ? action.projection : []
|
|
369
|
-
base.expand = action.expand ?? null
|
|
412
|
+
if (action.kind === "view") {
|
|
413
|
+
base.projection = Array.isArray(action.projection) ? action.projection : [];
|
|
414
|
+
base.expand = action.expand ?? null;
|
|
370
415
|
}
|
|
371
416
|
|
|
372
|
-
return base
|
|
417
|
+
return base;
|
|
373
418
|
}
|
|
374
419
|
|
|
375
420
|
function serializeFields(fields) {
|
|
376
|
-
if (fields === undefined || fields === null || typeof fields !==
|
|
377
|
-
return {}
|
|
421
|
+
if (fields === undefined || fields === null || typeof fields !== "object") {
|
|
422
|
+
return {};
|
|
378
423
|
}
|
|
379
|
-
const out = {}
|
|
424
|
+
const out = {};
|
|
380
425
|
for (const [name, spec] of Object.entries(fields)) {
|
|
381
|
-
if (spec === null || typeof spec !==
|
|
426
|
+
if (spec === null || typeof spec !== "object") continue;
|
|
382
427
|
out[name] = {
|
|
383
428
|
label: spec.label ?? null,
|
|
384
429
|
placeholder: spec.placeholder ?? null,
|
|
@@ -390,21 +435,25 @@ function serializeFields(fields) {
|
|
|
390
435
|
order: spec.order ?? null,
|
|
391
436
|
widget: spec.widget ?? null,
|
|
392
437
|
options: spec.options ?? null,
|
|
393
|
-
hasShowWhen: typeof spec.showWhen ===
|
|
394
|
-
hasRequireWhen: typeof spec.requireWhen ===
|
|
438
|
+
hasShowWhen: typeof spec.showWhen === "function",
|
|
439
|
+
hasRequireWhen: typeof spec.requireWhen === "function",
|
|
395
440
|
aiDescription: spec.aiDescription ?? null,
|
|
396
|
-
}
|
|
441
|
+
};
|
|
397
442
|
}
|
|
398
|
-
return out
|
|
443
|
+
return out;
|
|
399
444
|
}
|
|
400
445
|
|
|
401
446
|
function serializeFilters(filters) {
|
|
402
|
-
if (
|
|
403
|
-
|
|
447
|
+
if (
|
|
448
|
+
filters === undefined ||
|
|
449
|
+
filters === null ||
|
|
450
|
+
typeof filters !== "object"
|
|
451
|
+
) {
|
|
452
|
+
return {};
|
|
404
453
|
}
|
|
405
|
-
const out = {}
|
|
454
|
+
const out = {};
|
|
406
455
|
for (const [name, spec] of Object.entries(filters)) {
|
|
407
|
-
if (spec === null || typeof spec !==
|
|
456
|
+
if (spec === null || typeof spec !== "object") continue;
|
|
408
457
|
out[name] = {
|
|
409
458
|
label: spec.label ?? null,
|
|
410
459
|
placeholder: spec.placeholder ?? null,
|
|
@@ -417,9 +466,9 @@ function serializeFilters(filters) {
|
|
|
417
466
|
depends: spec.depends ?? null,
|
|
418
467
|
options: spec.options ?? null,
|
|
419
468
|
aiDescription: spec.aiDescription ?? null,
|
|
420
|
-
}
|
|
469
|
+
};
|
|
421
470
|
}
|
|
422
|
-
return out
|
|
471
|
+
return out;
|
|
423
472
|
}
|
|
424
473
|
|
|
425
474
|
function serializeReaction(reaction) {
|
|
@@ -428,13 +477,13 @@ function serializeReaction(reaction) {
|
|
|
428
477
|
on: reaction.on,
|
|
429
478
|
description: nullable(reaction.description),
|
|
430
479
|
tags: Array.isArray(reaction.tags) ? reaction.tags : [],
|
|
431
|
-
handler:
|
|
480
|
+
handler: "<function>",
|
|
432
481
|
retry: reaction.retry ?? null,
|
|
433
482
|
timeout: reaction.timeout ?? null,
|
|
434
483
|
concurrency: reaction.concurrency ?? null,
|
|
435
|
-
hasDedup: typeof reaction.dedup ===
|
|
436
|
-
hasAuthorize: typeof reaction.authorize ===
|
|
437
|
-
}
|
|
484
|
+
hasDedup: typeof reaction.dedup === "function",
|
|
485
|
+
hasAuthorize: typeof reaction.authorize === "function",
|
|
486
|
+
};
|
|
438
487
|
}
|
|
439
488
|
|
|
440
489
|
function serializeSchedule(sched) {
|
|
@@ -448,8 +497,8 @@ function serializeSchedule(sched) {
|
|
|
448
497
|
description: nullable(sched.description),
|
|
449
498
|
tags: Array.isArray(sched.tags) ? sched.tags : [],
|
|
450
499
|
input:
|
|
451
|
-
typeof sched.input ===
|
|
452
|
-
}
|
|
500
|
+
typeof sched.input === "function" ? "<function>" : (sched.input ?? null),
|
|
501
|
+
};
|
|
453
502
|
}
|
|
454
503
|
|
|
455
504
|
// =============================================================================
|
|
@@ -457,16 +506,16 @@ function serializeSchedule(sched) {
|
|
|
457
506
|
// =============================================================================
|
|
458
507
|
|
|
459
508
|
function* iterateActions(actions) {
|
|
460
|
-
if (actions === undefined) return
|
|
509
|
+
if (actions === undefined) return;
|
|
461
510
|
if (Array.isArray(actions)) {
|
|
462
|
-
for (const a of actions) yield a
|
|
463
|
-
return
|
|
511
|
+
for (const a of actions) yield a;
|
|
512
|
+
return;
|
|
464
513
|
}
|
|
465
514
|
for (const value of Object.values(actions)) {
|
|
466
515
|
if (Array.isArray(value)) {
|
|
467
|
-
for (const a of value) yield a
|
|
516
|
+
for (const a of value) yield a;
|
|
468
517
|
} else {
|
|
469
|
-
yield value
|
|
518
|
+
yield value;
|
|
470
519
|
}
|
|
471
520
|
}
|
|
472
521
|
}
|
|
@@ -474,36 +523,36 @@ function* iterateActions(actions) {
|
|
|
474
523
|
function derivePermission(action) {
|
|
475
524
|
// Projeção FIEL do `requires`: lista sai lista (semântica ANY) — projetar só a
|
|
476
525
|
// 1ª string faria a spec mentir por omissão pra quem lê o manifest.
|
|
477
|
-
const req = action.requires
|
|
478
|
-
if (typeof req ===
|
|
526
|
+
const req = action.requires;
|
|
527
|
+
if (typeof req === "string") return req;
|
|
479
528
|
if (Array.isArray(req)) {
|
|
480
|
-
const strs = req.filter((r) => typeof r ===
|
|
481
|
-
return strs.length > 0 ? strs : null
|
|
529
|
+
const strs = req.filter((r) => typeof r === "string");
|
|
530
|
+
return strs.length > 0 ? strs : null;
|
|
482
531
|
}
|
|
483
|
-
return null
|
|
532
|
+
return null;
|
|
484
533
|
}
|
|
485
534
|
|
|
486
535
|
function serializeAuthorize(authorize) {
|
|
487
|
-
if (authorize === undefined || authorize === null) return null
|
|
536
|
+
if (authorize === undefined || authorize === null) return null;
|
|
488
537
|
// DSL: pode chegar como string em frameworks (versão futura). Hoje é fn.
|
|
489
|
-
if (typeof authorize ===
|
|
490
|
-
if (typeof authorize ===
|
|
491
|
-
return null
|
|
538
|
+
if (typeof authorize === "string") return authorize;
|
|
539
|
+
if (typeof authorize === "function") return "<function>";
|
|
540
|
+
return null;
|
|
492
541
|
}
|
|
493
542
|
|
|
494
543
|
function serializeInvalidates(inv) {
|
|
495
|
-
if (inv === undefined || inv === null) return null
|
|
496
|
-
if (Array.isArray(inv)) return inv
|
|
497
|
-
if (typeof inv ===
|
|
498
|
-
return null
|
|
544
|
+
if (inv === undefined || inv === null) return null;
|
|
545
|
+
if (Array.isArray(inv)) return inv;
|
|
546
|
+
if (typeof inv === "function") return "<function>";
|
|
547
|
+
return null;
|
|
499
548
|
}
|
|
500
549
|
|
|
501
550
|
function safeToJsonSchema(schema, ctx) {
|
|
502
|
-
if (schema === undefined || schema === null) return null
|
|
551
|
+
if (schema === undefined || schema === null) return null;
|
|
503
552
|
try {
|
|
504
|
-
return ctx.toJsonSchema(schema)
|
|
553
|
+
return ctx.toJsonSchema(schema);
|
|
505
554
|
} catch (err) {
|
|
506
|
-
return { __unserializable: true, reason: err.message }
|
|
555
|
+
return { __unserializable: true, reason: err.message };
|
|
507
556
|
}
|
|
508
557
|
}
|
|
509
558
|
|
|
@@ -511,13 +560,13 @@ function lookupSourceModule(_action, _ctx) {
|
|
|
511
560
|
// Não há mecanismo confiável pra extrair o source module a partir do
|
|
512
561
|
// ActionDef em runtime (ESM não expõe). Reservamos o campo pra futura
|
|
513
562
|
// integração com `import.meta.url` no defineAction.
|
|
514
|
-
return null
|
|
563
|
+
return null;
|
|
515
564
|
}
|
|
516
565
|
|
|
517
566
|
function nullable(value) {
|
|
518
|
-
if (value === undefined || value === null) return null
|
|
519
|
-
if (typeof value ===
|
|
520
|
-
return value
|
|
567
|
+
if (value === undefined || value === null) return null;
|
|
568
|
+
if (typeof value === "string" && value.length === 0) return null;
|
|
569
|
+
return value;
|
|
521
570
|
}
|
|
522
571
|
|
|
523
572
|
// =============================================================================
|
|
@@ -525,14 +574,14 @@ function nullable(value) {
|
|
|
525
574
|
// =============================================================================
|
|
526
575
|
|
|
527
576
|
function emit(payload) {
|
|
528
|
-
process.stdout.write(JSON.stringify(payload) +
|
|
577
|
+
process.stdout.write(JSON.stringify(payload) + "\n");
|
|
529
578
|
}
|
|
530
579
|
|
|
531
580
|
function emitError(message) {
|
|
532
|
-
emit({ ok: false, error: message })
|
|
533
|
-
process.exit(1)
|
|
581
|
+
emit({ ok: false, error: message });
|
|
582
|
+
process.exit(1);
|
|
534
583
|
}
|
|
535
584
|
|
|
536
585
|
main().catch((err) => {
|
|
537
|
-
emitError(`runner crash: ${err.stack ?? err.message ?? String(err)}`)
|
|
538
|
-
})
|
|
586
|
+
emitError(`runner crash: ${err.stack ?? err.message ?? String(err)}`);
|
|
587
|
+
});
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
# ADR 0010 — PageHeader também representa o chrome compacto da página
|
|
2
2
|
|
|
3
|
-
- **Status:**
|
|
3
|
+
- **Status:** substituída pela ADR 0011.
|
|
4
4
|
- **Data:** 2026-09-09.
|
|
5
5
|
- **Complementa:** ADR 0005.
|
|
6
6
|
|
|
7
|
-
> **Atualização (2026-09-
|
|
8
|
-
>
|
|
9
|
-
> cabeçalho, mas o título do conteúdo passa a `PageIntro` e as ações são coordenadas pelo Opus.
|
|
7
|
+
> **Atualização (2026-09-11).** A ADR 0011 torna `PageShell` o único componente responsável pela
|
|
8
|
+
> barra persistente. `PageHeader` volta a ter uma única apresentação dentro da página.
|
|
10
9
|
|
|
11
10
|
## Contexto
|
|
12
11
|
|
|
@@ -17,8 +17,9 @@ anatomias concorrentes e faz um estado integral remover também a navegação pe
|
|
|
17
17
|
## Decisão
|
|
18
18
|
|
|
19
19
|
`PageShell` representa a moldura persistente de uma página dentro de um shell de aplicação. Ele
|
|
20
|
-
renderiza a
|
|
21
|
-
|
|
20
|
+
renderiza a barra, recebe a navegação conhecida pelo shell e oferece o alvo canônico para ações
|
|
21
|
+
declaradas pela `Page` descendente. Essa barra existe somente por `PageShell`; `PageHeader` não
|
|
22
|
+
possui variante visual para reproduzi-la dentro da página.
|
|
22
23
|
|
|
23
24
|
Quando uma `Page` está dentro de `PageShell`, sua forma curta transforma título e descrição em
|
|
24
25
|
`PageIntro`, dentro do conteúdo. `PageActions` continua declarado pela página, mas aparece na barra.
|
|
@@ -41,7 +42,8 @@ Ele apenas coordena a barra e a área em que uma única `Page` é renderizada.
|
|
|
41
42
|
- título e descrição deixam de ser mascarados por seletores globais;
|
|
42
43
|
- ações de página têm um único destino oficial na barra;
|
|
43
44
|
- estados integrais preservam a barra e escondem somente a introdução do conteúdo;
|
|
44
|
-
- a composição
|
|
45
|
+
- a composição de `Page` com `PageHeader` permanece disponível fora de `PageShell`, sem uma segunda
|
|
46
|
+
forma de produzir a barra;
|
|
45
47
|
- `PageActionsTarget` continua disponível apenas para workspaces imersivos que não usam
|
|
46
48
|
`PageShell`.
|
|
47
49
|
|
|
@@ -27,10 +27,12 @@ descritivo e Actions de interface. Produtos ativos podem ser descontinuados com
|
|
|
27
27
|
duplicados e relações literais que apontam para Action ou Entity inexistente. A declaração não
|
|
28
28
|
executa consulta, não contém driver e não substitui uma Action.
|
|
29
29
|
|
|
30
|
-
A autorização continua sendo responsabilidade da Action. `access.
|
|
30
|
+
A autorização continua sendo responsabilidade da Action. `access.permissionContexts` e
|
|
31
31
|
`access.organizationalScopes` documentam o alcance esperado para catálogo, Lens e revisão, mas o
|
|
32
32
|
runtime não os converte em autorização implícita. Essa separação impede que uma descrição
|
|
33
|
-
incompleta abra dados.
|
|
33
|
+
incompleta abra dados. O alias histórico `access.contexts` permanece aceito como entrada e
|
|
34
|
+
projetado no manifest durante a série 15.x por compatibilidade. Novos consumidores usam o nome
|
|
35
|
+
qualificado; o alias será removido somente numa versão major.
|
|
34
36
|
|
|
35
37
|
O manifest projeta a declaração integral. Tools de IA recebem a lista de produtos que expõem em
|
|
36
38
|
metadata, e o servidor MCP publica essa lista em `_meta['com.softize.opus/data-products']`. A
|