@softize/opus 15.0.1 → 15.2.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 CHANGED
@@ -7,6 +7,39 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
7
7
  `opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
8
8
  mudança cobra do seu código.
9
9
 
10
+ ## 15.2.0 — 2026-09-11
11
+
12
+ Produtos de Dados passam a ser declarações de primeira classe com `defineDataProduct` e registro
13
+ em `defineDomain({ dataProducts })`. Identidade, versão, responsável, grão, classificação,
14
+ natureza, Fontes, entities, acesso descritivo, interfaces e ciclo de vida seguem para o manifest.
15
+ O domínio e `opus check` detectam IDs duplicados e referências inexistentes. Actions oferecidas à
16
+ IA carregam os produtos que expõem; MCP projeta a relação em
17
+ `_meta['com.softize.opus/data-products']`. Autorização e recorte continuam obrigatoriamente nas
18
+ Actions — metadata de produto não abre acesso.
19
+
20
+ O `<Chat>` aceita `id` e `createdAt` nos itens, gera esses campos para mensagens que administra e
21
+ oferece `renderMessageActions`. O slot aparece em hover ou foco e permite que o app carregue ações
22
+ e detalhes contextuais sem acoplar o componente a uma implementação de observabilidade.
23
+
24
+ **Migração:** nenhuma para declarações existentes. Para catalogar um produto, declare-o, registre-o
25
+ no domínio e mantenha todas as Actions citadas em `interfaces` protegidas. Históricos antigos sem
26
+ `id` ou `createdAt` continuam renderizando.
27
+
28
+ ## 15.1.0 — 2026-09-10
29
+
30
+ `PageShell` passa a coordenar o chrome persistente quando shell e rota conhecem partes diferentes da
31
+ página. O shell fornece a navegação e mantém uma barra de `3rem`; a `Page` descendente continua
32
+ declarando título, descrição, ações e estados. As ações aparecem na barra sem portal montado pelo
33
+ consumidor, enquanto título e descrição formam o novo `PageIntro` dentro do conteúdo.
34
+
35
+ Em um estado integral, `PageShell` permanece visível e somente `PageIntro` é ocultado. A composição
36
+ anterior de `Page`, `PageHeader` e `PageBody` não muda fora do shell. `PageActionsTarget` continua
37
+ disponível para workspaces imersivos que já possuem uma moldura própria.
38
+
39
+ **Migração opcional:** substitua barras paralelas e seletores que escondem slots por
40
+ `PageShell navigation={...}` ao redor da rota. A página interna pode continuar na forma curta; use
41
+ `PageIntro` e `PageBody` somente quando precisar da composição explícita.
42
+
10
43
  ## 15.0.1 — 2026-09-09
11
44
 
12
45
  Correções sobre a 15.0.0, publicada horas antes, todas vindas da revisão dela. Nenhuma novidade de
package/README.md CHANGED
@@ -15,7 +15,7 @@ Protocolo de actions de ponta a ponta para TypeScript. Declare uma vez — input
15
15
  - **Contrato, não feature.** Tudo que entra no core padroniza forma; tudo que faz trabalho usa lib externa.
16
16
  - **Fail-closed por default.** Action sem `authorize` é negada. Audit default-on.
17
17
  - **Adapters plugáveis.** Cada categoria é uma superfície (subpath) com interface fechada e drivers trocáveis.
18
- - **Declaração é a fonte; doc é projeção.** As declarações (`defineContract`, `bindAction`, `defineEntity`) são a fonte dos contratos; manifest, OpenAPI e a doc gerada são projeções. `docs/protocol.md` descreve o protocolo em 16 seções (15 + glossário).
18
+ - **Declaração é a fonte; doc é projeção.** As declarações (`defineContract`, `bindAction`, `defineEntity`, `defineDataProduct`) são a fonte dos contratos; manifest, OpenAPI e a doc gerada são projeções. `docs/protocol.md` descreve o protocolo em 16 seções (15 + glossário).
19
19
 
20
20
  ## Superfícies
21
21
 
@@ -23,7 +23,7 @@ Um pacote (`@softize/opus`), uma versão. Cada categoria abaixo é um **subpath
23
23
 
24
24
  | Superfície | Drivers | Propósito |
25
25
  |---|---|---|
26
- | `@softize/opus` (core) | — | Protocolo, `defineContract`, `bindAction`, `defineAction`, `defineEntity`, runtime |
26
+ | `@softize/opus` (core) | — | Protocolo, Actions, entities, Produtos de Dados e runtime |
27
27
  | `@softize/opus/schema` | `/zod`, `/openapi` | Tipos lógicos (`t.*`) e gerador de OpenAPI |
28
28
  | `@softize/opus/server` | `/fastify`, `/node` | Transporte HTTP e montagem de endpoints |
29
29
  | `@softize/opus/client` | `/fetch` | Adapter de cliente para invocar actions |
@@ -95,7 +95,7 @@ registry/ # scaffolds + skills Opus — geração e conhecimento do SDK, não
95
95
  bin/ # CLI (opus create/setup/gen/copy/check/db/seed/pre-push/introspect/mcp) + libs
96
96
  docs/
97
97
  protocol.md # contrato completo (16 seções)
98
- data-layer.md · seeds.md · releasing.md · code-style.md
98
+ data-layer.md · data-products.md · seeds.md · releasing.md · code-style.md
99
99
  ```
