@softize/opus 12.0.1 → 12.1.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 +12 -0
- package/bin/cli.mjs +2 -1
- package/bin/lib/copy.mjs +186 -19
- package/docs/code-style.md +18 -1
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +5 -2
- package/src/ui/docs/content/communication.md +37 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ 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
|
+
## 12.1.0 — 2026-08-22
|
|
11
|
+
|
|
12
|
+
Interfaces que combinam copy fixa com nomes, contagens ou opções vindas da aplicação não
|
|
13
|
+
precisam mais excluir o arquivo inteiro do inventário. `base.json.copy.dynamic` classifica um
|
|
14
|
+
diagnóstico exato como `runtime-data` ou `message-template`, com motivo e referência
|
|
15
|
+
auditáveis. Dados de runtime ficam fora da política editorial; o texto-modelo de mensagens
|
|
16
|
+
interpoladas entra no inventário com seu papel semântico, sem alterar o conteúdo renderizado.
|
|
17
|
+
|
|
18
|
+
Cada declaração precisa corresponder a um único caminho, linha e campo. Quando o código muda
|
|
19
|
+
e o seletor fica órfão ou ambíguo, `opus copy` reprova e orienta a revisão. O inventário expõe
|
|
20
|
+
essas classificações em `dynamicSurfaces`, e o resumo do CLI informa sua quantidade.
|
|
21
|
+
|
|
10
22
|
## 12.0.1 — 2026-08-22
|
|
11
23
|
|
|
12
24
|
`Split` volta a atender layouts que precisam preservar uma medida física e a preferência da
|
package/bin/cli.mjs
CHANGED
|
@@ -254,7 +254,8 @@ async function cmdCopy(dir, flags) {
|
|
|
254
254
|
const coverage = result.inventory.coverage
|
|
255
255
|
const summary =
|
|
256
256
|
`${coverage.analyzedFiles} arquivo(s) analisado(s), ` +
|
|
257
|
-
`${coverage.coveredSources} fonte(s) com copy, ${coverage.extractedEntries} texto(s)`
|
|
257
|
+
`${coverage.coveredSources} fonte(s) com copy, ${coverage.extractedEntries} texto(s), ` +
|
|
258
|
+
`${result.inventory.dynamicSurfaces.length} superfície(s) dinâmica(s) declarada(s)`
|
|
258
259
|
if (flags.check) log('success', `✓ opus copy: ${location} está atualizado (${summary}).`)
|
|
259
260
|
else if (result.changed) log('success', `✓ opus copy: ${location} gerado atomicamente (${summary}).`)
|
|
260
261
|
else log('success', `✓ opus copy: ${location} já estava atualizado (${summary}).`)
|
package/bin/lib/copy.mjs
CHANGED
|
@@ -52,6 +52,13 @@ const IGNORED_FILE = /(?:\.d|\.spec|\.test|\.stories)\.[cm]?[jt]sx?$/u
|
|
|
52
52
|
// O Opus garante somente o namespace e o shape do identificador. O catálogo de regras,
|
|
53
53
|
// inclusive adições futuras, pertence à versão instalada da Base.
|
|
54
54
|
const COPY_RULE_ID = /^COPY\d{3}$/u
|
|
55
|
+
const COPY_DYNAMIC_KINDS = new Set(['message-template', 'runtime-data'])
|
|
56
|
+
const COPY_ROLES = new Set([
|
|
57
|
+
'badge', 'breadcrumb', 'button', 'description', 'dialog-body', 'empty-state', 'error',
|
|
58
|
+
'heading', 'helper-text', 'kicker', 'label', 'menu-item', 'message', 'notification',
|
|
59
|
+
'placeholder', 'success', 'tab', 'title', 'warning',
|
|
60
|
+
])
|
|
61
|
+
const AUDIT_REFERENCE = /^(?:https?:\/\/\S+|[a-z][a-z0-9-]*:\S+|(?:[A-Za-z0-9._-]+\/)+[A-Za-z0-9._#/-]+|[A-Za-z0-9._-]+\.(?:md|json|ya?ml)(?:#[^\s]+)?)$/u
|
|
55
62
|
const CONTRACT_MODULES = new Set(['@softize/opus', '@softize/opus/core'])
|
|
56
63
|
const CONTRACT_FACTORIES = new Set(['defineAction', 'defineContract'])
|
|
57
64
|
const ACTION_BINDING_KEYS = new Set(['authorize', 'background', 'emits', 'handler', 'idempotency', 'loads'])
|
|
@@ -839,10 +846,12 @@ function lineOf(sourceFile, node) {
|
|
|
839
846
|
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1
|
|
840
847
|
}
|
|
841
848
|
|
|
842
|
-
function diagnostic(file, sourceFile, node, field) {
|
|
849
|
+
function diagnostic(file, sourceFile, node, field, category = 'content') {
|
|
843
850
|
return {
|
|
844
851
|
source: file,
|
|
845
852
|
line: lineOf(sourceFile, node),
|
|
853
|
+
field,
|
|
854
|
+
category,
|
|
846
855
|
message:
|
|
847
856
|
`não foi possível extrair \`${field}\` estaticamente; use texto literal, concatenação ` +
|
|
848
857
|
'de literais, constante local ou I18nRef com `default` literal.',
|
|
@@ -929,13 +938,13 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
929
938
|
const optionItems = (object, prefix) => {
|
|
930
939
|
const kind = property(object, 'kind', sourceFile, checker)
|
|
931
940
|
if (kind.opaque !== undefined) {
|
|
932
|
-
diagnostics.push(diagnostic(file, sourceFile, kind.opaque, `${prefix}kind
|
|
941
|
+
diagnostics.push(diagnostic(file, sourceFile, kind.opaque, `${prefix}kind`, 'structure'))
|
|
933
942
|
return
|
|
934
943
|
}
|
|
935
944
|
if (kind.candidate === undefined) return
|
|
936
945
|
const kindText = staticText(propertyValue(kind.candidate), checker)?.text
|
|
937
946
|
if (kindText === undefined) {
|
|
938
|
-
diagnostics.push(diagnostic(file, sourceFile, propertyValue(kind.candidate), `${prefix}kind
|
|
947
|
+
diagnostics.push(diagnostic(file, sourceFile, propertyValue(kind.candidate), `${prefix}kind`, 'structure'))
|
|
939
948
|
return
|
|
940
949
|
}
|
|
941
950
|
if (kindText !== 'static') return
|
|
@@ -1075,7 +1084,7 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
1075
1084
|
const key = `${node.pos}:${node.end}:${field}`
|
|
1076
1085
|
if (transformDiagnostics.has(key)) return
|
|
1077
1086
|
transformDiagnostics.add(key)
|
|
1078
|
-
diagnostics.push(diagnostic(file, sourceFile, node, field))
|
|
1087
|
+
diagnostics.push(diagnostic(file, sourceFile, node, field, 'transform'))
|
|
1079
1088
|
}
|
|
1080
1089
|
|
|
1081
1090
|
function tailwindTransform(token) {
|
|
@@ -1502,13 +1511,13 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
1502
1511
|
if (component !== 'Select') return null
|
|
1503
1512
|
const native = attribute(opening.attributes, 'native')
|
|
1504
1513
|
if (native.opaque !== undefined) {
|
|
1505
|
-
diagnostics.push(diagnostic(file, sourceFile, native.opaque, '<Select>.native'))
|
|
1514
|
+
diagnostics.push(diagnostic(file, sourceFile, native.opaque, '<Select>.native', 'structure'))
|
|
1506
1515
|
return 'opaque'
|
|
1507
1516
|
}
|
|
1508
1517
|
if (native.candidate === undefined) return 'custom'
|
|
1509
1518
|
const value = attributeBoolean(native)
|
|
1510
1519
|
if (value === null) {
|
|
1511
|
-
diagnostics.push(diagnostic(file, sourceFile, attributeValue(native) ?? opening, '<Select>.native'))
|
|
1520
|
+
diagnostics.push(diagnostic(file, sourceFile, attributeValue(native) ?? opening, '<Select>.native', 'structure'))
|
|
1512
1521
|
return 'opaque'
|
|
1513
1522
|
}
|
|
1514
1523
|
return value ? 'native' : 'custom'
|
|
@@ -1547,7 +1556,15 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
1547
1556
|
if (slot.opaque !== undefined) diagnostics.push(diagnostic(file, sourceFile, slot.opaque, `<${component}>.${name}`))
|
|
1548
1557
|
else if (slot.candidate !== undefined) {
|
|
1549
1558
|
const raw = attributeValue(slot)
|
|
1550
|
-
if (raw === null || !noRenderedValue(raw))
|
|
1559
|
+
if (raw === null || !noRenderedValue(raw)) {
|
|
1560
|
+
diagnostics.push(diagnostic(
|
|
1561
|
+
file,
|
|
1562
|
+
sourceFile,
|
|
1563
|
+
raw ?? opening,
|
|
1564
|
+
`<${component}>.${name}`,
|
|
1565
|
+
raw === null ? 'missing-content' : 'content',
|
|
1566
|
+
))
|
|
1567
|
+
}
|
|
1551
1568
|
}
|
|
1552
1569
|
}
|
|
1553
1570
|
|
|
@@ -1594,8 +1611,16 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
1594
1611
|
}
|
|
1595
1612
|
}
|
|
1596
1613
|
} else {
|
|
1597
|
-
const value = attributeText(item)
|
|
1598
|
-
if (value === null)
|
|
1614
|
+
const value = raw === null ? null : attributeText(item)
|
|
1615
|
+
if (value === null) {
|
|
1616
|
+
diagnostics.push(diagnostic(
|
|
1617
|
+
file,
|
|
1618
|
+
sourceFile,
|
|
1619
|
+
raw ?? opening,
|
|
1620
|
+
`<${component}>.${name}`,
|
|
1621
|
+
raw === null ? 'missing-content' : 'content',
|
|
1622
|
+
))
|
|
1623
|
+
}
|
|
1599
1624
|
else {
|
|
1600
1625
|
const entry = { source: file, line: lineOf(sourceFile, value.node), role, text: value.text }
|
|
1601
1626
|
if (transformed.uppercase) entry.transform = 'uppercase'
|
|
@@ -1613,7 +1638,7 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
1613
1638
|
inheritedFor('className', 'style'),
|
|
1614
1639
|
)
|
|
1615
1640
|
if (transformed.uppercase) {
|
|
1616
|
-
diagnostics.push(diagnostic(file, sourceFile, opening, '<ActionTrigger>.label herdado do contrato sob uppercase'))
|
|
1641
|
+
diagnostics.push(diagnostic(file, sourceFile, opening, '<ActionTrigger>.label herdado do contrato sob uppercase', 'transform'))
|
|
1617
1642
|
}
|
|
1618
1643
|
}
|
|
1619
1644
|
}
|
|
@@ -1625,7 +1650,15 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
1625
1650
|
const raw = attributeValue(item)
|
|
1626
1651
|
const object = raw === null ? null : resolveBinding(raw, checker)
|
|
1627
1652
|
if (object?.kind === ts.SyntaxKind.NullKeyword || (ts.isIdentifier(object) && object.text === 'undefined')) continue
|
|
1628
|
-
if (!ts.isObjectLiteralExpression(object))
|
|
1653
|
+
if (!ts.isObjectLiteralExpression(object)) {
|
|
1654
|
+
diagnostics.push(diagnostic(
|
|
1655
|
+
file,
|
|
1656
|
+
sourceFile,
|
|
1657
|
+
raw ?? opening,
|
|
1658
|
+
`<${component}>.${name}`,
|
|
1659
|
+
raw === null ? 'structure' : 'content',
|
|
1660
|
+
))
|
|
1661
|
+
}
|
|
1629
1662
|
else for (const [nestedName, role] of nestedRoles) addProperty(object, nestedName, role, `<${component}>.${name}.`)
|
|
1630
1663
|
}
|
|
1631
1664
|
}
|
|
@@ -1636,7 +1669,15 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
1636
1669
|
else if (item.candidate !== undefined) {
|
|
1637
1670
|
const raw = attributeValue(item)
|
|
1638
1671
|
const listed = raw === null ? { values: [], opaque: [opening] } : objects(raw, checker)
|
|
1639
|
-
for (const opaque of listed.opaque)
|
|
1672
|
+
for (const opaque of listed.opaque) {
|
|
1673
|
+
diagnostics.push(diagnostic(
|
|
1674
|
+
file,
|
|
1675
|
+
sourceFile,
|
|
1676
|
+
opaque,
|
|
1677
|
+
`<${component}>.${name}`,
|
|
1678
|
+
raw === null ? 'structure' : 'content',
|
|
1679
|
+
))
|
|
1680
|
+
}
|
|
1640
1681
|
for (const [index, option] of listed.values.entries()) {
|
|
1641
1682
|
for (const [optionName, role] of optionRoles) {
|
|
1642
1683
|
if (mode === 'native' && !['label', 'group'].includes(optionName)) continue
|
|
@@ -1668,8 +1709,17 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
1668
1709
|
if (ariaLabelAttribute.opaque !== undefined) {
|
|
1669
1710
|
diagnostics.push(diagnostic(file, sourceFile, ariaLabelAttribute.opaque, `<${component}>.aria-label`))
|
|
1670
1711
|
} else if (ariaLabelAttribute.candidate !== undefined) {
|
|
1671
|
-
const
|
|
1672
|
-
|
|
1712
|
+
const raw = attributeValue(ariaLabelAttribute)
|
|
1713
|
+
const ariaLabel = raw === null ? null : attributeText(ariaLabelAttribute)
|
|
1714
|
+
if (ariaLabel === null) {
|
|
1715
|
+
diagnostics.push(diagnostic(
|
|
1716
|
+
file,
|
|
1717
|
+
sourceFile,
|
|
1718
|
+
raw ?? opening,
|
|
1719
|
+
`<${component}>.aria-label`,
|
|
1720
|
+
raw === null ? 'missing-content' : 'content',
|
|
1721
|
+
))
|
|
1722
|
+
}
|
|
1673
1723
|
if (ariaLabel !== null) {
|
|
1674
1724
|
hasStaticAriaLabel = true
|
|
1675
1725
|
entries.push({ source: file, line: lineOf(sourceFile, ariaLabel.node), role: 'label', text: ariaLabel.text })
|
|
@@ -1692,7 +1742,7 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
1692
1742
|
if (children.opaque !== undefined) return { kind: 'opaque', node: children.opaque }
|
|
1693
1743
|
if (children.candidate === undefined) return { kind: 'decorative' }
|
|
1694
1744
|
const raw = attributeValue(children)
|
|
1695
|
-
return raw === null ? { kind: '
|
|
1745
|
+
return raw === null ? { kind: 'decorative' } : childExpression(raw, inheritedUppercase)
|
|
1696
1746
|
}
|
|
1697
1747
|
const inheritedUppercase = component === 'ActionFormDialog'
|
|
1698
1748
|
? false
|
|
@@ -1710,7 +1760,9 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
1710
1760
|
}
|
|
1711
1761
|
if (value.kind === 'opaque') diagnostics.push(diagnostic(file, sourceFile, value.node, `<${component}> children`))
|
|
1712
1762
|
else if (value.kind === 'decorative') {
|
|
1713
|
-
if (!hasStaticAriaLabel && !OPTIONAL_CHILD_COMPONENTS.has(component))
|
|
1763
|
+
if (!hasStaticAriaLabel && !OPTIONAL_CHILD_COMPONENTS.has(component)) {
|
|
1764
|
+
diagnostics.push(diagnostic(file, sourceFile, element, `<${component}> children`, 'missing-content'))
|
|
1765
|
+
}
|
|
1714
1766
|
} else {
|
|
1715
1767
|
const texts = value.kind === 'text' ? [value] : value.values
|
|
1716
1768
|
for (const text of texts) {
|
|
@@ -1727,11 +1779,11 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
1727
1779
|
if (ts.isCallExpression(node) && factory !== null && node.arguments.length > 0) {
|
|
1728
1780
|
hasContract = true
|
|
1729
1781
|
if (!stableFactoryResult(node, checker, new Set())) {
|
|
1730
|
-
diagnostics.push(diagnostic(file, sourceFile, node, `${factory}(...) result
|
|
1782
|
+
diagnostics.push(diagnostic(file, sourceFile, node, `${factory}(...) result`, 'structure'))
|
|
1731
1783
|
} else {
|
|
1732
1784
|
const object = resolveBinding(node.arguments[0], checker)
|
|
1733
1785
|
if (ts.isObjectLiteralExpression(object)) extractContract(object)
|
|
1734
|
-
else diagnostics.push(diagnostic(file, sourceFile, node.arguments[0], `${factory}(...)
|
|
1786
|
+
else diagnostics.push(diagnostic(file, sourceFile, node.arguments[0], `${factory}(...)`, 'structure'))
|
|
1735
1787
|
}
|
|
1736
1788
|
}
|
|
1737
1789
|
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) extractJsx(node)
|
|
@@ -1834,6 +1886,7 @@ function copyConfiguration(root) {
|
|
|
1834
1886
|
inventoryPath,
|
|
1835
1887
|
exemptions: [],
|
|
1836
1888
|
transforms: [],
|
|
1889
|
+
dynamic: [],
|
|
1837
1890
|
exclude: [],
|
|
1838
1891
|
}
|
|
1839
1892
|
}
|
|
@@ -1854,6 +1907,7 @@ function copyConfiguration(root) {
|
|
|
1854
1907
|
inventoryPath: target,
|
|
1855
1908
|
exemptions: config?.exemptions ?? [],
|
|
1856
1909
|
transforms: config?.transforms ?? [],
|
|
1910
|
+
dynamic: config?.dynamic ?? [],
|
|
1857
1911
|
exclude: config?.exclude ?? [],
|
|
1858
1912
|
}
|
|
1859
1913
|
}
|
|
@@ -1914,7 +1968,118 @@ function findEntry(entries, selector) {
|
|
|
1914
1968
|
)
|
|
1915
1969
|
}
|
|
1916
1970
|
|
|
1971
|
+
function canonicalSource(value) {
|
|
1972
|
+
return (
|
|
1973
|
+
typeof value === 'string' &&
|
|
1974
|
+
value !== '' &&
|
|
1975
|
+
value.trim() === value &&
|
|
1976
|
+
value === value.normalize('NFC') &&
|
|
1977
|
+
!value.includes('\\') &&
|
|
1978
|
+
!path.posix.isAbsolute(value) &&
|
|
1979
|
+
!path.win32.isAbsolute(value) &&
|
|
1980
|
+
!/^[A-Za-z]:/u.test(value) &&
|
|
1981
|
+
value !== '.' &&
|
|
1982
|
+
value !== '..' &&
|
|
1983
|
+
path.posix.normalize(value) === value &&
|
|
1984
|
+
value.split('/').every((part) => part !== '' && part !== '.' && part !== '..')
|
|
1985
|
+
)
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
function applyDynamicMetadata(config, entries, diagnostics) {
|
|
1989
|
+
const surfaces = []
|
|
1990
|
+
if (!Array.isArray(config.dynamic)) {
|
|
1991
|
+
diagnostics.push(selectorDiagnostic('copy.dynamic deve ser uma lista.'))
|
|
1992
|
+
return surfaces
|
|
1993
|
+
}
|
|
1994
|
+
const consumed = new Set()
|
|
1995
|
+
for (const [index, declaration] of config.dynamic.entries()) {
|
|
1996
|
+
const template = declaration?.kind === 'message-template'
|
|
1997
|
+
const allowedFields = new Set([
|
|
1998
|
+
'source', 'line', 'field', 'kind', 'reason', 'reference',
|
|
1999
|
+
...(template ? ['role', 'text'] : []),
|
|
2000
|
+
])
|
|
2001
|
+
const normalizedReason = typeof declaration?.reason === 'string' ? declaration.reason.trim() : ''
|
|
2002
|
+
const normalizedReference = typeof declaration?.reference === 'string' ? declaration.reference.trim() : ''
|
|
2003
|
+
if (
|
|
2004
|
+
declaration === null ||
|
|
2005
|
+
typeof declaration !== 'object' ||
|
|
2006
|
+
Array.isArray(declaration) ||
|
|
2007
|
+
Object.keys(declaration).some((field) => !allowedFields.has(field)) ||
|
|
2008
|
+
!canonicalSource(declaration.source) ||
|
|
2009
|
+
!Number.isInteger(declaration.line) ||
|
|
2010
|
+
declaration.line < 1 ||
|
|
2011
|
+
typeof declaration.field !== 'string' ||
|
|
2012
|
+
declaration.field.trim() === '' ||
|
|
2013
|
+
!COPY_DYNAMIC_KINDS.has(declaration.kind) ||
|
|
2014
|
+
typeof declaration.reason !== 'string' ||
|
|
2015
|
+
normalizedReason.length < 12 ||
|
|
2016
|
+
normalizedReason.length > 500 ||
|
|
2017
|
+
/[\r\n]/u.test(declaration.reason) ||
|
|
2018
|
+
typeof declaration.reference !== 'string' ||
|
|
2019
|
+
normalizedReference.length < 8 ||
|
|
2020
|
+
normalizedReference.length > 500 ||
|
|
2021
|
+
!AUDIT_REFERENCE.test(normalizedReference) ||
|
|
2022
|
+
(template && (
|
|
2023
|
+
typeof declaration.text !== 'string' ||
|
|
2024
|
+
declaration.text.trim() === '' ||
|
|
2025
|
+
declaration.text !== declaration.text.trim() ||
|
|
2026
|
+
/[\r\n]/u.test(declaration.text) ||
|
|
2027
|
+
!COPY_ROLES.has(declaration.role)
|
|
2028
|
+
)) ||
|
|
2029
|
+
(!template && (declaration.text !== undefined || declaration.role !== undefined))
|
|
2030
|
+
) {
|
|
2031
|
+
diagnostics.push(selectorDiagnostic(
|
|
2032
|
+
`copy.dynamic[${index}] exige seletor, kind, reason e reference válidos` +
|
|
2033
|
+
(template ? ', além de text e role para message-template.' : '.'),
|
|
2034
|
+
))
|
|
2035
|
+
continue
|
|
2036
|
+
}
|
|
2037
|
+
const matches = diagnostics
|
|
2038
|
+
.map((item, diagnosticIndex) => ({ item, diagnosticIndex }))
|
|
2039
|
+
.filter(({ item, diagnosticIndex }) =>
|
|
2040
|
+
!consumed.has(diagnosticIndex) &&
|
|
2041
|
+
item.category === 'content' &&
|
|
2042
|
+
item.source === declaration.source &&
|
|
2043
|
+
item.line === declaration.line &&
|
|
2044
|
+
item.field === declaration.field,
|
|
2045
|
+
)
|
|
2046
|
+
if (matches.length !== 1) {
|
|
2047
|
+
diagnostics.push(selectorDiagnostic(
|
|
2048
|
+
`copy.dynamic[${index}] deve selecionar exatamente um diagnóstico de extração; encontrou ${matches.length}.`,
|
|
2049
|
+
))
|
|
2050
|
+
continue
|
|
2051
|
+
}
|
|
2052
|
+
consumed.add(matches[0].diagnosticIndex)
|
|
2053
|
+
const surface = {
|
|
2054
|
+
source: declaration.source,
|
|
2055
|
+
line: declaration.line,
|
|
2056
|
+
field: declaration.field,
|
|
2057
|
+
kind: declaration.kind,
|
|
2058
|
+
reason: normalizedReason,
|
|
2059
|
+
reference: normalizedReference,
|
|
2060
|
+
}
|
|
2061
|
+
if (template) {
|
|
2062
|
+
surface.role = declaration.role
|
|
2063
|
+
surface.text = declaration.text
|
|
2064
|
+
entries.push({
|
|
2065
|
+
source: declaration.source,
|
|
2066
|
+
line: declaration.line,
|
|
2067
|
+
role: declaration.role,
|
|
2068
|
+
text: declaration.text,
|
|
2069
|
+
dynamic: { kind: declaration.kind, field: declaration.field, reference: normalizedReference },
|
|
2070
|
+
})
|
|
2071
|
+
}
|
|
2072
|
+
surfaces.push(surface)
|
|
2073
|
+
}
|
|
2074
|
+
if (consumed.size > 0) {
|
|
2075
|
+
const remaining = diagnostics.filter((_, index) => !consumed.has(index))
|
|
2076
|
+
diagnostics.splice(0, diagnostics.length, ...remaining)
|
|
2077
|
+
}
|
|
2078
|
+
return surfaces
|
|
2079
|
+
}
|
|
2080
|
+
|
|
1917
2081
|
function applyProjectMetadata(config, entries, diagnostics) {
|
|
2082
|
+
const dynamicSurfaces = applyDynamicMetadata(config, entries, diagnostics)
|
|
1918
2083
|
if (!Array.isArray(config.exemptions)) {
|
|
1919
2084
|
diagnostics.push(selectorDiagnostic('copy.exemptions deve ser uma lista.'))
|
|
1920
2085
|
} else {
|
|
@@ -1993,6 +2158,7 @@ function applyProjectMetadata(config, entries, diagnostics) {
|
|
|
1993
2158
|
entries[entryIndex].transform = transform.transform
|
|
1994
2159
|
}
|
|
1995
2160
|
}
|
|
2161
|
+
return dynamicSurfaces
|
|
1996
2162
|
}
|
|
1997
2163
|
|
|
1998
2164
|
export function extractCopyInventory(root) {
|
|
@@ -2050,7 +2216,7 @@ export function extractCopyInventory(root) {
|
|
|
2050
2216
|
'nenhum arquivo JavaScript/TypeScript elegível foi analisado; o inventário v2 exige ao menos uma fonte rastreável.',
|
|
2051
2217
|
))
|
|
2052
2218
|
}
|
|
2053
|
-
applyProjectMetadata(config, entries, diagnostics)
|
|
2219
|
+
const dynamicSurfaces = applyProjectMetadata(config, entries, diagnostics)
|
|
2054
2220
|
|
|
2055
2221
|
const coveredSources = new Set(entries.map((entry) => entry.source)).size
|
|
2056
2222
|
|
|
@@ -2066,6 +2232,7 @@ export function extractCopyInventory(root) {
|
|
|
2066
2232
|
},
|
|
2067
2233
|
sources,
|
|
2068
2234
|
entries,
|
|
2235
|
+
dynamicSurfaces,
|
|
2069
2236
|
},
|
|
2070
2237
|
diagnostics,
|
|
2071
2238
|
}
|
package/docs/code-style.md
CHANGED
|
@@ -131,6 +131,23 @@ autoral da interface. Arrays, objetos ou discriminadores dinâmicos falham visiv
|
|
|
131
131
|
inequívoca; entidades JSX são decodificadas antes do inventário. Fragmentos cuja concatenação
|
|
132
132
|
exigiria adivinhar espaços continuam reprovando.
|
|
133
133
|
|
|
134
|
+
Quando o diagnóstico corresponde a dados reais da aplicação ou a uma mensagem interpolada,
|
|
135
|
+
`copy.dynamic` permite classificar somente aquela superfície sem excluir o arquivo. Cada item
|
|
136
|
+
seleciona exatamente um diagnóstico por `source`, `line` e `field`, registra `reason` e
|
|
137
|
+
`reference` auditáveis e usa um destes contratos:
|
|
138
|
+
|
|
139
|
+
- `runtime-data` reconhece nomes, contagens, status ou opções fornecidos em runtime. Nenhuma
|
|
140
|
+
entrada editorial é fabricada;
|
|
141
|
+
- `message-template` exige `role` e `text`. O texto-modelo entra no inventário e passa pela
|
|
142
|
+
política da Base, mas não substitui nem altera o conteúdo renderizado.
|
|
143
|
+
|
|
144
|
+
As declarações aparecem em `dynamicSurfaces` no inventário. Se o source mudar e o seletor não
|
|
145
|
+
encontrar exatamente um diagnóstico, `opus copy` falha. Assim, a classificação permanece
|
|
146
|
+
estreita e revisável, sem transformar metadata em uma exclusão silenciosa. O mecanismo aceita
|
|
147
|
+
somente diagnósticos de conteúdo; incerteza estrutural, `className`, `style`, transformação ou
|
|
148
|
+
modo de renderização continua bloqueante. Superfície vazia também não pode ser reclassificada
|
|
149
|
+
como dado de runtime.
|
|
150
|
+
|
|
134
151
|
Um `opus copy --check` verde **não significa que toda a copy do produto foi coberta**. Ficam
|
|
135
152
|
fora, deliberadamente:
|
|
136
153
|
|
|
@@ -138,7 +155,7 @@ fora, deliberadamente:
|
|
|
138
155
|
seu papel semântico;
|
|
139
156
|
- texto vindo de API, banco, catálogo i18n importado ou variável de runtime em superfícies
|
|
140
157
|
que o Opus não classifica. Quando esse conteúdo ocupa uma prop ou filho mapeado, a extração
|
|
141
|
-
falha e pede
|
|
158
|
+
falha e pede copy estática ou uma declaração estreita em `copy.dynamic`;
|
|
142
159
|
- props ou componentes de terceiros sem contrato Opus;
|
|
143
160
|
- copy escondida em CSS ou pseudo-elemento. Para uma transformação conhecida que o AST não
|
|
144
161
|
enxerga globalmente, use `copy.transforms`; em componente Opus mapeado, `className` opaco
|
package/package.json
CHANGED
|
@@ -31,8 +31,11 @@ existentes, mantendo navegação observável e componentes reutilizáveis sem la
|
|
|
31
31
|
|
|
32
32
|
Rodar `opus check`, testes focados, typecheck e build da superfície. Se a interface alterou
|
|
33
33
|
copy em contrato ou componente Opus mapeado, regenerar com `opus copy` e executar
|
|
34
|
-
`base copy check`.
|
|
35
|
-
|
|
34
|
+
`base copy check`. Quando uma superfície mapeada recebe dados da aplicação, usar
|
|
35
|
+
`copy.dynamic` com seletor exato, motivo e referência: `runtime-data` para valores sem autoria
|
|
36
|
+
editorial; `message-template` com `role` e texto-modelo para mensagens interpoladas. Não excluir
|
|
37
|
+
o arquivo inteiro nem contornar o diagnóstico com `aria-label` paralelo. Revisar cada declaração
|
|
38
|
+
dinâmica como fronteira de cobertura, mesmo com os gates verdes.
|
|
36
39
|
Inspecionar visualmente a rota real e validar navegação por URL quando aplicável.
|
|
37
40
|
|
|
38
41
|
## Limites
|
|
@@ -60,6 +60,43 @@ visual que o protocolo não representa com segurança.
|
|
|
60
60
|
`ActionTrigger.itemLabel` é conteúdo de domínio em runtime (o nome do registro alvo), não
|
|
61
61
|
é copy editorial estável; `label` e `confirm.*` são as superfícies editoriais cobertas.
|
|
62
62
|
|
|
63
|
+
Quando uma prop mapeada recebe dados da aplicação, declare a superfície em
|
|
64
|
+
`base.json.copy.dynamic` em vez de excluir o arquivo inteiro. O seletor usa o caminho, a
|
|
65
|
+
linha e o campo exibido pelo diagnóstico. `runtime-data` registra valores como nomes,
|
|
66
|
+
contagens e opções vindas de API sem transformá-los em copy editorial. `message-template`
|
|
67
|
+
acrescenta ao inventário um texto-modelo com papel semântico, para que a parte autoral de uma
|
|
68
|
+
mensagem interpolada continue passando pela política da Base. As declarações não alteram o
|
|
69
|
+
que o React renderiza; um seletor que deixa de corresponder a um único diagnóstico reprova o
|
|
70
|
+
gate e precisa ser revisto. Diagnósticos estruturais ou de transformação visual não podem ser
|
|
71
|
+
dispensados por `copy.dynamic`; ausência de conteúdo também continua bloqueante.
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"copy": {
|
|
76
|
+
"dynamic": [
|
|
77
|
+
{
|
|
78
|
+
"source": "src/sessions.tsx",
|
|
79
|
+
"line": 42,
|
|
80
|
+
"field": "<Select>.options",
|
|
81
|
+
"kind": "runtime-data",
|
|
82
|
+
"reason": "As opções são nomes de sessões retornados pela aplicação.",
|
|
83
|
+
"reference": "domain:sessions"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"source": "src/sessions.tsx",
|
|
87
|
+
"line": 58,
|
|
88
|
+
"field": "<DialogDescription> children",
|
|
89
|
+
"kind": "message-template",
|
|
90
|
+
"role": "dialog-body",
|
|
91
|
+
"text": "Excluir {nome}?",
|
|
92
|
+
"reason": "O nome identifica a sessão selecionada na confirmação.",
|
|
93
|
+
"reference": "domain:sessions"
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
63
100
|
O mapeamento completo, o limite de cobertura e os comandos do gate estão em
|
|
64
101
|
`docs/code-style.md` do pacote.
|
|
65
102
|
|