@ontrails/warden 1.0.0-beta.46 → 1.0.0-beta.48

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @ontrails/warden
2
2
 
3
+ ## 1.0.0-beta.48
4
+
5
+ ## 1.0.0-beta.47
6
+
7
+ ### Patch Changes
8
+
9
+ - [`ea2f296`](https://github.com/outfitter-dev/trails/commit/ea2f296e8119cf79bd03aeca3f0aa804f2fbcdd6): Classify adjacent package-route variants in the authored Warden AST regression
10
+ fixtures as explicit preserves so the completed transition remains auditable.
11
+ - [`90d394c`](https://github.com/outfitter-dev/trails/commit/90d394c005fdf6b898ba7052d0b56755af0f4954): Derive nested worktree, repository, and submodule collection boundaries in the
12
+ shared Source walker. Regrade and Warden now observe one directly targeted
13
+ working tree per run, and Regrade audit summaries expose boundary skip counts.
14
+
3
15
  ## 1.0.0-beta.46
4
16
 
5
17
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ontrails/warden",
3
- "version": "1.0.0-beta.46",
3
+ "version": "1.0.0-beta.48",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/outfitter-dev/trails.git",
@@ -32,20 +32,20 @@
32
32
  "clean": "rm -rf dist *.tsbuildinfo"
33
33
  },
34
34
  "dependencies": {
35
- "@ontrails/adapter-kit": "^1.0.0-beta.46",
36
- "@ontrails/cli": "^1.0.0-beta.46",
37
- "@ontrails/config": "^1.0.0-beta.46",
38
- "@ontrails/permits": "^1.0.0-beta.46",
39
- "@ontrails/source": "^1.0.0-beta.46",
40
- "@ontrails/store": "^1.0.0-beta.46",
35
+ "@ontrails/adapter-kit": "^1.0.0-beta.48",
36
+ "@ontrails/cli": "^1.0.0-beta.48",
37
+ "@ontrails/config": "^1.0.0-beta.48",
38
+ "@ontrails/permits": "^1.0.0-beta.48",
39
+ "@ontrails/source": "^1.0.0-beta.48",
40
+ "@ontrails/store": "^1.0.0-beta.48",
41
41
  "oxc-resolver": "11.19.1",
42
42
  "zod": "^4.3.5"
43
43
  },
44
44
  "devDependencies": {
45
- "@ontrails/testing": "^1.0.0-beta.46"
45
+ "@ontrails/testing": "^1.0.0-beta.48"
46
46
  },
47
47
  "peerDependencies": {
48
- "@ontrails/core": "^1.0.0-beta.46",
49
- "@ontrails/topography": "^1.0.0-beta.46"
48
+ "@ontrails/core": "^1.0.0-beta.48",
49
+ "@ontrails/topography": "^1.0.0-beta.48"
50
50
  }
51
51
  }
package/src/cli.ts CHANGED
@@ -63,7 +63,12 @@ import {
63
63
  collectCrudTableIds as collectCrudTableIdsFromAst,
64
64
  collectReconcileTableIds as collectReconcileTableIdsFromAst,
65
65
  } from './rules/source/stores.js';
66
- import { findTrailDefinitions, parse } from '@ontrails/source';
66
+ import {
67
+ collectSourceTree,
68
+ findTrailDefinitions,
69
+ parse,
70
+ } from '@ontrails/source';
71
+ import type { SourceTreeCollection } from '@ontrails/source';
67
72
  import { collectFileCrudCoverage } from './rules/incomplete-crud.js';
68
73
  import { wardenRules, wardenTopoRules } from './rules/index.js';
69
74
  import { getWardenRuleMetadata } from './rules/metadata.js';