100
100
 
101
101
  ## Seeds de projeto
package/bin/lib/check.mjs CHANGED
@@ -105,12 +105,13 @@ const UI_SURFACE_PAIRS = new Map([
105
105
  // locais homônimos.
106
106
  const UI_STRUCTURAL_PARENTS = new Map([
107
107
  ["PageHeader", new Set(["Page"])],
108
+ ["PageIntro", new Set(["Page"])],
108
109
  ["PageBody", new Set(["Page"])],
109
- ["PageTitle", new Set(["PageHeader"])],
110
+ ["PageTitle", new Set(["PageHeader", "PageIntro"])],
110
111
  ["PageBack", new Set(["PageHeader"])],
111
112
  ["PageNavigation", new Set(["PageHeader"])],
112
- ["PageDescription", new Set(["PageHeader"])],
113
- ["PageActions", new Set(["PageHeader"])],
113
+ ["PageDescription", new Set(["PageHeader", "PageIntro"])],
114
+ ["PageActions", new Set(["PageHeader", "PageIntro"])],
114
115
  ["ContentHeader", new Set(["Content"])],
115
116
  ["ContentBody", new Set(["Content"])],
116
117
  ["ContentTitle", new Set(["ContentHeader"])],
@@ -166,6 +167,7 @@ const UI_STRUCTURAL_ROOTS = new Map([
166
167
  "Page",
167
168
  {
168
169
  header: "PageHeader",
170
+ alternateHeader: "PageIntro",
169
171
  body: "PageBody",
170
172
  shorthand: new Set(["title", "description", "actions"]),
171
173
  // O contador saiu do título da página (ADR 0009). Tirar `count` do shorthand não proíbe
@@ -187,6 +189,7 @@ const UI_STRUCTURAL_ROOTS = new Map([
187
189
 
188
190
  const UI_STRICT_DIRECT_COMPONENTS = new Set([
189
191
  "PageHeader",
192
+ "PageIntro",
190
193
  "PageBody",
191
194
  "PageTitle",
192
195
  "PageBack",
@@ -204,6 +207,8 @@ const UI_STRICT_DIRECT_COMPONENTS = new Set([
204
207
  "AlertActions",
205
208
  ]);
206
209
 
210
+ const UI_STRUCTURAL_CONTAINERS = new Set(["PageShell"]);
211
+
207
212
  const UI_STRUCTURAL_HEADERS = new Map([
208
213
  [
209
214
  "PageHeader",
@@ -219,6 +224,14 @@ const UI_STRUCTURAL_HEADERS = new Map([
219
224
  shorthand: new Set(),
220
225
  },
221
226
  ],
227
+ [
228
+ "PageIntro",
229
+ {
230
+ title: "PageTitle",
231
+ optional: new Set(["PageDescription", "PageActions"]),
232
+ shorthand: new Set(),
233
+ },
234
+ ],
222
235
  [
223
236
  "ContentHeader",
224
237
  {
@@ -310,6 +323,54 @@ const UI_LEGACY_VARIANTS = new Map([
310
323
 
311
324
  /** Os três nomes que denotam action/contrato — o arquivo sem nenhum deles é pulado. */
312
325
  export const ACTION_MARKERS = ["defineAction", "defineContract", "bindAction"];
326
+ export const DATA_PRODUCT_MARKERS = ["defineDataProduct", "defineEntity"];
327
+
328
+ /** Extrai Produtos de Dados e entities literais para validar a linhagem sem executar o app. */
329
+ export function parseDataProducts(fileName, sourceText) {
330
+ const sf = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
331
+ const products = [];
332
+ const entities = [];
333
+ const lineOf = (node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
334
+ const prop = (obj, key) => obj.properties.find((item) =>
335
+ ts.isPropertyAssignment(item) && item.name?.getText(sf) === key)?.initializer;
336
+ const stringProp = (obj, key) => {
337
+ const value = prop(obj, key);
338
+ return value && ts.isStringLiteral(value) ? value.text : null;
339
+ };
340
+ const stringList = (obj, key) => {
341
+ const value = prop(obj, key);
342
+ if (!value || !ts.isArrayLiteralExpression(value)) return null;
343
+ if (!value.elements.every(ts.isStringLiteral)) return null;
344
+ return value.elements.map((item) => item.text);
345
+ };
346
+ const isExported = (call) => {
347
+ const declaration = call.parent;
348
+ const statement = ts.isVariableDeclaration(declaration) ? declaration.parent?.parent : undefined;
349
+ return statement !== undefined && ts.isVariableStatement(statement) && (statement.modifiers ?? []).some((item) => item.kind === ts.SyntaxKind.ExportKeyword);
350
+ };
351
+ const visit = (node) => {
352
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.arguments[0] && ts.isObjectLiteralExpression(node.arguments[0])) {
353
+ const obj = node.arguments[0];
354
+ if (node.expression.text === "defineDataProduct") {
355
+ const versionNode = prop(obj, "version");
356
+ products.push({
357
+ id: stringProp(obj, "id"),
358
+ version: versionNode && ts.isNumericLiteral(versionNode) ? Number(versionNode.text) : null,
359
+ interfaces: stringList(obj, "interfaces"),
360
+ entities: stringList(obj, "entities"),
361
+ exported: isExported(node),
362
+ line: lineOf(node),
363
+ });
364
+ } else if (node.expression.text === "defineEntity") {
365
+ const name = stringProp(obj, "name");
366
+ if (name !== null) entities.push(name);
367
+ }
368
+ }
369
+ ts.forEachChild(node, visit);
370
+ };
371
+ visit(sf);
372
+ return { products, entities };
373
+ }
313
374
 
314
375
  /**
315
376
  * Extrai `defineAction`/`defineContract`/`bindAction` de um source. Puro/sintático —
@@ -503,14 +564,21 @@ export function checkProject(sources) {
503
564
  const findings = [];
504
565
  const contractsByIdent = new Map(); // ident → item | 'ambiguous'
505
566
  const binds = []; // { item, file }
567
+ const productDeclarations = [];
568
+ const entityNames = new Set();
569
+ const actionNames = new Set();
506
570
 
507
571
  for (const { file, text } of sources) {
572
+ const declarations = parseDataProducts(file, text);
573
+ for (const entity of declarations.entities) entityNames.add(entity);
574
+ for (const product of declarations.products) productDeclarations.push({ product, file });
508
575
  for (const item of parseActions(file, text)) {
509
576
  if (item.form === "bindAction") {
510
577
  actions += 1;
511
578
  binds.push({ item, file });
512
579
  } else if (item.form === "defineContract") {
513
580
  contracts += 1;
581
+ if (item.name !== null) actionNames.add(item.name);
514
582
  if (item.ident !== null) {
515
583
  contractsByIdent.set(
516
584
  item.ident,
@@ -519,11 +587,45 @@ export function checkProject(sources) {
519
587
  }
520
588
  } else {
521
589
  actions += 1;
590
+ if (item.name !== null) actionNames.add(item.name);
522
591
  }
523
592
  for (const f of lintAction(item)) findings.push({ ...f, file });
524
593
  }
525
594
  }
526
595
 
596
+ const productIds = new Set();
597
+ for (const { product, file } of productDeclarations) {
598
+ const finding = (rule, message) => findings.push({
599
+ rule, level: "error", action: product.id ?? "(Produto de Dados sem id)",
600
+ line: product.line, file, message,
601
+ });
602
+ if (!product.exported) finding("data-product-export", "defineDataProduct precisa ser `export const` para compor o domínio e o catálogo.");
603
+ if (product.id === null || !ACTION_NAME_RE.test(product.id)) {
604
+ finding("data-product-id", "Produto de Dados precisa de `id` namespaced literal, como `sales.leads`.");
605
+ } else if (productIds.has(product.id)) {
606
+ finding("data-product-duplicate", `Produto de Dados "${product.id}" está declarado mais de uma vez.`);
607
+ } else {
608
+ productIds.add(product.id);
609
+ }
610
+ if (product.version === null || !Number.isInteger(product.version) || product.version < 1) {
611
+ finding("data-product-version", "Produto de Dados precisa de `version` literal inteira e positiva.");
612
+ }
613
+ if (product.interfaces === null || product.interfaces.length === 0) {
614
+ finding("data-product-interfaces", "Produto de Dados precisa declarar ao menos uma Action literal em `interfaces`.");
615
+ } else {
616
+ for (const action of product.interfaces) {
617
+ if (!actionNames.has(action)) finding("data-product-interface", `A interface "${action}" não corresponde a uma Action ou contrato declarado no projeto.`);
618
+ }
619
+ }
620
+ if (product.entities === null) {
621
+ finding("data-product-entities", "Produto de Dados precisa declarar `entities` como lista literal.");
622
+ } else {
623
+ for (const entity of product.entities) {
624
+ if (!entityNames.has(entity)) finding("data-product-entity", `A Entity "${entity}" não foi declarada no projeto.`);
625
+ }
626
+ }
627
+ }
628
+
527
629
  for (const { item, file } of binds) {
528
630
  const contract = contractsByIdent.get(item.contractRef);
529
631
  if (contract === undefined || contract === "ambiguous") continue;
@@ -542,7 +644,7 @@ export function checkProject(sources) {
542
644
  }
543
645
  }
544
646
 
545
- return { actions, contracts, findings };
647
+ return { actions, contracts, dataProducts: productDeclarations.length, findings };
546
648
  }
547
649
 
548
650
  /** Migrações de UI que falhariam apenas visualmente. Linhas que são só comentário não
@@ -793,6 +895,7 @@ function structuralJsxAncestorName(node, imports) {
793
895
  if (
794
896
  imported !== undefined &&
795
897
  (UI_STRUCTURAL_ROOTS.has(imported) ||
898
+ UI_STRUCTURAL_CONTAINERS.has(imported) ||
796
899
  UI_STRUCTURAL_PARENTS.has(imported) ||
797
900
  [...UI_STRUCTURAL_PARENTS.values()].some((parents) =>
798
901
  parents.has(imported),
@@ -805,6 +908,18 @@ function structuralJsxAncestorName(node, imports) {
805
908
  return null;
806
909
  }
807
910
 
911
+ function hasJsxAncestor(node, imports, component) {
912
+ let current = node.parent;
913
+ while (current !== undefined) {
914
+ if (ts.isJsxElement(current) || ts.isJsxSelfClosingElement(current)) {
915
+ const local = jsxLocalName(current);
916
+ if (local !== null && imports.get(local) === component) return true;
917
+ }
918
+ current = current.parent;
919
+ }
920
+ return false;
921
+ }
922
+
808
923
  function directStructuralChildren(node, imports) {
809
924
  if (!ts.isJsxElement(node)) return { names: [], hasOpaque: false };
810
925
  const names = [];
@@ -966,8 +1081,15 @@ export function checkUiStructure(file, text) {
966
1081
  attributes.has(attribute),
967
1082
  );
968
1083
  const children = directStructuralChildren(node, imports);
1084
+ const insidePageShell =
1085
+ component === "Page" && hasJsxAncestor(node, imports, "PageShell");
1086
+ const acceptedHeaders = (
1087
+ insidePageShell
1088
+ ? [root.alternateHeader]
1089
+ : [root.header, root.alternateHeader]
1090
+ ).filter(Boolean);
969
1091
  const structural =
970
- children.names.includes(root.header) ||
1092
+ acceptedHeaders.some((header) => children.names.includes(header)) ||
971
1093
  children.names.includes(root.body);
972
1094
  // A prop removida saiu do `shorthand`, então sozinha ela não marca o nó como forma curta:
973
1095
  // ele cai no ramo explícito, que cobra header e body — uma segunda mensagem inteiramente
@@ -981,18 +1103,18 @@ export function checkUiStructure(file, text) {
981
1103
  add(
982
1104
  node,
983
1105
  component,
984
- `${component} não permite misturar propriedades de shorthand com ${root.header}/${root.body}.`,
1106
+ `${component} não permite misturar propriedades de shorthand com ${acceptedHeaders.join("/")}/${root.body}.`,
985
1107
  "ui-structure-mode",
986
1108
  );
987
1109
  } else if (!shorthand) {
988
- const headers = children.names.filter(
989
- (name) => name === root.header,
1110
+ const headers = children.names.filter((name) =>
1111
+ acceptedHeaders.includes(name),
990
1112
  ).length;
991
1113
  const bodies = children.names.filter(
992
1114
  (name) => name === root.body,
993
1115
  ).length;
994
1116
  const unexpected = children.names.some(
995
- (name) => name !== root.header && name !== root.body,
1117
+ (name) => !acceptedHeaders.includes(name) && name !== root.body,
996
1118
  );
997
1119
  if (
998
1120
  headers !== 1 ||
@@ -1003,7 +1125,7 @@ export function checkUiStructure(file, text) {
1003
1125
  add(
1004
1126
  node,
1005
1127
  component,
1006
- `${component} explícito exige exatamente um ${root.header} e um ${root.body} como filhos diretos.`,
1128
+ `${component} explícito exige exatamente um ${acceptedHeaders.join(" ou ")} e um ${root.body} como filhos diretos.`,
1007
1129
  );
1008
1130
  }
1009
1131
  }
@@ -1375,7 +1497,7 @@ export async function scanDir(rootDir) {
1375
1497
  const sources = [];
1376
1498
  for (const file of files) {
1377
1499
  const text = readProjectFile(rootDir, path.relative(rootDir, file)).content;
1378
- if (!ACTION_MARKERS.some((m) => text.includes(m))) continue;
1500
+ if (![...ACTION_MARKERS, ...DATA_PRODUCT_MARKERS].some((m) => text.includes(m))) continue;
1379
1501
  sources.push({ file: path.relative(rootDir, file), text });
1380
1502
  }
1381
1503
  const checked = checkProject(sources);
@@ -1391,6 +1513,7 @@ export async function scanDir(rootDir) {
1391
1513
  files: files.length,
1392
1514
  actions: checked.actions,
1393
1515
  contracts: checked.contracts,
1516
+ dataProducts: checked.dataProducts,
1394
1517
  findings: [...checked.findings, ...uiFindings],
1395
1518
  hasMarker,
1396
1519
  };
@@ -27,6 +27,7 @@ function shapeDomain(d) {
27
27
  repository: d.hasRepository,
28
28
  service: d.hasService,
29
29
  entities: d.entities ?? [],
30
+ dataProducts: d.dataProducts ?? [],
30
31
  actions: d.actions.map(shapeAction),
31
32
  reactions: d.reactions,
32
33
  schedules: d.schedules,
@@ -178,6 +178,7 @@ function serializeDomain(domain, ctx) {
178
178
  hasRepository: domain.repository !== undefined,
179
179
  hasService: domain.service !== undefined,
180
180
  entities: entityList,
181
+ dataProducts: serializeDataProducts(domain.dataProducts),
181
182
  dicts: serializeDicts(domain.dicts),
182
183
  actions: Array.from(iterateActions(domain.actions), (a) =>
183
184
  serializeAction(a, ctx),
@@ -194,6 +195,42 @@ function serializeDomain(domain, ctx) {
194
195
  }
195
196
  }
196
197
 
198
+ function serializeDataProducts(source) {
199
+ if (source === undefined || source === null) return []
200
+ const products = Array.isArray(source)
201
+ ? source
202
+ : typeof source === 'object'
203
+ ? Object.values(source)
204
+ : []
205
+ return products.map((product) => ({
206
+ id: product.id,
207
+ version: product.version,
208
+ label: product.label,
209
+ description: product.description,
210
+ owner: product.owner,
211
+ grain: product.grain,
212
+ classification: product.classification,
213
+ nature: product.nature,
214
+ sources: Array.isArray(product.sources)
215
+ ? product.sources.map((source) => ({
216
+ id: source.id,
217
+ label: source.label,
218
+ description: nullable(source.description),
219
+ }))
220
+ : [],
221
+ entities: Array.isArray(product.entities) ? product.entities : [],
222
+ access: {
223
+ contexts: Array.isArray(product.access?.contexts) ? product.access.contexts : [],
224
+ organizationalScopes: Array.isArray(product.access?.organizationalScopes)
225
+ ? product.access.organizationalScopes
226
+ : [],
227
+ },
228
+ interfaces: Array.isArray(product.interfaces) ? product.interfaces : [],
229
+ status: product.status ?? 'active',
230
+ replacedBy: nullable(product.replacedBy),
231
+ }))
232
+ }
233
+
197
234
  /** Serializa as entidades (`defineEntity`) com campos + docs — a fonte que vai pro
198
235
  * manifest/lente. Ignora valores que não parecem EntityConfig (name+fields). */
199
236
  function serializeEntities(src) {
@@ -28,10 +28,10 @@ function asStringArray(init) {
28
28
  return []
29
29
  }
30
30
 
31
- /** Extrai actions/reactions/schedules de um source. Puro/sintático — testável. */
31
+ /** Extrai actions/reactions/schedules/Produtos de Dados de um source. Puro/sintático — testável. */
32
32
  export function parseStructure(fileName, sourceText) {
33
33
  const sf = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
34
- const out = { actions: [], reactions: [], schedules: [] }
34
+ const out = { actions: [], reactions: [], schedules: [], dataProducts: [] }
35
35
  const line = (n) => sf.getLineAndCharacterOfPosition(n.getStart(sf)).line + 1
36
36
 
37
37
  function visit(node) {
@@ -64,6 +64,17 @@ export function parseStructure(fileName, sourceText) {
64
64
  every: asString(prop(obj, 'every', sf)),
65
65
  line: line(node),
66
66
  })
67
+ } else if (fn === 'defineDataProduct') {
68
+ out.dataProducts.push({
69
+ id: asString(prop(obj, 'id', sf)),
70
+ version: (() => {
71
+ const value = prop(obj, 'version', sf)
72
+ return value && ts.isNumericLiteral(value) ? Number(value.text) : null
73
+ })(),
74
+ entities: asStringArray(prop(obj, 'entities', sf)),
75
+ interfaces: asStringArray(prop(obj, 'interfaces', sf)),
76
+ line: line(node),
77
+ })
67
78
  }
68
79
  }
69
80
  ts.forEachChild(node, visit)
@@ -94,15 +105,16 @@ export function deriveWiring(model) {
94
105
  export async function introspect(rootDir) {
95
106
  rootDir = canonicalProjectDirectory(rootDir)
96
107
  const files = await walkTsFiles(rootDir)
97
- const model = { actions: [], reactions: [], schedules: [] }
108
+ const model = { actions: [], reactions: [], schedules: [], dataProducts: [] }
98
109
  for (const file of files) {
99
110
  const text = readProjectFile(rootDir, path.relative(rootDir, file)).content
100
- if (!/define(Action|Reaction|Schedule)/.test(text)) continue
111
+ if (!/define(Action|Reaction|Schedule|DataProduct)/.test(text)) continue
101
112
  const s = parseStructure(file, text)
102
113
  const rel = path.relative(rootDir, file)
103
114
  for (const a of s.actions) model.actions.push({ ...a, file: rel })
104
115
  for (const r of s.reactions) model.reactions.push({ ...r, file: rel })
105
116
  for (const sc of s.schedules) model.schedules.push({ ...sc, file: rel })
117
+ for (const product of s.dataProducts) model.dataProducts.push({ ...product, file: rel })
106
118
  }
107
119
  return { ...model, wiring: deriveWiring(model) }
108
120
  }
@@ -4,6 +4,9 @@
4
4
  > estados. Ela foi invertida: `PageState` passa a ocultá-lo em `loading`, `error` e `empty`, e a
5
5
  > restaurá-lo em `ready`. A lista de responsabilidades abaixo já descreve o comportamento novo; o
6
6
  > adendo no fim deste arquivo registra o motivo, a alternativa descartada e o que se perde.
7
+ >
8
+ > **Atualização (2026-09-10).** Dentro de `PageShell`, o estado integral oculta somente
9
+ > `PageIntro`. A barra pertence à moldura persistente e continua visível (ADR 0011).
7
10
 
8
11
  ## Contexto
9
12
 
@@ -4,6 +4,10 @@
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.
10
+
7
11
  ## Contexto
8
12
 
9
13
  Aplicações com sidebar passaram a montar uma barra superior separada de `PageHeader` para reunir
@@ -0,0 +1,70 @@
1
+ # ADR 0011 — PageShell coordena o chrome persistente da página
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-09-10.
5
+ - **Revisa:** ADR 0010.
6
+
7
+ ## Contexto
8
+
9
+ A ADR 0010 reuniu título, navegação e ações em `PageHeader`, mas partiu do pressuposto de que uma
10
+ única camada conhece todos esses elementos. Em aplicações com navegação persistente, o shell conhece
11
+ o breadcrumb e a moldura enquanto a rota conhece o título, as ações e o estado do conteúdo.
12
+
13
+ Sem um contrato para essa divisão, consumidores criam uma barra com `PaneHeader`, projetam ações
14
+ manualmente e escondem partes do `PageHeader` com CSS. O resultado parece correto, mas mantém duas
15
+ anatomias concorrentes e faz um estado integral remover também a navegação persistente do shell.
16
+
17
+ ## Decisão
18
+
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.
22
+
23
+ Quando uma `Page` está dentro de `PageShell`, sua forma curta transforma título e descrição em
24
+ `PageIntro`, dentro do conteúdo. `PageActions` continua declarado pela página, mas aparece na barra.
25
+ Na forma explícita, a página usa `PageIntro` e `PageBody`. Fora de `PageShell`, a composição anterior
26
+ com `PageHeader` continua válida; `PageIntro` também pode ser escolhido quando o conteúdo precisa de
27
+ uma introdução sem navegação própria. As duas regiões não são equivalentes: `PageHeader` reúne o
28
+ cabeçalho completo, enquanto `PageIntro` dá mais presença ao título dentro do conteúdo.
29
+
30
+ `PageState` oculta somente `PageIntro`. A barra do `PageShell` permanece visível porque preserva
31
+ navegação e ações globais mesmo quando o conteúdo carrega, falha ou está vazio. Uma ação que depende
32
+ do conteúdo deve se omitir por estado na própria rota.
33
+
34
+ `PageShell` não substitui o shell completo da aplicação, não inclui sidebar e não cria navegação.
35
+ Ele apenas coordena a barra e a área em que uma única `Page` é renderizada.
36
+
37
+ ## Consequências
38
+
39
+ - shell e rota podem continuar conhecendo partes diferentes da página sem criar componentes locais
40
+ de chrome;
41
+ - título e descrição deixam de ser mascarados por seletores globais;
42
+ - ações de página têm um único destino oficial na barra;
43
+ - 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
+ - `PageActionsTarget` continua disponível apenas para workspaces imersivos que não usam
46
+ `PageShell`.
47
+
48
+ ## Alternativas consideradas
49
+
50
+ ### Passar breadcrumb a cada rota
51
+
52
+ Manteria toda a anatomia dentro de `Page`, mas faria cada página receber e repassar contexto que já
53
+ pertence ao shell. Também duplicaria essa infraestrutura em seções com navegação própria.
54
+
55
+ ### Tornar o portal local um padrão da aplicação
56
+
57
+ Resolveria somente o GrandBrasil e deixaria o design system sem contrato para a mesma divisão de
58
+ responsabilidade em outros consumidores.
59
+
60
+ ### Fazer a barra desaparecer em estados integrais
61
+
62
+ Repetiria o comportamento do header embutido, mas removeria navegação persistente por causa do
63
+ estado do conteúdo. A moldura do shell não deve oscilar com a consulta da rota.
64
+
65
+ ## Verificação
66
+
67
+ - testes cobrem a projeção das ações, a permanência da barra e a ocultação de `PageIntro` em estados
68
+ integrais;
69
+ - a documentação demonstra a composição de `PageShell`, `PageIntro` e `Page` sem CSS externo;
70
+ - consumidores removem barras paralelas e `PageActionsTarget` ao adotar o novo contrato.
@@ -0,0 +1,72 @@
1
+ # ADR 0012 — Produtos de Dados são declarações de primeira classe
2
+
3
+ - Status: aceita
4
+ - Data: 2026-09-11
5
+
6
+ ## Contexto e forças
7
+
8
+ Entities descrevem estrutura persistida e Actions descrevem interfaces autorizadas. Nenhuma das
9
+ duas declara, porém, qual conjunto governado existe para responder uma pergunta de negócio. Sem
10
+ esse artefato, relatórios, agentes e catálogos reconstroem a relação entre Fonte, Entity e Action
11
+ por nomes ou tabelas paralelas. A mesma análise pode então receber uma definição diferente a cada
12
+ novo consumidor.
13
+
14
+ Uma view de banco não resolve o problema: ela pode ser uma implementação eficiente, mas não
15
+ carrega por si só identidade pública, responsável, grão, classificação, linhagem, interfaces ou
16
+ ciclo de vida. Também não queremos que metadata descritiva contorne o pipeline de autorização da
17
+ Action.
18
+
19
+ ## Decisão
20
+
21
+ O core expõe `defineDataProduct`. O produto tem identidade namespaced e estável, versão inteira,
22
+ nome e descrição humanas, responsável, grão, classificação, natureza, Fontes, entities, acesso
23
+ descritivo e Actions de interface. Produtos ativos podem ser descontinuados com `status:
24
+ 'deprecated'` e `replacedBy`.
25
+
26
+ `defineDomain({ dataProducts })` registra os produtos. O domínio e `opus check` recusam IDs
27
+ duplicados e relações literais que apontam para Action ou Entity inexistente. A declaração não
28
+ executa consulta, não contém driver e não substitui uma Action.
29
+
30
+ A autorização continua sendo responsabilidade da Action. `access.contexts` e
31
+ `access.organizationalScopes` documentam o alcance esperado para catálogo, Lens e revisão, mas o
32
+ runtime não os converte em autorização implícita. Essa separação impede que uma descrição
33
+ incompleta abra dados.
34
+
35
+ O manifest projeta a declaração integral. Tools de IA recebem a lista de produtos que expõem em
36
+ metadata, e o servidor MCP publica essa lista em `_meta['com.softize.opus/data-products']`. A
37
+ camada que compõe MCPs pode persistir a linhagem das chamadas bem-sucedidas sem conhecer tabelas
38
+ ou inferir produtos pelo nome da tool.
39
+
40
+ ## Alternativas consideradas
41
+
42
+ ### Tratar Entity como Produto de Dados
43
+
44
+ Rejeitada. Uma Entity descreve estrutura e invariantes; um produto pode combinar várias entities,
45
+ ter outro grão e oferecer mais de uma interface.
46
+
47
+ ### Usar view ou tabela consolidada como identidade
48
+
49
+ Rejeitada como contrato. Views continuam válidas como implementação de performance, mas trocar a
50
+ materialização não deve trocar a identidade consumida por relatório, agente ou interface.
51
+
52
+ ### Manter um catálogo manual fora do domínio
53
+
54
+ Rejeitada. O catálogo voltaria a divergir das Actions e entities que realmente existem.
55
+
56
+ ## Consequências
57
+
58
+ Um produto novo exige decisão explícita sobre semântica, acesso e linhagem antes de ser publicado.
59
+ O custo adicional é intencional. Mudança de forma observável incrementa `version`; substituição
60
+ preserva o ID antigo como descontinuado durante a migração dos consumidores.
61
+
62
+ Relatórios e agentes passam a depender da identidade do produto, enquanto cada Action permanece
63
+ a fronteira executável e autorizada. O Opus Lens pode apresentar a cadeia `Fonte/Entity → Produto
64
+ de Dados → Action` a partir do manifest.
65
+
66
+ ## Verificação
67
+
68
+ - testes de construção e registro cobrem formato, duplicidade, ciclo de vida e referências;
69
+ - `opus check` cobre declarações literais, exportação e drift de Action/Entity;
70
+ - o manifest preserva todos os campos;
71
+ - runtime de IA e MCP projetam a identidade do produto por interface;
72
+ - Opus Lens lê a declaração e deriva a linhagem sem heurística.