@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 +33 -0
- package/README.md +3 -3
- package/bin/lib/check.mjs +134 -11
- package/bin/lib/gen-manifest.mjs +1 -0
- package/bin/lib/gen-runner.mjs +37 -0
- package/bin/lib/introspect.mjs +16 -4
- package/docs/adr/0004-page-content-state-is-composed.md +3 -0
- package/docs/adr/0010-page-header-owns-page-chrome.md +4 -0
- package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +70 -0
- package/docs/adr/0012-data-products-are-first-class-declarations.md +72 -0
- package/docs/data-products.md +66 -0
- package/docs/protocol.md +12 -0
- package/package.json +15 -14
- package/registry/skills/build-opus-ui/references/ui-patterns.md +12 -2
- package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +0 -0
- package/src/core/data-product.ts +121 -0
- package/src/core/domain.ts +56 -2
- package/src/core/index.ts +9 -0
- package/src/core/runtime.ts +27 -2
- package/src/core/types.ts +2 -0
- package/src/mcp/index.ts +1 -0
- package/src/ui/components/patterns/page.tsx +200 -18
- package/src/ui/components/patterns/surface-header.tsx +13 -4
- package/src/ui/components/primitives/chat.tsx +58 -14
- package/src/ui/docs/content/chat.md +6 -0
- package/src/ui/docs/content/page.md +99 -26
- package/src/ui/meta.ts +1 -1
- package/src/ui/react.tsx +4 -0
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,
|
|
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(
|
|
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 ${
|
|
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)
|
|
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
|
|
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 ${
|
|
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
|
};
|
package/bin/lib/gen-manifest.mjs
CHANGED
package/bin/lib/gen-runner.mjs
CHANGED
|
@@ -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) {
|
package/bin/lib/introspect.mjs
CHANGED
|
@@ -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.
|