@ontrails/warden 1.0.0-beta.30 → 1.0.0-beta.32
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 +49 -0
- package/README.md +22 -0
- package/bin/warden.ts +1 -0
- package/package.json +9 -9
- package/src/cli.ts +43 -7
- package/src/command.ts +23 -0
- package/src/config.ts +9 -0
- package/src/index.ts +2 -0
- package/src/project-context.ts +31 -2
- package/src/rules/implementation-returns-result.ts +1 -3
- package/src/rules/index.ts +1 -0
- package/src/rules/metadata.ts +3 -3
- package/src/rules/permit-governance.ts +1 -1
- package/src/rules/public-internal-deep-imports.ts +3 -63
- package/src/rules/surface-facet-coherence.ts +1 -1
- package/src/rules/trail-fork-coaching.ts +1 -1
- package/src/rules/types.ts +26 -3
- package/src/trails/surface-facet-coherence.trail.ts +1 -1
- package/src/trails/trail-fork-coaching.trail.ts +1 -1
- package/src/workspaces.ts +30 -69
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# @ontrails/warden
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.32
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 3e5c0fc: Export shared diagnostic base types from core and align governance diagnostic
|
|
8
|
+
severity vocabulary across adapter checks, permits, and Warden.
|
|
9
|
+
- f3c4fef: Export a shared `escapeRegExp` helper from core and migrate first-party callers off local copies.
|
|
10
|
+
- cb0a9d8: Export shared workspace package discovery helpers from core and migrate first-party discovery callers.
|
|
11
|
+
- fe72b84: Fold remaining Regrade and Warden scan-target surfaces onto the shared path-scope vocabulary.
|
|
12
|
+
- Updated dependencies [3e5c0fc]
|
|
13
|
+
- Updated dependencies [f3c4fef]
|
|
14
|
+
- Updated dependencies [cb0a9d8]
|
|
15
|
+
- Updated dependencies [8db145e]
|
|
16
|
+
- Updated dependencies [2b819f4]
|
|
17
|
+
- Updated dependencies [21c6dda]
|
|
18
|
+
- Updated dependencies [860ef32]
|
|
19
|
+
- Updated dependencies [fe72b84]
|
|
20
|
+
- @ontrails/adapter-kit@1.0.0-beta.32
|
|
21
|
+
- @ontrails/core@1.0.0-beta.32
|
|
22
|
+
- @ontrails/permits@1.0.0-beta.32
|
|
23
|
+
- @ontrails/topographer@1.0.0-beta.32
|
|
24
|
+
- @ontrails/config@1.0.0-beta.32
|
|
25
|
+
- @ontrails/cli@1.0.0-beta.32
|
|
26
|
+
- @ontrails/store@1.0.0-beta.32
|
|
27
|
+
|
|
28
|
+
## 1.0.0-beta.31
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
- ee9f3ae: Let Warden fix capabilities declare downstream scan targets and have Regrade
|
|
33
|
+
honor those targets for Warden-backed term-rewrite classes.
|
|
34
|
+
|
|
35
|
+
Dogfood the first safe facet-to-trailhead prose rewrite through project-local
|
|
36
|
+
Warden rules and Regrade.
|
|
37
|
+
|
|
38
|
+
- a0126d9: Add Warden `scope.exclude` globs through project config and the Trails CLI
|
|
39
|
+
wrapper so governance runs can exclude local notes, scratch space, and generated
|
|
40
|
+
state without dropping durable skills or plugin assets from scope.
|
|
41
|
+
- 6a26a08: Rename Warden governance scope controls from jurisdiction ignore settings to `scope.exclude` across config, CLI, and Trails surfaces.
|
|
42
|
+
- Updated dependencies [4cd5d4e]
|
|
43
|
+
- Updated dependencies [38907cc]
|
|
44
|
+
- @ontrails/core@1.0.0-beta.31
|
|
45
|
+
- @ontrails/adapter-kit@1.0.0-beta.31
|
|
46
|
+
- @ontrails/cli@1.0.0-beta.31
|
|
47
|
+
- @ontrails/config@1.0.0-beta.31
|
|
48
|
+
- @ontrails/permits@1.0.0-beta.31
|
|
49
|
+
- @ontrails/store@1.0.0-beta.31
|
|
50
|
+
- @ontrails/topographer@1.0.0-beta.31
|
|
51
|
+
|
|
3
52
|
## 1.0.0-beta.30
|
|
4
53
|
|
|
5
54
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -47,6 +47,28 @@ Warden uses the shared Trails project-root resolver when a caller does not pass
|
|
|
47
47
|
|
|
48
48
|
This is the right home for repo-specific migration checks or governance that has not earned a place in `@ontrails/warden` itself.
|
|
49
49
|
|
|
50
|
+
## Scope
|
|
51
|
+
|
|
52
|
+
Use `warden.scope.exclude` in `trails.config.*` to keep generated, scratch, or local planning paths outside Warden governance while leaving durable project paths in scope.
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"warden": {
|
|
57
|
+
"scope": {
|
|
58
|
+
"exclude": [".scratch/**", ".agents/notes/**"]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
For a single run, pass one or more root-relative globs:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
warden --scope-exclude '.scratch/**' --scope-exclude '.agents/notes/**'
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Scope is a governance boundary, not a migration scan boundary. Regrade uses its own `scope.exclude` / `--exclude` controls for migration plans.
|
|
71
|
+
|
|
50
72
|
A rule module may export `rule`, `rules`, `sourceRule`, `sourceRules`, `topoRule`, or `topoRules`. Rules without explicit metadata receive default repo-local source-static or topo-aware metadata so short migration rules can run without extra ceremony. Project-aware source rules that provide `checkWithContext()` default to repo-local project-static metadata.
|
|
51
73
|
|
|
52
74
|
```typescript
|
package/bin/warden.ts
CHANGED
|
@@ -19,6 +19,7 @@ Options:
|
|
|
19
19
|
--fail-on <value> error or warning
|
|
20
20
|
--format <value> summary, github, or json
|
|
21
21
|
--drafts <value> include, exclude, or only
|
|
22
|
+
--scope-exclude <glob> Root-relative path glob Warden should not govern
|
|
22
23
|
--lock <value> auto, cached, refresh, or skip
|
|
23
24
|
--no-lock-mutation Do not write lock artifacts
|
|
24
25
|
--strict Fail on warnings
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ontrails/warden",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.32",
|
|
4
4
|
"bin": {
|
|
5
5
|
"warden": "./bin/warden.ts"
|
|
6
6
|
},
|
|
@@ -28,21 +28,21 @@
|
|
|
28
28
|
"clean": "rm -rf dist *.tsbuildinfo"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@ontrails/adapter-kit": "^1.0.0-beta.
|
|
32
|
-
"@ontrails/cli": "^1.0.0-beta.
|
|
33
|
-
"@ontrails/config": "^1.0.0-beta.
|
|
34
|
-
"@ontrails/permits": "^1.0.0-beta.
|
|
35
|
-
"@ontrails/store": "^1.0.0-beta.
|
|
31
|
+
"@ontrails/adapter-kit": "^1.0.0-beta.32",
|
|
32
|
+
"@ontrails/cli": "^1.0.0-beta.32",
|
|
33
|
+
"@ontrails/config": "^1.0.0-beta.32",
|
|
34
|
+
"@ontrails/permits": "^1.0.0-beta.32",
|
|
35
|
+
"@ontrails/store": "^1.0.0-beta.32",
|
|
36
36
|
"oxc-parser": "^0.121.0",
|
|
37
37
|
"oxc-resolver": "11.19.1",
|
|
38
38
|
"oxc-walker": "^1.0.0",
|
|
39
39
|
"zod": "^4.3.5"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@ontrails/testing": "^1.0.0-beta.
|
|
42
|
+
"@ontrails/testing": "^1.0.0-beta.32"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@ontrails/core": "^1.0.0-beta.
|
|
46
|
-
"@ontrails/topographer": "^1.0.0-beta.
|
|
45
|
+
"@ontrails/core": "^1.0.0-beta.32",
|
|
46
|
+
"@ontrails/topographer": "^1.0.0-beta.32"
|
|
47
47
|
}
|
|
48
48
|
}
|
package/src/cli.ts
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
import { isAbsolute, relative, resolve } from 'node:path';
|
|
9
9
|
|
|
10
10
|
import { resolveTrailsProjectRoot } from '@ontrails/config';
|
|
11
|
+
import { getContourReferences, matchesAnyPathGlob } from '@ontrails/core';
|
|
11
12
|
import type { Topo } from '@ontrails/core';
|
|
12
13
|
import { deriveTopoGraph } from '@ontrails/topographer';
|
|
13
14
|
import type { TopoGraph } from '@ontrails/topographer';
|
|
14
|
-
import { getContourReferences } from '@ontrails/core';
|
|
15
15
|
|
|
16
16
|
import type {
|
|
17
17
|
EffectiveWardenConfig,
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
WardenDepth,
|
|
21
21
|
WardenFailOn,
|
|
22
22
|
WardenFormat,
|
|
23
|
+
WardenScope,
|
|
23
24
|
WardenLockMode,
|
|
24
25
|
} from './config.js';
|
|
25
26
|
import { runWardenAdapterChecks } from './adapter-check.js';
|
|
@@ -105,6 +106,8 @@ export interface WardenRunOptions {
|
|
|
105
106
|
readonly fix?: boolean | undefined;
|
|
106
107
|
/** Output format requested by the caller. */
|
|
107
108
|
readonly format?: WardenFormat | undefined;
|
|
109
|
+
/** Root-relative source paths that Warden should not govern. */
|
|
110
|
+
readonly scope?: WardenScope | undefined;
|
|
108
111
|
/** Lockfile mode requested by the caller. */
|
|
109
112
|
readonly lock?: WardenLockMode | undefined;
|
|
110
113
|
/** Suppress lockfile mutation for CI/pre-push callers. */
|
|
@@ -260,6 +263,29 @@ const filterSourceFilesByDraftMode = (
|
|
|
260
263
|
draftModeIncludesFile(sourceFile.filePath, drafts)
|
|
261
264
|
);
|
|
262
265
|
|
|
266
|
+
const rootRelativeScopePath = (rootDir: string, filePath: string): string =>
|
|
267
|
+
relative(rootDir, filePath).replaceAll('\\', '/');
|
|
268
|
+
|
|
269
|
+
const filterSourceFilesByScope = (
|
|
270
|
+
sourceFiles: readonly SourceFile[],
|
|
271
|
+
rootDir: string,
|
|
272
|
+
scope: WardenScope
|
|
273
|
+
): readonly SourceFile[] => {
|
|
274
|
+
if (scope.exclude.length === 0) {
|
|
275
|
+
return sourceFiles;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return sourceFiles.filter(
|
|
279
|
+
(sourceFile) =>
|
|
280
|
+
!matchesAnyPathGlob(
|
|
281
|
+
rootRelativeScopePath(rootDir, sourceFile.filePath),
|
|
282
|
+
scope.exclude
|
|
283
|
+
)
|
|
284
|
+
);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const EMPTY_WARDEN_SCOPE: WardenScope = { exclude: [] };
|
|
288
|
+
|
|
263
289
|
const collectDevPermitTestFiles = (dir: string): readonly string[] => {
|
|
264
290
|
const glob = new Bun.Glob('**/*.ts');
|
|
265
291
|
let matches: IterableIterator<string>;
|
|
@@ -799,6 +825,7 @@ const collectFileImportResolutions = (
|
|
|
799
825
|
context: MutableProjectContext
|
|
800
826
|
): void => {
|
|
801
827
|
const resolutionsByFile = collectProjectImportResolutions({
|
|
828
|
+
publicWorkspaces: context.publicWorkspaces,
|
|
802
829
|
rootDir,
|
|
803
830
|
sourceFiles,
|
|
804
831
|
});
|
|
@@ -813,6 +840,7 @@ const collectFileDocumentedImportResolutions = (
|
|
|
813
840
|
context: MutableProjectContext
|
|
814
841
|
): void => {
|
|
815
842
|
const resolutionsByFile = collectProjectDocumentationImportResolutions({
|
|
843
|
+
publicWorkspaces: context.publicWorkspaces,
|
|
816
844
|
rootDir,
|
|
817
845
|
sourceFiles,
|
|
818
846
|
});
|
|
@@ -824,7 +852,8 @@ const collectFileDocumentedImportResolutions = (
|
|
|
824
852
|
const buildProjectContext = (
|
|
825
853
|
sourceFiles: readonly SourceFile[],
|
|
826
854
|
rootDir: string,
|
|
827
|
-
appTopos: readonly Topo[] = []
|
|
855
|
+
appTopos: readonly Topo[] = [],
|
|
856
|
+
scope: WardenScope = EMPTY_WARDEN_SCOPE
|
|
828
857
|
): ProjectContext => {
|
|
829
858
|
const context = createMutableProjectContext();
|
|
830
859
|
const typeScriptSourceFiles = sourceFiles.filter(
|
|
@@ -833,7 +862,9 @@ const buildProjectContext = (
|
|
|
833
862
|
const documentationSourceFiles = sourceFiles.filter(
|
|
834
863
|
(sourceFile) => sourceFile.kind === 'documentation'
|
|
835
864
|
);
|
|
836
|
-
context.publicWorkspaces = collectPublicWorkspaces(rootDir
|
|
865
|
+
context.publicWorkspaces = collectPublicWorkspaces(rootDir, {
|
|
866
|
+
exclude: scope.exclude,
|
|
867
|
+
});
|
|
837
868
|
|
|
838
869
|
if (appTopos.length > 0) {
|
|
839
870
|
for (const appTopo of appTopos) {
|
|
@@ -1145,6 +1176,7 @@ interface WardenLintResult {
|
|
|
1145
1176
|
const lintFiles = async (
|
|
1146
1177
|
rootDir: string,
|
|
1147
1178
|
drafts: EffectiveWardenConfig['drafts'],
|
|
1179
|
+
scope: EffectiveWardenConfig['scope'],
|
|
1148
1180
|
topoTargets: readonly WardenTopoTarget[],
|
|
1149
1181
|
extraTopoRules: readonly TopoAwareWardenRule[],
|
|
1150
1182
|
extraSourceRules: readonly WardenRule[],
|
|
@@ -1159,14 +1191,16 @@ const lintFiles = async (
|
|
|
1159
1191
|
};
|
|
1160
1192
|
}
|
|
1161
1193
|
|
|
1162
|
-
const sourceFiles =
|
|
1163
|
-
await loadSourceFiles(rootDir),
|
|
1164
|
-
|
|
1194
|
+
const sourceFiles = filterSourceFilesByScope(
|
|
1195
|
+
filterSourceFilesByDraftMode(await loadSourceFiles(rootDir), drafts),
|
|
1196
|
+
rootDir,
|
|
1197
|
+
scope
|
|
1165
1198
|
);
|
|
1166
1199
|
const context = buildProjectContext(
|
|
1167
1200
|
sourceFiles,
|
|
1168
1201
|
rootDir,
|
|
1169
|
-
topoTargets.map((target) => target.topo)
|
|
1202
|
+
topoTargets.map((target) => target.topo),
|
|
1203
|
+
scope
|
|
1170
1204
|
);
|
|
1171
1205
|
const allDiagnostics: WardenDiagnostic[] = [
|
|
1172
1206
|
...lintSourceFiles(sourceFiles, context, extraSourceRules, selector),
|
|
@@ -1339,6 +1373,7 @@ const buildCliConfigLayer = (options: WardenRunOptions): WardenConfigLayer => ({
|
|
|
1339
1373
|
...(options.drafts ? { drafts: options.drafts } : {}),
|
|
1340
1374
|
...(options.failOn ? { failOn: options.failOn } : {}),
|
|
1341
1375
|
...(options.format ? { format: options.format } : {}),
|
|
1376
|
+
...(options.scope ? { scope: options.scope } : {}),
|
|
1342
1377
|
...(options.lock ? { lock: options.lock } : {}),
|
|
1343
1378
|
...(options.noLockMutation === undefined
|
|
1344
1379
|
? {}
|
|
@@ -1476,6 +1511,7 @@ export const runWarden = async (
|
|
|
1476
1511
|
? await lintFiles(
|
|
1477
1512
|
rootDir,
|
|
1478
1513
|
effectiveConfig.drafts,
|
|
1514
|
+
effectiveConfig.scope,
|
|
1479
1515
|
topoTargets,
|
|
1480
1516
|
[...projectRules.topoRules, ...(options.extraTopoRules ?? [])],
|
|
1481
1517
|
[...projectRules.sourceRules, ...(options.extraSourceRules ?? [])],
|
package/src/command.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
WardenDraftsMode,
|
|
27
27
|
WardenFailOn,
|
|
28
28
|
WardenFormat,
|
|
29
|
+
WardenScope,
|
|
29
30
|
WardenLockMode,
|
|
30
31
|
} from './config.js';
|
|
31
32
|
import {
|
|
@@ -57,6 +58,7 @@ interface MutableWardenConfigLayer {
|
|
|
57
58
|
drafts?: WardenDraftsMode | undefined;
|
|
58
59
|
failOn?: WardenFailOn | undefined;
|
|
59
60
|
format?: WardenFormat | undefined;
|
|
61
|
+
scope?: WardenScope | undefined;
|
|
60
62
|
lock?: WardenLockMode | undefined;
|
|
61
63
|
noLockMutation?: boolean | undefined;
|
|
62
64
|
}
|
|
@@ -95,6 +97,12 @@ const splitApps = (value: string): readonly string[] =>
|
|
|
95
97
|
.map((entry) => entry.trim())
|
|
96
98
|
.filter((entry) => entry.length > 0);
|
|
97
99
|
|
|
100
|
+
const splitCommaDelimitedValues = (value: string): readonly string[] =>
|
|
101
|
+
value
|
|
102
|
+
.split(',')
|
|
103
|
+
.map((entry) => entry.trim())
|
|
104
|
+
.filter((entry) => entry.length > 0);
|
|
105
|
+
|
|
98
106
|
const isAllowedValue = <T extends string>(
|
|
99
107
|
value: string,
|
|
100
108
|
allowed: readonly T[]
|
|
@@ -248,6 +256,7 @@ const parseTokens = (
|
|
|
248
256
|
'no-lock-mutation': { type: 'boolean' },
|
|
249
257
|
'pre-push': { type: 'boolean' },
|
|
250
258
|
'root-dir': { type: 'string' },
|
|
259
|
+
'scope-exclude': { multiple: true, type: 'string' },
|
|
251
260
|
strict: { type: 'boolean' },
|
|
252
261
|
...valueAliasParseOptions,
|
|
253
262
|
},
|
|
@@ -397,6 +406,19 @@ const applyCommandOption = (
|
|
|
397
406
|
state.configPath = value;
|
|
398
407
|
return;
|
|
399
408
|
}
|
|
409
|
+
if (token.name === 'scope-exclude') {
|
|
410
|
+
if (value === undefined) {
|
|
411
|
+
state.diagnostics.push(
|
|
412
|
+
diagnostic({ message: '--scope-exclude requires a path glob.' })
|
|
413
|
+
);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const existing = state.cli.scope?.exclude ?? [];
|
|
417
|
+
state.cli.scope = {
|
|
418
|
+
exclude: [...existing, ...splitCommaDelimitedValues(value)],
|
|
419
|
+
};
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
400
422
|
if (token.name === 'fix') {
|
|
401
423
|
state.fix = true;
|
|
402
424
|
return;
|
|
@@ -821,6 +843,7 @@ const buildRunOptions = ({
|
|
|
821
843
|
lock: cli.lock,
|
|
822
844
|
noLockMutation: cli.noLockMutation,
|
|
823
845
|
rootDir,
|
|
846
|
+
scope: cli.scope,
|
|
824
847
|
topos,
|
|
825
848
|
}),
|
|
826
849
|
env,
|
package/src/config.ts
CHANGED
|
@@ -10,6 +10,13 @@ export const wardenDraftsValues = ['include', 'exclude', 'only'] as const;
|
|
|
10
10
|
|
|
11
11
|
const appNameSchema = z.string().min(1);
|
|
12
12
|
|
|
13
|
+
const wardenScopeSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
exclude: z.array(z.string().min(1)).default([]),
|
|
16
|
+
})
|
|
17
|
+
.strict()
|
|
18
|
+
.default({ exclude: [] });
|
|
19
|
+
|
|
13
20
|
const wardenConfigObjectSchema = z
|
|
14
21
|
.object({
|
|
15
22
|
apps: z.array(appNameSchema).min(1).optional(),
|
|
@@ -18,6 +25,7 @@ const wardenConfigObjectSchema = z
|
|
|
18
25
|
failOn: z.enum(wardenFailOnValues).default('error'),
|
|
19
26
|
format: z.enum(wardenFormatValues).default('summary'),
|
|
20
27
|
lock: z.enum(wardenLockValues).default('auto'),
|
|
28
|
+
scope: wardenScopeSchema,
|
|
21
29
|
})
|
|
22
30
|
.strict();
|
|
23
31
|
|
|
@@ -31,6 +39,7 @@ export type WardenDepth = (typeof wardenDepthValues)[number];
|
|
|
31
39
|
export type WardenDraftsMode = (typeof wardenDraftsValues)[number];
|
|
32
40
|
export type WardenFailOn = (typeof wardenFailOnValues)[number];
|
|
33
41
|
export type WardenFormat = (typeof wardenFormatValues)[number];
|
|
42
|
+
export type WardenScope = z.output<typeof wardenScopeSchema>;
|
|
34
43
|
export type WardenLockMode = (typeof wardenLockValues)[number];
|
|
35
44
|
|
|
36
45
|
export interface WardenConfigLayer extends Partial<WardenConfig> {
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ export type {
|
|
|
18
18
|
WardenFixCapability,
|
|
19
19
|
WardenFixClass,
|
|
20
20
|
WardenFixEdit,
|
|
21
|
+
WardenFixScanTargets,
|
|
21
22
|
WardenFixSafety,
|
|
22
23
|
WardenGuidance,
|
|
23
24
|
WardenGuidanceLink,
|
|
@@ -100,6 +101,7 @@ export type {
|
|
|
100
101
|
WardenDraftsMode,
|
|
101
102
|
WardenFailOn,
|
|
102
103
|
WardenFormat,
|
|
104
|
+
WardenScope,
|
|
103
105
|
WardenLockMode,
|
|
104
106
|
EffectiveWardenConfig,
|
|
105
107
|
ResolveWardenConfigOptions,
|
package/src/project-context.ts
CHANGED
|
@@ -81,16 +81,42 @@ const exportAliasesForWorkspaces = (
|
|
|
81
81
|
return aliases;
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
+
const resolveOptionsWithWorkspaceAliases = (
|
|
85
|
+
publicWorkspaces: ReadonlyMap<string, WardenPublicWorkspace> | undefined,
|
|
86
|
+
resolveOptions: WardenResolverOptions['resolveOptions'] | undefined
|
|
87
|
+
): WardenResolverOptions['resolveOptions'] => {
|
|
88
|
+
if (!publicWorkspaces) {
|
|
89
|
+
return resolveOptions;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const workspaceAliases = exportAliasesForWorkspaces(publicWorkspaces);
|
|
93
|
+
return {
|
|
94
|
+
...resolveOptions,
|
|
95
|
+
alias: {
|
|
96
|
+
...workspaceAliases,
|
|
97
|
+
...resolveOptions?.alias,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
84
102
|
export const collectProjectImportResolutions = ({
|
|
103
|
+
publicWorkspaces,
|
|
85
104
|
resolveOptions,
|
|
86
105
|
rootDir,
|
|
87
106
|
sourceFiles,
|
|
88
107
|
}: {
|
|
108
|
+
readonly publicWorkspaces?: ReadonlyMap<string, WardenPublicWorkspace>;
|
|
89
109
|
readonly resolveOptions?: WardenResolverOptions['resolveOptions'];
|
|
90
110
|
readonly rootDir: string;
|
|
91
111
|
readonly sourceFiles: readonly WardenProjectContextSourceFile[];
|
|
92
112
|
}): ReadonlyMap<string, readonly WardenImportResolution[]> => {
|
|
93
|
-
const resolver = createWardenResolver({
|
|
113
|
+
const resolver = createWardenResolver({
|
|
114
|
+
resolveOptions: resolveOptionsWithWorkspaceAliases(
|
|
115
|
+
publicWorkspaces,
|
|
116
|
+
resolveOptions
|
|
117
|
+
),
|
|
118
|
+
rootDir,
|
|
119
|
+
});
|
|
94
120
|
const resolutionsByFile = new Map<
|
|
95
121
|
string,
|
|
96
122
|
readonly WardenImportResolution[]
|
|
@@ -118,13 +144,16 @@ export const collectProjectImportResolutions = ({
|
|
|
118
144
|
};
|
|
119
145
|
|
|
120
146
|
export const collectProjectDocumentationImportResolutions = ({
|
|
147
|
+
publicWorkspaces: providedPublicWorkspaces,
|
|
121
148
|
rootDir,
|
|
122
149
|
sourceFiles,
|
|
123
150
|
}: {
|
|
151
|
+
readonly publicWorkspaces?: ReadonlyMap<string, WardenPublicWorkspace>;
|
|
124
152
|
readonly rootDir: string;
|
|
125
153
|
readonly sourceFiles: readonly WardenProjectContextSourceFile[];
|
|
126
154
|
}): ReadonlyMap<string, readonly WardenImportResolution[]> => {
|
|
127
|
-
const publicWorkspaces =
|
|
155
|
+
const publicWorkspaces =
|
|
156
|
+
providedPublicWorkspaces ?? collectPublicWorkspaces(rootDir);
|
|
128
157
|
const resolver = createWardenResolver({
|
|
129
158
|
resolveOptions: { alias: exportAliasesForWorkspaces(publicWorkspaces) },
|
|
130
159
|
rootDir,
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { existsSync, readFileSync } from 'node:fs';
|
|
10
10
|
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
11
|
+
import { escapeRegExp } from '@ontrails/core';
|
|
11
12
|
import type { AstNode } from './ast.js';
|
|
12
13
|
import {
|
|
13
14
|
collectScopeFrameBindings,
|
|
@@ -288,9 +289,6 @@ const extractIdentifierName = (node: AstNode | undefined): string | null =>
|
|
|
288
289
|
|
|
289
290
|
const DEFAULT_RESULT_TYPE_NAMES = new Set(['Result']);
|
|
290
291
|
|
|
291
|
-
const escapeRegExp = (value: string): string =>
|
|
292
|
-
value.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
293
|
-
|
|
294
292
|
const hasGenericTypeReference = (
|
|
295
293
|
annotationText: string,
|
|
296
294
|
typeName: string
|
package/src/rules/index.ts
CHANGED
package/src/rules/metadata.ts
CHANGED
|
@@ -610,7 +610,7 @@ const builtinWardenRuleMetadataInput = {
|
|
|
610
610
|
guidance: {
|
|
611
611
|
docs: [
|
|
612
612
|
{
|
|
613
|
-
label: '
|
|
613
|
+
label: 'Trailheads ADR',
|
|
614
614
|
path: 'docs/adr/drafts/20260603-surface-facets-shape-dense-topos.md',
|
|
615
615
|
},
|
|
616
616
|
],
|
|
@@ -620,10 +620,10 @@ const builtinWardenRuleMetadataInput = {
|
|
|
620
620
|
'Record explicit visibility-widening acceptance and stable-description metadata when a facet intentionally widens visibility.',
|
|
621
621
|
],
|
|
622
622
|
summary:
|
|
623
|
-
'Keep
|
|
623
|
+
'Keep trailhead maps reviewable before they reach MCP projection.',
|
|
624
624
|
},
|
|
625
625
|
invariant:
|
|
626
|
-
'
|
|
626
|
+
'Trailhead maps avoid selector overlap, hidden visibility widening, and drift-prone dynamic selectors.',
|
|
627
627
|
tier: 'source-static',
|
|
628
628
|
},
|
|
629
629
|
'trail-fork-coaching': {
|
|
@@ -10,7 +10,7 @@ const toWardenDiagnostic = (
|
|
|
10
10
|
line: 1,
|
|
11
11
|
message: diagnostic.message,
|
|
12
12
|
rule: `permit.${diagnostic.rule}`,
|
|
13
|
-
severity: diagnostic.severity
|
|
13
|
+
severity: diagnostic.severity,
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
export const permitGovernance: TopoAwareWardenRule = {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, realpathSync } from 'node:fs';
|
|
2
2
|
import { resolve, sep } from 'node:path';
|
|
3
|
+
import { matchesPathGlob } from '@ontrails/core';
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
6
|
extractStringLiteral,
|
|
@@ -303,50 +304,6 @@ const rootBarrelDiagnostics = (
|
|
|
303
304
|
const stripLeadingDotSlash = (path: string): string =>
|
|
304
305
|
path.startsWith('./') ? path.slice(2) : path;
|
|
305
306
|
|
|
306
|
-
const escapeRegExp = (value: string): string =>
|
|
307
|
-
value.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
308
|
-
|
|
309
|
-
const wildcardPatternSource = (pattern: string): string => {
|
|
310
|
-
let regexSource = '';
|
|
311
|
-
for (let index = 0; index < pattern.length; index += 1) {
|
|
312
|
-
const char = pattern[index];
|
|
313
|
-
if (char === '*') {
|
|
314
|
-
if (pattern[index + 1] === '*') {
|
|
315
|
-
regexSource += '.*';
|
|
316
|
-
index += 1;
|
|
317
|
-
} else {
|
|
318
|
-
regexSource += '[^/]*';
|
|
319
|
-
}
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
regexSource += escapeRegExp(char ?? '');
|
|
323
|
-
}
|
|
324
|
-
return regexSource;
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
const segmentWildcardPatternCovers = (
|
|
328
|
-
filePath: string,
|
|
329
|
-
pattern: string
|
|
330
|
-
): boolean => new RegExp(`^${wildcardPatternSource(pattern)}$`).test(filePath);
|
|
331
|
-
|
|
332
|
-
const deepWildcardPatternCovers = (
|
|
333
|
-
filePath: string,
|
|
334
|
-
pattern: string,
|
|
335
|
-
globIndex: number
|
|
336
|
-
): boolean => {
|
|
337
|
-
const prefix = pattern.slice(0, globIndex + 1);
|
|
338
|
-
if (!filePath.startsWith(prefix)) {
|
|
339
|
-
return false;
|
|
340
|
-
}
|
|
341
|
-
const suffixPattern = pattern.slice(globIndex + '/**/'.length);
|
|
342
|
-
if (suffixPattern.length === 0) {
|
|
343
|
-
return true;
|
|
344
|
-
}
|
|
345
|
-
const remainingPath = filePath.slice(prefix.length);
|
|
346
|
-
const regexSource = `^(?:.*/)?${wildcardPatternSource(suffixPattern)}$`;
|
|
347
|
-
return new RegExp(regexSource).test(remainingPath);
|
|
348
|
-
};
|
|
349
|
-
|
|
350
307
|
const filePatternCovers = (filePath: string, pattern: string): boolean => {
|
|
351
308
|
const normalizedFilePath = normalizePath(stripLeadingDotSlash(filePath));
|
|
352
309
|
const normalizedPattern = normalizePath(stripLeadingDotSlash(pattern));
|
|
@@ -359,27 +316,10 @@ const filePatternCovers = (filePath: string, pattern: string): boolean => {
|
|
|
359
316
|
if (normalizedPattern === '**') {
|
|
360
317
|
return true;
|
|
361
318
|
}
|
|
362
|
-
if (normalizedPattern.
|
|
363
|
-
const prefix = normalizedPattern.slice(0, -2);
|
|
364
|
-
return normalizedFilePath.startsWith(prefix);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const globIndex = normalizedPattern.indexOf('/**/');
|
|
368
|
-
if (globIndex === -1) {
|
|
369
|
-
if (normalizedPattern.includes('*')) {
|
|
370
|
-
return segmentWildcardPatternCovers(
|
|
371
|
-
normalizedFilePath,
|
|
372
|
-
normalizedPattern
|
|
373
|
-
);
|
|
374
|
-
}
|
|
319
|
+
if (!normalizedPattern.includes('*') && !normalizedPattern.includes('?')) {
|
|
375
320
|
return normalizedFilePath.startsWith(`${normalizedPattern}/`);
|
|
376
321
|
}
|
|
377
|
-
|
|
378
|
-
return deepWildcardPatternCovers(
|
|
379
|
-
normalizedFilePath,
|
|
380
|
-
normalizedPattern,
|
|
381
|
-
globIndex
|
|
382
|
-
);
|
|
322
|
+
return matchesPathGlob(normalizedFilePath, normalizedPattern);
|
|
383
323
|
};
|
|
384
324
|
|
|
385
325
|
const filesCoverTarget = (
|
|
@@ -356,7 +356,7 @@ export const surfaceFacetCoherence: WardenRule = {
|
|
|
356
356
|
return diagnostics;
|
|
357
357
|
},
|
|
358
358
|
description:
|
|
359
|
-
'Coach
|
|
359
|
+
'Coach trailhead maps away from selector overlap, hidden visibility widening, and drift-prone dynamic selectors.',
|
|
360
360
|
name: RULE_NAME,
|
|
361
361
|
severity: 'warn',
|
|
362
362
|
};
|
|
@@ -554,7 +554,7 @@ const diagnosticForField = (
|
|
|
554
554
|
.map((option) => `"${option}"`)
|
|
555
555
|
.join(
|
|
556
556
|
', '
|
|
557
|
-
)}). This may be a trail fork hidden as a surface accommodation. If branches change semantics (intent, permits, errors, outputs, lifecycle, or side effects) or structure (selected trail identity), split them into distinct trails, a composing trail, or a
|
|
557
|
+
)}). This may be a trail fork hidden as a surface accommodation. If branches change semantics (intent, permits, errors, outputs, lifecycle, or side effects) or structure (selected trail identity), split them into distinct trails, a composing trail, or a trailhead that preserves member identity.`,
|
|
558
558
|
rule: RULE_NAME,
|
|
559
559
|
severity: 'warn',
|
|
560
560
|
});
|
package/src/rules/types.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
DiagnosticSeverity,
|
|
3
|
+
Intent,
|
|
4
|
+
RuleDiagnosticBase,
|
|
5
|
+
ScanTargets,
|
|
6
|
+
Topo,
|
|
7
|
+
} from '@ontrails/core';
|
|
2
8
|
import type { TopoGraph } from '@ontrails/topographer';
|
|
3
9
|
|
|
4
10
|
import type { WardenDepth } from '../config.js';
|
|
@@ -8,7 +14,7 @@ import type { WardenPublicWorkspace } from '../workspaces.js';
|
|
|
8
14
|
/**
|
|
9
15
|
* Severity level for warden diagnostics.
|
|
10
16
|
*/
|
|
11
|
-
export type WardenSeverity =
|
|
17
|
+
export type WardenSeverity = DiagnosticSeverity;
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
20
|
* Execution tier for a Warden rule.
|
|
@@ -130,6 +136,21 @@ export interface WardenFixEdit {
|
|
|
130
136
|
readonly replacement: string;
|
|
131
137
|
}
|
|
132
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Source targets a fix class can inspect when projected into downstream tools.
|
|
141
|
+
*
|
|
142
|
+
* Warden itself decides which committed files it scans. This metadata is for
|
|
143
|
+
* consumers such as Regrade that need to derive a narrower collection before
|
|
144
|
+
* invoking the rule against an explicit downstream root.
|
|
145
|
+
*/
|
|
146
|
+
export type WardenFixScanTargets = ScanTargets & {
|
|
147
|
+
/**
|
|
148
|
+
* @deprecated Compatibility bridge for Warden-backed Regrade classes that
|
|
149
|
+
* predate PathScope. Prefer Regrade collection scope for new callers.
|
|
150
|
+
*/
|
|
151
|
+
readonly ignoredDirectories?: readonly string[];
|
|
152
|
+
};
|
|
153
|
+
|
|
133
154
|
/**
|
|
134
155
|
* Per-finding fix metadata attached to a diagnostic.
|
|
135
156
|
*
|
|
@@ -164,6 +185,8 @@ export interface WardenFixCapability {
|
|
|
164
185
|
readonly class: WardenFixClass;
|
|
165
186
|
/** Default safety for fixes this rule emits. */
|
|
166
187
|
readonly safety: WardenFixSafety;
|
|
188
|
+
/** Downstream scan targets for tools that project this fix capability. */
|
|
189
|
+
readonly scanTargets?: WardenFixScanTargets | undefined;
|
|
167
190
|
}
|
|
168
191
|
|
|
169
192
|
/**
|
|
@@ -191,7 +214,7 @@ export interface WardenRuleMetadata {
|
|
|
191
214
|
/**
|
|
192
215
|
* A single diagnostic reported by a warden rule.
|
|
193
216
|
*/
|
|
194
|
-
export interface WardenDiagnostic {
|
|
217
|
+
export interface WardenDiagnostic extends RuleDiagnosticBase {
|
|
195
218
|
/** Rule identifier, e.g. "no-throw-in-implementation" */
|
|
196
219
|
readonly rule: string;
|
|
197
220
|
/** Optional rule-local diagnostic code for checks with multiple stable findings. */
|
|
@@ -10,7 +10,7 @@ export const trailForkCoachingTrail = wrapRule({
|
|
|
10
10
|
filePath: 'users.ts',
|
|
11
11
|
line: 9,
|
|
12
12
|
message:
|
|
13
|
-
'Trail "users.manage" branches on input.action ("create", "delete"). This may be a trail fork hidden as a surface accommodation. If branches change semantics (intent, permits, errors, outputs, lifecycle, or side effects) or structure (selected trail identity), split them into distinct trails, a composing trail, or a
|
|
13
|
+
'Trail "users.manage" branches on input.action ("create", "delete"). This may be a trail fork hidden as a surface accommodation. If branches change semantics (intent, permits, errors, outputs, lifecycle, or side effects) or structure (selected trail identity), split them into distinct trails, a composing trail, or a trailhead that preserves member identity.',
|
|
14
14
|
rule: 'trail-fork-coaching',
|
|
15
15
|
severity: 'warn',
|
|
16
16
|
},
|
package/src/workspaces.ts
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Workspace-manifest discovery for Warden project-aware rules.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { realpathSync } from 'node:fs';
|
|
6
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
7
|
+
|
|
8
|
+
import { listWorkspacePackages, matchesAnyPathGlob } from '@ontrails/core';
|
|
7
9
|
|
|
8
10
|
export interface WardenPublicWorkspace {
|
|
9
11
|
readonly name: string;
|
|
@@ -15,8 +17,8 @@ export interface WardenPublicWorkspace {
|
|
|
15
17
|
readonly files?: readonly string[] | undefined;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
interface
|
|
19
|
-
readonly
|
|
20
|
+
export interface WardenWorkspaceCollectionOptions {
|
|
21
|
+
readonly exclude?: readonly string[];
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
interface PackageManifest {
|
|
@@ -37,55 +39,12 @@ const normalizeRealPath = (path: string): string => {
|
|
|
37
39
|
}
|
|
38
40
|
};
|
|
39
41
|
|
|
42
|
+
const rootRelativePath = (rootDir: string, path: string): string =>
|
|
43
|
+
normalizePath(relative(rootDir, path));
|
|
44
|
+
|
|
40
45
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
41
46
|
Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
42
47
|
|
|
43
|
-
const readJson = <T>(path: string): T | undefined => {
|
|
44
|
-
try {
|
|
45
|
-
return JSON.parse(readFileSync(path, 'utf8')) as T;
|
|
46
|
-
} catch {
|
|
47
|
-
return undefined;
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const workspacePatternsFromManifest = (
|
|
52
|
-
manifest: RootManifest | undefined
|
|
53
|
-
): readonly string[] => {
|
|
54
|
-
const { workspaces } = manifest ?? {};
|
|
55
|
-
if (Array.isArray(workspaces)) {
|
|
56
|
-
return workspaces.filter(
|
|
57
|
-
(pattern): pattern is string => typeof pattern === 'string'
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const packages = isRecord(workspaces) ? workspaces['packages'] : undefined;
|
|
62
|
-
return Array.isArray(packages)
|
|
63
|
-
? packages.filter(
|
|
64
|
-
(pattern): pattern is string => typeof pattern === 'string'
|
|
65
|
-
)
|
|
66
|
-
: [];
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const workspaceDirsForPattern = (
|
|
70
|
-
rootDir: string,
|
|
71
|
-
pattern: string
|
|
72
|
-
): readonly string[] => {
|
|
73
|
-
if (!pattern.endsWith('/*')) {
|
|
74
|
-
const workspaceDir = join(rootDir, pattern);
|
|
75
|
-
return existsSync(workspaceDir) ? [workspaceDir] : [];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const groupDir = join(rootDir, pattern.slice(0, -2));
|
|
79
|
-
if (!existsSync(groupDir)) {
|
|
80
|
-
return [];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return readdirSync(groupDir, { withFileTypes: true })
|
|
84
|
-
.filter((entry) => entry.isDirectory())
|
|
85
|
-
.map((entry) => join(groupDir, entry.name))
|
|
86
|
-
.toSorted();
|
|
87
|
-
};
|
|
88
|
-
|
|
89
48
|
const packageLocalName = (name: string): string =>
|
|
90
49
|
name.split('/').at(-1) ?? name;
|
|
91
50
|
|
|
@@ -208,29 +167,31 @@ const publicWorkspaceFromManifest = (
|
|
|
208
167
|
};
|
|
209
168
|
|
|
210
169
|
export const collectPublicWorkspaces = (
|
|
211
|
-
rootDir: string
|
|
170
|
+
rootDir: string,
|
|
171
|
+
options: WardenWorkspaceCollectionOptions = {}
|
|
212
172
|
): ReadonlyMap<string, WardenPublicWorkspace> => {
|
|
213
173
|
const normalizedRoot = normalizeRealPath(rootDir);
|
|
214
|
-
const rootManifest = readJson<RootManifest>(
|
|
215
|
-
join(normalizedRoot, 'package.json')
|
|
216
|
-
);
|
|
217
174
|
const workspaces = new Map<string, WardenPublicWorkspace>();
|
|
218
175
|
|
|
219
|
-
for (const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
176
|
+
for (const workspacePackage of listWorkspacePackages<PackageManifest>(
|
|
177
|
+
normalizedRoot
|
|
178
|
+
)) {
|
|
179
|
+
const workspace = publicWorkspaceFromManifest(
|
|
180
|
+
workspacePackage.packageJsonPath,
|
|
181
|
+
workspacePackage.manifest
|
|
182
|
+
);
|
|
183
|
+
if (
|
|
184
|
+
workspace &&
|
|
185
|
+
!matchesAnyPathGlob(
|
|
186
|
+
rootRelativePath(normalizedRoot, workspace.rootDir),
|
|
187
|
+
options.exclude ?? []
|
|
188
|
+
) &&
|
|
189
|
+
!matchesAnyPathGlob(
|
|
190
|
+
rootRelativePath(normalizedRoot, workspace.packageJsonPath),
|
|
191
|
+
options.exclude ?? []
|
|
192
|
+
)
|
|
193
|
+
) {
|
|
194
|
+
workspaces.set(workspace.name, workspace);
|
|
234
195
|
}
|
|
235
196
|
}
|
|
236
197
|
|