basalt-ui 1.9.0 → 1.11.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/AGENTS.md +2 -1
- package/CHANGELOG.md +18 -0
- package/README.md +64 -34
- package/agent/rules/basalt-agent.md +49 -6
- package/configs/oxlint-plugin.js +279 -0
- package/configs/oxlint-plugin.test.ts +216 -1
- package/configs/oxlint.json +5 -2
- package/dist/agent/ai-sdk-transport.d.ts +3 -52
- package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
- package/dist/agent/ai-sdk-transport.js +108 -24
- package/dist/agent/coalesce.d.ts +26 -0
- package/dist/agent/coalesce.d.ts.map +1 -0
- package/dist/agent/coalesce.js +69 -0
- package/dist/agent/foreign.d.ts +97 -0
- package/dist/agent/foreign.d.ts.map +1 -0
- package/dist/agent/foreign.js +18 -0
- package/dist/agent/history.d.ts +2 -0
- package/dist/agent/history.d.ts.map +1 -1
- package/dist/agent/id.d.ts +35 -0
- package/dist/agent/id.d.ts.map +1 -0
- package/dist/agent/id.js +14 -0
- package/dist/agent/index.d.ts +9 -4
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +15 -3
- package/dist/agent/merge.d.ts +8 -0
- package/dist/agent/merge.d.ts.map +1 -0
- package/dist/agent/merge.js +23 -0
- package/dist/agent/part-list.d.ts.map +1 -1
- package/dist/agent/part-list.js +16 -6
- package/dist/agent/parts.d.ts +100 -21
- package/dist/agent/parts.d.ts.map +1 -1
- package/dist/agent/parts.js +128 -11
- package/dist/agent/transport.d.ts +39 -3
- package/dist/agent/transport.d.ts.map +1 -1
- package/dist/agent/transport.js +20 -17
- package/dist/agent/use-agent-stream.d.ts +2 -1
- package/dist/agent/use-agent-stream.d.ts.map +1 -1
- package/dist/agent/use-agent-stream.js +5 -2
- package/dist/agent/use-agent-thread-runs.d.ts +9 -3
- package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.js +76 -23
- package/dist/agent-chat/index.d.ts +6 -3
- package/dist/agent-chat/index.d.ts.map +1 -1
- package/dist/agent-chat/index.js +2 -0
- package/dist/agent-chat/thread-message.d.ts +24 -7
- package/dist/agent-chat/thread-message.d.ts.map +1 -1
- package/dist/agent-chat/thread-message.js +102 -43
- package/dist/agent-chat/tool-chip.d.ts +21 -0
- package/dist/agent-chat/tool-chip.d.ts.map +1 -0
- package/dist/agent-chat/tool-chip.js +120 -0
- package/dist/cli/index.d.ts +33 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +131 -0
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +1 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/surfaces.d.ts +11 -1
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +22 -1
- package/llms.txt +10 -3
- package/package.json +6 -1
- package/src/agent/ai-sdk-transport.test.ts +567 -0
- package/src/agent/ai-sdk-transport.ts +206 -53
- package/src/agent/coalesce.test.ts +194 -0
- package/src/agent/coalesce.ts +118 -0
- package/src/agent/foreign.test.ts +47 -0
- package/src/agent/foreign.ts +140 -0
- package/src/agent/history.ts +2 -0
- package/src/agent/id.test.ts +71 -0
- package/src/agent/id.ts +43 -0
- package/src/agent/index.ts +26 -3
- package/src/agent/merge.test.ts +119 -0
- package/src/agent/merge.ts +67 -0
- package/src/agent/part-list.test.tsx +82 -0
- package/src/agent/part-list.tsx +31 -6
- package/src/agent/parts.test.ts +603 -0
- package/src/agent/parts.ts +262 -32
- package/src/agent/thread.test.ts +147 -0
- package/src/agent/transport.test.ts +70 -0
- package/src/agent/transport.ts +72 -20
- package/src/agent/use-agent-stream.test.tsx +261 -0
- package/src/agent/use-agent-stream.ts +13 -3
- package/src/agent/use-agent-thread-runs.foreign.test.tsx +133 -0
- package/src/agent/use-agent-thread-runs.resume.test.tsx +289 -0
- package/src/agent/use-agent-thread-runs.test.tsx +781 -0
- package/src/agent/use-agent-thread-runs.ts +177 -27
- package/src/agent/use-agent-thread-runs.wedge.test.tsx +301 -0
- package/src/agent-chat/index.ts +8 -3
- package/src/agent-chat/thread-message.test.tsx +211 -0
- package/src/agent-chat/thread-message.tsx +235 -62
- package/src/agent-chat/tool-chip.test.tsx +139 -0
- package/src/agent-chat/tool-chip.tsx +219 -0
- package/src/charts/primitives/ChartFrame.test.tsx +8 -5
- package/src/cli/index.ts +285 -0
- package/src/dashboard/stat-card.test.tsx +5 -3
- package/src/guard/check-source.test.ts +8 -4
- package/src/guard/index.ts +1 -3
- package/src/index.ts +2 -0
- package/src/provider/index.test.tsx +6 -5
- package/src/surfaces.ts +23 -1
- package/src/theme/use-basalt-spacing.test.tsx +7 -4
- package/src/tokens/build-fonts-css.test.ts +3 -2
package/src/cli/index.ts
CHANGED
|
@@ -165,6 +165,25 @@ export type BasaltConfig = {
|
|
|
165
165
|
* { exemptRules: { 'inline-display': ['agent'] } } // inline-display never fires under src/agent/**
|
|
166
166
|
*/
|
|
167
167
|
exemptRules?: Partial<Record<GuardKind, string[]>>
|
|
168
|
+
/**
|
|
169
|
+
* Declares an intentional `ai` package major-version skew across workspace packages, exempting
|
|
170
|
+
* `doctor`'s `ai-major-parity` hard check. The value IS the reason — a bare `true` is rejected,
|
|
171
|
+
* because the whole point of this key is that the pairing is written down, not just switched off.
|
|
172
|
+
* A written declaration is the pin `basalt/ai-sdk-major`'s own comment notes is otherwise missing.
|
|
173
|
+
*
|
|
174
|
+
* Semantics: absent (or present but not a non-empty string) → the skew still hard-fails exactly as
|
|
175
|
+
* without this key. A non-empty string with a skew present → `doctor` passes, echoing both the
|
|
176
|
+
* skew and this reason. A non-empty string with NO skew present → `doctor` warns that the
|
|
177
|
+
* exemption is stale and can be deleted (an exemption nobody re-checks is how a real, later skew
|
|
178
|
+
* slips through unnoticed).
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* // apps/api streams on ai@5; apps/dashboard parses on ai@7; a producer-side TransformStream
|
|
182
|
+
* // neutralizes the one enum value that differs between the two majors.
|
|
183
|
+
* { aiMajorSkewReason: 'apps/api pinned to ai@5, apps/dashboard on ai@7 — the skew is neutralized '
|
|
184
|
+
* + 'by a producer-side TransformStream in apps/api/src/stream-transform.ts' }
|
|
185
|
+
*/
|
|
186
|
+
aiMajorSkewReason?: string
|
|
168
187
|
}
|
|
169
188
|
|
|
170
189
|
const DEFAULT_ROOT = 'src'
|
|
@@ -1560,6 +1579,202 @@ type DoctorResult = {
|
|
|
1560
1579
|
warnings: number
|
|
1561
1580
|
}
|
|
1562
1581
|
|
|
1582
|
+
type WorkspacePackage = { readonly name: string; readonly dir: string }
|
|
1583
|
+
|
|
1584
|
+
/** {@link collectWorkspacePackages}'s return: the walked packages plus whether a `workspaces`
|
|
1585
|
+
* field was present at all (distinguishes "nothing to compare" from "not a workspace root"). */
|
|
1586
|
+
type WorkspaceWalk = {
|
|
1587
|
+
readonly packages: readonly WorkspacePackage[]
|
|
1588
|
+
readonly hasWorkspacesField: boolean
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/**
|
|
1592
|
+
* Every subdirectory of `base` (name only), or `[]` if `base` doesn't exist / isn't a directory.
|
|
1593
|
+
* `node_modules` is always excluded — a wildcard workspaces pattern must never descend into a
|
|
1594
|
+
* dependency tree. `skipDotDirs` additionally excludes dot-directories; it is only set by the
|
|
1595
|
+
* `*`/`**` wildcard descent in {@link expandPatternSegments}, never for an explicitly named
|
|
1596
|
+
* literal segment, so a pattern like `.internal/*` still resolves.
|
|
1597
|
+
*/
|
|
1598
|
+
function subdirNames(base: string, options: { skipDotDirs?: boolean } = {}): string[] {
|
|
1599
|
+
let entries: string[]
|
|
1600
|
+
try {
|
|
1601
|
+
entries = readdirSync(base)
|
|
1602
|
+
} catch {
|
|
1603
|
+
return []
|
|
1604
|
+
}
|
|
1605
|
+
return entries.filter((name) => {
|
|
1606
|
+
if (name === 'node_modules') return false
|
|
1607
|
+
if (options.skipDotDirs && name.startsWith('.')) return false
|
|
1608
|
+
try {
|
|
1609
|
+
return statSync(resolve(base, name)).isDirectory()
|
|
1610
|
+
} catch {
|
|
1611
|
+
return false
|
|
1612
|
+
}
|
|
1613
|
+
})
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
/**
|
|
1617
|
+
* Expands one glob PATTERN's segments against the filesystem, starting from `baseDir`. Supports
|
|
1618
|
+
* three segment kinds: a literal name, a single `*` wildcard (any one directory), and a `**`
|
|
1619
|
+
* wildcard (zero or more directory levels, i.e. arbitrary depth) — covering a trailing single
|
|
1620
|
+
* wildcard, an interior wildcard between literal segments, and a trailing double-wildcard alike,
|
|
1621
|
+
* not just one trailing single-wildcard segment.
|
|
1622
|
+
*/
|
|
1623
|
+
function expandPatternSegments(baseDir: string, segments: readonly string[]): string[] {
|
|
1624
|
+
if (segments.length === 0) return [baseDir]
|
|
1625
|
+
const [head, ...rest] = segments
|
|
1626
|
+
|
|
1627
|
+
if (head === '**') {
|
|
1628
|
+
// Zero levels: `**` consumes nothing and the rest of the pattern matches right here.
|
|
1629
|
+
const matches = expandPatternSegments(baseDir, rest)
|
|
1630
|
+
// One-or-more levels: descend into every subdirectory, `**` still active.
|
|
1631
|
+
for (const name of subdirNames(baseDir, { skipDotDirs: true })) {
|
|
1632
|
+
matches.push(...expandPatternSegments(resolve(baseDir, name), segments))
|
|
1633
|
+
}
|
|
1634
|
+
return matches
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
const names =
|
|
1638
|
+
head === '*'
|
|
1639
|
+
? subdirNames(baseDir, { skipDotDirs: true })
|
|
1640
|
+
: subdirNames(baseDir).filter((n) => n === head)
|
|
1641
|
+
return names.flatMap((name) => expandPatternSegments(resolve(baseDir, name), rest))
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
/**
|
|
1645
|
+
* Expands a root `package.json`'s `workspaces` patterns into concrete package directories.
|
|
1646
|
+
* Supports literal segments, a single-wildcard segment, and a double-wildcard segment, both
|
|
1647
|
+
* interior and trailing (e.g. `packages` followed by a double wildcard, or an interior single
|
|
1648
|
+
* wildcard between two literal segments) — not just one trailing single-wildcard segment. A
|
|
1649
|
+
* candidate directory that has no `package.json` of its own is skipped, not reported — an empty
|
|
1650
|
+
* scaffold directory is not a workspace package. Also honours npm/bun/yarn `!`-prefixed exclusion
|
|
1651
|
+
* entries (e.g. `["packages/*", "!packages/legacy"]`) — both the included and excluded patterns
|
|
1652
|
+
* are expanded through the same machinery, then the excluded set is subtracted.
|
|
1653
|
+
*/
|
|
1654
|
+
function expandWorkspaceGlobs(cwd: string, patterns: readonly string[]): string[] {
|
|
1655
|
+
const expand = (pattern: string): string[] =>
|
|
1656
|
+
expandPatternSegments(
|
|
1657
|
+
cwd,
|
|
1658
|
+
pattern.split('/').filter((s) => s.length > 0),
|
|
1659
|
+
)
|
|
1660
|
+
|
|
1661
|
+
const excluded = new Set(
|
|
1662
|
+
patterns
|
|
1663
|
+
.filter((pattern) => pattern.startsWith('!'))
|
|
1664
|
+
.flatMap((pattern) => expand(pattern.replace(/^!+/, ''))),
|
|
1665
|
+
)
|
|
1666
|
+
|
|
1667
|
+
const dirs: string[] = []
|
|
1668
|
+
for (const pattern of patterns) {
|
|
1669
|
+
if (pattern.startsWith('!')) continue
|
|
1670
|
+
for (const abs of expand(pattern)) {
|
|
1671
|
+
if (excluded.has(abs)) continue
|
|
1672
|
+
if (existsSync(resolve(abs, 'package.json'))) dirs.push(abs)
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
return dirs
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
/**
|
|
1679
|
+
* Every workspace package declared by `cwd`'s root `package.json` (npm/bun/yarn `workspaces`
|
|
1680
|
+
* field, array or `{ packages: [...] }` object form), named + directory-resolved — PLUS the root
|
|
1681
|
+
* manifest itself, labelled `"{name} (root)"`. The root is walked because it commonly carries a
|
|
1682
|
+
* hoisted `ai` dev dependency that can itself skew against a workspace package's own major — that
|
|
1683
|
+
* is the producer/consumer shape the whole check exists to catch, and it is invisible if only the
|
|
1684
|
+
* expanded workspace dirs are inspected. `hasWorkspacesField` is `false` (and `packages` empty)
|
|
1685
|
+
* when `cwd` has no `package.json`, no `workspaces` field, or the field is a shape this doesn't
|
|
1686
|
+
* recognize — a repo-wide walk is a bonus check, not something that should ever throw doctor over
|
|
1687
|
+
* an unrecognized `workspaces` shape.
|
|
1688
|
+
*/
|
|
1689
|
+
function collectWorkspacePackages(cwd: string): WorkspaceWalk {
|
|
1690
|
+
const raw = readIfExists(resolve(cwd, 'package.json'))
|
|
1691
|
+
if (raw === null) return { packages: [], hasWorkspacesField: false }
|
|
1692
|
+
|
|
1693
|
+
let parsed: { workspaces?: unknown; name?: string }
|
|
1694
|
+
try {
|
|
1695
|
+
parsed = JSON.parse(raw) as { workspaces?: unknown; name?: string }
|
|
1696
|
+
} catch {
|
|
1697
|
+
return { packages: [], hasWorkspacesField: false }
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
const patterns = isStringArray(parsed.workspaces)
|
|
1701
|
+
? parsed.workspaces
|
|
1702
|
+
: parsed.workspaces !== null &&
|
|
1703
|
+
typeof parsed.workspaces === 'object' &&
|
|
1704
|
+
isStringArray((parsed.workspaces as { packages?: unknown }).packages)
|
|
1705
|
+
? (parsed.workspaces as { packages: string[] }).packages
|
|
1706
|
+
: null
|
|
1707
|
+
if (patterns === null) return { packages: [], hasWorkspacesField: false }
|
|
1708
|
+
|
|
1709
|
+
const rootName = parsed.name && parsed.name.length > 0 ? `${parsed.name} (root)` : '(root)'
|
|
1710
|
+
const workspacePackages = expandWorkspaceGlobs(cwd, patterns).map((dir) => ({
|
|
1711
|
+
name: readPackageName(dir),
|
|
1712
|
+
dir,
|
|
1713
|
+
}))
|
|
1714
|
+
|
|
1715
|
+
return {
|
|
1716
|
+
packages: [{ name: rootName, dir: cwd }, ...workspacePackages],
|
|
1717
|
+
hasWorkspacesField: true,
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
/** Narrows to a string array — shared by `collectWorkspacePackages`'s two `workspaces` shapes. */
|
|
1722
|
+
function isStringArray(value: unknown): value is string[] {
|
|
1723
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === 'string')
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
/**
|
|
1727
|
+
* First run of digits in a semver range string (`^7.0.15` → `7`), or null if there is none.
|
|
1728
|
+
*
|
|
1729
|
+
* Duplicated verbatim in `configs/oxlint-plugin.js` — that file must stay import-free from this
|
|
1730
|
+
* package (it loads via `jsPlugins` out of a consumer's node_modules, before this package's own
|
|
1731
|
+
* code is necessarily resolvable), so the duplication is structural, not an oversight. Keep both
|
|
1732
|
+
* copies in sync by hand; a future parsing fix (pre-release suffixes, say) must land in both.
|
|
1733
|
+
*/
|
|
1734
|
+
function majorOf(range: string | undefined): number | null {
|
|
1735
|
+
if (typeof range !== 'string') return null
|
|
1736
|
+
const match = range.match(/\d+/)
|
|
1737
|
+
return match === null ? null : Number(match[0])
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
/** The `ai` package's declared major at `dir`'s own `package.json`, or null if undeclared/unreadable. */
|
|
1741
|
+
function aiMajorAt(dir: string): number | null {
|
|
1742
|
+
const raw = readIfExists(resolve(dir, 'package.json'))
|
|
1743
|
+
if (raw === null) return null
|
|
1744
|
+
try {
|
|
1745
|
+
const pkg = JSON.parse(raw) as {
|
|
1746
|
+
dependencies?: Record<string, string>
|
|
1747
|
+
devDependencies?: Record<string, string>
|
|
1748
|
+
peerDependencies?: Record<string, string>
|
|
1749
|
+
}
|
|
1750
|
+
return majorOf(
|
|
1751
|
+
pkg.dependencies?.['ai'] ?? pkg.devDependencies?.['ai'] ?? pkg.peerDependencies?.['ai'],
|
|
1752
|
+
)
|
|
1753
|
+
} catch {
|
|
1754
|
+
return null
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
/**
|
|
1759
|
+
* Splits `basalt.aiMajorSkewReason` into a validated reason (or null) plus whether the key was
|
|
1760
|
+
* present at all but failed validation. A present-but-invalid value (anything other than a
|
|
1761
|
+
* non-empty string — including a bare `true`) is treated as absent for the pass/fail decision, same
|
|
1762
|
+
* as a missing key, but doctor's failure message still calls out that the key exists and is
|
|
1763
|
+
* malformed rather than silently falling back to the plain "no exemption" message — a forgotten key
|
|
1764
|
+
* and a broken one are different mistakes and deserve different guidance.
|
|
1765
|
+
*/
|
|
1766
|
+
function resolveAiMajorSkewReason(cfg: BasaltConfig): {
|
|
1767
|
+
reason: string | null
|
|
1768
|
+
presentButInvalid: boolean
|
|
1769
|
+
} {
|
|
1770
|
+
const raw = cfg.aiMajorSkewReason
|
|
1771
|
+
if (raw === undefined) return { reason: null, presentButInvalid: false }
|
|
1772
|
+
if (typeof raw === 'string' && raw.trim().length > 0) {
|
|
1773
|
+
return { reason: raw, presentButInvalid: false }
|
|
1774
|
+
}
|
|
1775
|
+
return { reason: null, presentButInvalid: true }
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1563
1778
|
/**
|
|
1564
1779
|
* Check a consumer repo's basalt integration and print a pass/warn report.
|
|
1565
1780
|
*
|
|
@@ -1570,6 +1785,17 @@ type DoctorResult = {
|
|
|
1570
1785
|
*
|
|
1571
1786
|
* Hard failures (exit non-zero):
|
|
1572
1787
|
* 1. .basalt/manifest.json exists (init was run).
|
|
1788
|
+
* 6. `ai-major-parity`: every workspace package that declares the `ai` package agrees on its
|
|
1789
|
+
* major version. `basalt/ai-sdk-major` (the oxlint plugin rule) cannot catch this — it checks
|
|
1790
|
+
* a linted file's NEAREST package.json, so a lint run scoped to one workspace package only
|
|
1791
|
+
* ever sees that package's own `ai` major and is perfectly happy; the cross-package skew (one
|
|
1792
|
+
* package streaming on `ai@5`, another parsing it on `ai@7`) only a repo-wide manifest walk
|
|
1793
|
+
* can see. Skipped entirely when `cwd` has no `workspaces` field or none of its packages
|
|
1794
|
+
* declare `ai`. A detected skew is exempt-able via `basalt.aiMajorSkewReason` (a non-empty
|
|
1795
|
+
* reason string, not a bare `true`) in the consumer's package.json — an intentional
|
|
1796
|
+
* producer/consumer split neutralized by a transform is a locked decision, not a defect, and
|
|
1797
|
+
* the written reason IS the pin that a permanently-failing guard would otherwise lack. Missing
|
|
1798
|
+
* or invalid (empty/non-string) still hard-fails exactly as if the key were absent.
|
|
1573
1799
|
*
|
|
1574
1800
|
* Warnings (non-fatal):
|
|
1575
1801
|
* 2. The installed node_modules/basalt-ui version matches the manifest's basaltVersion
|
|
@@ -1581,10 +1807,14 @@ type DoctorResult = {
|
|
|
1581
1807
|
* `bunx basalt-ui` npm fetch; best-effort, skipped if node_modules is absent).
|
|
1582
1808
|
* 5. `basaltAppPlugin`'s (basalt-ui/vite) default icon filenames exist under public/ (only when
|
|
1583
1809
|
* a public/ dir exists at all — skipped otherwise).
|
|
1810
|
+
* 7. A declared `basalt.aiMajorSkewReason` when the ai majors currently AGREE — the exemption is
|
|
1811
|
+
* stale and can be deleted; an exemption nobody revisits is how a real, later skew slips
|
|
1812
|
+
* through unnoticed.
|
|
1584
1813
|
*
|
|
1585
1814
|
* Returns the exit code: 0 = all good, 1 = one or more hard failures.
|
|
1586
1815
|
*/
|
|
1587
1816
|
export function doctor(cwd: string = process.cwd()): number {
|
|
1817
|
+
const cfg = readBasaltConfig(cwd)
|
|
1588
1818
|
const result: DoctorResult = { hardFailures: 0, warnings: 0 }
|
|
1589
1819
|
const lines: string[] = [`\nbasalt-ui doctor — ${cwd}\n`]
|
|
1590
1820
|
|
|
@@ -1728,6 +1958,61 @@ export function doctor(cwd: string = process.cwd()): number {
|
|
|
1728
1958
|
}
|
|
1729
1959
|
}
|
|
1730
1960
|
|
|
1961
|
+
// ── Hard check 6: ai package major version parity across workspace packages ─
|
|
1962
|
+
// basalt/ai-sdk-major (the lint rule) is per-file against the NEAREST package.json, so a lint
|
|
1963
|
+
// run scoped to one workspace package only ever sees that package's own `ai` major and is
|
|
1964
|
+
// perfectly happy. The real defect is CROSS-package (argo defect 1: apps/api on ai@5 producing a
|
|
1965
|
+
// stream apps/dashboard's ai@7 client can't parse) and only a repo-wide manifest walk sees it —
|
|
1966
|
+
// this is that walk. Hard failure, not a warning: a skewed pair throws at runtime, it doesn't
|
|
1967
|
+
// just look different. `basalt.aiMajorSkewReason` exempts an INTENTIONAL skew (e.g. a
|
|
1968
|
+
// producer-side TransformStream neutralizing the divergent enum value) — see the BasaltConfig
|
|
1969
|
+
// JSDoc for its exact semantics; a stale declaration (skew resolved, reason still present) warns
|
|
1970
|
+
// instead of passing silently.
|
|
1971
|
+
const workspaceWalk = collectWorkspacePackages(cwd)
|
|
1972
|
+
const aiMajors = workspaceWalk.packages
|
|
1973
|
+
.map((pkg) => ({ name: pkg.name, major: aiMajorAt(pkg.dir) }))
|
|
1974
|
+
.filter((entry): entry is { name: string; major: number } => entry.major !== null)
|
|
1975
|
+
const { reason: aiMajorSkewReason, presentButInvalid: aiMajorSkewReasonInvalid } =
|
|
1976
|
+
resolveAiMajorSkewReason(cfg)
|
|
1977
|
+
if (aiMajors.length > 0) {
|
|
1978
|
+
const distinctMajors = new Set(aiMajors.map((entry) => entry.major))
|
|
1979
|
+
if (distinctMajors.size > 1) {
|
|
1980
|
+
const summary = aiMajors.map((entry) => `${entry.name}@ai${entry.major}`).join(', ')
|
|
1981
|
+
if (aiMajorSkewReason !== null) {
|
|
1982
|
+
pass(
|
|
1983
|
+
`ai package major version mismatch across workspace packages: ${summary} — exempted via ` +
|
|
1984
|
+
`basalt.aiMajorSkewReason: "${aiMajorSkewReason}"`,
|
|
1985
|
+
)
|
|
1986
|
+
} else if (aiMajorSkewReasonInvalid) {
|
|
1987
|
+
fail(
|
|
1988
|
+
`ai package major version mismatch across workspace packages: ${summary} — ` +
|
|
1989
|
+
'basalt.aiMajorSkewReason is present but is not a non-empty string (a bare `true` is ' +
|
|
1990
|
+
'not accepted); the exemption must carry a written reason, e.g. ' +
|
|
1991
|
+
'`aiMajorSkewReason: "apps/api on ai@5, apps/dashboard on ai@7, neutralized by a ' +
|
|
1992
|
+
'producer-side TransformStream"`.',
|
|
1993
|
+
)
|
|
1994
|
+
} else {
|
|
1995
|
+
fail(`ai package major version mismatch across workspace packages: ${summary}`)
|
|
1996
|
+
}
|
|
1997
|
+
} else {
|
|
1998
|
+
const [major] = distinctMajors
|
|
1999
|
+
if (aiMajorSkewReason !== null) {
|
|
2000
|
+
warn(
|
|
2001
|
+
`basalt.aiMajorSkewReason ("${aiMajorSkewReason}") is declared but the ai package major ` +
|
|
2002
|
+
`already matches across all ${aiMajors.length} workspace package(s) declaring it ` +
|
|
2003
|
+
`(ai@${major}) — the exemption is no longer needed and can be deleted.`,
|
|
2004
|
+
)
|
|
2005
|
+
} else {
|
|
2006
|
+
pass(
|
|
2007
|
+
`ai package major matches across ${aiMajors.length} workspace package(s) declaring it (ai@${major})`,
|
|
2008
|
+
)
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
} else if (workspaceWalk.hasWorkspacesField) {
|
|
2012
|
+
// A hard check that can silently not run is worse than one that is absent — say so.
|
|
2013
|
+
pass('ai-major-parity: no workspace package declares the ai dependency — nothing to compare')
|
|
2014
|
+
}
|
|
2015
|
+
|
|
1731
2016
|
// ── Report ──────────────────────────────────────────────────────────────────
|
|
1732
2017
|
lines.push('')
|
|
1733
2018
|
if (result.hardFailures === 0 && result.warnings === 0) {
|
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
* leaving the prop off, so the absence case asserts no rail and no verdict text at all — which a
|
|
14
14
|
* future `tone = tone ?? 'good'` style default would immediately fail.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
* `renderToStaticMarkup` inside a real `MantineProvider`
|
|
18
|
-
* style on server-rendered markup.
|
|
16
|
+
* A DOM harness now exists (`tests/setup/dom.ts`, preloaded via the root `bunfig.toml`; see
|
|
17
|
+
* `theme/use-basalt-spacing.test.tsx`'s doc) — `renderToStaticMarkup` inside a real `MantineProvider`
|
|
18
|
+
* is used deliberately here instead, since the rail is an inline style on server-rendered markup.
|
|
19
|
+
* Converting to the DOM harness would only be worth it if a future assertion here needed live DOM
|
|
20
|
+
* behavior (e.g. computed styles, focus/hover interaction) rather than the static markup string.
|
|
19
21
|
*/
|
|
20
22
|
import { MantineProvider } from '@mantine/core'
|
|
21
23
|
import { describe, expect, test } from 'bun:test'
|
|
@@ -209,11 +209,15 @@ describe('mantine-shade-index', () => {
|
|
|
209
209
|
})
|
|
210
210
|
|
|
211
211
|
// The grace-minor doctrine (package CLAUDE.md): a kind that rejects previously-passing code
|
|
212
|
-
// lands as `warn` for one minor
|
|
213
|
-
//
|
|
214
|
-
|
|
212
|
+
// lands as `warn` for one minor, then its GRACE_PERIOD_KINDS entry is deleted and it becomes an
|
|
213
|
+
// error. This one ran its grace across FOUR minors — introduced 1.7.0, then deferred by 1.8.0
|
|
214
|
+
// (shipped the same day as 1.7.0), 1.9.0 (carried the chart-layer batch the same consumer was
|
|
215
|
+
// waiting on) and 1.10.0 (shipped without the promotion at all). Promoted in 1.11.0, verified
|
|
216
|
+
// against the only consumer first: argo's `check-theme` reports zero violations of any kind, so
|
|
217
|
+
// the promotion breaks nothing that was passing.
|
|
218
|
+
it('is an error now that its grace period has ended', () => {
|
|
215
219
|
const f = find(`<Text c="yellow.7" />`)
|
|
216
|
-
expect(f.find((x) => x.kind === 'mantine-shade-index')?.severity).toBe('
|
|
220
|
+
expect(f.find((x) => x.kind === 'mantine-shade-index')?.severity).toBe('error')
|
|
217
221
|
})
|
|
218
222
|
})
|
|
219
223
|
|
package/src/guard/index.ts
CHANGED
|
@@ -296,9 +296,7 @@ export const DEFAULT_GUARD_CONFIG: GuardConfig = {
|
|
|
296
296
|
* 'raw-font-family': 'introduced 1.4.0 — promote to error in 1.5.0',
|
|
297
297
|
* }
|
|
298
298
|
*/
|
|
299
|
-
const GRACE_PERIOD_KINDS: Partial<Record<GuardKind, string>> = {
|
|
300
|
-
'mantine-shade-index': 'introduced 1.7.0 — promote to error in 1.10.0',
|
|
301
|
-
}
|
|
299
|
+
const GRACE_PERIOD_KINDS: Partial<Record<GuardKind, string>> = {}
|
|
302
300
|
|
|
303
301
|
/** A kind's effective severity: consumer override first, then the grace table, then `error`. */
|
|
304
302
|
function severityOf(kind: GuardKind, cfg: GuardConfig): GuardSeverity {
|
package/src/index.ts
CHANGED
|
@@ -118,6 +118,7 @@ export {
|
|
|
118
118
|
Composer,
|
|
119
119
|
threadPartRenderers,
|
|
120
120
|
ThreadTranscript,
|
|
121
|
+
ToolChip,
|
|
121
122
|
} from './agent-chat'
|
|
122
123
|
export type {
|
|
123
124
|
ThreadWorkspaceProps,
|
|
@@ -126,6 +127,7 @@ export type {
|
|
|
126
127
|
ThreadDetailPanelProps,
|
|
127
128
|
ComposerProps,
|
|
128
129
|
ThreadTranscriptProps,
|
|
130
|
+
ToolChipProps,
|
|
129
131
|
} from './agent-chat'
|
|
130
132
|
export { createThreadsStore, heuristicOutcome, useAgentThreadRuns } from './agent'
|
|
131
133
|
export type {
|
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
* The `BasaltBridge density wiring` block below covers the layer `composeInjectedCss` alone can't:
|
|
11
11
|
* that `BasaltBridge` (not just the pure `composeInjectedCss` helper) actually reads
|
|
12
12
|
* `theme.other.basaltDensity` off the RUNNING theme and feeds it through `buildDensityCss` into the
|
|
13
|
-
* injected `<style>`, end to end from `createBasaltTheme(undefined, { density })`.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* injected `<style>`, end to end from `createBasaltTheme(undefined, { density })`. A DOM harness now
|
|
14
|
+
* exists (`tests/setup/dom.ts`, preloaded via the root `bunfig.toml`) — this suite still uses
|
|
15
|
+
* `react-dom/server`'s `renderToStaticMarkup` deliberately, since the injected `<style>` is plain
|
|
16
|
+
* text content on server-rendered markup, not something that needs a live DOM to assert on.
|
|
17
|
+
* Converting this to mount through the DOM harness would only be worth it if a future assertion here
|
|
18
|
+
* needed live DOM behavior (event handling, layout, effects) rather than the static markup string.
|
|
18
19
|
*/
|
|
19
20
|
import { describe, expect, test } from 'bun:test'
|
|
20
21
|
import { renderToStaticMarkup } from 'react-dom/server'
|
package/src/surfaces.ts
CHANGED
|
@@ -362,7 +362,7 @@ export const SURFACES = {
|
|
|
362
362
|
skill: ['basalt-app'],
|
|
363
363
|
guardKinds: [],
|
|
364
364
|
description:
|
|
365
|
-
'Headless streaming-chat layer: useAgentStream, aiSdkTransport (recommended default) + edenTransport, PartList, plus the multi-thread createThreadsStore + useAgentThreadRuns + outcome-resolver seam (Mantine-free)',
|
|
365
|
+
'Headless streaming-chat layer: useAgentStream, aiSdkTransport (recommended default) + edenTransport, isResumable/ResumableAgentTransport (stream-resumption seam), PartList, coalesceParts, the ForeignPart/definePartRenderers/narrowAgentPart open part-registry seam, plus the multi-thread createThreadsStore + useAgentThreadRuns + outcome-resolver seam (Mantine-free)',
|
|
366
366
|
optionalPeers: ['ai', 'use-stick-to-bottom'],
|
|
367
367
|
globs: {
|
|
368
368
|
shipped: [],
|
|
@@ -371,6 +371,28 @@ export const SURFACES = {
|
|
|
371
371
|
// @visx/* ban dropped — `basalt/visx-boundary` now bans it universally outside charts.
|
|
372
372
|
forbiddenImports: [...MANTINE_BANS],
|
|
373
373
|
},
|
|
374
|
+
'./agent-chat': {
|
|
375
|
+
kind: 'doctrine',
|
|
376
|
+
layer: 'mantine-coupled',
|
|
377
|
+
rule: 'agent',
|
|
378
|
+
skill: ['basalt-app'],
|
|
379
|
+
guardKinds: [],
|
|
380
|
+
description:
|
|
381
|
+
'Mantine-styled thread-chat components over basalt-ui/agent: ThreadWorkspace, ThreadFeed, ThreadOutcomeCard, ThreadDetailPanel, Composer, ThreadTranscript (open part-renderer registry via its renderers/fallbackRenderer props), threadPartRenderers, ToolChip (Mantine-coupled). remend and motion are required, not optional — ThreadTranscript imports content/markdown.tsx (remend eagerly), and ThreadFeed/ThreadDetailPanel import motion/react eagerly, so this subpath fails to resolve without both installed even though peerDependenciesMeta marks them optional (npm has no per-subpath optionality).',
|
|
382
|
+
optionalPeers: [
|
|
383
|
+
'ai',
|
|
384
|
+
'motion',
|
|
385
|
+
'remend',
|
|
386
|
+
'use-stick-to-bottom',
|
|
387
|
+
'react-markdown',
|
|
388
|
+
'remark-gfm',
|
|
389
|
+
'shiki',
|
|
390
|
+
'@shikijs/langs',
|
|
391
|
+
'@shikijs/themes',
|
|
392
|
+
'beautiful-mermaid',
|
|
393
|
+
],
|
|
394
|
+
forbiddenImports: [],
|
|
395
|
+
},
|
|
374
396
|
'./content': {
|
|
375
397
|
kind: 'doctrine',
|
|
376
398
|
layer: 'mantine-coupled',
|
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
* with every other density test still green, since those all exercise `deriveSpacing`/
|
|
7
7
|
* `createBasaltTheme` directly, never this hook.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* doc for that same
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* A DOM harness now exists (`tests/setup/dom.ts`, preloaded via the root `bunfig.toml`; see
|
|
10
|
+
* `tokens/build-fonts-css.test.ts`'s doc for that same note elsewhere) — this suite still uses
|
|
11
|
+
* `react-dom/server`'s `renderToStaticMarkup` deliberately: it's enough to invoke a hook inside a
|
|
12
|
+
* REAL `MantineProvider` tree without a DOM, so these assertions exercise the actual
|
|
13
|
+
* `useMantineTheme()` read, not a re-implementation of it. Converting to the DOM harness would only
|
|
14
|
+
* be worth it if a future assertion here needed the hook to react to a live DOM event (e.g. a
|
|
15
|
+
* `ResizeObserver`-driven remeasure) rather than a single render-time read.
|
|
13
16
|
*/
|
|
14
17
|
import { MantineProvider } from '@mantine/core'
|
|
15
18
|
import type { MantineProviderProps } from '@mantine/core'
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* `buildFontsCss` — the pure CSS-building half of the `{ fonts }` option (see
|
|
3
3
|
* `../theme/fonts-option.test.ts` for the `theme.other.basaltFonts` round-trip this consumes).
|
|
4
4
|
* `BasaltBridge` calls this directly to append `--basalt-font-*` declarations to the same injected
|
|
5
|
-
* `<style>` as the palette CSS
|
|
6
|
-
* function
|
|
5
|
+
* `<style>` as the palette CSS. A DOM harness now exists (`tests/setup/dom.ts`, preloaded via the
|
|
6
|
+
* root `bunfig.toml`), but this pure function needs no render at all — it's the testable seam on
|
|
7
|
+
* its own, so this suite calls it directly rather than mounting anything.
|
|
7
8
|
*/
|
|
8
9
|
import { describe, expect, test } from 'bun:test'
|
|
9
10
|
import { buildFontsCss } from './index'
|