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
9 changes: 5 additions & 4 deletions scripts/__tests__/committed-source-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,20 +157,21 @@ function readCommittedBlobs(

/**
* The same enumeration and blob read as `createCommittedSourceTree`, handed over as text: the
* production sources and workspace manifests committed at `treeish`. A whole-tree measurement
* production sources and workspace manifests committed at `treeish`, plus every committed path. A whole-tree measurement
* (the layering ratchets) needs the corpus rather than a reader, and taking it from here is what
* keeps its file set identical to the closure walker's.
*/
// fallow-ignore-next-line unused-export -- consumed by scripts/layering, outside fallow's scope
export function readCommittedSources(
repoRoot: string,
treeish: string,
): { sources: Map<string, string>; manifests: Map<string, string> } {
const { sources, manifests } = committedSourceSet(listCommittedTree(repoRoot, treeish));
): { sources: Map<string, string>; manifests: Map<string, string>; files: string[] } {
const files = listCommittedTree(repoRoot, treeish);
const { sources, manifests } = committedSourceSet(files);
const blobs = readCommittedBlobs(repoRoot, treeish, [...sources, ...manifests]);
const only = (files: readonly string[]) =>
new Map(files.flatMap((file) => (blobs.has(file) ? [[file, blobs.get(file)!] as const] : [])));
return { sources: only(sources), manifests: only(manifests) };
return { sources: only(sources), manifests: only(manifests), files };
}

/**
Expand Down
21 changes: 19 additions & 2 deletions scripts/layering/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@
// static edge reads up the daemon-local order core < resources < execution < sessions <
// server (R81). The global spine
// ranks all five layers at daemon-server's rank, so R5/R6/R80 are blind inside the zone.
// - Over WORKSPACE PACKAGE EXPORTS: no package but test-kit publishes test support (fixtures,
// fakes, mocks, harnesses, `__tests__/`, `./testing/*`); the offender set is a merge-base
// ratchet, and production code may not import those exports or test-kit (R83).
// - Over CONTRACTS PRODUCTION SOURCE: contracts owns vocabulary only — host, process, and timer
// mechanics belong in capture-kit or an adapter (R18).
// R6, R9, and the R10 R7 counts are ratchets with no written-down reference: each is the same
Expand Down Expand Up @@ -109,6 +112,7 @@ import {
} from './daemon-modularity.ts';
import {
checkPackageBoundaries,
readTrackedPackageManifests,
packageBoundariesSummary,
workspaceSpecifierTargets,
} from './package-boundaries.ts';
Expand All @@ -134,12 +138,17 @@ import { checkDaemonPlatformBoundary } from './daemon-platform-boundary.ts';
import { checkDaemonPlatformRuntimeInventory } from './daemon-platform-runtime-inventory.ts';
import { checkDaemonClientEntry } from './daemon-client-entry.ts';
import { checkDaemonProcessReach } from './daemon-process-reach.ts';
import {
checkPackageExportSurface,
packageExportSurfaceSummary,
} from './package-export-surface.ts';
import { checkDaemonLayers, DAEMON_ROOT } from './daemon-layers.ts';
import { checkSessionAuthorityOverlay, handlerOwnedOverlay } from './session-authority-overlay.ts';
import {
DAEMON_SOURCE_ROOT,
listTrackedPlatformZoneFiles,
listTrackedProductionSources,
listTrackedPackageFiles,
listTrackedSrcUtilsFiles,
listTrackedTypeScriptFiles,
} from './tracked-sources.ts';
Expand Down Expand Up @@ -435,7 +444,9 @@ function report(
`concrete platform imports in every executable and type-only form; ` +
`no production daemon file reaches the root platform-runtime family or a hub that ` +
`does (R76); the daemon process reaches no caller-side replay source module over ` +
`static or dynamic edges (R82); and the handler-owned SessionState/SessionStore ` +
`static or dynamic edges (R82); ` +
`${packageExportSurfaceSummary(ratchets.testOnlyExports, reference.testOnlyExports)}; and ` +
`the handler-owned SessionState/SessionStore ` +
`authority overlay holds at or under the merge-base (R75, ` +
`${handlerOwnedShapeFiles} shape / ${handlerOwnedAuthorityFiles} authority files); and ` +
`every production daemon file lives under a layer folder with no upward static edges ` +
Expand Down Expand Up @@ -522,6 +533,7 @@ export const LAYERING_RULE_IDS = [
'daemon-client-entry',
'session-authority-overlay',
'daemon-layers',
'package-export-surface',
] as const;

export type LayeringRuleId = (typeof LAYERING_RULE_IDS)[number];
Expand Down Expand Up @@ -595,6 +607,8 @@ export const LAYERING_RULES: Readonly<Record<LayeringRuleId, LayeringRule>> = {
context.sourceFiles.filter((file) => file.startsWith(DAEMON_SOURCE_ROOT)),
context.edges,
),
'package-export-surface': (context) =>
checkPackageExportSurface(context.ratchets.testOnlyExports, context.reference.testOnlyExports),
};

export function main(): number {
Expand All @@ -607,7 +621,10 @@ export function main(): number {
const parse = memoizedImportParser();
const edges = resolveImportEdges(sources, workspaceSpecifierTargets(repoRoot), parse);
// Measured once and threaded: the rules and the success line must report the same numbers.
const ratchets = measureRatchets(sources, edges);
const ratchets = measureRatchets(sources, edges, {
manifests: readTrackedPackageManifests(repoRoot),
packageFiles: listTrackedPackageFiles(repoRoot),
});
const reference = mergeBaseRatchets(repoRoot, parse);
const context: LayeringContext = {
sourceFiles,
Expand Down
13 changes: 5 additions & 8 deletions scripts/layering/daemon-platform-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
findDaemonPlatformDependencies,
} from './daemon-platform-boundary.ts';

const daemonFile = 'packages/daemon/src/terminal-boundary-fixture.ts';
const daemonFile = 'packages/daemon/src/terminal-boundary-case.ts';

function dependencies(source: string, file = daemonFile) {
return findDaemonPlatformDependencies([{ path: file, source }]);
Expand Down Expand Up @@ -92,7 +92,7 @@ test('R65 recognizes side-effect and aliased imports, including nested daemon pa
].join('\n');

assert.deepEqual(
dependencies(source, 'packages/daemon/src/sessions/handlers/terminal-boundary-fixture.ts').map(
dependencies(source, 'packages/daemon/src/sessions/handlers/terminal-boundary-case.ts').map(
(found) => ({
kind: found.kind,
line: found.line,
Expand Down Expand Up @@ -269,13 +269,10 @@ test('R65 rejects platform selection inside cleanup orchestrators', () => {

test('R65 ignores non-daemon and test-shaped records even when their syntax is red', () => {
const source = "import { platform } from '../../../src/platforms/android/runtime.ts';";
assert.deepEqual(dependencies(source, 'src/core/terminal-boundary-fixture.ts'), []);
assert.deepEqual(dependencies(source, 'src/core/terminal-boundary-case.ts'), []);
assert.deepEqual(dependencies(source, 'packages/daemon/src/terminal-boundary-case.test.ts'), []);
assert.deepEqual(
dependencies(source, 'packages/daemon/src/terminal-boundary-fixture.test.ts'),
[],
);
assert.deepEqual(
dependencies(source, 'packages/daemon/src/__tests__/terminal-boundary-fixture.ts'),
dependencies(source, 'packages/daemon/src/__tests__/terminal-boundary-case.ts'),
[],
);
});
Expand Down
16 changes: 8 additions & 8 deletions scripts/layering/daemon-platform-runtime-inventory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,18 @@ for (const [file, target, symbol] of [
test('R76 rejects an edge with the pair and its line', () => {
const sources = {
'src/platform-runtime-android-tool-host.ts': 'export function createAndroidToolHost() {}\n',
'packages/daemon/src/fixture.ts':
'packages/daemon/src/subject.ts':
"import { createAndroidToolHost } from '../../../src/platform-runtime-android-tool-host.ts';\n" +
'void createAndroidToolHost;\n',
};
const found = edgeViolations(sources, 'packages/daemon/src/fixture.ts');
const found = edgeViolations(sources, 'packages/daemon/src/subject.ts');
assert.equal(found.length, 1);
assert.equal(found[0]!.rule, DAEMON_PLATFORM_RUNTIME_RULE);
assert.equal(found[0]!.line, 1);
assert.match(found[0]!.message, /daemon coupling to root platform mechanics is banned/);
assert.match(
found[0]!.message,
/packages\/daemon\/src\/fixture\.ts -> src\/platform-runtime-android-tool-host\.ts/,
/packages\/daemon\/src\/subject\.ts -> src\/platform-runtime-android-tool-host\.ts/,
);
});

Expand Down Expand Up @@ -127,10 +127,10 @@ test('R76 ignores test-shaped and non-daemon importers', () => {
test('R76 ignores retired-zone targets, which R65 owns', () => {
const sources = {
'src/platforms/android.ts': 'export const legacy = 1;\n',
'packages/daemon/src/fixture.ts':
'packages/daemon/src/subject.ts':
'import { legacy } from "../../../src/platforms/android.ts";\nvoid legacy;\n',
};
assert.deepEqual(edgeViolations(sources, 'packages/daemon/src/fixture.ts'), []);
assert.deepEqual(edgeViolations(sources, 'packages/daemon/src/subject.ts'), []);
});

test('the root composition family is src/platform-runtime.ts plus src/platform-runtime-*.ts only', () => {
Expand All @@ -150,13 +150,13 @@ test('R76 catches a daemon import of a root hub that reaches the platform family
'src/hub-runtime.ts':
"import { gateway } from './platform-runtime.ts';\nvoid gateway;\n" +
'export function hubFact() {}\n',
'packages/daemon/src/fixture.ts':
'packages/daemon/src/subject.ts':
"import { hubFact } from '../../../src/hub-runtime.ts';\nvoid hubFact;\n",
};
const found = edgeViolations(sources, 'packages/daemon/src/fixture.ts');
const found = edgeViolations(sources, 'packages/daemon/src/subject.ts');
assert.equal(found.length, 1);
assert.equal(found[0]!.rule, DAEMON_PLATFORM_RUNTIME_RULE);
assert.match(found[0]!.message, /packages\/daemon\/src\/fixture\.ts -> src\/hub-runtime\.ts/);
assert.match(found[0]!.message, /packages\/daemon\/src\/subject\.ts -> src\/hub-runtime\.ts/);
});

test('R76 does not turn an intra-daemon hop into a hub edge', () => {
Expand Down
1 change: 0 additions & 1 deletion scripts/layering/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ export const UNRANKED_ZONES: ReadonlySet<string> = new Set([
'host-kit',
'capture-kit',
'provision-kit',
'test-kit',
...PLATFORMS.map((family) => `platform-${family}`),
'provider-webdriver',
'provider-limrun',
Expand Down
39 changes: 4 additions & 35 deletions scripts/layering/package-boundaries-facade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { test } from 'node:test';
import {
checkPackageInternalSites,
checkRootSites,
isTestConsumerFile,
specifierSites,
workspacePackagesFromManifests,
} from './package-boundaries.ts';
Expand Down Expand Up @@ -46,7 +45,7 @@ test('a production root file importing the ./testing/ namespace is refused', ()
true,
);
assert.equal(violations.length, 1);
assert.match(violations[0]!.message, /publishes test data to test files only/);
assert.match(violations[0]!.message, /test-only export/);
});

test('a test root file importing ./testing/ passes, and a named entry stays open to all', () => {
Expand Down Expand Up @@ -105,7 +104,7 @@ test('the ./testing/ refusal bites inside packages and through the vi.mock scan'
});

test('a package-local test/ tree is a test consumer, its src/ tree is not', () => {
// The file vocabulary TEST_ONLY_FIXTURE_SUBPATHS' predicate uses: a package's
// The file vocabulary R83's consumer check uses: a package's
// own harness tree (`packages/<pkg>/test/`) imports fixtures just like the
// root `test/` tree does; the same file under src/ does not (#3546 review).
const consumer = workspacePackagesFromManifests(
Expand Down Expand Up @@ -133,42 +132,12 @@ test('a package-local test/ tree is a test consumer, its src/ tree is not', () =
);
const denied = checkPackageInternalSites(
consumer,
sites('packages/daemon-side/src/harness.ts', fixture),
sites('packages/daemon-side/src/inventory.ts', fixture),
[daemon, consumer],
true,
);
assert.equal(denied.length, 1);
assert.match(denied[0]!.message, /publishes test data to test files only/);
});

test('isTestConsumerFile accepts the test vocabulary and denies production files', () => {
// One spelling serves both test-data gates (the ./testing/ façade check and the
// TEST_ONLY_FIXTURE_SUBPATHS predicate), so the vocabulary itself is pinned here:
// every accepted shape forwards to both gates, and the shapes that once spelled
// differently across the two copies (.test.tsx, .mts, a root __tests__/ tree) are
// in the accepted set (#3546 review).
for (const file of [
'test/integration/consumer.ts',
'packages/daemon-side/test/harness.ts',
'src/daemon/__tests__/consumer.ts',
'__tests__/root-consumer.ts',
'scripts/layering/daemon-layers.test.tsx',
'src/daemon/session-store.test.mts',
'src/foo.test.js',
'src/foo.test.cjs',
]) {
assert.equal(isTestConsumerFile(file), true, file);
}
for (const file of [
'src/commands/inventory.ts',
'packages/kernel/src/errors.ts',
'scripts/layering/package-boundaries.ts',
'testbed/notes.ts',
'docs/writing/tests.ts',
'src/foo.ts',
]) {
assert.equal(isTestConsumerFile(file), false, file);
}
assert.match(denied[0]!.message, /test-only export/);
});

test('a pattern entry matches deep specifiers, and an unnamed one is refused in scripts/', () => {
Expand Down
Loading
Loading