@ontrails/warden 1.0.0-beta.39 → 1.0.0-beta.41
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 +125 -0
- package/README.md +5 -5
- package/package.json +10 -12
- package/src/cli.ts +49 -44
- package/src/command.ts +2 -2
- package/src/drift.ts +4 -3
- package/src/index.ts +3 -2
- package/src/project-context.ts +3 -3
- package/src/resolve.ts +2 -2
- package/src/rules/captured-kernel.ts +375 -0
- package/src/rules/circular-refs.ts +12 -16
- package/src/rules/cli-command-route-coherence.ts +1 -1
- package/src/rules/composes-declarations.ts +15 -13
- package/src/rules/context-no-surface-types.ts +4 -3
- package/src/rules/dead-internal-trail.ts +3 -3
- package/src/rules/dead-public-trail.ts +3 -3
- package/src/rules/draft-file-marking.ts +4 -9
- package/src/rules/draft-visible-debt.ts +3 -8
- package/src/rules/duplicate-public-contract.ts +50 -5
- package/src/rules/{contour-exists.ts → entity-exists.ts} +67 -64
- package/src/rules/entity-ids.ts +15 -0
- package/src/rules/error-mapping-completeness.ts +2 -2
- package/src/rules/example-valid.ts +84 -84
- package/src/rules/fires-declarations.ts +27 -22
- package/src/rules/governed-symbol-residue.ts +339 -32
- package/src/rules/implementation-returns-result.ts +8 -8
- package/src/rules/incomplete-accessor-for-standard-op.ts +15 -15
- package/src/rules/incomplete-crud.ts +42 -40
- package/src/rules/index.ts +9 -6
- package/src/rules/intent-propagation.ts +3 -5
- package/src/rules/layer-field-name-drift.ts +2 -2
- package/src/rules/library-projection-coherence.ts +2 -2
- package/src/rules/metadata.ts +37 -15
- package/src/rules/missing-reconcile.ts +3 -4
- package/src/rules/missing-visibility.ts +2 -1
- package/src/rules/no-destructured-compose.ts +14 -12
- package/src/rules/no-direct-implementation-call.ts +12 -7
- package/src/rules/no-legacy-cli-alias-export.ts +2 -2
- package/src/rules/no-native-error-result.ts +2 -2
- package/src/rules/no-redundant-result-error-wrap.ts +10 -10
- package/src/rules/no-sync-result-assumption.ts +84 -78
- package/src/rules/no-throw-in-detour-recover.ts +2 -2
- package/src/rules/no-throw-in-implementation.ts +13 -7
- package/src/rules/no-top-level-surface.ts +2 -2
- package/src/rules/on-references-exist.ts +3 -3
- package/src/rules/orphaned-signal.ts +4 -5
- package/src/rules/owner-projection-parity.ts +2 -2
- package/src/rules/public-export-example-coverage.ts +10 -4
- package/src/rules/public-internal-deep-imports.ts +2 -3
- package/src/rules/read-intent-fires.ts +4 -3
- package/src/rules/reference-exists.ts +20 -21
- package/src/rules/registry-names.ts +6 -4
- package/src/rules/resolved-import-boundary.ts +1 -1
- package/src/rules/resource-declarations.ts +21 -17
- package/src/rules/resource-exists.ts +4 -2
- package/src/rules/resource-id-grammar.ts +2 -2
- package/src/rules/resource-mock-coverage.ts +2 -2
- package/src/rules/retired-vocabulary.ts +242 -5
- 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/static-resource-accessor-preference.ts +15 -11
- package/src/rules/surface-overlay-coherence.ts +1 -1
- package/src/rules/surface-trailhead-coherence.ts +2 -2
- package/src/rules/trail-fork-coaching.ts +27 -18
- package/src/rules/trail-versioning-source.ts +16 -12
- package/src/rules/trail-versioning-topo.ts +2 -2
- package/src/rules/trailhead-override-divergence.ts +2 -2
- package/src/rules/types.ts +7 -7
- package/src/rules/unmaterialized-activation-source.ts +1 -0
- package/src/rules/unreachable-detour-shadowing.ts +2 -2
- package/src/rules/valid-describe-refs.ts +2 -2
- package/src/rules/warden-export-symmetry.ts +3 -3
- package/src/rules/warden-rules-use-ast.ts +12 -17
- package/src/trails/activation-orphan.trail.ts +5 -5
- package/src/trails/captured-kernel.trail.ts +108 -0
- package/src/trails/circular-refs.trail.ts +6 -6
- package/src/trails/cli-command-route-coherence.trail.ts +2 -2
- package/src/trails/composes-declarations.trail.ts +1 -1
- package/src/trails/context-no-surface-types.trail.ts +1 -1
- package/src/trails/dead-internal-trail.trail.ts +2 -2
- package/src/trails/dead-public-trail.trail.ts +1 -1
- package/src/trails/deprecation-without-guidance.trail.ts +1 -1
- package/src/trails/duplicate-public-contract.trail.ts +3 -3
- package/src/trails/entity-exists.trail.ts +21 -0
- package/src/trails/example-valid.trail.ts +3 -3
- package/src/trails/fires-declarations.trail.ts +1 -1
- package/src/trails/{fork-without-preserved-blaze.trail.ts → fork-without-preserved-implementation.trail.ts} +4 -4
- package/src/trails/implementation-returns-result.trail.ts +1 -1
- package/src/trails/incomplete-accessor-for-standard-op.trail.ts +4 -4
- package/src/trails/index.ts +3 -2
- package/src/trails/intent-propagation.trail.ts +2 -2
- package/src/trails/library-projection-coherence.trail.ts +2 -2
- package/src/trails/marker-schema-unsupported.trail.ts +1 -1
- package/src/trails/missing-visibility.trail.ts +1 -1
- package/src/trails/no-destructured-compose.trail.ts +5 -5
- package/src/trails/no-direct-implementation-call.trail.ts +1 -1
- package/src/trails/no-redundant-result-error-wrap.trail.ts +2 -2
- package/src/trails/no-sync-result-assumption.trail.ts +2 -2
- package/src/trails/no-throw-in-detour-recover.trail.ts +1 -1
- package/src/trails/no-throw-in-implementation.trail.ts +1 -1
- package/src/trails/on-references-exist.trail.ts +1 -1
- package/src/trails/pending-force.trail.ts +1 -1
- package/src/trails/permit-governance.trail.ts +2 -2
- package/src/trails/prefer-schema-inference.trail.ts +1 -1
- package/src/trails/public-output-schema.trail.ts +2 -2
- package/src/trails/public-union-output-discriminants.trail.ts +1 -1
- package/src/trails/read-intent-fires.trail.ts +1 -1
- package/src/trails/reference-exists.trail.ts +5 -5
- package/src/trails/resource-declarations.trail.ts +1 -1
- package/src/trails/resource-exists.trail.ts +1 -1
- package/src/trails/run.ts +4 -6
- package/src/trails/scheduled-destroy-intent.trail.ts +2 -2
- package/src/trails/schema.ts +9 -9
- package/src/trails/signal-graph-coaching.trail.ts +2 -2
- package/src/trails/static-resource-accessor-preference.trail.ts +1 -1
- package/src/trails/surface-overlay-coherence.trail.ts +1 -1
- package/src/trails/trail-fork-coaching.trail.ts +1 -1
- package/src/trails/unmaterialized-activation-source.trail.ts +5 -5
- package/src/trails/valid-detour-contract.trail.ts +1 -1
- package/src/trails/version-gap.trail.ts +1 -1
- package/src/trails/version-pinned-compose.trail.ts +1 -1
- package/src/trails/version-without-examples.trail.ts +1 -1
- package/src/trails/warden-rules-use-ast.trail.ts +2 -2
- package/src/trails/webhook-route-collision.trail.ts +2 -2
- package/src/trails/wrap-rule.ts +17 -17
- package/src/ast.ts +0 -137
- package/src/rules/ast.ts +0 -4143
- package/src/rules/contour-ids.ts +0 -15
- package/src/trails/contour-exists.trail.ts +0 -21
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
offsetToLine,
|
|
7
7
|
parse,
|
|
8
8
|
walk,
|
|
9
|
-
} from '
|
|
10
|
-
import type { AstNode } from '
|
|
9
|
+
} from '@ontrails/source';
|
|
10
|
+
import type { AstNode } from '@ontrails/source';
|
|
11
11
|
import { isTestFile } from './scan.js';
|
|
12
12
|
import { collectTrailIds } from './specs.js';
|
|
13
13
|
import type {
|
|
@@ -21,8 +21,8 @@ import {
|
|
|
21
21
|
getNodeDeclaration,
|
|
22
22
|
getNodeDeclarations,
|
|
23
23
|
getNodeElements,
|
|
24
|
-
getNodeExportKind,
|
|
25
24
|
getNodeExported,
|
|
25
|
+
getNodeExportKind,
|
|
26
26
|
getNodeId,
|
|
27
27
|
getNodeKey,
|
|
28
28
|
getNodeLeft,
|
|
@@ -35,8 +35,8 @@ import {
|
|
|
35
35
|
offsetToLine,
|
|
36
36
|
parse,
|
|
37
37
|
walk,
|
|
38
|
-
} from '
|
|
39
|
-
import type { AstNode } from '
|
|
38
|
+
} from '@ontrails/source';
|
|
39
|
+
import type { AstNode } from '@ontrails/source';
|
|
40
40
|
import { registeredRuleNames } from './registry-names.js';
|
|
41
41
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
42
42
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Self-governance rule: warden rules must inspect the AST via the helpers in
|
|
3
|
-
*
|
|
3
|
+
* `../source/*` helpers rather than regex-scanning raw source text. Raw-text scans
|
|
4
4
|
* produce false positives on string literals, template payloads, and
|
|
5
5
|
* docstrings — see TRL-335 and ADR-0036.
|
|
6
6
|
*
|
|
@@ -16,13 +16,12 @@
|
|
|
16
16
|
* bug as scanning with one — see TRL-345.
|
|
17
17
|
* 4. `rawNodeFieldCastSite` — asserting AST nodes through
|
|
18
18
|
* `as unknown as { field?: ... }` where a typed accessor exists on
|
|
19
|
-
*
|
|
19
|
+
* `../source/*`.
|
|
20
20
|
*
|
|
21
21
|
* This rule is path-anchored to this package's own `src/rules/` directory so
|
|
22
22
|
* it never fires against a consumer repo that happens to share the same
|
|
23
|
-
* folder layout.
|
|
24
|
-
*
|
|
25
|
-
* anything under `__tests__` are also excluded.
|
|
23
|
+
* folder layout. Support modules such as `types.ts`, `index.ts`,
|
|
24
|
+
* `registry-names.ts`, and anything under `__tests__` are excluded.
|
|
26
25
|
*/
|
|
27
26
|
import { basename as pathBasename, dirname, resolve, sep } from 'node:path';
|
|
28
27
|
import { fileURLToPath } from 'node:url';
|
|
@@ -54,8 +53,8 @@ import {
|
|
|
54
53
|
offsetToLine,
|
|
55
54
|
parse,
|
|
56
55
|
walk,
|
|
57
|
-
} from '
|
|
58
|
-
import type { AstNode } from '
|
|
56
|
+
} from '@ontrails/source';
|
|
57
|
+
import type { AstNode } from '@ontrails/source';
|
|
59
58
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
60
59
|
|
|
61
60
|
const RULE_NAME = 'warden-rules-use-ast';
|
|
@@ -109,12 +108,10 @@ const SELF_RULES_DIRS: ReadonlySet<string> = new Set(
|
|
|
109
108
|
|
|
110
109
|
/**
|
|
111
110
|
* Stems of files in `src/rules/` (and their bundled `dist/rules/` twins) that
|
|
112
|
-
* are
|
|
113
|
-
*
|
|
114
|
-
* the others are support modules without a `check()` function.
|
|
111
|
+
* are not themselves Warden rules and therefore must not be checked. These
|
|
112
|
+
* are support modules without a `check()` function.
|
|
115
113
|
*/
|
|
116
114
|
const EXCLUDED_STEMS: readonly string[] = [
|
|
117
|
-
'ast',
|
|
118
115
|
'index',
|
|
119
116
|
'registry-names',
|
|
120
117
|
'scan',
|
|
@@ -125,9 +122,7 @@ const EXCLUDED_STEMS: readonly string[] = [
|
|
|
125
122
|
|
|
126
123
|
/**
|
|
127
124
|
* Both `.ts` (source layout) and `.js` (dist layout) basenames must be
|
|
128
|
-
* excluded so the rule stays silent when pointed at a bundled tree.
|
|
129
|
-
* dist-layout `ast.js` contains the same raw-text parser entry point as
|
|
130
|
-
* `ast.ts` and would false-positive if scanned.
|
|
125
|
+
* excluded so the rule stays silent when pointed at a bundled tree.
|
|
131
126
|
*/
|
|
132
127
|
const EXCLUDED_BASENAMES: ReadonlySet<string> = new Set(
|
|
133
128
|
EXCLUDED_STEMS.flatMap((stem) => [`${stem}.ts`, `${stem}.js`])
|
|
@@ -374,7 +369,7 @@ const regexConstructionSite = (node: AstNode): RegexConstructionSite | null => {
|
|
|
374
369
|
};
|
|
375
370
|
|
|
376
371
|
const DIAGNOSTIC_ADVICE =
|
|
377
|
-
'Warden rules must inspect
|
|
372
|
+
'Warden rules must inspect source code via @ontrails/source helpers, not regex-scan raw source text. ' +
|
|
378
373
|
'Use findStringLiterals, findTrailDefinitions, findConfigProperty, or a similar AST walker. ' +
|
|
379
374
|
'Raw-text scanning produces false positives on string literals, template payloads, and docstrings — see TRL-335, ADR-0036.';
|
|
380
375
|
|
|
@@ -514,7 +509,7 @@ const detectRawNodeFieldCast = (node: AstNode): DetectedSite | null => {
|
|
|
514
509
|
const fields = entries.map(([field, accessor]) => `${field} -> ${accessor}`);
|
|
515
510
|
return {
|
|
516
511
|
identifier: expression,
|
|
517
|
-
message: `${RULE_NAME}: raw AST node-field cast should use typed helpers from
|
|
512
|
+
message: `${RULE_NAME}: raw AST node-field cast should use typed helpers from the shared source helpers (${fields.join(', ')}). Raw node-field casts drift from the shared source helper surface.`,
|
|
518
513
|
severity: 'warn',
|
|
519
514
|
start: node.start,
|
|
520
515
|
};
|
|
@@ -1108,7 +1103,7 @@ export const wardenRulesUseAst: WardenRule = {
|
|
|
1108
1103
|
return analyze(sourceCode, filePath, ast);
|
|
1109
1104
|
},
|
|
1110
1105
|
description:
|
|
1111
|
-
'Enforces that warden rules inspect
|
|
1106
|
+
'Enforces that warden rules inspect source code via @ontrails/source helpers rather than regex-scanning raw source text or hand-casting node fields.',
|
|
1112
1107
|
name: RULE_NAME,
|
|
1113
1108
|
severity: 'error',
|
|
1114
1109
|
};
|
|
@@ -9,7 +9,7 @@ const orphanSignal = signal('invoice.paid', {
|
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
const orphanConsumer = trail('invoice.audit', {
|
|
12
|
-
|
|
12
|
+
implementation: () => Result.ok({ ok: true }),
|
|
13
13
|
input: z.object({ invoiceId: z.string() }),
|
|
14
14
|
on: [orphanSignal],
|
|
15
15
|
output: z.object({ ok: z.boolean() }),
|
|
@@ -21,24 +21,24 @@ const producedSignal = signal('invoice.created', {
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
const producerTrail = trail('invoice.create', {
|
|
24
|
-
|
|
24
|
+
fires: [producedSignal],
|
|
25
|
+
implementation: async (_input, ctx) => {
|
|
25
26
|
await ctx.fire?.(producedSignal, { invoiceId: 'inv_1' });
|
|
26
27
|
return Result.ok({ invoiceId: 'inv_1' });
|
|
27
28
|
},
|
|
28
|
-
fires: [producedSignal],
|
|
29
29
|
input: z.object({}),
|
|
30
30
|
output: z.object({ invoiceId: z.string() }),
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
const producedConsumer = trail('invoice.index', {
|
|
34
|
-
|
|
34
|
+
implementation: () => Result.ok({ ok: true }),
|
|
35
35
|
input: z.object({ invoiceId: z.string() }),
|
|
36
36
|
on: [producedSignal],
|
|
37
37
|
output: z.object({ ok: z.boolean() }),
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
const scheduledConsumer = trail('invoice.reconcile', {
|
|
41
|
-
|
|
41
|
+
implementation: () => Result.ok({ ok: true }),
|
|
42
42
|
input: z.object({}),
|
|
43
43
|
on: [schedule('schedule.invoice.reconcile', { cron: '0 * * * *' })],
|
|
44
44
|
output: z.object({ ok: z.boolean() }),
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { capturedKernel } from '../rules/captured-kernel.js';
|
|
2
|
+
import { wrapRule } from './wrap-rule.js';
|
|
3
|
+
|
|
4
|
+
export const capturedKernelTrail = wrapRule({
|
|
5
|
+
examples: [
|
|
6
|
+
{
|
|
7
|
+
expected: {
|
|
8
|
+
diagnostics: [
|
|
9
|
+
{
|
|
10
|
+
filePath: 'packages/core/src/kernel.ts',
|
|
11
|
+
guidance: {
|
|
12
|
+
steps: [
|
|
13
|
+
'Review whether the public subpath should become an owned package surface, move back behind the package root, or be split into a better-owned package.',
|
|
14
|
+
'If the imported capability is reusable source-code machinery, serves at least two independently owned toolchain capabilities, exposes one genuinely shared contract, and owns no verdict, migration plan, graph query, or surface rendering, consider relocating it to @ontrails/source.',
|
|
15
|
+
'Otherwise, preserve the current owner or choose another doctrinal owner.',
|
|
16
|
+
],
|
|
17
|
+
summary:
|
|
18
|
+
'Review ownership before an internal re-exported kernel hardens into a public package seam.',
|
|
19
|
+
},
|
|
20
|
+
line: 1,
|
|
21
|
+
message:
|
|
22
|
+
'@ontrails/core export target "@ontrails/core/kernel" re-exports internal target "./internal/kernel.js" and is consumed by external production packages @ontrails/cli, @ontrails/store. Review ownership of the exported subpath and its captured kernel before it becomes a durable public seam.',
|
|
23
|
+
rule: 'captured-kernel',
|
|
24
|
+
severity: 'warn',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
input: {
|
|
29
|
+
filePath: 'packages/core/src/kernel.ts',
|
|
30
|
+
importResolutionsByFile: {
|
|
31
|
+
'packages/cli/src/index.ts': [
|
|
32
|
+
{
|
|
33
|
+
crossesPackageBoundary: true,
|
|
34
|
+
importSource: '@ontrails/core/kernel',
|
|
35
|
+
importerPath: 'packages/cli/src/index.ts',
|
|
36
|
+
isInternalTarget: false,
|
|
37
|
+
line: 1,
|
|
38
|
+
packageName: '@ontrails/core',
|
|
39
|
+
packageRoot: 'packages/core',
|
|
40
|
+
resolvedPath: 'packages/core/src/kernel.ts',
|
|
41
|
+
usesPublicExport: true,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
'packages/core/src/kernel.ts': [
|
|
45
|
+
{
|
|
46
|
+
crossesPackageBoundary: false,
|
|
47
|
+
importSource: './internal/kernel.js',
|
|
48
|
+
importerPath: 'packages/core/src/kernel.ts',
|
|
49
|
+
isInternalTarget: true,
|
|
50
|
+
line: 1,
|
|
51
|
+
packageName: '@ontrails/core',
|
|
52
|
+
packageRoot: 'packages/core',
|
|
53
|
+
resolvedPath: 'packages/core/src/internal/kernel.ts',
|
|
54
|
+
usesPublicExport: false,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
'packages/store/src/index.ts': [
|
|
58
|
+
{
|
|
59
|
+
crossesPackageBoundary: true,
|
|
60
|
+
importSource: '@ontrails/core/kernel',
|
|
61
|
+
importerPath: 'packages/store/src/index.ts',
|
|
62
|
+
isInternalTarget: false,
|
|
63
|
+
line: 1,
|
|
64
|
+
packageName: '@ontrails/core',
|
|
65
|
+
packageRoot: 'packages/core',
|
|
66
|
+
resolvedPath: 'packages/core/src/kernel.ts',
|
|
67
|
+
usesPublicExport: true,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
knownTrailIds: [],
|
|
72
|
+
publicWorkspaces: {
|
|
73
|
+
'@ontrails/cli': {
|
|
74
|
+
exportTargets: {
|
|
75
|
+
'@ontrails/cli': 'packages/cli/src/index.ts',
|
|
76
|
+
},
|
|
77
|
+
hasExports: true,
|
|
78
|
+
name: '@ontrails/cli',
|
|
79
|
+
packageJsonPath: 'packages/cli/package.json',
|
|
80
|
+
rootDir: 'packages/cli',
|
|
81
|
+
},
|
|
82
|
+
'@ontrails/core': {
|
|
83
|
+
exportTargets: {
|
|
84
|
+
'@ontrails/core': 'packages/core/src/index.ts',
|
|
85
|
+
'@ontrails/core/kernel': 'packages/core/src/kernel.ts',
|
|
86
|
+
},
|
|
87
|
+
hasExports: true,
|
|
88
|
+
name: '@ontrails/core',
|
|
89
|
+
packageJsonPath: 'packages/core/package.json',
|
|
90
|
+
rootDir: 'packages/core',
|
|
91
|
+
},
|
|
92
|
+
'@ontrails/store': {
|
|
93
|
+
exportTargets: {
|
|
94
|
+
'@ontrails/store': 'packages/store/src/index.ts',
|
|
95
|
+
},
|
|
96
|
+
hasExports: true,
|
|
97
|
+
name: '@ontrails/store',
|
|
98
|
+
packageJsonPath: 'packages/store/package.json',
|
|
99
|
+
rootDir: 'packages/store',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
sourceCode: "export { kernel } from './internal/kernel.js';\n",
|
|
103
|
+
},
|
|
104
|
+
name: 'Flags captured kernels consumed by multiple production packages',
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
rule: capturedKernel,
|
|
108
|
+
});
|
|
@@ -6,23 +6,23 @@ export const circularRefsTrail = wrapRule({
|
|
|
6
6
|
{
|
|
7
7
|
expected: { diagnostics: [] },
|
|
8
8
|
input: {
|
|
9
|
-
|
|
9
|
+
entityReferencesByName: {
|
|
10
10
|
gist: ['user'],
|
|
11
11
|
user: [],
|
|
12
12
|
},
|
|
13
|
-
filePath: '
|
|
14
|
-
|
|
13
|
+
filePath: 'entities.ts',
|
|
14
|
+
knownEntityIds: ['gist', 'user'],
|
|
15
15
|
knownTrailIds: [],
|
|
16
|
-
sourceCode: `const user =
|
|
16
|
+
sourceCode: `const user = entity("user", {
|
|
17
17
|
id: z.string().uuid(),
|
|
18
18
|
}, { identity: "id" });
|
|
19
19
|
|
|
20
|
-
const gist =
|
|
20
|
+
const gist = entity("gist", {
|
|
21
21
|
id: z.string().uuid(),
|
|
22
22
|
ownerId: user.id(),
|
|
23
23
|
}, { identity: "id" });`,
|
|
24
24
|
},
|
|
25
|
-
name: 'Acyclic
|
|
25
|
+
name: 'Acyclic entity references stay clean',
|
|
26
26
|
},
|
|
27
27
|
],
|
|
28
28
|
rule: circularRefs,
|
|
@@ -5,16 +5,16 @@ import { cliCommandRouteCoherence } from '../rules/cli-command-route-coherence.j
|
|
|
5
5
|
import { wrapTopoRule } from './wrap-rule.js';
|
|
6
6
|
|
|
7
7
|
const searchTrail = trail('wayfind.search', {
|
|
8
|
-
blaze: () => Result.ok([]),
|
|
9
8
|
cli: {
|
|
10
9
|
aliases: ['find'],
|
|
11
10
|
},
|
|
11
|
+
implementation: () => Result.ok([]),
|
|
12
12
|
input: z.object({ query: z.string() }),
|
|
13
13
|
output: z.array(z.string()),
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
const collidingTrail = trail('wayfind.find', {
|
|
17
|
-
|
|
17
|
+
implementation: () => Result.ok([]),
|
|
18
18
|
input: z.object({ query: z.string() }),
|
|
19
19
|
output: z.array(z.string()),
|
|
20
20
|
});
|
|
@@ -9,7 +9,7 @@ export const composesDeclarationsTrail = wrapRule({
|
|
|
9
9
|
filePath: 'clean.ts',
|
|
10
10
|
sourceCode: `trail("entity.onboard", {
|
|
11
11
|
composes: ["entity.create"],
|
|
12
|
-
|
|
12
|
+
implementation: async (input, ctx) => {
|
|
13
13
|
const result = await ctx.compose("entity.create", input);
|
|
14
14
|
return Result.ok(result);
|
|
15
15
|
}
|
|
@@ -9,7 +9,7 @@ export const contextNoSurfaceTypesTrail = wrapRule({
|
|
|
9
9
|
filePath: 'clean.ts',
|
|
10
10
|
sourceCode: `import { trail, Result } from "@ontrails/core";
|
|
11
11
|
trail("entity.show", {
|
|
12
|
-
|
|
12
|
+
implementation: async (input, ctx) => {
|
|
13
13
|
return Result.ok({ name: "test" });
|
|
14
14
|
}
|
|
15
15
|
})`,
|
|
@@ -11,12 +11,12 @@ export const deadInternalTrailTrail = wrapRule({
|
|
|
11
11
|
knownTrailIds: ['entity.public', 'entity.sync'],
|
|
12
12
|
sourceCode: `trail('entity.public', {
|
|
13
13
|
composes: ['entity.sync'],
|
|
14
|
-
|
|
14
|
+
implementation: async (_input, ctx) => ctx.compose('entity.sync', {}),
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
trail('entity.sync', {
|
|
18
18
|
visibility: 'internal',
|
|
19
|
-
|
|
19
|
+
implementation: async () => Result.ok({}),
|
|
20
20
|
});`,
|
|
21
21
|
},
|
|
22
22
|
name: 'Internal trails stay clean when another trail composes them',
|
|
@@ -20,7 +20,7 @@ export const deadPublicTrailTrail = wrapRule({
|
|
|
20
20
|
filePath: 'packages/regrade/src/downstream/report.ts',
|
|
21
21
|
knownTrailIds: ['regrade.downstream.report', 'regrade'],
|
|
22
22
|
sourceCode: `export const regradeReportTrail = trail('regrade.downstream.report', {
|
|
23
|
-
|
|
23
|
+
implementation: async () => Result.ok({}),
|
|
24
24
|
});`,
|
|
25
25
|
topoTrailIds: ['regrade'],
|
|
26
26
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { topo } from '@ontrails/core';
|
|
2
|
-
import { deriveTopoGraph } from '@ontrails/
|
|
2
|
+
import { deriveTopoGraph } from '@ontrails/topography';
|
|
3
3
|
|
|
4
4
|
import { deprecationWithoutGuidance } from '../rules/trail-versioning-topo.js';
|
|
5
5
|
import { wrapTopoRule } from './wrap-rule.js';
|
|
@@ -8,13 +8,13 @@ const sharedInput = z.object({ target: z.string() });
|
|
|
8
8
|
const sharedOutput = z.object({ ok: z.boolean() });
|
|
9
9
|
|
|
10
10
|
const canonicalTrail = trail('survey.diff', {
|
|
11
|
-
|
|
11
|
+
implementation: () => Result.ok({ ok: true }),
|
|
12
12
|
input: sharedInput,
|
|
13
13
|
output: sharedOutput,
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
const duplicateTrail = trail('diff', {
|
|
17
|
-
|
|
17
|
+
implementation: () => Result.ok({ ok: true }),
|
|
18
18
|
input: sharedInput,
|
|
19
19
|
output: sharedOutput,
|
|
20
20
|
});
|
|
@@ -28,7 +28,7 @@ export const duplicatePublicContractTrail = wrapTopoRule({
|
|
|
28
28
|
filePath: '<topo>',
|
|
29
29
|
line: 1,
|
|
30
30
|
message:
|
|
31
|
-
'Likely duplicate public trail contracts "diff", "survey.diff" share the same input, output, intent, permits, resources,
|
|
31
|
+
'Likely duplicate public trail contracts "diff", "survey.diff" share the same input, output, intent, permits, resources, entities, composes, signals, and detours. Keep one contract with aliases/input mappings, compose a distinct wrapper, or document why these public contracts are separate.',
|
|
32
32
|
rule: 'duplicate-public-contract',
|
|
33
33
|
severity: 'warn',
|
|
34
34
|
},
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { entityExists } from '../rules/entity-exists.js';
|
|
2
|
+
import { wrapRule } from './wrap-rule.js';
|
|
3
|
+
|
|
4
|
+
export const entityExistsTrail = wrapRule({
|
|
5
|
+
examples: [
|
|
6
|
+
{
|
|
7
|
+
expected: { diagnostics: [] },
|
|
8
|
+
input: {
|
|
9
|
+
filePath: 'entity.ts',
|
|
10
|
+
knownEntityIds: ['user'],
|
|
11
|
+
knownTrailIds: ['user.create'],
|
|
12
|
+
sourceCode: `trail("user.create", {
|
|
13
|
+
entities: [user],
|
|
14
|
+
implementation: async (input, ctx) => Result.ok({ ok: true }),
|
|
15
|
+
})`,
|
|
16
|
+
},
|
|
17
|
+
name: 'Declared entities resolve to known project entities',
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
rule: entityExists,
|
|
21
|
+
});
|
|
@@ -6,8 +6,8 @@ export const exampleValidTrail = wrapRule({
|
|
|
6
6
|
{
|
|
7
7
|
expected: { diagnostics: [] },
|
|
8
8
|
input: {
|
|
9
|
-
filePath: '
|
|
10
|
-
sourceCode: `const user =
|
|
9
|
+
filePath: 'entities.ts',
|
|
10
|
+
sourceCode: `const user = entity("user", {
|
|
11
11
|
id: z.string().uuid(),
|
|
12
12
|
name: z.string(),
|
|
13
13
|
}, {
|
|
@@ -18,7 +18,7 @@ export const exampleValidTrail = wrapRule({
|
|
|
18
18
|
identity: "id",
|
|
19
19
|
});`,
|
|
20
20
|
},
|
|
21
|
-
name: '
|
|
21
|
+
name: 'Entity examples validate against their schema',
|
|
22
22
|
},
|
|
23
23
|
],
|
|
24
24
|
rule: exampleValid,
|
|
@@ -10,7 +10,7 @@ export const firesDeclarationsTrail = wrapRule({
|
|
|
10
10
|
sourceCode: `const entityCreated = signal("entity.created", { payload: z.object({}) });
|
|
11
11
|
trail("entity.onboard", {
|
|
12
12
|
fires: [entityCreated],
|
|
13
|
-
|
|
13
|
+
implementation: async (input, ctx) => {
|
|
14
14
|
await ctx.fire(entityCreated, { id: input.id });
|
|
15
15
|
return Result.ok({});
|
|
16
16
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { forkWithoutPreservedImplementation } from '../rules/trail-versioning-source.js';
|
|
2
2
|
import { wrapRule } from './wrap-rule.js';
|
|
3
3
|
|
|
4
|
-
export const
|
|
4
|
+
export const forkWithoutPreservedImplementationTrail = wrapRule({
|
|
5
5
|
examples: [
|
|
6
6
|
{
|
|
7
7
|
expected: { diagnostics: [] },
|
|
@@ -20,12 +20,12 @@ trail('versioned.clean', {
|
|
|
20
20
|
},
|
|
21
21
|
},
|
|
22
22
|
},
|
|
23
|
-
|
|
23
|
+
implementation: async () => Result.ok({ message: 'ok' }),
|
|
24
24
|
});
|
|
25
25
|
`,
|
|
26
26
|
},
|
|
27
27
|
name: 'Revision entries use transpose',
|
|
28
28
|
},
|
|
29
29
|
],
|
|
30
|
-
rule:
|
|
30
|
+
rule: forkWithoutPreservedImplementation,
|
|
31
31
|
});
|
|
@@ -9,10 +9,10 @@ type Connection = Readonly<Record<string, Accessor>>;
|
|
|
9
9
|
|
|
10
10
|
const noop = (): undefined => undefined;
|
|
11
11
|
|
|
12
|
-
const buildResource = (id: string,
|
|
12
|
+
const buildResource = (id: string, entityName: string, accessor: Accessor) =>
|
|
13
13
|
resource<Connection>(id, {
|
|
14
|
-
create: () => Result.ok({ [
|
|
15
|
-
mock: () => ({ [
|
|
14
|
+
create: () => Result.ok({ [entityName]: accessor }),
|
|
15
|
+
mock: () => ({ [entityName]: accessor }),
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
const buildCrudTrail = (
|
|
@@ -20,7 +20,7 @@ const buildCrudTrail = (
|
|
|
20
20
|
resourceValue: ReturnType<typeof buildResource>
|
|
21
21
|
) =>
|
|
22
22
|
trail(trailId, {
|
|
23
|
-
|
|
23
|
+
implementation: () => Result.ok({ ok: true }),
|
|
24
24
|
input: z.object({}),
|
|
25
25
|
output: z.object({ ok: z.boolean() }),
|
|
26
26
|
pattern: 'crud',
|
package/src/trails/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { activationOrphanTrail } from './activation-orphan.trail.js';
|
|
2
|
+
export { capturedKernelTrail } from './captured-kernel.trail.js';
|
|
2
3
|
export { cliCommandRouteCoherenceTrail } from './cli-command-route-coherence.trail.js';
|
|
3
4
|
export { circularRefsTrail } from './circular-refs.trail.js';
|
|
4
|
-
export {
|
|
5
|
+
export { entityExistsTrail } from './entity-exists.trail.js';
|
|
5
6
|
export { contextNoSurfaceTypesTrail } from './context-no-surface-types.trail.js';
|
|
6
7
|
export { composesDeclarationsTrail } from './composes-declarations.trail.js';
|
|
7
8
|
export { deadInternalTrailTrail } from './dead-internal-trail.trail.js';
|
|
@@ -14,7 +15,7 @@ export { duplicateExportedSymbolTrail } from './duplicate-exported-symbol.trail.
|
|
|
14
15
|
export { errorMappingCompletenessTrail } from './error-mapping-completeness.trail.js';
|
|
15
16
|
export { exampleValidTrail } from './example-valid.trail.js';
|
|
16
17
|
export { firesDeclarationsTrail } from './fires-declarations.trail.js';
|
|
17
|
-
export {
|
|
18
|
+
export { forkWithoutPreservedImplementationTrail } from './fork-without-preserved-implementation.trail.js';
|
|
18
19
|
export { governedSymbolResidueTrail } from './governed-symbol-residue.trail.js';
|
|
19
20
|
export { implementationReturnsResultTrail } from './implementation-returns-result.trail.js';
|
|
20
21
|
export { incompleteAccessorForStandardOpTrail } from './incomplete-accessor-for-standard-op.trail.js';
|
|
@@ -11,12 +11,12 @@ export const intentPropagationTrail = wrapRule({
|
|
|
11
11
|
sourceCode: `trail('entity.read', {
|
|
12
12
|
intent: 'read',
|
|
13
13
|
composes: ['entity.lookup'],
|
|
14
|
-
|
|
14
|
+
implementation: async (_input, ctx) => ctx.compose('entity.lookup', {}),
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
trail('entity.lookup', {
|
|
18
18
|
intent: 'read',
|
|
19
|
-
|
|
19
|
+
implementation: async () => Result.ok({}),
|
|
20
20
|
});`,
|
|
21
21
|
trailIntentsById: {
|
|
22
22
|
'entity.lookup': 'read',
|
|
@@ -7,13 +7,13 @@ import { wrapTopoRule } from './wrap-rule.js';
|
|
|
7
7
|
const output = z.object({ ok: z.boolean() });
|
|
8
8
|
|
|
9
9
|
const dotted = trail('widget.ping', {
|
|
10
|
-
|
|
10
|
+
implementation: () => Result.ok({ ok: true }),
|
|
11
11
|
input: z.object({}),
|
|
12
12
|
output,
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
const kebab = trail('widget-ping', {
|
|
16
|
-
|
|
16
|
+
implementation: () => Result.ok({ ok: true }),
|
|
17
17
|
input: z.object({}),
|
|
18
18
|
output,
|
|
19
19
|
});
|
|
@@ -12,7 +12,7 @@ trail('versioned.schema', {
|
|
|
12
12
|
version: 2,
|
|
13
13
|
input: z.object({ name: z.string() }),
|
|
14
14
|
output: z.object({ message: z.string() }),
|
|
15
|
-
|
|
15
|
+
implementation: async () => Result.ok({ message: 'ok' }),
|
|
16
16
|
});
|
|
17
17
|
`,
|
|
18
18
|
},
|
|
@@ -12,7 +12,7 @@ export const missingVisibilityTrail = wrapRule({
|
|
|
12
12
|
sourceCode: `trail('entity.resolve', {
|
|
13
13
|
visibility: 'internal',
|
|
14
14
|
composeInput: z.object({ forkedFrom: z.string() }),
|
|
15
|
-
|
|
15
|
+
implementation: async () => Result.ok({}),
|
|
16
16
|
});`,
|
|
17
17
|
},
|
|
18
18
|
name: 'Composition-only trails stay quiet when already internal',
|
|
@@ -9,10 +9,10 @@ export const noDestructuredComposeTrail = wrapRule({
|
|
|
9
9
|
filePath: 'clean.ts',
|
|
10
10
|
sourceCode: `trail("entity.onboard", {
|
|
11
11
|
composes: ["entity.create"],
|
|
12
|
-
|
|
12
|
+
implementation: async (input, ctx) => ctx.compose("entity.create", input),
|
|
13
13
|
});`,
|
|
14
14
|
},
|
|
15
|
-
name: 'Clean
|
|
15
|
+
name: 'Clean implementation using ctx.compose directly',
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
expected: {
|
|
@@ -21,7 +21,7 @@ export const noDestructuredComposeTrail = wrapRule({
|
|
|
21
21
|
filePath: 'destructured.ts',
|
|
22
22
|
line: 4,
|
|
23
23
|
message:
|
|
24
|
-
'Trail "entity.onboard" destructures compose from the
|
|
24
|
+
'Trail "entity.onboard" destructures compose from the implementation context. Use ctx.compose(...) directly so composition stays visible and Warden can recognize composed Result values.',
|
|
25
25
|
rule: 'no-destructured-compose',
|
|
26
26
|
severity: 'warn',
|
|
27
27
|
},
|
|
@@ -31,13 +31,13 @@ export const noDestructuredComposeTrail = wrapRule({
|
|
|
31
31
|
filePath: 'destructured.ts',
|
|
32
32
|
sourceCode: `trail("entity.onboard", {
|
|
33
33
|
composes: ["entity.create"],
|
|
34
|
-
|
|
34
|
+
implementation: async (input, ctx) => {
|
|
35
35
|
const { compose } = ctx;
|
|
36
36
|
return compose("entity.create", input);
|
|
37
37
|
},
|
|
38
38
|
});`,
|
|
39
39
|
},
|
|
40
|
-
name: 'Warns when compose is destructured from the
|
|
40
|
+
name: 'Warns when compose is destructured from the implementation context',
|
|
41
41
|
},
|
|
42
42
|
],
|
|
43
43
|
rule: noDestructuredCompose,
|
|
@@ -9,7 +9,7 @@ export const noDirectImplementationCallTrail = wrapRule({
|
|
|
9
9
|
filePath: 'clean.ts',
|
|
10
10
|
sourceCode: `const data = await ctx.compose("entity.show", { id: "1" });`,
|
|
11
11
|
},
|
|
12
|
-
name: 'Clean code using ctx.compose instead of .
|
|
12
|
+
name: 'Clean code using ctx.compose instead of .implementation()',
|
|
13
13
|
},
|
|
14
14
|
],
|
|
15
15
|
rule: noDirectImplementationCall,
|
|
@@ -10,7 +10,7 @@ export const noRedundantResultErrorWrapTrail = wrapRule({
|
|
|
10
10
|
sourceCode: `import { Result, trail } from "@ontrails/core";
|
|
11
11
|
|
|
12
12
|
trail("entity.load", {
|
|
13
|
-
|
|
13
|
+
implementation: async (input, ctx) => {
|
|
14
14
|
const loaded = await ctx.compose("entity.fetch", input);
|
|
15
15
|
if (loaded.isErr()) {
|
|
16
16
|
return loaded;
|
|
@@ -39,7 +39,7 @@ trail("entity.load", {
|
|
|
39
39
|
sourceCode: `import { Result, trail } from "@ontrails/core";
|
|
40
40
|
|
|
41
41
|
trail("entity.load", {
|
|
42
|
-
|
|
42
|
+
implementation: async (input, ctx) => {
|
|
43
43
|
const loaded = await ctx.compose("entity.fetch", input);
|
|
44
44
|
if (loaded.isErr()) {
|
|
45
45
|
return Result.err(loaded.error);
|