@@ -238,29 +243,28 @@ export interface WardenReport {
238
243
  * surfaces that should not contribute most committed-source diagnostics.
239
244
  */
240
245
  const collectFilesMatching = (
241
- dir: string,
246
+ collection: NonNullable<ReturnType<typeof collectSourceTree>>,
242
247
  pattern: string,
243
248
  dot = false
244
249
  ): readonly string[] => {
245
250
  const glob = new Bun.Glob(pattern);
246
- let matches: IterableIterator<string>;
247
- try {
248
- matches = glob.scanSync({ cwd: dir, dot, onlyFiles: true });
249
- } catch {
250
- return [];
251
- }
252
-
253
251
  const files: string[] = [];
254
- for (const match of matches) {
255
- if (isWardenSourceScanTarget(match)) {
256
- files.push(`${dir}/${match}`);
252
+ for (const file of collection.files) {
253
+ if (
254
+ (dot ||
255
+ !file.path.split('/').some((segment) => segment.startsWith('.'))) &&
256
+ glob.match(file.path) &&
257
+ isWardenSourceScanTarget(file.path)
258
+ ) {
259
+ files.push(file.absolutePath);
257
260
  }
258
261
  }
259
262
  return files;
260
263
  };
261
264
 
262
- const collectTsFiles = (dir: string): readonly string[] =>
263
- collectFilesMatching(dir, '**/*.ts');
265
+ const collectTsFiles = (
266
+ collection: NonNullable<ReturnType<typeof collectSourceTree>>
267
+ ): readonly string[] => collectFilesMatching(collection, '**/*.ts');
264
268
 
265
269
  const draftModeIncludesFile = (
266
270
  filePath: string,
@@ -309,41 +313,42 @@ const filterSourceFilesByScope = (
309
313
 
310
314
  const EMPTY_WARDEN_SCOPE: WardenScope = { exclude: [] };
311
315
 
312
- const collectDevPermitTestFiles = (dir: string): readonly string[] => {
316
+ const collectDevPermitTestFiles = (
317
+ collection: NonNullable<ReturnType<typeof collectSourceTree>>
318
+ ): readonly string[] => {
313
319
  const glob = new Bun.Glob('**/*.ts');
314
- let matches: IterableIterator<string>;
315
- try {
316
- matches = glob.scanSync({ cwd: dir, onlyFiles: true });
317
- } catch {
318
- return [];
319
- }
320
-
321
320
  const files: string[] = [];
322
- for (const match of matches) {
323
- if (isWardenDevPermitTestScanTarget(match)) {
324
- files.push(`${dir}/${match}`);
321
+ for (const file of collection.files) {
322
+ if (
323
+ !file.path.split('/').some((segment) => segment.startsWith('.')) &&
324
+ glob.match(file.path) &&
325
+ isWardenDevPermitTestScanTarget(file.path)
326
+ ) {
327
+ files.push(file.absolutePath);
325
328
  }
326
329
  }
327
330
  return files;
328
331
  };
329
332
 
330
- const collectTextScanFiles = (dir: string): readonly string[] => [
331
- ...collectFilesMatching(dir, '**/*.sh', true),
332
- ...collectFilesMatching(dir, '**/*.bash', true),
333
- ...collectFilesMatching(dir, '**/*.zsh', true),
334
- ...collectFilesMatching(dir, '**/*.yml', true),
335
- ...collectFilesMatching(dir, '**/*.yaml', true),
336
- ...collectFilesMatching(dir, 'plugin/**/*.json', true),
337
- ...collectFilesMatching(dir, 'plugin/**/*.jsonc', true),
338
- ...collectFilesMatching(dir, 'plugin/**/*.toml', true),
339
- ...collectFilesMatching(dir, 'plugin/**/*.cjs', true),
340
- ...collectFilesMatching(dir, 'plugin/**/*.mjs', true),
341
- ...collectFilesMatching(dir, 'scripts/**/*.json', true),
342
- ...collectFilesMatching(dir, 'scripts/**/*.jsonc', true),
343
- ...collectFilesMatching(dir, 'scripts/**/*.toml', true),
344
- ...collectFilesMatching(dir, 'scripts/**/*.cjs', true),
345
- ...collectFilesMatching(dir, 'scripts/**/*.mjs', true),
346
- ...collectFilesMatching(dir, '**/package.json', true),
333
+ const collectTextScanFiles = (
334
+ collection: NonNullable<ReturnType<typeof collectSourceTree>>
335
+ ): readonly string[] => [
336
+ ...collectFilesMatching(collection, '**/*.sh', true),
337
+ ...collectFilesMatching(collection, '**/*.bash', true),
338
+ ...collectFilesMatching(collection, '**/*.zsh', true),
339
+ ...collectFilesMatching(collection, '**/*.yml', true),
340
+ ...collectFilesMatching(collection, '**/*.yaml', true),
341
+ ...collectFilesMatching(collection, 'plugin/**/*.json', true),
342
+ ...collectFilesMatching(collection, 'plugin/**/*.jsonc', true),
343
+ ...collectFilesMatching(collection, 'plugin/**/*.toml', true),
344
+ ...collectFilesMatching(collection, 'plugin/**/*.cjs', true),
345
+ ...collectFilesMatching(collection, 'plugin/**/*.mjs', true),
346
+ ...collectFilesMatching(collection, 'scripts/**/*.json', true),
347
+ ...collectFilesMatching(collection, 'scripts/**/*.jsonc', true),
348
+ ...collectFilesMatching(collection, 'scripts/**/*.toml', true),
349
+ ...collectFilesMatching(collection, 'scripts/**/*.cjs', true),
350
+ ...collectFilesMatching(collection, 'scripts/**/*.mjs', true),
351
+ ...collectFilesMatching(collection, '**/package.json', true),
347
352
  ];
348
353
 
349
354
  const isDocumentationScanTarget = (match: string): boolean => {
@@ -367,19 +372,19 @@ const isDocumentationScanTarget = (match: string): boolean => {
367
372
  );
368
373
  };
369
374
 
370
- const collectDocumentationFiles = (dir: string): readonly string[] => {
375
+ const collectDocumentationFiles = (
376
+ collection: NonNullable<ReturnType<typeof collectSourceTree>>
377
+ ): readonly string[] => {
371
378
  const glob = new Bun.Glob('**/*.md');
372
- let matches: IterableIterator<string>;
373
- try {
374
- matches = glob.scanSync({ cwd: dir, onlyFiles: true });
375
- } catch {
376
- return [];
377
- }
378
-
379
379
  const files: string[] = [];
380
- for (const match of matches) {
381
- if (isWardenSourceScanTarget(match) && isDocumentationScanTarget(match)) {
382
- files.push(`${dir}/${match}`);
380
+ for (const file of collection.files) {
381
+ if (
382
+ !file.path.split('/').some((segment) => segment.startsWith('.')) &&
383
+ glob.match(file.path) &&
384
+ isWardenSourceScanTarget(file.path) &&
385
+ isDocumentationScanTarget(file.path)
386
+ ) {
387
+ files.push(file.absolutePath);
383
388
  }
384
389
  }
385
390
  return files;
@@ -692,12 +697,34 @@ const collectReconcileTableIds = (
692
697
  }
693
698
  };
694
699
 
695
- const loadSourceFiles = async (
696
- rootDir: string
697
- ): Promise<readonly SourceFile[]> => {
700
+ interface LoadedSourceFiles {
701
+ readonly collection: SourceTreeCollection | null;
702
+ readonly sourceFiles: readonly SourceFile[];
703
+ }
704
+
705
+ const loadSourceFiles = async (rootDir: string): Promise<LoadedSourceFiles> => {
698
706
  const sourceFiles: SourceFile[] = [];
707
+ const collection = collectSourceTree(rootDir, {
708
+ classify: (entry) => {
709
+ if (
710
+ entry.kind === 'directory' &&
711
+ ['.git', 'dist', 'node_modules'].includes(entry.name)
712
+ ) {
713
+ return { action: 'skip', reason: 'ignored-directory' };
714
+ }
715
+ if (entry.kind === 'directory') {
716
+ return { action: 'recurse' };
717
+ }
718
+ return entry.kind === 'file'
719
+ ? { action: 'collect' }
720
+ : { action: 'skip', reason: 'unsupported-entry' };
721
+ },
722
+ });
723
+ if (!collection) {
724
+ return { collection, sourceFiles };
725
+ }
699
726
 
700
- for (const filePath of collectTsFiles(rootDir)) {
727
+ for (const filePath of collectTsFiles(collection)) {
701
728
  try {
702
729
  sourceFiles.push({
703
730
  filePath,
@@ -709,7 +736,7 @@ const loadSourceFiles = async (
709
736
  }
710
737
  }
711
738
 
712
- for (const filePath of collectTextScanFiles(rootDir)) {
739
+ for (const filePath of collectTextScanFiles(collection)) {
713
740
  try {
714
741
  sourceFiles.push({
715
742
  filePath,
@@ -721,7 +748,7 @@ const loadSourceFiles = async (
721
748
  }
722
749
  }
723
750
 
724
- for (const filePath of collectDocumentationFiles(rootDir)) {
751
+ for (const filePath of collectDocumentationFiles(collection)) {
725
752
  try {
726
753
  sourceFiles.push({
727
754
  filePath,
@@ -733,7 +760,7 @@ const loadSourceFiles = async (
733
760
  }
734
761
  }
735
762
 
736
- for (const filePath of collectDevPermitTestFiles(rootDir)) {
763
+ for (const filePath of collectDevPermitTestFiles(collection)) {
737
764
  try {
738
765
  sourceFiles.push({
739
766
  filePath,
@@ -745,7 +772,7 @@ const loadSourceFiles = async (
745
772
  }
746
773
  }
747
774
 
748
- return sourceFiles;
775
+ return { collection, sourceFiles };
749
776
  };
750
777
 
751
778
  const collectTopoKnownIds = (
@@ -983,6 +1010,7 @@ const collectAuthoredMcpSurfaceBindingSets = (
983
1010
 
984
1011
  const buildProjectContext = (
985
1012
  sourceFiles: readonly SourceFile[],
1013
+ collection: SourceTreeCollection | null,
986
1014
  rootDir: string,
987
1015
  appTopos: readonly Topo[] = [],
988
1016
  scope: WardenScope = EMPTY_WARDEN_SCOPE,
@@ -1003,6 +1031,11 @@ const buildProjectContext = (
1003
1031
  (sourceFile) => sourceFile.kind === 'documentation'
1004
1032
  );
1005
1033
  context.publicWorkspaces = collectPublicWorkspaces(rootDir, {
1034
+ collectedPackageJsonPaths: new Set(
1035
+ collection?.files
1036
+ .filter((file) => file.path.endsWith('package.json'))
1037
+ .map((file) => file.path)
1038
+ ),
1006
1039
  exclude: scope.exclude,
1007
1040
  });
1008
1041
  context.governedVocabularyHistoryRequired =
@@ -1366,13 +1399,15 @@ const lintFiles = async (
1366
1399
  };
1367
1400
  }
1368
1401
 
1402
+ const loaded = await loadSourceFiles(rootDir);
1369
1403
  const sourceFiles = filterSourceFilesByScope(
1370
- filterSourceFilesByDraftMode(await loadSourceFiles(rootDir), drafts),
1404
+ filterSourceFilesByDraftMode(loaded.sourceFiles, drafts),
1371
1405
  rootDir,
1372
1406
  scope
1373
1407
  );
1374
1408
  const context = buildProjectContext(
1375
1409
  sourceFiles,
1410
+ loaded.collection,
1376
1411
  rootDir,
1377
1412
  topoTargets.map((target) => target.topo),
1378
1413
  scope,
@@ -744,6 +744,16 @@ export const governedVocabularyTransitions =
744
744
  reason:
745
745
  'Preserve transition regression fixtures and the intentional negative resolver assertion after the public route is retired.',
746
746
  },
747
+ {
748
+ paths: [
749
+ 'apps/trails/src/__tests__/regrade.test.ts',
750
+ 'packages/regrade/src/downstream/__tests__/ast-rewrite.test.ts',
751
+ 'packages/regrade/src/downstream/__tests__/vocabulary.test.ts',
752
+ ],
753
+ pattern: '^@ontrails/warden/ast(?:\\.js|\\.utils|/utils)$',
754
+ reason:
755
+ 'Preserve the adjacent package-route variants used by the authored Warden AST transition regression fixtures.',
756
+ },
747
757
  ],
748
758
  reviewForms: [],
749
759
  safeRewriteForms: {
package/src/workspaces.ts CHANGED
@@ -2,10 +2,14 @@
2
2
  * Workspace-manifest discovery for Warden project-aware rules.
3
3
  */
4
4
 
5
- import { realpathSync } from 'node:fs';
6
- import { dirname, join, relative, resolve } from 'node:path';
5
+ import { readFileSync, realpathSync } from 'node:fs';
6
+ import { dirname, join, posix, relative, resolve } from 'node:path';
7
7
 
8
- import { listWorkspacePackages, matchesAnyPathGlob } from '@ontrails/core';
8
+ import {
9
+ listWorkspacePackages,
10
+ listWorkspacePatterns,
11
+ matchesAnyPathGlob,
12
+ } from '@ontrails/core';
9
13
 
10
14
  export interface WardenPublicWorkspace {
11
15
  readonly name: string;
@@ -18,6 +22,8 @@ export interface WardenPublicWorkspace {
18
22
  }
19
23
 
20
24
  export interface WardenWorkspaceCollectionOptions {
25
+ /** Root-relative package manifests observed by the owning source collection. */
26
+ readonly collectedPackageJsonPaths?: ReadonlySet<string> | undefined;
21
27
  readonly exclude?: readonly string[];
22
28
  }
23
29
 
@@ -27,6 +33,7 @@ interface PackageManifest {
27
33
  readonly files?: unknown;
28
34
  readonly name?: unknown;
29
35
  readonly private?: unknown;
36
+ readonly workspaces?: unknown;
30
37
  }
31
38
 
32
39
  const normalizePath = (path: string): string => path.replaceAll('\\', '/');
@@ -42,6 +49,60 @@ const normalizeRealPath = (path: string): string => {
42
49
  const rootRelativePath = (rootDir: string, path: string): string =>
43
50
  normalizePath(relative(rootDir, path));
44
51
 
52
+ const readPackageManifest = (path: string): PackageManifest | undefined => {
53
+ try {
54
+ return JSON.parse(readFileSync(path, 'utf8')) as PackageManifest;
55
+ } catch {
56
+ return undefined;
57
+ }
58
+ };
59
+
60
+ const matchesWorkspacePattern = (
61
+ workspacePath: string,
62
+ pattern: string
63
+ ): boolean => {
64
+ const normalizedPattern = normalizePath(pattern).replace(/^\.\//, '');
65
+ return normalizedPattern.endsWith('/*')
66
+ ? posix.dirname(workspacePath) === normalizedPattern.slice(0, -2)
67
+ : workspacePath === normalizedPattern;
68
+ };
69
+
70
+ interface WorkspaceManifestCandidate {
71
+ readonly manifest: PackageManifest;
72
+ readonly packageJsonPath: string;
73
+ }
74
+
75
+ const collectedWorkspaceManifests = (
76
+ rootDir: string,
77
+ packageJsonPaths: ReadonlySet<string>
78
+ ): readonly WorkspaceManifestCandidate[] => {
79
+ if (!packageJsonPaths.has('package.json')) {
80
+ return [];
81
+ }
82
+ const rootManifest = readPackageManifest(join(rootDir, 'package.json'));
83
+ const patterns = listWorkspacePatterns(rootManifest);
84
+
85
+ return [...packageJsonPaths]
86
+ .filter((path) => path !== 'package.json' && path.endsWith('/package.json'))
87
+ .toSorted()
88
+ .flatMap((path): readonly WorkspaceManifestCandidate[] => {
89
+ const normalizedPath = normalizePath(path).replace(/^\.\//, '');
90
+ const workspacePath = posix.dirname(normalizedPath);
91
+ if (
92
+ posix.isAbsolute(normalizedPath) ||
93
+ normalizedPath.startsWith('../') ||
94
+ !patterns.some((pattern) =>
95
+ matchesWorkspacePattern(workspacePath, pattern)
96
+ )
97
+ ) {
98
+ return [];
99
+ }
100
+ const packageJsonPath = resolve(rootDir, normalizedPath);
101
+ const manifest = readPackageManifest(packageJsonPath);
102
+ return manifest ? [{ manifest, packageJsonPath }] : [];
103
+ });
104
+ };
105
+
45
106
  const isRecord = (value: unknown): value is Record<string, unknown> =>
46
107
  Boolean(value && typeof value === 'object' && !Array.isArray(value));
47
108
 
@@ -172,10 +233,14 @@ export const collectPublicWorkspaces = (
172
233
  ): ReadonlyMap<string, WardenPublicWorkspace> => {
173
234
  const normalizedRoot = normalizeRealPath(rootDir);
174
235
  const workspaces = new Map<string, WardenPublicWorkspace>();
236
+ const candidates = options.collectedPackageJsonPaths
237
+ ? collectedWorkspaceManifests(
238
+ normalizedRoot,
239
+ options.collectedPackageJsonPaths
240
+ )
241
+ : listWorkspacePackages<PackageManifest>(normalizedRoot);
175
242
 
176
- for (const workspacePackage of listWorkspacePackages<PackageManifest>(
177
- normalizedRoot
178
- )) {
243
+ for (const workspacePackage of candidates) {
179
244
  const workspace = publicWorkspaceFromManifest(
180
245
  workspacePackage.packageJsonPath,
181
246
  workspacePackage.manifest