@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +59 -16
  2. package/bin/lib/check.mjs +98 -15
  3. package/bin/lib/copy.mjs +811 -314
  4. package/bin/lib/gen-manifest.mjs +24 -23
  5. package/bin/lib/gen-runner.mjs +198 -149
  6. package/docs/adr/0010-page-header-owns-page-chrome.md +3 -4
  7. package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +5 -3
  8. package/docs/adr/0012-data-products-are-first-class-declarations.md +4 -2
  9. package/docs/adr/0012-modal-header-only-names-the-surface.md +45 -0
  10. package/docs/adr/0013-presentation-is-a-portable-action-oriented-artifact.md +92 -0
  11. package/docs/code-style.md +24 -19
  12. package/docs/data-products.md +7 -1
  13. package/package.json +18 -15
  14. package/registry/skills/build-opus-ui/references/ui-patterns.md +43 -26
  15. package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +0 -0
  16. package/src/core/data-product.ts +51 -7
  17. package/src/core/index.ts +3 -0
  18. package/src/core/presentation.ts +512 -0
  19. package/src/presentation/index.ts +1 -0
  20. package/src/ui/components/patterns/action-list-dialog.tsx +26 -9
  21. package/src/ui/components/patterns/confirm.tsx +34 -29
  22. package/src/ui/components/patterns/form-dialog.tsx +20 -8
  23. package/src/ui/components/patterns/list.tsx +26 -9
  24. package/src/ui/components/patterns/page.tsx +99 -139
  25. package/src/ui/components/patterns/presentation.tsx +316 -0
  26. package/src/ui/components/patterns/sidebar.tsx +3 -3
  27. package/src/ui/components/patterns/trigger.tsx +38 -17
  28. package/src/ui/components/primitives/button-group.tsx +53 -43
  29. package/src/ui/components/primitives/command.tsx +30 -72
  30. package/src/ui/components/primitives/dialog.tsx +23 -89
  31. package/src/ui/components/primitives/drawer.tsx +8 -34
  32. package/src/ui/docs/content/action-form-dialog.md +12 -10
  33. package/src/ui/docs/content/action-list-dialog.md +22 -17
  34. package/src/ui/docs/content/button.md +52 -35
  35. package/src/ui/docs/content/communication.md +26 -26
  36. package/src/ui/docs/content/dialog.md +173 -154
  37. package/src/ui/docs/content/drawer.md +12 -11
  38. package/src/ui/docs/content/page.md +72 -91
  39. package/src/ui/docs/content/presentation.md +158 -0
  40. package/src/ui/docs/registry.tsx +6 -0
  41. package/src/ui/meta.ts +8 -2
  42. package/src/ui/react.tsx +10 -3
@@ -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 'node:path'
65
- import { pathToFileURL, fileURLToPath } from 'node:url'
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 !== 'string' || configPath.length === 0) {
78
- return emitError('Caminho do config ausente (argv[2])')
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, 'src', 'core', 'index.ts'),
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, 'src', 'seed', 'index.ts'),
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, 'src', 'schema', 'index.ts'),
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('zod-to-json-schema')
111
+ const { zodToJsonSchema } = await import("zod-to-json-schema");
106
112
 
107
- const pkgUrl = pathToFileURL(path.join(PACKAGE_ROOT, 'package.json')).href
108
- const pkg = await import(pkgUrl, { with: { type: 'json' } }).then(
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 !== 'object') {
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 === 'string' ? cfg.output : '.gen'
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: 'openApi3', $refStrategy: 'none' }),
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('config.seeds inválido — use um array de declarações registradas')
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('config.seeds contém uma declaração inválida')
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
- contexts: Array.isArray(product.access?.contexts) ? product.access.contexts : [],
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 !== 'object') return []
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 (ent === null || typeof ent !== 'object' || typeof ent.fields !== 'object') continue
241
- const fields = []
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 === 'string' ? ent.name : key,
255
- description: typeof ent.description === 'string' ? ent.description : null,
256
- table: typeof ent.table === 'string' ? ent.table : null,
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 !== 'object') {
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 !== 'object') continue
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 === 'dict' &&
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 === 'string' ? params.doc : null,
296
- presentation: typeof params.presentation === 'string' ? params.presentation : null,
297
- }
298
- continue
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
- typeof dict.keys === 'function' &&
303
- typeof dict.metaFor === 'function'
304
- ) {
305
- const keys = dict.keys()
306
- const values = {}
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 === 'simple' || action.kind === 'form') {
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 === 'form') {
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 === 'list') {
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 === 'view') {
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 !== 'object') {
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 !== 'object') continue
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 === 'function',
394
- hasRequireWhen: typeof spec.requireWhen === 'function',
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 (filters === undefined || filters === null || typeof filters !== 'object') {
403
- return {}
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 !== 'object') continue
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: '<function>',
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 === 'function',
436
- hasAuthorize: typeof reaction.authorize === 'function',
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 === 'function' ? '<function>' : (sched.input ?? null),
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 === 'string') return 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 === 'string')
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 === 'string') return authorize
490
- if (typeof authorize === 'function') return '<function>'
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 === 'function') return '<function>'
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 === 'string' && value.length === 0) return null
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) + '\n')
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:** aceita.
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-10).** A ADR 0011 acrescenta `PageShell` para o caso em que shell e rota
8
- > conhecem partes diferentes da mesma página. A barra continua sendo a apresentação compacta do
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 apresentação em barra de `PageHeader`, recebe a navegação conhecida pelo shell e oferece
21
- o alvo canônico para ações declaradas pela `Page` descendente.
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 anterior de `Page` permanece compatível fora de `PageShell`;
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.contexts` e
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