@ontrails/warden 0.2.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/CHANGELOG.md +1556 -0
- package/README.md +200 -0
- package/bin/warden.ts +51 -0
- package/package.json +51 -0
- package/src/adapter-check.ts +136 -0
- package/src/cli.ts +2159 -0
- package/src/command.ts +1167 -0
- package/src/config.ts +193 -0
- package/src/draft.ts +22 -0
- package/src/drift.ts +260 -0
- package/src/fix.ts +126 -0
- package/src/formatters.ts +178 -0
- package/src/guide.ts +245 -0
- package/src/index.ts +295 -0
- package/src/project-context.ts +446 -0
- package/src/project-rules.ts +290 -0
- package/src/regrade-history.ts +188 -0
- package/src/regrade-receipt.ts +484 -0
- package/src/resolve.ts +531 -0
- package/src/rules/activation-orphan.ts +97 -0
- package/src/rules/captured-kernel.ts +375 -0
- package/src/rules/circular-refs.ts +150 -0
- package/src/rules/cli-command-route-coherence.ts +177 -0
- package/src/rules/composes-declarations.ts +839 -0
- package/src/rules/context-no-surface-types.ts +214 -0
- package/src/rules/dead-internal-trail.ts +161 -0
- package/src/rules/dead-public-trail.ts +258 -0
- package/src/rules/draft-file-marking.ts +155 -0
- package/src/rules/draft-visible-debt.ts +83 -0
- package/src/rules/duplicate-exported-symbol.ts +172 -0
- package/src/rules/duplicate-public-contract.ts +137 -0
- package/src/rules/entity-exists.ts +254 -0
- package/src/rules/entity-ids.ts +15 -0
- package/src/rules/error-mapping-completeness.ts +290 -0
- package/src/rules/example-valid.ts +395 -0
- package/src/rules/fires-declarations.ts +740 -0
- package/src/rules/governed-symbol-residue.ts +535 -0
- package/src/rules/governed-vocabulary-permutation-watch.ts +77 -0
- package/src/rules/implementation-returns-result.ts +1733 -0
- package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
- package/src/rules/incomplete-crud.ts +583 -0
- package/src/rules/index.ts +328 -0
- package/src/rules/intent-propagation.ts +125 -0
- package/src/rules/layer-field-name-drift.ts +102 -0
- package/src/rules/library-render-coherence.ts +97 -0
- package/src/rules/metadata.ts +929 -0
- package/src/rules/missing-reconcile.ts +97 -0
- package/src/rules/missing-visibility.ts +111 -0
- package/src/rules/no-destructured-compose.ts +196 -0
- package/src/rules/no-dev-permit-in-source.ts +99 -0
- package/src/rules/no-direct-implementation-call.ts +52 -0
- package/src/rules/no-legacy-cli-alias-export.ts +247 -0
- package/src/rules/no-legacy-layer-imports.ts +211 -0
- package/src/rules/no-native-error-result.ts +118 -0
- package/src/rules/no-redundant-result-error-wrap.ts +382 -0
- package/src/rules/no-retired-cross-vocabulary.ts +203 -0
- package/src/rules/no-sync-result-assumption.ts +1199 -0
- package/src/rules/no-throw-in-detour-recover.ts +225 -0
- package/src/rules/no-throw-in-implementation.ts +48 -0
- package/src/rules/no-top-level-surface.ts +371 -0
- package/src/rules/on-references-exist.ts +194 -0
- package/src/rules/orphaned-signal.ts +149 -0
- package/src/rules/owner-render-parity.ts +143 -0
- package/src/rules/permit-governance.ts +25 -0
- package/src/rules/prefer-schema-inference.ts +141 -0
- package/src/rules/public-export-example-coverage.ts +561 -0
- package/src/rules/public-internal-deep-imports.ts +454 -0
- package/src/rules/public-output-schema.ts +29 -0
- package/src/rules/public-union-output-discriminants.ts +150 -0
- package/src/rules/read-intent-fires.ts +188 -0
- package/src/rules/reference-exists.ts +97 -0
- package/src/rules/registry-names.ts +171 -0
- package/src/rules/resolved-import-boundary.ts +146 -0
- package/src/rules/resource-declarations.ts +697 -0
- package/src/rules/resource-exists.ts +181 -0
- package/src/rules/resource-id-grammar.ts +65 -0
- package/src/rules/resource-mock-coverage.ts +115 -0
- package/src/rules/retired-vocabulary.ts +1407 -0
- package/src/rules/scan.ts +59 -0
- package/src/rules/scheduled-destroy-intent.ts +44 -0
- package/src/rules/signal-graph-coaching.ts +220 -0
- package/src/rules/source/composition.ts +165 -0
- package/src/rules/source/drafts.ts +164 -0
- package/src/rules/source/entities.ts +618 -0
- package/src/rules/source/pragmas.ts +45 -0
- package/src/rules/source/resources.ts +64 -0
- package/src/rules/source/signals.ts +397 -0
- package/src/rules/source/stores.ts +310 -0
- package/src/rules/specs.ts +388 -0
- package/src/rules/static-resource-accessor-preference.ts +654 -0
- package/src/rules/structure.ts +234 -0
- package/src/rules/surface-overlay-coherence.ts +262 -0
- package/src/rules/surface-trailhead-coherence.ts +366 -0
- package/src/rules/trail-fork-coaching.ts +625 -0
- package/src/rules/trail-versioning-source.ts +1076 -0
- package/src/rules/trail-versioning-topo.ts +172 -0
- package/src/rules/trailhead-override-divergence.ts +356 -0
- package/src/rules/types.ts +464 -0
- package/src/rules/unmaterialized-activation-source.ts +85 -0
- package/src/rules/unreachable-detour-shadowing.ts +343 -0
- package/src/rules/valid-describe-refs.ts +224 -0
- package/src/rules/valid-detour-contract.ts +78 -0
- package/src/rules/warden-export-symmetry.ts +540 -0
- package/src/rules/warden-rules-use-ast.ts +1109 -0
- package/src/rules/webhook-route-collision.ts +306 -0
- package/src/rules/workspace-lock-ownership.ts +34 -0
- package/src/trails/activation-orphan.trail.ts +84 -0
- package/src/trails/captured-kernel.trail.ts +108 -0
- package/src/trails/circular-refs.trail.ts +29 -0
- package/src/trails/cli-command-route-coherence.trail.ts +47 -0
- package/src/trails/composes-declarations.trail.ts +22 -0
- package/src/trails/context-no-surface-types.trail.ts +21 -0
- package/src/trails/dead-internal-trail.trail.ts +26 -0
- package/src/trails/dead-public-trail.trail.ts +31 -0
- package/src/trails/deprecation-without-guidance.trail.ts +21 -0
- package/src/trails/draft-file-marking.trail.ts +16 -0
- package/src/trails/draft-visible-debt.trail.ts +16 -0
- package/src/trails/duplicate-exported-symbol.trail.ts +48 -0
- package/src/trails/duplicate-public-contract.trail.ts +47 -0
- package/src/trails/entity-exists.trail.ts +21 -0
- package/src/trails/error-mapping-completeness.trail.ts +30 -0
- package/src/trails/example-valid.trail.ts +25 -0
- package/src/trails/fires-declarations.trail.ts +23 -0
- package/src/trails/fork-without-preserved-implementation.trail.ts +31 -0
- package/src/trails/governed-symbol-residue.trail.ts +24 -0
- package/src/trails/governed-vocabulary-permutation-watch.trail.ts +16 -0
- package/src/trails/implementation-returns-result.trail.ts +20 -0
- package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
- package/src/trails/incomplete-crud.trail.ts +39 -0
- package/src/trails/index.ts +91 -0
- package/src/trails/intent-propagation.trail.ts +30 -0
- package/src/trails/layer-field-name-drift.trail.ts +39 -0
- package/src/trails/library-render-coherence.trail.ts +43 -0
- package/src/trails/marker-schema-unsupported.trail.ts +23 -0
- package/src/trails/missing-reconcile.trail.ts +33 -0
- package/src/trails/missing-visibility.trail.ts +22 -0
- package/src/trails/no-destructured-compose.trail.ts +44 -0
- package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
- package/src/trails/no-direct-implementation-call.trail.ts +16 -0
- package/src/trails/no-legacy-cli-alias-export.trail.ts +41 -0
- package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
- package/src/trails/no-native-error-result.trail.ts +18 -0
- package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
- package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
- package/src/trails/no-sync-result-assumption.trail.ts +19 -0
- package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
- package/src/trails/no-throw-in-implementation.trail.ts +20 -0
- package/src/trails/no-top-level-surface.trail.ts +43 -0
- package/src/trails/on-references-exist.trail.ts +21 -0
- package/src/trails/orphaned-signal.trail.ts +36 -0
- package/src/trails/owner-render-parity.trail.ts +26 -0
- package/src/trails/pending-force.trail.ts +21 -0
- package/src/trails/permit-governance.trail.ts +51 -0
- package/src/trails/prefer-schema-inference.trail.ts +21 -0
- package/src/trails/public-export-example-coverage.trail.ts +16 -0
- package/src/trails/public-internal-deep-imports.trail.ts +94 -0
- package/src/trails/public-output-schema.trail.ts +55 -0
- package/src/trails/public-union-output-discriminants.trail.ts +33 -0
- package/src/trails/read-intent-fires.trail.ts +20 -0
- package/src/trails/reference-exists.trail.ts +25 -0
- package/src/trails/resolved-import-boundary.trail.ts +109 -0
- package/src/trails/resource-declarations.trail.ts +25 -0
- package/src/trails/resource-exists.trail.ts +27 -0
- package/src/trails/resource-id-grammar.trail.ts +39 -0
- package/src/trails/resource-mock-coverage.trail.ts +40 -0
- package/src/trails/run.ts +205 -0
- package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
- package/src/trails/schema.ts +295 -0
- package/src/trails/signal-graph-coaching.trail.ts +77 -0
- package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
- package/src/trails/surface-overlay-coherence.trail.ts +24 -0
- package/src/trails/surface-trailhead-coherence.trail.ts +25 -0
- package/src/trails/topo.ts +6 -0
- package/src/trails/trail-fork-coaching.trail.ts +42 -0
- package/src/trails/trailhead-override-divergence.trail.ts +47 -0
- package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
- package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
- package/src/trails/valid-describe-refs.trail.ts +18 -0
- package/src/trails/valid-detour-contract.trail.ts +71 -0
- package/src/trails/version-gap.trail.ts +35 -0
- package/src/trails/version-pinned-compose.trail.ts +23 -0
- package/src/trails/version-without-examples.trail.ts +38 -0
- package/src/trails/warden-export-symmetry.trail.ts +16 -0
- package/src/trails/warden-rules-use-ast.trail.ts +64 -0
- package/src/trails/webhook-route-collision.trail.ts +50 -0
- package/src/trails/workspace-lock-ownership.trail.ts +16 -0
- package/src/trails/wrap-rule.ts +269 -0
- package/src/workspaces.ts +264 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory that wraps a WardenRule as a trail.
|
|
3
|
+
*
|
|
4
|
+
* Keeps each rule trail file minimal — just the import + examples.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { InternalError, trail, Result } from '@ontrails/core';
|
|
8
|
+
import type { Trail } from '@ontrails/core';
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
ProjectAwareWardenRule,
|
|
12
|
+
ProjectContext,
|
|
13
|
+
TopoAwareWardenRule,
|
|
14
|
+
WardenRule,
|
|
15
|
+
} from '../rules/types.js';
|
|
16
|
+
import { getWardenRuleMetadata } from '../rules/metadata.js';
|
|
17
|
+
import {
|
|
18
|
+
projectAwareRuleInput,
|
|
19
|
+
ruleInput,
|
|
20
|
+
ruleOutput,
|
|
21
|
+
topoAwareRuleInput,
|
|
22
|
+
} from './schema.js';
|
|
23
|
+
import type {
|
|
24
|
+
ProjectAwareRuleInput,
|
|
25
|
+
RuleInput,
|
|
26
|
+
RuleOutput,
|
|
27
|
+
TopoAwareRuleInput,
|
|
28
|
+
} from './schema.js';
|
|
29
|
+
|
|
30
|
+
interface WrapRuleOptions {
|
|
31
|
+
/** The existing warden rule to wrap. */
|
|
32
|
+
readonly rule: WardenRule;
|
|
33
|
+
/** Trail examples for testing and documentation. */
|
|
34
|
+
readonly examples: Trail<RuleInput, RuleOutput>['examples'];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface WrapProjectAwareRuleOptions {
|
|
38
|
+
/** The existing project-aware warden rule to wrap. */
|
|
39
|
+
readonly rule: ProjectAwareWardenRule;
|
|
40
|
+
/** Trail examples for testing and documentation. */
|
|
41
|
+
readonly examples: Trail<ProjectAwareRuleInput, RuleOutput>['examples'];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const buildRuleMeta = (rule: WardenRule | TopoAwareWardenRule) => {
|
|
45
|
+
const metadata = getWardenRuleMetadata(rule);
|
|
46
|
+
return {
|
|
47
|
+
category: 'governance',
|
|
48
|
+
...(metadata ? { warden: metadata } : {}),
|
|
49
|
+
severity: rule.severity,
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const buildGovernedHistoryContext = (
|
|
54
|
+
input: ProjectAwareRuleInput
|
|
55
|
+
): Pick<
|
|
56
|
+
ProjectContext,
|
|
57
|
+
| 'governedVocabularyHistoryByTransitionId'
|
|
58
|
+
| 'governedVocabularyHistoryIssues'
|
|
59
|
+
| 'governedVocabularyHistoryRequired'
|
|
60
|
+
> => ({
|
|
61
|
+
...(input.governedVocabularyHistories
|
|
62
|
+
? {
|
|
63
|
+
governedVocabularyHistoryByTransitionId: new Map(
|
|
64
|
+
input.governedVocabularyHistories.map((history) => [
|
|
65
|
+
history.transitionId,
|
|
66
|
+
history,
|
|
67
|
+
])
|
|
68
|
+
),
|
|
69
|
+
}
|
|
70
|
+
: {}),
|
|
71
|
+
...(input.governedVocabularyHistoryIssues
|
|
72
|
+
? {
|
|
73
|
+
governedVocabularyHistoryIssues:
|
|
74
|
+
input.governedVocabularyHistoryIssues.map((issue) => ({
|
|
75
|
+
message: issue.message,
|
|
76
|
+
path: issue.path,
|
|
77
|
+
...(issue.transitionId === undefined
|
|
78
|
+
? {}
|
|
79
|
+
: { transitionId: issue.transitionId }),
|
|
80
|
+
})),
|
|
81
|
+
}
|
|
82
|
+
: {}),
|
|
83
|
+
...(input.governedVocabularyHistoryRequired === undefined
|
|
84
|
+
? {}
|
|
85
|
+
: {
|
|
86
|
+
governedVocabularyHistoryRequired:
|
|
87
|
+
input.governedVocabularyHistoryRequired,
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export const buildProjectContext = (
|
|
92
|
+
input: ProjectAwareRuleInput
|
|
93
|
+
): ProjectContext => ({
|
|
94
|
+
...(input.unownedWorkspaceLocks
|
|
95
|
+
? { unownedWorkspaceLocks: input.unownedWorkspaceLocks }
|
|
96
|
+
: {}),
|
|
97
|
+
...(input.authoredMcpSurfaceBindingSets
|
|
98
|
+
? { authoredMcpSurfaceBindingSets: input.authoredMcpSurfaceBindingSets }
|
|
99
|
+
: {}),
|
|
100
|
+
...(input.entityReferencesByName
|
|
101
|
+
? {
|
|
102
|
+
entityReferencesByName: new Map(
|
|
103
|
+
Object.entries(input.entityReferencesByName)
|
|
104
|
+
),
|
|
105
|
+
}
|
|
106
|
+
: {}),
|
|
107
|
+
...(input.crudTableIds ? { crudTableIds: new Set(input.crudTableIds) } : {}),
|
|
108
|
+
...(input.crudCoverageByEntity
|
|
109
|
+
? {
|
|
110
|
+
crudCoverageByEntity: new Map(
|
|
111
|
+
Object.entries(input.crudCoverageByEntity).map(
|
|
112
|
+
([entityId, operations]) => [
|
|
113
|
+
entityId,
|
|
114
|
+
new Set(operations) as ReadonlySet<string>,
|
|
115
|
+
]
|
|
116
|
+
)
|
|
117
|
+
),
|
|
118
|
+
}
|
|
119
|
+
: {}),
|
|
120
|
+
...(input.knownEntityIds
|
|
121
|
+
? { knownEntityIds: new Set(input.knownEntityIds) }
|
|
122
|
+
: {}),
|
|
123
|
+
...(input.importResolutionsByFile
|
|
124
|
+
? {
|
|
125
|
+
importResolutionsByFile: new Map(
|
|
126
|
+
Object.entries(input.importResolutionsByFile)
|
|
127
|
+
),
|
|
128
|
+
}
|
|
129
|
+
: {}),
|
|
130
|
+
...(input.documentedImportResolutionsByFile
|
|
131
|
+
? {
|
|
132
|
+
documentedImportResolutionsByFile: new Map(
|
|
133
|
+
Object.entries(input.documentedImportResolutionsByFile)
|
|
134
|
+
),
|
|
135
|
+
}
|
|
136
|
+
: {}),
|
|
137
|
+
...(input.exportedSymbolDefinitionsByName
|
|
138
|
+
? {
|
|
139
|
+
exportedSymbolDefinitionsByName: new Map(
|
|
140
|
+
Object.entries(input.exportedSymbolDefinitionsByName)
|
|
141
|
+
),
|
|
142
|
+
}
|
|
143
|
+
: {}),
|
|
144
|
+
...buildGovernedHistoryContext(input),
|
|
145
|
+
...(input.publicWorkspaces
|
|
146
|
+
? { publicWorkspaces: new Map(Object.entries(input.publicWorkspaces)) }
|
|
147
|
+
: {}),
|
|
148
|
+
knownTrailIds: input.knownTrailIds
|
|
149
|
+
? new Set(input.knownTrailIds)
|
|
150
|
+
: new Set<string>(),
|
|
151
|
+
...(input.composeTargetTrailIds
|
|
152
|
+
? { composeTargetTrailIds: new Set(input.composeTargetTrailIds) }
|
|
153
|
+
: {}),
|
|
154
|
+
...(input.knownResourceIds
|
|
155
|
+
? { knownResourceIds: new Set(input.knownResourceIds) }
|
|
156
|
+
: {}),
|
|
157
|
+
...(input.knownSignalIds
|
|
158
|
+
? { knownSignalIds: new Set(input.knownSignalIds) }
|
|
159
|
+
: {}),
|
|
160
|
+
...(input.onTargetSignalIds
|
|
161
|
+
? { onTargetSignalIds: new Set(input.onTargetSignalIds) }
|
|
162
|
+
: {}),
|
|
163
|
+
...(input.reconcileTableIds
|
|
164
|
+
? { reconcileTableIds: new Set(input.reconcileTableIds) }
|
|
165
|
+
: {}),
|
|
166
|
+
...(input.trailIntentsById
|
|
167
|
+
? { trailIntentsById: new Map(Object.entries(input.trailIntentsById)) }
|
|
168
|
+
: {}),
|
|
169
|
+
...(input.topoTrailIds ? { topoTrailIds: new Set(input.topoTrailIds) } : {}),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Wrap an existing `WardenRule` as a trail with typed input/output.
|
|
174
|
+
*
|
|
175
|
+
* The trail ID follows the pattern `warden.rule.<rule-name>`.
|
|
176
|
+
*/
|
|
177
|
+
export function wrapRule(
|
|
178
|
+
options: WrapProjectAwareRuleOptions
|
|
179
|
+
): Trail<ProjectAwareRuleInput, RuleOutput>;
|
|
180
|
+
export function wrapRule(
|
|
181
|
+
options: WrapRuleOptions
|
|
182
|
+
): Trail<RuleInput, RuleOutput>;
|
|
183
|
+
export function wrapRule(
|
|
184
|
+
options: WrapRuleOptions | WrapProjectAwareRuleOptions
|
|
185
|
+
): Trail<RuleInput, RuleOutput> | Trail<ProjectAwareRuleInput, RuleOutput> {
|
|
186
|
+
const { rule, examples } = options;
|
|
187
|
+
const isProjectAware = 'checkWithContext' in rule;
|
|
188
|
+
|
|
189
|
+
if (isProjectAware) {
|
|
190
|
+
const projectAwareRule = rule as ProjectAwareWardenRule;
|
|
191
|
+
return trail(`warden.rule.${rule.name}`, {
|
|
192
|
+
description: rule.description,
|
|
193
|
+
examples: examples as Trail<
|
|
194
|
+
ProjectAwareRuleInput,
|
|
195
|
+
RuleOutput
|
|
196
|
+
>['examples'],
|
|
197
|
+
implementation: (input: ProjectAwareRuleInput) => {
|
|
198
|
+
const context = buildProjectContext(input);
|
|
199
|
+
const diagnostics = projectAwareRule.checkWithContext(
|
|
200
|
+
input.sourceCode,
|
|
201
|
+
input.filePath,
|
|
202
|
+
context
|
|
203
|
+
);
|
|
204
|
+
return Result.ok({ diagnostics: [...diagnostics] });
|
|
205
|
+
},
|
|
206
|
+
input: projectAwareRuleInput,
|
|
207
|
+
intent: 'read',
|
|
208
|
+
meta: buildRuleMeta(rule),
|
|
209
|
+
output: ruleOutput,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return trail(`warden.rule.${rule.name}`, {
|
|
214
|
+
description: rule.description,
|
|
215
|
+
examples: examples as Trail<RuleInput, RuleOutput>['examples'],
|
|
216
|
+
implementation: (input: RuleInput) => {
|
|
217
|
+
const diagnostics = rule.check(input.sourceCode, input.filePath);
|
|
218
|
+
return Result.ok({ diagnostics: [...diagnostics] });
|
|
219
|
+
},
|
|
220
|
+
input: ruleInput,
|
|
221
|
+
intent: 'read',
|
|
222
|
+
meta: buildRuleMeta(rule),
|
|
223
|
+
output: ruleOutput,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
interface WrapTopoRuleOptions {
|
|
228
|
+
/** The existing topo-aware warden rule to wrap. */
|
|
229
|
+
readonly rule: TopoAwareWardenRule;
|
|
230
|
+
/** Trail examples for testing and documentation. */
|
|
231
|
+
readonly examples: Trail<TopoAwareRuleInput, RuleOutput>['examples'];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Wrap an existing `TopoAwareWardenRule` as a trail.
|
|
236
|
+
*
|
|
237
|
+
* Mirrors `wrapRule` for the per-topo dispatch path. Topo-aware rules run
|
|
238
|
+
* once per topo against the compiled runtime graph rather than per file,
|
|
239
|
+
* so the trail accepts the live `Topo` as input.
|
|
240
|
+
*/
|
|
241
|
+
export const wrapTopoRule = (
|
|
242
|
+
options: WrapTopoRuleOptions
|
|
243
|
+
): Trail<TopoAwareRuleInput, RuleOutput> => {
|
|
244
|
+
const { rule, examples } = options;
|
|
245
|
+
return trail(`warden.rule.${rule.name}`, {
|
|
246
|
+
description: rule.description,
|
|
247
|
+
examples,
|
|
248
|
+
implementation: async (input: TopoAwareRuleInput) => {
|
|
249
|
+
try {
|
|
250
|
+
const diagnostics = await rule.checkTopo(input.topo, {
|
|
251
|
+
graph: input.graph,
|
|
252
|
+
});
|
|
253
|
+
return Result.ok({ diagnostics: [...diagnostics] });
|
|
254
|
+
} catch (error) {
|
|
255
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
256
|
+
return Result.err(
|
|
257
|
+
new InternalError(
|
|
258
|
+
`Topo-aware rule "${rule.name}" threw while inspecting topo: ${cause.message}`,
|
|
259
|
+
{ cause }
|
|
260
|
+
)
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
input: topoAwareRuleInput,
|
|
265
|
+
intent: 'read',
|
|
266
|
+
meta: buildRuleMeta(rule),
|
|
267
|
+
output: ruleOutput,
|
|
268
|
+
});
|
|
269
|
+
};
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace-manifest discovery for Warden project-aware rules.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
6
|
+
import { dirname, join, posix, relative, resolve } from 'node:path';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
listWorkspacePackages,
|
|
10
|
+
listWorkspacePatterns,
|
|
11
|
+
matchesAnyPathGlob,
|
|
12
|
+
} from '@ontrails/core';
|
|
13
|
+
|
|
14
|
+
export interface WardenPublicWorkspace {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly rootDir: string;
|
|
17
|
+
readonly packageJsonPath: string;
|
|
18
|
+
readonly hasExports: boolean;
|
|
19
|
+
readonly bin?: Readonly<Record<string, string>> | undefined;
|
|
20
|
+
readonly exportTargets?: Readonly<Record<string, string>> | undefined;
|
|
21
|
+
readonly files?: readonly string[] | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface WardenWorkspaceCollectionOptions {
|
|
25
|
+
/** Root-relative package manifests observed by the owning source collection. */
|
|
26
|
+
readonly collectedPackageJsonPaths?: ReadonlySet<string> | undefined;
|
|
27
|
+
readonly exclude?: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface PackageManifest {
|
|
31
|
+
readonly bin?: unknown;
|
|
32
|
+
readonly exports?: unknown;
|
|
33
|
+
readonly files?: unknown;
|
|
34
|
+
readonly name?: unknown;
|
|
35
|
+
readonly private?: unknown;
|
|
36
|
+
readonly workspaces?: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const normalizePath = (path: string): string => path.replaceAll('\\', '/');
|
|
40
|
+
|
|
41
|
+
const normalizeRealPath = (path: string): string => {
|
|
42
|
+
try {
|
|
43
|
+
return normalizePath(realpathSync(path));
|
|
44
|
+
} catch {
|
|
45
|
+
return normalizePath(resolve(path));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const rootRelativePath = (rootDir: string, path: string): string =>
|
|
50
|
+
normalizePath(relative(rootDir, path));
|
|
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
|
+
|
|
106
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
107
|
+
Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
108
|
+
|
|
109
|
+
const packageLocalName = (name: string): string =>
|
|
110
|
+
name.split('/').at(-1) ?? name;
|
|
111
|
+
|
|
112
|
+
const normalizeBin = (
|
|
113
|
+
name: string,
|
|
114
|
+
value: unknown
|
|
115
|
+
): Readonly<Record<string, string>> | undefined => {
|
|
116
|
+
if (typeof value === 'string') {
|
|
117
|
+
return { [packageLocalName(name)]: value };
|
|
118
|
+
}
|
|
119
|
+
if (!isRecord(value)) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const entries = Object.entries(value).filter(
|
|
124
|
+
(entry): entry is [string, string] => typeof entry[1] === 'string'
|
|
125
|
+
);
|
|
126
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const normalizeFiles = (value: unknown): readonly string[] | undefined => {
|
|
130
|
+
if (!Array.isArray(value)) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const files = value.filter(
|
|
135
|
+
(entry): entry is string => typeof entry === 'string'
|
|
136
|
+
);
|
|
137
|
+
return files.length > 0 ? files : undefined;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const resolveExportTarget = (
|
|
141
|
+
target: unknown,
|
|
142
|
+
depth = 0
|
|
143
|
+
): string | undefined => {
|
|
144
|
+
if (typeof target === 'string') {
|
|
145
|
+
return target;
|
|
146
|
+
}
|
|
147
|
+
if (!isRecord(target) || depth > 8) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const condition of ['bun', 'import', 'default', 'require'] as const) {
|
|
152
|
+
const conditionalTarget = target[condition];
|
|
153
|
+
const resolvedTarget = resolveExportTarget(conditionalTarget, depth + 1);
|
|
154
|
+
if (resolvedTarget) {
|
|
155
|
+
return resolvedTarget;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const exportSpecifierFromKey = (
|
|
162
|
+
packageName: string,
|
|
163
|
+
key: string
|
|
164
|
+
): string | undefined => {
|
|
165
|
+
if (key === '.') {
|
|
166
|
+
return packageName;
|
|
167
|
+
}
|
|
168
|
+
if (!key.startsWith('./') || key.includes('*')) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
return `${packageName}/${key.slice(2)}`;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const normalizeExportTargets = (
|
|
175
|
+
packageDir: string,
|
|
176
|
+
packageName: string,
|
|
177
|
+
exportsValue: unknown
|
|
178
|
+
): Readonly<Record<string, string>> | undefined => {
|
|
179
|
+
if (!isRecord(exportsValue)) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const targets: Record<string, string> = {};
|
|
184
|
+
for (const [key, value] of Object.entries(exportsValue)) {
|
|
185
|
+
const specifier = exportSpecifierFromKey(packageName, key);
|
|
186
|
+
const target = resolveExportTarget(value);
|
|
187
|
+
if (!specifier || !target) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
targets[specifier] = normalizeRealPath(join(packageDir, target));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return Object.keys(targets).length > 0 ? targets : undefined;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const publicWorkspaceFromManifest = (
|
|
197
|
+
packageJsonPath: string,
|
|
198
|
+
manifest: PackageManifest
|
|
199
|
+
): WardenPublicWorkspace | undefined => {
|
|
200
|
+
if (typeof manifest.name !== 'string') {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
if (!manifest.name.startsWith('@ontrails/')) {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
if (manifest.private === true) {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const packageDir = dirname(packageJsonPath);
|
|
211
|
+
const bin = normalizeBin(manifest.name, manifest.bin);
|
|
212
|
+
const exportTargets = normalizeExportTargets(
|
|
213
|
+
packageDir,
|
|
214
|
+
manifest.name,
|
|
215
|
+
manifest.exports
|
|
216
|
+
);
|
|
217
|
+
const files = normalizeFiles(manifest.files);
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
...(bin ? { bin } : {}),
|
|
221
|
+
...(exportTargets ? { exportTargets } : {}),
|
|
222
|
+
...(files ? { files } : {}),
|
|
223
|
+
hasExports: manifest.exports !== undefined,
|
|
224
|
+
name: manifest.name,
|
|
225
|
+
packageJsonPath: normalizeRealPath(packageJsonPath),
|
|
226
|
+
rootDir: normalizeRealPath(dirname(packageJsonPath)),
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export const collectPublicWorkspaces = (
|
|
231
|
+
rootDir: string,
|
|
232
|
+
options: WardenWorkspaceCollectionOptions = {}
|
|
233
|
+
): ReadonlyMap<string, WardenPublicWorkspace> => {
|
|
234
|
+
const normalizedRoot = normalizeRealPath(rootDir);
|
|
235
|
+
const workspaces = new Map<string, WardenPublicWorkspace>();
|
|
236
|
+
const candidates = options.collectedPackageJsonPaths
|
|
237
|
+
? collectedWorkspaceManifests(
|
|
238
|
+
normalizedRoot,
|
|
239
|
+
options.collectedPackageJsonPaths
|
|
240
|
+
)
|
|
241
|
+
: listWorkspacePackages<PackageManifest>(normalizedRoot);
|
|
242
|
+
|
|
243
|
+
for (const workspacePackage of candidates) {
|
|
244
|
+
const workspace = publicWorkspaceFromManifest(
|
|
245
|
+
workspacePackage.packageJsonPath,
|
|
246
|
+
workspacePackage.manifest
|
|
247
|
+
);
|
|
248
|
+
if (
|
|
249
|
+
workspace &&
|
|
250
|
+
!matchesAnyPathGlob(
|
|
251
|
+
rootRelativePath(normalizedRoot, workspace.rootDir),
|
|
252
|
+
options.exclude ?? []
|
|
253
|
+
) &&
|
|
254
|
+
!matchesAnyPathGlob(
|
|
255
|
+
rootRelativePath(normalizedRoot, workspace.packageJsonPath),
|
|
256
|
+
options.exclude ?? []
|
|
257
|
+
)
|
|
258
|
+
) {
|
|
259
|
+
workspaces.set(workspace.name, workspace);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return workspaces;
|
|
264
|
+
};
|