@ontrails/warden 1.0.0-beta.14 → 1.0.0-beta.16
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 +149 -0
- package/README.md +77 -34
- package/bin/warden.ts +22 -0
- package/package.json +26 -6
- package/src/ast.ts +28 -0
- package/src/cli.ts +1169 -116
- package/src/command.ts +927 -0
- package/src/config.ts +184 -0
- package/src/drift.ts +76 -34
- package/src/formatters.ts +54 -7
- package/src/guide.ts +234 -0
- package/src/index.ts +151 -38
- package/src/project-context.ts +163 -0
- package/src/resolve.ts +530 -0
- package/src/rules/activation-orphan.ts +97 -0
- package/src/rules/ast.ts +2884 -124
- package/src/rules/circular-refs.ts +154 -0
- package/src/rules/{context-no-trailhead-types.ts → context-no-surface-types.ts} +72 -12
- package/src/rules/contour-exists.ts +251 -0
- package/src/rules/contour-ids.ts +15 -0
- package/src/rules/cross-declarations.ts +277 -69
- package/src/rules/dead-internal-trail.ts +141 -0
- package/src/rules/draft-file-marking.ts +51 -3
- package/src/rules/draft-visible-debt.ts +40 -15
- package/src/rules/error-mapping-completeness.ts +288 -0
- package/src/rules/example-valid.ts +401 -0
- package/src/rules/fires-declarations.ts +758 -0
- package/src/rules/implementation-returns-result.ts +1042 -122
- package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
- package/src/rules/incomplete-crud.ts +580 -0
- package/src/rules/index.ts +156 -19
- package/src/rules/intent-propagation.ts +127 -0
- package/src/rules/layer-field-name-drift.ts +96 -0
- package/src/rules/metadata.ts +508 -0
- package/src/rules/missing-reconcile.ts +98 -0
- package/src/rules/missing-visibility.ts +110 -0
- package/src/rules/no-dev-permit-in-source.ts +99 -0
- package/src/rules/no-direct-implementation-call.ts +1 -1
- package/src/rules/no-legacy-layer-imports.ts +193 -0
- package/src/rules/no-native-error-result.ts +111 -0
- package/src/rules/no-sync-result-assumption.ts +1131 -96
- package/src/rules/no-throw-in-detour-recover.ts +225 -0
- package/src/rules/no-throw-in-implementation.ts +6 -4
- package/src/rules/on-references-exist.ts +194 -0
- package/src/rules/orphaned-signal.ts +150 -0
- package/src/rules/owner-projection-parity.ts +146 -0
- package/src/rules/permit-governance.ts +25 -0
- package/src/rules/public-internal-deep-imports.ts +517 -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 +187 -0
- package/src/rules/reference-exists.ts +98 -0
- package/src/rules/registry-names.ts +113 -0
- package/src/rules/resolved-import-boundary.ts +146 -0
- package/src/rules/{provision-declarations.ts → resource-declarations.ts} +208 -138
- package/src/rules/{provision-exists.ts → resource-exists.ts} +46 -51
- package/src/rules/resource-id-grammar.ts +65 -0
- package/src/rules/scan.ts +0 -26
- package/src/rules/scheduled-destroy-intent.ts +44 -0
- package/src/rules/signal-graph-coaching.ts +191 -0
- package/src/rules/specs.ts +5 -1
- package/src/rules/static-resource-accessor-preference.ts +657 -0
- package/src/rules/types.ts +187 -7
- package/src/rules/unmaterialized-activation-source.ts +84 -0
- package/src/rules/unreachable-detour-shadowing.ts +344 -0
- package/src/rules/valid-describe-refs.ts +160 -32
- package/src/rules/valid-detour-contract.ts +78 -0
- package/src/rules/warden-export-symmetry.ts +533 -0
- package/src/rules/warden-rules-use-ast.ts +996 -0
- package/src/rules/webhook-route-collision.ts +243 -0
- package/src/trails/activation-orphan.trail.ts +81 -0
- package/src/trails/circular-refs.trail.ts +29 -0
- package/src/trails/{context-no-trailhead-types.trail.ts → context-no-surface-types.trail.ts} +4 -4
- package/src/trails/contour-exists.trail.ts +21 -0
- package/src/trails/dead-internal-trail.trail.ts +26 -0
- package/src/trails/draft-file-marking.trail.ts +16 -0
- package/src/trails/draft-visible-debt.trail.ts +16 -0
- package/src/trails/error-mapping-completeness.trail.ts +29 -0
- package/src/trails/example-valid.trail.ts +25 -0
- package/src/trails/fires-declarations.trail.ts +23 -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 +56 -8
- package/src/trails/intent-propagation.trail.ts +30 -0
- package/src/trails/layer-field-name-drift.trail.ts +39 -0
- package/src/trails/missing-reconcile.trail.ts +33 -0
- package/src/trails/missing-visibility.trail.ts +22 -0
- package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
- package/src/trails/no-legacy-layer-imports.trail.ts +35 -0
- package/src/trails/no-native-error-result.trail.ts +18 -0
- package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
- package/src/trails/on-references-exist.trail.ts +21 -0
- package/src/trails/orphaned-signal.trail.ts +36 -0
- package/src/trails/owner-projection-parity.trail.ts +26 -0
- package/src/trails/permit-governance.trail.ts +51 -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/{provision-declarations.trail.ts → resource-declarations.trail.ts} +6 -6
- package/src/trails/{provision-exists.trail.ts → resource-exists.trail.ts} +7 -7
- package/src/trails/resource-id-grammar.trail.ts +39 -0
- package/src/trails/run.ts +133 -24
- package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
- package/src/trails/schema.ts +122 -4
- package/src/trails/signal-graph-coaching.trail.ts +74 -0
- package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
- package/src/trails/unmaterialized-activation-source.trail.ts +69 -0
- package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
- package/src/trails/valid-detour-contract.trail.ts +71 -0
- package/src/trails/warden-export-symmetry.trail.ts +16 -0
- package/src/trails/warden-rules-use-ast.trail.ts +45 -0
- package/src/trails/webhook-route-collision.trail.ts +50 -0
- package/src/trails/wrap-rule.ts +133 -14
- package/src/workspaces.ts +238 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/cli.d.ts +0 -46
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -247
- package/dist/cli.js.map +0 -1
- package/dist/draft.d.ts +0 -5
- package/dist/draft.d.ts.map +0 -1
- package/dist/draft.js +0 -16
- package/dist/draft.js.map +0 -1
- package/dist/drift.d.ts +0 -29
- package/dist/drift.d.ts.map +0 -1
- package/dist/drift.js +0 -61
- package/dist/drift.js.map +0 -1
- package/dist/formatters.d.ts +0 -30
- package/dist/formatters.d.ts.map +0 -1
- package/dist/formatters.js +0 -98
- package/dist/formatters.js.map +0 -1
- package/dist/index.d.ts +0 -37
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -38
- package/dist/index.js.map +0 -1
- package/dist/rules/ast.d.ts +0 -82
- package/dist/rules/ast.d.ts.map +0 -1
- package/dist/rules/ast.js +0 -363
- package/dist/rules/ast.js.map +0 -1
- package/dist/rules/context-no-surface-types.d.ts +0 -12
- package/dist/rules/context-no-surface-types.d.ts.map +0 -1
- package/dist/rules/context-no-surface-types.js +0 -96
- package/dist/rules/context-no-surface-types.js.map +0 -1
- package/dist/rules/context-no-trailhead-types.d.ts +0 -12
- package/dist/rules/context-no-trailhead-types.d.ts.map +0 -1
- package/dist/rules/context-no-trailhead-types.js +0 -96
- package/dist/rules/context-no-trailhead-types.js.map +0 -1
- package/dist/rules/cross-declarations.d.ts +0 -13
- package/dist/rules/cross-declarations.d.ts.map +0 -1
- package/dist/rules/cross-declarations.js +0 -264
- package/dist/rules/cross-declarations.js.map +0 -1
- package/dist/rules/draft-file-marking.d.ts +0 -6
- package/dist/rules/draft-file-marking.d.ts.map +0 -1
- package/dist/rules/draft-file-marking.js +0 -65
- package/dist/rules/draft-file-marking.js.map +0 -1
- package/dist/rules/draft-visible-debt.d.ts +0 -12
- package/dist/rules/draft-visible-debt.d.ts.map +0 -1
- package/dist/rules/draft-visible-debt.js +0 -45
- package/dist/rules/draft-visible-debt.js.map +0 -1
- package/dist/rules/follow-declarations.d.ts +0 -13
- package/dist/rules/follow-declarations.d.ts.map +0 -1
- package/dist/rules/follow-declarations.js +0 -264
- package/dist/rules/follow-declarations.js.map +0 -1
- package/dist/rules/implementation-returns-result.d.ts +0 -13
- package/dist/rules/implementation-returns-result.d.ts.map +0 -1
- package/dist/rules/implementation-returns-result.js +0 -277
- package/dist/rules/implementation-returns-result.js.map +0 -1
- package/dist/rules/index.d.ts +0 -20
- package/dist/rules/index.d.ts.map +0 -1
- package/dist/rules/index.js +0 -49
- package/dist/rules/index.js.map +0 -1
- package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
- package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
- package/dist/rules/no-direct-impl-in-route.js +0 -47
- package/dist/rules/no-direct-impl-in-route.js.map +0 -1
- package/dist/rules/no-direct-implementation-call.d.ts +0 -12
- package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
- package/dist/rules/no-direct-implementation-call.js +0 -39
- package/dist/rules/no-direct-implementation-call.js.map +0 -1
- package/dist/rules/no-sync-result-assumption.d.ts +0 -6
- package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
- package/dist/rules/no-sync-result-assumption.js +0 -98
- package/dist/rules/no-sync-result-assumption.js.map +0 -1
- package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
- package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
- package/dist/rules/no-throw-in-detour-target.js +0 -87
- package/dist/rules/no-throw-in-detour-target.js.map +0 -1
- package/dist/rules/no-throw-in-implementation.d.ts +0 -9
- package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
- package/dist/rules/no-throw-in-implementation.js +0 -34
- package/dist/rules/no-throw-in-implementation.js.map +0 -1
- package/dist/rules/prefer-schema-inference.d.ts +0 -7
- package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
- package/dist/rules/prefer-schema-inference.js +0 -86
- package/dist/rules/prefer-schema-inference.js.map +0 -1
- package/dist/rules/provision-declarations.d.ts +0 -14
- package/dist/rules/provision-declarations.d.ts.map +0 -1
- package/dist/rules/provision-declarations.js +0 -344
- package/dist/rules/provision-declarations.js.map +0 -1
- package/dist/rules/provision-exists.d.ts +0 -6
- package/dist/rules/provision-exists.d.ts.map +0 -1
- package/dist/rules/provision-exists.js +0 -90
- package/dist/rules/provision-exists.js.map +0 -1
- package/dist/rules/scan.d.ts +0 -8
- package/dist/rules/scan.d.ts.map +0 -1
- package/dist/rules/scan.js +0 -32
- package/dist/rules/scan.js.map +0 -1
- package/dist/rules/service-declarations.d.ts +0 -16
- package/dist/rules/service-declarations.d.ts.map +0 -1
- package/dist/rules/service-declarations.js +0 -346
- package/dist/rules/service-declarations.js.map +0 -1
- package/dist/rules/service-exists.d.ts +0 -8
- package/dist/rules/service-exists.d.ts.map +0 -1
- package/dist/rules/service-exists.js +0 -91
- package/dist/rules/service-exists.js.map +0 -1
- package/dist/rules/specs.d.ts +0 -29
- package/dist/rules/specs.d.ts.map +0 -1
- package/dist/rules/specs.js +0 -192
- package/dist/rules/specs.js.map +0 -1
- package/dist/rules/structure.d.ts +0 -13
- package/dist/rules/structure.d.ts.map +0 -1
- package/dist/rules/structure.js +0 -142
- package/dist/rules/structure.js.map +0 -1
- package/dist/rules/types.d.ts +0 -54
- package/dist/rules/types.d.ts.map +0 -1
- package/dist/rules/types.js +0 -2
- package/dist/rules/types.js.map +0 -1
- package/dist/rules/valid-describe-refs.d.ts +0 -7
- package/dist/rules/valid-describe-refs.d.ts.map +0 -1
- package/dist/rules/valid-describe-refs.js +0 -51
- package/dist/rules/valid-describe-refs.js.map +0 -1
- package/dist/rules/valid-detour-refs.d.ts +0 -6
- package/dist/rules/valid-detour-refs.d.ts.map +0 -1
- package/dist/rules/valid-detour-refs.js +0 -117
- package/dist/rules/valid-detour-refs.js.map +0 -1
- package/dist/trails/context-no-surface-types.trail.d.ts +0 -13
- package/dist/trails/context-no-surface-types.trail.d.ts.map +0 -1
- package/dist/trails/context-no-surface-types.trail.js +0 -21
- package/dist/trails/context-no-surface-types.trail.js.map +0 -1
- package/dist/trails/context-no-trailhead-types.trail.d.ts +0 -13
- package/dist/trails/context-no-trailhead-types.trail.d.ts.map +0 -1
- package/dist/trails/context-no-trailhead-types.trail.js +0 -21
- package/dist/trails/context-no-trailhead-types.trail.js.map +0 -1
- package/dist/trails/cross-declarations.trail.d.ts +0 -13
- package/dist/trails/cross-declarations.trail.d.ts.map +0 -1
- package/dist/trails/cross-declarations.trail.js +0 -22
- package/dist/trails/cross-declarations.trail.js.map +0 -1
- package/dist/trails/follow-declarations.trail.d.ts +0 -13
- package/dist/trails/follow-declarations.trail.d.ts.map +0 -1
- package/dist/trails/follow-declarations.trail.js +0 -22
- package/dist/trails/follow-declarations.trail.js.map +0 -1
- package/dist/trails/implementation-returns-result.trail.d.ts +0 -13
- package/dist/trails/implementation-returns-result.trail.d.ts.map +0 -1
- package/dist/trails/implementation-returns-result.trail.js +0 -20
- package/dist/trails/implementation-returns-result.trail.js.map +0 -1
- package/dist/trails/index.d.ts +0 -16
- package/dist/trails/index.d.ts.map +0 -1
- package/dist/trails/index.js +0 -15
- package/dist/trails/index.js.map +0 -1
- package/dist/trails/no-direct-impl-in-route.trail.d.ts +0 -13
- package/dist/trails/no-direct-impl-in-route.trail.d.ts.map +0 -1
- package/dist/trails/no-direct-impl-in-route.trail.js +0 -22
- package/dist/trails/no-direct-impl-in-route.trail.js.map +0 -1
- package/dist/trails/no-direct-implementation-call.trail.d.ts +0 -13
- package/dist/trails/no-direct-implementation-call.trail.d.ts.map +0 -1
- package/dist/trails/no-direct-implementation-call.trail.js +0 -16
- package/dist/trails/no-direct-implementation-call.trail.js.map +0 -1
- package/dist/trails/no-sync-result-assumption.trail.d.ts +0 -13
- package/dist/trails/no-sync-result-assumption.trail.d.ts.map +0 -1
- package/dist/trails/no-sync-result-assumption.trail.js +0 -19
- package/dist/trails/no-sync-result-assumption.trail.js.map +0 -1
- package/dist/trails/no-throw-in-detour-target.trail.d.ts +0 -15
- package/dist/trails/no-throw-in-detour-target.trail.d.ts.map +0 -1
- package/dist/trails/no-throw-in-detour-target.trail.js +0 -20
- package/dist/trails/no-throw-in-detour-target.trail.js.map +0 -1
- package/dist/trails/no-throw-in-implementation.trail.d.ts +0 -13
- package/dist/trails/no-throw-in-implementation.trail.d.ts.map +0 -1
- package/dist/trails/no-throw-in-implementation.trail.js +0 -20
- package/dist/trails/no-throw-in-implementation.trail.js.map +0 -1
- package/dist/trails/prefer-schema-inference.trail.d.ts +0 -13
- package/dist/trails/prefer-schema-inference.trail.d.ts.map +0 -1
- package/dist/trails/prefer-schema-inference.trail.js +0 -21
- package/dist/trails/prefer-schema-inference.trail.js.map +0 -1
- package/dist/trails/provision-declarations.trail.d.ts +0 -13
- package/dist/trails/provision-declarations.trail.d.ts.map +0 -1
- package/dist/trails/provision-declarations.trail.js +0 -25
- package/dist/trails/provision-declarations.trail.js.map +0 -1
- package/dist/trails/provision-exists.trail.d.ts +0 -15
- package/dist/trails/provision-exists.trail.d.ts.map +0 -1
- package/dist/trails/provision-exists.trail.js +0 -27
- package/dist/trails/provision-exists.trail.js.map +0 -1
- package/dist/trails/run.d.ts +0 -17
- package/dist/trails/run.d.ts.map +0 -1
- package/dist/trails/run.js +0 -39
- package/dist/trails/run.js.map +0 -1
- package/dist/trails/schema.d.ts +0 -53
- package/dist/trails/schema.d.ts.map +0 -1
- package/dist/trails/schema.js +0 -42
- package/dist/trails/schema.js.map +0 -1
- package/dist/trails/service-declarations.trail.d.ts +0 -26
- package/dist/trails/service-declarations.trail.d.ts.map +0 -1
- package/dist/trails/service-declarations.trail.js +0 -27
- package/dist/trails/service-declarations.trail.js.map +0 -1
- package/dist/trails/service-exists.trail.d.ts +0 -32
- package/dist/trails/service-exists.trail.d.ts.map +0 -1
- package/dist/trails/service-exists.trail.js +0 -29
- package/dist/trails/service-exists.trail.js.map +0 -1
- package/dist/trails/topo.d.ts +0 -3
- package/dist/trails/topo.d.ts.map +0 -1
- package/dist/trails/topo.js +0 -5
- package/dist/trails/topo.js.map +0 -1
- package/dist/trails/valid-describe-refs.trail.d.ts +0 -15
- package/dist/trails/valid-describe-refs.trail.d.ts.map +0 -1
- package/dist/trails/valid-describe-refs.trail.js +0 -18
- package/dist/trails/valid-describe-refs.trail.js.map +0 -1
- package/dist/trails/valid-detour-refs.trail.d.ts +0 -15
- package/dist/trails/valid-detour-refs.trail.d.ts.map +0 -1
- package/dist/trails/valid-detour-refs.trail.js +0 -24
- package/dist/trails/valid-detour-refs.trail.js.map +0 -1
- package/dist/trails/wrap-rule.d.ts +0 -29
- package/dist/trails/wrap-rule.d.ts.map +0 -1
- package/dist/trails/wrap-rule.js +0 -47
- package/dist/trails/wrap-rule.js.map +0 -1
- package/src/__tests__/cli.test.ts +0 -311
- package/src/__tests__/cross-declarations.test.ts +0 -303
- package/src/__tests__/drift.test.ts +0 -144
- package/src/__tests__/formatters.test.ts +0 -157
- package/src/__tests__/implementation-returns-result.test.ts +0 -129
- package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
- package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
- package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
- package/src/__tests__/prefer-schema-inference.test.ts +0 -84
- package/src/__tests__/provision-declarations.test.ts +0 -318
- package/src/__tests__/provision-exists.test.ts +0 -122
- package/src/__tests__/rules.test.ts +0 -227
- package/src/__tests__/trails.test.ts +0 -19
- package/src/__tests__/valid-describe-refs.test.ts +0 -60
- package/src/__tests__/wrap-rule.test.ts +0 -41
- package/src/rules/no-direct-impl-in-route.ts +0 -81
- package/src/rules/no-throw-in-detour-target.ts +0 -150
- package/src/rules/valid-detour-refs.ts +0 -189
- package/src/trails/no-direct-impl-in-route.trail.ts +0 -22
- package/src/trails/no-throw-in-detour-target.trail.ts +0 -20
- package/src/trails/valid-detour-refs.trail.ts +0 -24
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
package/src/config.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import type { WardenDiagnostic } from './rules/types.js';
|
|
4
|
+
|
|
5
|
+
export const wardenDepthValues = ['source', 'project', 'topo', 'all'] as const;
|
|
6
|
+
export const wardenFailOnValues = ['error', 'warning'] as const;
|
|
7
|
+
export const wardenFormatValues = ['summary', 'github', 'json'] as const;
|
|
8
|
+
export const wardenLockValues = ['auto', 'cached', 'refresh', 'skip'] as const;
|
|
9
|
+
export const wardenDraftsValues = ['include', 'exclude', 'only'] as const;
|
|
10
|
+
|
|
11
|
+
const appNameSchema = z.string().min(1);
|
|
12
|
+
|
|
13
|
+
const wardenConfigObjectSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
apps: z.array(appNameSchema).min(1).optional(),
|
|
16
|
+
depth: z.enum(wardenDepthValues).default('all'),
|
|
17
|
+
drafts: z.enum(wardenDraftsValues).default('include'),
|
|
18
|
+
failOn: z.enum(wardenFailOnValues).default('error'),
|
|
19
|
+
format: z.enum(wardenFormatValues).default('summary'),
|
|
20
|
+
lock: z.enum(wardenLockValues).default('auto'),
|
|
21
|
+
})
|
|
22
|
+
.strict();
|
|
23
|
+
|
|
24
|
+
export const wardenConfigSchema = wardenConfigObjectSchema
|
|
25
|
+
.optional()
|
|
26
|
+
.transform((value) => wardenConfigObjectSchema.parse(value ?? {}));
|
|
27
|
+
|
|
28
|
+
export type WardenConfig = z.output<typeof wardenConfigSchema>;
|
|
29
|
+
export type WardenConfigInput = z.input<typeof wardenConfigSchema>;
|
|
30
|
+
export type WardenDepth = (typeof wardenDepthValues)[number];
|
|
31
|
+
export type WardenDraftsMode = (typeof wardenDraftsValues)[number];
|
|
32
|
+
export type WardenFailOn = (typeof wardenFailOnValues)[number];
|
|
33
|
+
export type WardenFormat = (typeof wardenFormatValues)[number];
|
|
34
|
+
export type WardenLockMode = (typeof wardenLockValues)[number];
|
|
35
|
+
|
|
36
|
+
export interface WardenConfigLayer extends Partial<WardenConfig> {
|
|
37
|
+
readonly noLockMutation?: boolean | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface EffectiveWardenConfig extends WardenConfig {
|
|
41
|
+
readonly noLockMutation: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ResolveWardenConfigOptions {
|
|
45
|
+
readonly cli?: WardenConfigLayer | undefined;
|
|
46
|
+
readonly config?: WardenConfigInput | undefined;
|
|
47
|
+
readonly defaults?: Partial<WardenConfig> | undefined;
|
|
48
|
+
readonly env?: Record<string, string | undefined> | undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface WardenConfigResolution {
|
|
52
|
+
readonly diagnostics: readonly WardenDiagnostic[];
|
|
53
|
+
readonly effectiveConfig: EffectiveWardenConfig;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const baseWardenConfig = (): WardenConfig => {
|
|
57
|
+
const omittedSection: unknown = undefined;
|
|
58
|
+
return wardenConfigSchema.parse(omittedSection);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const cleanUndefinedValues = <T extends Record<string, unknown>>(
|
|
62
|
+
value: T
|
|
63
|
+
): Partial<T> =>
|
|
64
|
+
Object.fromEntries(
|
|
65
|
+
Object.entries(value).filter(([, entry]) => entry !== undefined)
|
|
66
|
+
) as Partial<T>;
|
|
67
|
+
|
|
68
|
+
const splitApps = (value: string): readonly string[] =>
|
|
69
|
+
value
|
|
70
|
+
.split(',')
|
|
71
|
+
.map((entry) => entry.trim())
|
|
72
|
+
.filter((entry) => entry.length > 0);
|
|
73
|
+
|
|
74
|
+
const readEnvLayer = (
|
|
75
|
+
env: Record<string, string | undefined>
|
|
76
|
+
): Partial<WardenConfig> =>
|
|
77
|
+
cleanUndefinedValues({
|
|
78
|
+
apps: env['TRAILS_APPS'] ? splitApps(env['TRAILS_APPS']) : undefined,
|
|
79
|
+
depth: env['TRAILS_DEPTH'],
|
|
80
|
+
drafts: env['TRAILS_DRAFTS'],
|
|
81
|
+
failOn: env['TRAILS_FAIL_ON'],
|
|
82
|
+
format: env['TRAILS_FORMAT'],
|
|
83
|
+
lock: env['TRAILS_LOCK'],
|
|
84
|
+
}) as Partial<WardenConfig>;
|
|
85
|
+
|
|
86
|
+
const configDiagnostic = (message: string): WardenDiagnostic => ({
|
|
87
|
+
filePath: '<warden-config>',
|
|
88
|
+
line: 1,
|
|
89
|
+
message,
|
|
90
|
+
rule: 'warden-config',
|
|
91
|
+
severity: 'error',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const formatIssues = (error: z.ZodError): string =>
|
|
95
|
+
error.issues
|
|
96
|
+
.map((issue) => {
|
|
97
|
+
const path = issue.path.length > 0 ? issue.path.join('.') : '<root>';
|
|
98
|
+
return `${path}: ${issue.message}`;
|
|
99
|
+
})
|
|
100
|
+
.join('; ');
|
|
101
|
+
|
|
102
|
+
const parseConfigLayer = (
|
|
103
|
+
label: string,
|
|
104
|
+
value: WardenConfigInput | undefined
|
|
105
|
+
): {
|
|
106
|
+
readonly data: Partial<WardenConfig>;
|
|
107
|
+
readonly diagnostics: readonly WardenDiagnostic[];
|
|
108
|
+
} => {
|
|
109
|
+
if (value === undefined) {
|
|
110
|
+
return { data: {}, diagnostics: [] };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const parsed = wardenConfigSchema.safeParse(value);
|
|
114
|
+
if (parsed.success) {
|
|
115
|
+
if (typeof value !== 'object' || value === null) {
|
|
116
|
+
return { data: parsed.data, diagnostics: [] };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
data: Object.fromEntries(
|
|
121
|
+
Object.keys(value).map((key) => [
|
|
122
|
+
key,
|
|
123
|
+
parsed.data[key as keyof WardenConfig],
|
|
124
|
+
])
|
|
125
|
+
) as Partial<WardenConfig>,
|
|
126
|
+
diagnostics: [],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
data: {},
|
|
132
|
+
diagnostics: [
|
|
133
|
+
configDiagnostic(
|
|
134
|
+
`Invalid ${label} Warden config: ${formatIssues(parsed.error)}`
|
|
135
|
+
),
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const resolveWardenConfig = ({
|
|
141
|
+
cli,
|
|
142
|
+
config,
|
|
143
|
+
defaults,
|
|
144
|
+
env = {},
|
|
145
|
+
}: ResolveWardenConfigOptions = {}): WardenConfigResolution => {
|
|
146
|
+
const { noLockMutation = false, ...cliConfig } = cli ?? {};
|
|
147
|
+
const defaultLayer = wardenConfigSchema.parse({
|
|
148
|
+
...baseWardenConfig(),
|
|
149
|
+
...defaults,
|
|
150
|
+
});
|
|
151
|
+
const configLayer = parseConfigLayer('file', config);
|
|
152
|
+
const envLayer = parseConfigLayer('environment', readEnvLayer(env));
|
|
153
|
+
const merged = {
|
|
154
|
+
...defaultLayer,
|
|
155
|
+
...configLayer.data,
|
|
156
|
+
...envLayer.data,
|
|
157
|
+
...cleanUndefinedValues(cliConfig),
|
|
158
|
+
};
|
|
159
|
+
const parsed = wardenConfigSchema.safeParse(merged);
|
|
160
|
+
const diagnostics = [...configLayer.diagnostics, ...envLayer.diagnostics];
|
|
161
|
+
|
|
162
|
+
if (!parsed.success) {
|
|
163
|
+
return {
|
|
164
|
+
diagnostics: [
|
|
165
|
+
...diagnostics,
|
|
166
|
+
configDiagnostic(
|
|
167
|
+
`Invalid effective Warden config: ${formatIssues(parsed.error)}`
|
|
168
|
+
),
|
|
169
|
+
],
|
|
170
|
+
effectiveConfig: {
|
|
171
|
+
...defaultLayer,
|
|
172
|
+
noLockMutation,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
diagnostics,
|
|
179
|
+
effectiveConfig: {
|
|
180
|
+
...parsed.data,
|
|
181
|
+
noLockMutation,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
};
|
package/src/drift.ts
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Topo lock drift detection.
|
|
3
3
|
*
|
|
4
|
-
* Compares the
|
|
5
|
-
*
|
|
6
|
-
* updating the
|
|
7
|
-
* legacy single-line hash format.
|
|
4
|
+
* Compares the `topo.lock` artifact hash listed in `trails.lock` against a
|
|
5
|
+
* freshly generated TopoGraph hash to detect when the trail topology has
|
|
6
|
+
* changed without updating the artifact family.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
import { existsSync, statSync } from 'node:fs';
|
|
11
10
|
|
|
12
11
|
import type { Topo } from '@ontrails/core';
|
|
13
12
|
import {
|
|
14
|
-
|
|
13
|
+
deriveTrailsDir,
|
|
15
14
|
NotFoundError,
|
|
16
15
|
ValidationError,
|
|
17
16
|
} from '@ontrails/core';
|
|
18
|
-
import { resolveTrailsDir } from '@ontrails/core/internal/trails-db';
|
|
19
17
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
createTopoStore,
|
|
19
|
+
deriveTopoGraph,
|
|
20
|
+
deriveTopoGraphHash,
|
|
21
|
+
isTopoArtifactRegenerationError,
|
|
22
|
+
readLockManifest,
|
|
23
|
+
} from '@ontrails/topographer';
|
|
24
|
+
import type { LockManifest } from '@ontrails/topographer';
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Result of a drift check comparing committed trails.lock against the current state.
|
|
@@ -36,6 +37,44 @@ export interface DriftResult {
|
|
|
36
37
|
readonly currentHash: string;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
interface BlockedLockRead {
|
|
41
|
+
readonly drift: DriftResult;
|
|
42
|
+
readonly kind: 'blocked-lock-read';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const blockedDrift = (reason: string): DriftResult => ({
|
|
46
|
+
blockedReason: reason,
|
|
47
|
+
committedHash: null,
|
|
48
|
+
currentHash: 'blocked',
|
|
49
|
+
stale: true,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const blockedLockRead = (reason: string): BlockedLockRead => ({
|
|
53
|
+
drift: blockedDrift(reason),
|
|
54
|
+
kind: 'blocked-lock-read',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const readCommittedLockManifest = async (
|
|
58
|
+
rootDir: string
|
|
59
|
+
): Promise<BlockedLockRead | LockManifest | null> => {
|
|
60
|
+
const trailsDir = deriveTrailsDir({ rootDir });
|
|
61
|
+
try {
|
|
62
|
+
return existsSync(rootDir) && statSync(rootDir).isDirectory()
|
|
63
|
+
? await readLockManifest({ dir: trailsDir })
|
|
64
|
+
: null;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (isTopoArtifactRegenerationError(error)) {
|
|
67
|
+
return blockedLockRead(error.message);
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const isBlockedLockRead = (
|
|
74
|
+
result: BlockedLockRead | LockManifest | null
|
|
75
|
+
): result is BlockedLockRead =>
|
|
76
|
+
result !== null && 'kind' in result && result.kind === 'blocked-lock-read';
|
|
77
|
+
|
|
39
78
|
/**
|
|
40
79
|
* Check whether the committed trails.lock is stale compared to the current topology.
|
|
41
80
|
*
|
|
@@ -46,47 +85,50 @@ export const checkDrift = async (
|
|
|
46
85
|
topo?: Topo | undefined
|
|
47
86
|
): Promise<DriftResult> => {
|
|
48
87
|
try {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
88
|
+
const lockManifest = await readCommittedLockManifest(rootDir);
|
|
89
|
+
if (isBlockedLockRead(lockManifest)) {
|
|
90
|
+
return lockManifest.drift;
|
|
91
|
+
}
|
|
92
|
+
const topoArtifact =
|
|
93
|
+
lockManifest?.artifacts.find(
|
|
94
|
+
(artifact) => artifact.role === 'topo' && artifact.path === 'topo.lock'
|
|
95
|
+
) ?? null;
|
|
96
|
+
if (lockManifest !== null && topoArtifact === null) {
|
|
97
|
+
return blockedDrift(
|
|
98
|
+
'trails.lock does not contain a topo.lock artifact. Regenerate with `trails topo compile`.'
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const readStoredHash = (): string | undefined => {
|
|
57
102
|
try {
|
|
58
|
-
return createTopoStore({ rootDir }).exports.get()?.
|
|
103
|
+
return createTopoStore({ rootDir }).exports.get()?.topoGraphHash;
|
|
59
104
|
} catch (error) {
|
|
60
105
|
if (error instanceof NotFoundError) {
|
|
61
106
|
return;
|
|
62
107
|
}
|
|
63
108
|
throw error;
|
|
64
109
|
}
|
|
65
|
-
}
|
|
110
|
+
};
|
|
66
111
|
const currentHash =
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
: hashTrailheadMap(generateTrailheadMap(topo)));
|
|
112
|
+
topo === undefined
|
|
113
|
+
? (readStoredHash() ?? 'unknown')
|
|
114
|
+
: deriveTopoGraphHash(deriveTopoGraph(topo));
|
|
71
115
|
|
|
72
116
|
return {
|
|
73
|
-
committedHash:
|
|
117
|
+
committedHash: topoArtifact?.sha256 ?? null,
|
|
74
118
|
currentHash,
|
|
75
119
|
stale:
|
|
76
|
-
|
|
120
|
+
topoArtifact !== null &&
|
|
77
121
|
currentHash !== 'unknown' &&
|
|
78
|
-
|
|
122
|
+
topoArtifact.sha256 !== currentHash,
|
|
79
123
|
};
|
|
80
124
|
} catch (error) {
|
|
81
|
-
if (
|
|
125
|
+
if (
|
|
126
|
+
!(error instanceof ValidationError) &&
|
|
127
|
+
!isTopoArtifactRegenerationError(error)
|
|
128
|
+
) {
|
|
82
129
|
throw error;
|
|
83
130
|
}
|
|
84
131
|
|
|
85
|
-
return
|
|
86
|
-
blockedReason: error.message,
|
|
87
|
-
committedHash: null,
|
|
88
|
-
currentHash: 'blocked',
|
|
89
|
-
stale: true,
|
|
90
|
-
};
|
|
132
|
+
return blockedDrift(error.message);
|
|
91
133
|
}
|
|
92
134
|
};
|
package/src/formatters.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { WardenReport } from './cli.js';
|
|
10
|
-
import type { WardenSeverity } from './rules/types.js';
|
|
10
|
+
import type { WardenGuidanceLink, WardenSeverity } from './rules/types.js';
|
|
11
11
|
|
|
12
12
|
/** Map warden severity to GitHub Actions annotation level. */
|
|
13
13
|
const ghLevel: Record<WardenSeverity, string> = {
|
|
@@ -36,7 +36,7 @@ export const formatGitHubAnnotations = (report: WardenReport): string => {
|
|
|
36
36
|
lines.push(`::error::drift: ${report.drift.blockedReason}`);
|
|
37
37
|
} else if (report.drift?.stale) {
|
|
38
38
|
lines.push(
|
|
39
|
-
'::error::drift: trails.lock is stale (regenerate with `trails topo
|
|
39
|
+
'::error::drift: trails.lock is stale (regenerate with `trails topo compile`)'
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -68,9 +68,56 @@ export const formatJson = (report: WardenReport): string => {
|
|
|
68
68
|
);
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
const formatGuidanceLink = (link: WardenGuidanceLink): string => {
|
|
72
|
+
if (link.path !== undefined) {
|
|
73
|
+
return `[${link.label}](${link.path})`;
|
|
74
|
+
}
|
|
75
|
+
if (link.url !== undefined) {
|
|
76
|
+
return `[${link.label}](${link.url})`;
|
|
77
|
+
}
|
|
78
|
+
return link.label;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Format diagnostic guidance as indented markdown lines. */
|
|
82
|
+
const diagnosticGuidanceLines = (
|
|
83
|
+
d: WardenReport['diagnostics'][number]
|
|
84
|
+
): readonly string[] => {
|
|
85
|
+
const { guidance } = d;
|
|
86
|
+
if (guidance === undefined) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const lines = [` - Next: ${guidance.summary}`];
|
|
91
|
+
if (guidance.steps !== undefined && guidance.steps.length > 0) {
|
|
92
|
+
lines.push(
|
|
93
|
+
` - Steps: ${guidance.steps
|
|
94
|
+
.map((step, index) => `${String(index + 1)}. ${step}`)
|
|
95
|
+
.join(' ')}`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (guidance.docs !== undefined && guidance.docs.length > 0) {
|
|
99
|
+
lines.push(` - Docs: ${guidance.docs.map(formatGuidanceLink).join(', ')}`);
|
|
100
|
+
}
|
|
101
|
+
if (guidance.commands !== undefined && guidance.commands.length > 0) {
|
|
102
|
+
lines.push(
|
|
103
|
+
` - Commands: ${guidance.commands.map((cmd) => `\`${cmd}\``).join(', ')}`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (guidance.relatedRules !== undefined && guidance.relatedRules.length > 0) {
|
|
107
|
+
lines.push(
|
|
108
|
+
` - Related: ${guidance.relatedRules.map((rule) => `\`${rule}\``).join(', ')}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return lines;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** Format a diagnostic as markdown lines. */
|
|
115
|
+
const diagnosticLines = (
|
|
116
|
+
d: WardenReport['diagnostics'][number]
|
|
117
|
+
): readonly string[] => [
|
|
118
|
+
`- \`${d.filePath}:${String(d.line)}\` — ${d.rule}: ${d.message}`,
|
|
119
|
+
...diagnosticGuidanceLines(d),
|
|
120
|
+
];
|
|
74
121
|
|
|
75
122
|
/** Render a severity group as a headed markdown section, or empty array. */
|
|
76
123
|
const severitySection = (
|
|
@@ -80,7 +127,7 @@ const severitySection = (
|
|
|
80
127
|
if (diagnostics.length === 0) {
|
|
81
128
|
return [];
|
|
82
129
|
}
|
|
83
|
-
return ['', `### ${heading}`, ...diagnostics.
|
|
130
|
+
return ['', `### ${heading}`, ...diagnostics.flatMap(diagnosticLines)];
|
|
84
131
|
};
|
|
85
132
|
|
|
86
133
|
/** Render a drift section if stale, otherwise empty array. */
|
|
@@ -99,7 +146,7 @@ const driftSection = (drift: WardenReport['drift']): readonly string[] => {
|
|
|
99
146
|
return [
|
|
100
147
|
'',
|
|
101
148
|
'### Drift',
|
|
102
|
-
'- trails.lock is stale (regenerate with `trails topo
|
|
149
|
+
'- trails.lock is stale (regenerate with `trails topo compile`)',
|
|
103
150
|
];
|
|
104
151
|
};
|
|
105
152
|
|
package/src/guide.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
WardenGuidance,
|
|
3
|
+
WardenGuidanceLink,
|
|
4
|
+
WardenRuleConcern,
|
|
5
|
+
WardenRuleLifecycle,
|
|
6
|
+
WardenRuleScope,
|
|
7
|
+
WardenRuleTier,
|
|
8
|
+
WardenSeverity,
|
|
9
|
+
} from './rules/index.js';
|
|
10
|
+
import {
|
|
11
|
+
listWardenRuleMetadata,
|
|
12
|
+
wardenRules,
|
|
13
|
+
wardenTopoRules,
|
|
14
|
+
} from './rules/index.js';
|
|
15
|
+
import type { WardenDepth } from './config.js';
|
|
16
|
+
|
|
17
|
+
export const wardenGuideFormatValues = [
|
|
18
|
+
'markdown',
|
|
19
|
+
'agent-json',
|
|
20
|
+
'manifest',
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export type WardenGuideFormat = (typeof wardenGuideFormatValues)[number];
|
|
24
|
+
|
|
25
|
+
export interface WardenRuleGuideEntry {
|
|
26
|
+
readonly concern: WardenRuleConcern;
|
|
27
|
+
readonly depth: WardenDepth;
|
|
28
|
+
readonly description: string;
|
|
29
|
+
readonly docs: readonly WardenGuidanceLink[];
|
|
30
|
+
readonly guidance?: WardenGuidance | undefined;
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly invariant: string;
|
|
33
|
+
readonly lifecycle: WardenRuleLifecycle;
|
|
34
|
+
readonly scope: WardenRuleScope;
|
|
35
|
+
readonly severity: WardenSeverity;
|
|
36
|
+
readonly tier: WardenRuleTier;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface WardenGuideManifest {
|
|
40
|
+
readonly generatedFrom: {
|
|
41
|
+
readonly package: '@ontrails/warden';
|
|
42
|
+
readonly registries: readonly ['wardenRules', 'wardenTopoRules'];
|
|
43
|
+
readonly source: 'builtin-rule-metadata';
|
|
44
|
+
};
|
|
45
|
+
readonly kind: 'trails-warden-guide-manifest';
|
|
46
|
+
readonly ruleCount: number;
|
|
47
|
+
readonly rules: readonly WardenRuleGuideEntry[];
|
|
48
|
+
readonly version: 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface WardenAgentRuleGuide {
|
|
52
|
+
readonly appliesAt: {
|
|
53
|
+
readonly depth: WardenDepth;
|
|
54
|
+
readonly scope: WardenRuleScope;
|
|
55
|
+
readonly tier: WardenRuleTier;
|
|
56
|
+
};
|
|
57
|
+
readonly concern: WardenRuleConcern;
|
|
58
|
+
readonly guidance?: WardenGuidance | undefined;
|
|
59
|
+
readonly id: string;
|
|
60
|
+
readonly invariant: string;
|
|
61
|
+
readonly severity: WardenSeverity;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface WardenAgentGuide {
|
|
65
|
+
readonly instructions: readonly string[];
|
|
66
|
+
readonly kind: 'trails-warden-agent-guide';
|
|
67
|
+
readonly rules: readonly WardenAgentRuleGuide[];
|
|
68
|
+
readonly version: 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const lookupRule = (id: string) =>
|
|
72
|
+
wardenRules.get(id) ?? wardenTopoRules.get(id);
|
|
73
|
+
|
|
74
|
+
const compareRuleEntries = (
|
|
75
|
+
[left]: readonly [string, unknown],
|
|
76
|
+
[right]: readonly [string, unknown]
|
|
77
|
+
): number => left.localeCompare(right);
|
|
78
|
+
|
|
79
|
+
export const buildWardenGuideManifest = (): WardenGuideManifest => {
|
|
80
|
+
const rules = listWardenRuleMetadata()
|
|
81
|
+
.toSorted(compareRuleEntries)
|
|
82
|
+
.map(([id, metadata]) => {
|
|
83
|
+
const rule = lookupRule(id);
|
|
84
|
+
const docs = metadata.guidance?.docs ?? [];
|
|
85
|
+
return {
|
|
86
|
+
concern: metadata.concern,
|
|
87
|
+
depth: metadata.depth,
|
|
88
|
+
description: rule?.description ?? '',
|
|
89
|
+
docs,
|
|
90
|
+
guidance: metadata.guidance,
|
|
91
|
+
id,
|
|
92
|
+
invariant: metadata.invariant,
|
|
93
|
+
lifecycle: metadata.lifecycle,
|
|
94
|
+
scope: metadata.scope,
|
|
95
|
+
severity: rule?.severity ?? 'warn',
|
|
96
|
+
tier: metadata.tier,
|
|
97
|
+
} satisfies WardenRuleGuideEntry;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
generatedFrom: {
|
|
102
|
+
package: '@ontrails/warden',
|
|
103
|
+
registries: ['wardenRules', 'wardenTopoRules'],
|
|
104
|
+
source: 'builtin-rule-metadata',
|
|
105
|
+
},
|
|
106
|
+
kind: 'trails-warden-guide-manifest',
|
|
107
|
+
ruleCount: rules.length,
|
|
108
|
+
rules,
|
|
109
|
+
version: 1,
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const formatGuideLink = (link: WardenGuidanceLink): string => {
|
|
114
|
+
if (link.path) {
|
|
115
|
+
return `[${link.label}](${link.path})`;
|
|
116
|
+
}
|
|
117
|
+
if (link.url) {
|
|
118
|
+
return `[${link.label}](${link.url})`;
|
|
119
|
+
}
|
|
120
|
+
return link.label;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const renderOptionalList = (
|
|
124
|
+
lines: string[],
|
|
125
|
+
label: string,
|
|
126
|
+
items: readonly string[] | undefined
|
|
127
|
+
): void => {
|
|
128
|
+
if (items === undefined || items.length === 0) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
lines.push(`- ${label}:`);
|
|
133
|
+
for (const [index, item] of items.entries()) {
|
|
134
|
+
lines.push(` ${index + 1}. ${item}`);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const renderRuleMarkdown = (rule: WardenRuleGuideEntry): readonly string[] => {
|
|
139
|
+
const lines = [
|
|
140
|
+
`### \`${rule.id}\``,
|
|
141
|
+
'',
|
|
142
|
+
`- Severity: \`${rule.severity}\``,
|
|
143
|
+
`- Concern: \`${rule.concern}\``,
|
|
144
|
+
`- Depth: \`${rule.depth}\``,
|
|
145
|
+
`- Tier: \`${rule.tier}\``,
|
|
146
|
+
`- Scope: \`${rule.scope}\``,
|
|
147
|
+
`- Lifecycle: \`${rule.lifecycle.state}\``,
|
|
148
|
+
`- Invariant: ${rule.invariant}`,
|
|
149
|
+
`- Description: ${rule.description}`,
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
if (rule.lifecycle.retireWhen) {
|
|
153
|
+
lines.push(`- Retire when: ${rule.lifecycle.retireWhen}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (rule.guidance) {
|
|
157
|
+
lines.push('', `Guidance: ${rule.guidance.summary}`);
|
|
158
|
+
renderOptionalList(lines, 'Steps', rule.guidance.steps);
|
|
159
|
+
renderOptionalList(lines, 'Commands', rule.guidance.commands);
|
|
160
|
+
if (rule.docs.length > 0) {
|
|
161
|
+
lines.push(`- Docs: ${rule.docs.map(formatGuideLink).join(', ')}`);
|
|
162
|
+
}
|
|
163
|
+
if (rule.guidance.relatedRules && rule.guidance.relatedRules.length > 0) {
|
|
164
|
+
lines.push(
|
|
165
|
+
`- Related rules: ${rule.guidance.relatedRules.map((id) => `\`${id}\``).join(', ')}`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return lines;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export const formatWardenGuideMarkdown = (
|
|
174
|
+
manifest: WardenGuideManifest
|
|
175
|
+
): string => {
|
|
176
|
+
const lines = [
|
|
177
|
+
'# Trails Warden Guide',
|
|
178
|
+
'',
|
|
179
|
+
'Generated from `@ontrails/warden` built-in rule metadata and live rule registries.',
|
|
180
|
+
'',
|
|
181
|
+
'## Summary',
|
|
182
|
+
'',
|
|
183
|
+
`- Rules: ${manifest.ruleCount}`,
|
|
184
|
+
`- Guided rules: ${manifest.rules.filter((rule) => rule.guidance).length}`,
|
|
185
|
+
'',
|
|
186
|
+
'## Rules',
|
|
187
|
+
'',
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
for (const rule of manifest.rules) {
|
|
191
|
+
lines.push(...renderRuleMarkdown(rule), '');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return lines.join('\n').trimEnd();
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export const buildWardenAgentGuide = (
|
|
198
|
+
manifest: WardenGuideManifest
|
|
199
|
+
): WardenAgentGuide => ({
|
|
200
|
+
instructions: [
|
|
201
|
+
'Treat Warden rules as enforceable Trails doctrine when working in this repository.',
|
|
202
|
+
'Prefer the rule guidance summary and ordered steps over diagnostic prose when deciding how to remediate a finding.',
|
|
203
|
+
'When guidance is absent, use the invariant, concern, tier, and scope as classification metadata rather than inventing a rule-specific fix.',
|
|
204
|
+
],
|
|
205
|
+
kind: 'trails-warden-agent-guide',
|
|
206
|
+
rules: manifest.rules.map((rule) => ({
|
|
207
|
+
appliesAt: {
|
|
208
|
+
depth: rule.depth,
|
|
209
|
+
scope: rule.scope,
|
|
210
|
+
tier: rule.tier,
|
|
211
|
+
},
|
|
212
|
+
concern: rule.concern,
|
|
213
|
+
guidance: rule.guidance,
|
|
214
|
+
id: rule.id,
|
|
215
|
+
invariant: rule.invariant,
|
|
216
|
+
severity: rule.severity,
|
|
217
|
+
})),
|
|
218
|
+
version: 1,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
export const formatWardenGuide = (
|
|
222
|
+
manifest: WardenGuideManifest,
|
|
223
|
+
format: WardenGuideFormat
|
|
224
|
+
): string => {
|
|
225
|
+
if (format === 'markdown') {
|
|
226
|
+
return formatWardenGuideMarkdown(manifest);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (format === 'agent-json') {
|
|
230
|
+
return JSON.stringify(buildWardenAgentGuide(manifest), null, 2);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return JSON.stringify(manifest, null, 2);
|
|
234
|
+
};
|