Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions docs/reference/repository-health.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
id: repository-health
title: 'Repository Health Controls'
kind: reference
version: '1.4.0'
last_updated: '2026-07-30'
last_verified: '2026-07-30'
version: '1.4.1'
last_updated: '2026-08-04'
last_verified: '2026-08-04'
review_cadence_days: 30
status: stable
tags: [reference, governance, quality, security, releases]
Expand Down Expand Up @@ -37,6 +37,11 @@ The profile definitions in the same file describe the scripts and package
metadata required for Node, browser, CLI, React Native, WASM, private,
documentation, and conformance projects. Profiles standardize the externally
observable contract without requiring every package to use the same build tool.
Packages that compile authored files outside their own workspace directory
declare those paths as `sourceRoots`; CI treats a change beneath any declared
root as a direct package change, including its dedicated browser or mobile
consumer gates.

For every public package, the control also enforces the exact supported
Node.js 22 runtime floor, explicit public npm access, and an explicit
tree-shaking side-effect declaration.
Expand Down
2 changes: 1 addition & 1 deletion governance/browser-artifact-policy.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"lastReviewed": "2026-07-30",
"lastReviewed": "2026-08-04",
"owner": "ts-stack-maintainers",
"reportRetentionDays": 30,
"growthPolicy": "Every browser consumer is measured from its exact packed dependency graph with Vite and esbuild (or the governed platform equivalent). A budget increase requires a versioned source change, composition evidence, and explicit review; generated reports preserve package/module composition for comparison.",
Expand Down
2 changes: 2 additions & 0 deletions governance/repository-health/projects.json
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,7 @@
{
"path": "packages/wallet/wallet-toolbox/client",
"name": "@bsv/wallet-toolbox-client",
"sourceRoots": ["packages/wallet/wallet-toolbox/src"],
"owner": "ts-stack-maintainers",
"area": "wallet",
"profile": "browser-library",
Expand All @@ -737,6 +738,7 @@
{
"path": "packages/wallet/wallet-toolbox/mobile",
"name": "@bsv/wallet-toolbox-mobile",
"sourceRoots": ["packages/wallet/wallet-toolbox/src"],
"owner": "ts-stack-maintainers",
"area": "wallet",
"profile": "react-native-library",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"profile": "browser",
"maximumBytes": {
"vite": {
"raw": 1525000,
"raw": 1527000,
"gzip": 360000,
"brotli": 283000
},
Expand Down
15 changes: 11 additions & 4 deletions scripts/ci-affected-scope.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,9 @@ export function changedLockfileImporters(baseSource, headSource) {
.sort((left, right) => left.localeCompare(right))
}

function projectOwnsFile(projectPath, file) {
return projectPath !== '.' && (file === projectPath || file.startsWith(`${projectPath}/`))
function projectOwnsFile(project, file) {
const roots = [project.path, ...(project.sourceRoots ?? [])]
return roots.some(root => root !== '.' && (file === root || file.startsWith(`${root}/`)))
}

function documentationOnlyProjectFile(projectPath, file) {
Expand Down Expand Up @@ -165,7 +166,7 @@ export function selectWorkspaceScope(projects, changedFiles, changedImporters =
if (
files.some(
file =>
projectOwnsFile(project.path, file) && !documentationOnlyProjectFile(project.path, file)
projectOwnsFile(project, file) && !documentationOnlyProjectFile(project.path, file)
) ||
changedImporters.includes(project.path)
) {
Expand Down Expand Up @@ -275,7 +276,13 @@ function loadProjects() {
const manifest = JSON.parse(
readFileSync(path.join(REPOSITORY_ROOT, project.path, 'package.json'), 'utf8')
)
return { name: manifest.name, path: normalized(project.path), manifest }
return {
...project,
name: manifest.name,
path: normalized(project.path),
sourceRoots: (project.sourceRoots ?? []).map(normalized),
manifest
}
})
}

Expand Down
22 changes: 22 additions & 0 deletions scripts/ci-affected-scope.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,28 @@ test('workspace scope tests direct changes, typechecks dependents, and builds de
assert.deepEqual(scope.build, ['@bsv/base', '@bsv/consumer', '@bsv/direct'])
})

test('workspace scope directly selects packages that compile a shared source root', () => {
const sharedProjects = [
...projects,
{
name: '@bsv/direct-browser',
path: 'packages/direct/browser',
sourceRoots: ['packages/direct/src'],
manifest: { name: '@bsv/direct-browser' }
},
{
name: '@bsv/direct-mobile',
path: 'packages/direct/mobile',
sourceRoots: ['packages/direct/src'],
manifest: { name: '@bsv/direct-mobile' }
}
]

const scope = selectWorkspaceScope(sharedProjects, ['packages/direct/src/index.ts'])

assert.deepEqual(scope.direct, ['@bsv/direct', '@bsv/direct-browser', '@bsv/direct-mobile'])
})

test('documentation and QA policy changes do not fan out package tests', () => {
assert.deepEqual(
selectWorkspaceScope(projects, [
Expand Down
34 changes: 34 additions & 0 deletions scripts/repository-health.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,39 @@ function validateHostPeerDependencies(project, prefix) {
return errors
}

function validateSourceRoots(project, prefix) {
if (project.sourceRoots === undefined) return []
const roots = project.sourceRoots
if (!Array.isArray(roots) || roots.length === 0) {
return [`${prefix} sourceRoots must be a non-empty array`]
}

const errors = []
for (const root of roots) {
const parts = typeof root === 'string' ? root.split('/') : []
if (
!isNonEmptyString(root) ||
root.startsWith('/') ||
root.includes('\\') ||
parts.some(part => part === '' || part === '.' || part === '..')
) {
errors.push(`${prefix} has invalid source root ${JSON.stringify(root)}`)
continue
}
if (root === project.path || root.startsWith(`${project.path}/`)) {
errors.push(`${prefix} source root ${root} is already owned by the project path`)
}
}
for (const duplicate of duplicateValues(roots)) {
errors.push(`${prefix} repeats source root ${duplicate}`)
}
const canonical = [...roots].sort((left, right) => left.localeCompare(right))
if (JSON.stringify(roots) !== JSON.stringify(canonical)) {
errors.push(`${prefix} sourceRoots must use canonical lexical order`)
}
return errors
}

function validateConsumerProfiles(project, prefix) {
const profiles = project.consumerProfiles
if (project.release !== 'npm-oidc') {
Expand Down Expand Up @@ -352,6 +385,7 @@ function validateProjectMetadata(project, registry) {
errors.push(
...validateDeclarationDependencies(project, prefix),
...validateHostPeerDependencies(project, prefix),
...validateSourceRoots(project, prefix),
...validateConsumerProfiles(project, prefix)
)
return errors
Expand Down
20 changes: 20 additions & 0 deletions scripts/repository-health.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,26 @@ test('current repository health controls and ratchet are internally consistent',
assert.equal(result.findings.length, 0)
})

test('shared package source roots are explicit and repository-relative', () => {
const sharedSourceProjects = projects.projects.filter(project => project.sourceRoots)
assert.deepEqual(
sharedSourceProjects.map(project => [project.name, project.sourceRoots]),
[
['@bsv/wallet-toolbox-client', ['packages/wallet/wallet-toolbox/src']],
['@bsv/wallet-toolbox-mobile', ['packages/wallet/wallet-toolbox/src']]
]
)

const invalidProjects = structuredClone(projects)
invalidProjects.projects.find(
project => project.name === '@bsv/wallet-toolbox-client'
).sourceRoots = ['../wallet-toolbox/src']
assert.match(
validateProjectRegistry(invalidProjects, discoverWorkspaceProjects()).join('\n'),
/invalid source root/
)
})

test('CI performance baseline retains representative full and targeted cohorts', () => {
const baseline = readJson(path.join(REPOSITORY_ROOT, 'governance/ci-performance-baseline.json'))
assert.deepEqual(validateCiPerformanceBaseline(baseline), [])
Expand Down
Loading