@ontrails/warden 1.0.0-beta.24 → 1.0.0-beta.29

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +35 -1
  3. package/package.json +10 -9
  4. package/src/ast.ts +109 -0
  5. package/src/cli.ts +44 -4
  6. package/src/command.ts +24 -34
  7. package/src/drift.ts +24 -8
  8. package/src/index.ts +9 -0
  9. package/src/project-rules.ts +290 -0
  10. package/src/resolve.ts +8 -7
  11. package/src/rules/ast.ts +837 -6
  12. package/src/rules/cli-command-route-coherence.ts +177 -0
  13. package/src/rules/composes-declarations.ts +210 -77
  14. package/src/rules/context-no-surface-types.ts +15 -12
  15. package/src/rules/contour-exists.ts +7 -7
  16. package/src/rules/dead-internal-trail.ts +11 -4
  17. package/src/rules/dead-public-trail.ts +258 -0
  18. package/src/rules/duplicate-public-contract.ts +91 -0
  19. package/src/rules/error-mapping-completeness.ts +5 -3
  20. package/src/rules/example-valid.ts +6 -12
  21. package/src/rules/fires-declarations.ts +41 -64
  22. package/src/rules/implementation-returns-result.ts +208 -76
  23. package/src/rules/incomplete-crud.ts +20 -19
  24. package/src/rules/index.ts +15 -0
  25. package/src/rules/layer-field-name-drift.ts +12 -6
  26. package/src/rules/library-projection-coherence.ts +100 -0
  27. package/src/rules/metadata.ts +102 -1
  28. package/src/rules/no-destructured-compose.ts +16 -14
  29. package/src/rules/no-native-error-result.ts +15 -8
  30. package/src/rules/no-redundant-result-error-wrap.ts +82 -29
  31. package/src/rules/no-sync-result-assumption.ts +70 -68
  32. package/src/rules/no-top-level-surface.ts +46 -64
  33. package/src/rules/on-references-exist.ts +1 -1
  34. package/src/rules/owner-projection-parity.ts +10 -13
  35. package/src/rules/public-export-example-coverage.ts +29 -20
  36. package/src/rules/public-internal-deep-imports.ts +2 -2
  37. package/src/rules/read-intent-fires.ts +8 -8
  38. package/src/rules/registry-names.ts +10 -0
  39. package/src/rules/resource-declarations.ts +20 -31
  40. package/src/rules/resource-exists.ts +2 -2
  41. package/src/rules/resource-id-grammar.ts +2 -2
  42. package/src/rules/resource-mock-coverage.ts +2 -2
  43. package/src/rules/static-resource-accessor-preference.ts +26 -34
  44. package/src/rules/surface-facet-coherence.ts +21 -29
  45. package/src/rules/trail-fork-coaching.ts +616 -0
  46. package/src/rules/trail-versioning-source.ts +56 -78
  47. package/src/rules/types.ts +2 -0
  48. package/src/rules/unreachable-detour-shadowing.ts +16 -21
  49. package/src/rules/valid-describe-refs.ts +14 -12
  50. package/src/rules/warden-export-symmetry.ts +42 -35
  51. package/src/rules/warden-rules-use-ast.ts +168 -50
  52. package/src/trails/cli-command-route-coherence.trail.ts +47 -0
  53. package/src/trails/dead-public-trail.trail.ts +31 -0
  54. package/src/trails/duplicate-public-contract.trail.ts +47 -0
  55. package/src/trails/error-mapping-completeness.trail.ts +1 -0
  56. package/src/trails/index.ts +5 -0
  57. package/src/trails/library-projection-coherence.trail.ts +43 -0
  58. package/src/trails/schema.ts +4 -0
  59. package/src/trails/trail-fork-coaching.trail.ts +42 -0
  60. package/src/trails/warden-rules-use-ast.trail.ts +19 -0
  61. package/src/trails/wrap-rule.ts +1 -0
@@ -13,14 +13,26 @@
13
13
 
14
14
  import {
15
15
  buildSignalIdentifierResolver,
16
+ deriveConstString,
16
17
  extractStringLiteral,
17
- findConfigProperty,
18
18
  findBlazeBodies,
19
+ findConfigProperty,
19
20
  findTrailDefinitions,
21
+ getNodeBodyNode,
22
+ getNodeBodyStatements,
23
+ getNodeDeclarations,
24
+ getNodeId,
25
+ getNodeInit,
26
+ getNodeKey,
27
+ getNodeKind,
28
+ getNodeLeft,
29
+ getNodeObject,
30
+ getNodeProperties,
31
+ getNodeProperty,
32
+ getNodeValueNode,
20
33
  identifierName,
21
34
  offsetToLine,
22
35
  parse,
23
- deriveConstString,
24
36
  walkScope,
25
37
  } from './ast.js';
26
38
  import type { AstNode, SignalIdentifierResolver } from './ast.js';
@@ -34,11 +46,9 @@ import type { WardenDiagnostic, WardenRule } from './types.js';
34
46
  /**
35
47
  * Resolve an array element to a static signal ID when possible.
36
48
  *
37
- * Returns null for entries the rule can't statically resolve callers should
38
- * treat "unresolved" as "trust the runtime" rather than a missing declaration.
39
- * In particular, object-form references (e.g. `fires: [orderPlaced]` where
40
- * `orderPlaced` is a `Signal` imported from elsewhere) resolve via runtime
41
- * normalization in `trail()`, not at lint time.
49
+ * Returns null for entries the rule can't statically resolve. Unresolved entries
50
+ * are not proof of missingness, but they also do not suppress diagnostics for
51
+ * statically resolved `ctx.fire()` calls that target different signals.
42
52
  */
43
53
  const resolveFireElementId = (
44
54
  element: AstNode,
@@ -99,10 +109,10 @@ interface DeclaredFires {
99
109
  /**
100
110
  * Extract declared fires from a `fires: [...]` array.
101
111
  *
102
- * Object-form entries (`fires: [someSignal]`) cannot be resolved at lint time;
103
- * they're normalized at runtime by `trail()`. When any entry is unresolved,
104
- * the rule reports `hasUnresolved: true`, and callers should suppress the
105
- * "undeclared" diagnostic since the declared set is incomplete from our view.
112
+ * Object-form entries (`fires: [someSignal]`) can be unresolved at lint time
113
+ * when they come from imports the rule cannot prove. Those unresolved entries
114
+ * do not soften concrete undeclared `ctx.fire()` calls; this mirrors
115
+ * `composes-declarations` and keeps real drift hard-failing.
106
116
  */
107
117
  const resolveDeclaredFiresElements = (
108
118
  elements: readonly AstNode[],
@@ -147,12 +157,8 @@ const extractMemberPair = (
147
157
  return null;
148
158
  }
149
159
 
150
- const objName = identifierName(
151
- (callee as unknown as { object?: AstNode }).object
152
- );
153
- const propName = identifierName(
154
- (callee as unknown as { property?: AstNode }).property
155
- );
160
+ const objName = identifierName(getNodeObject(callee));
161
+ const propName = identifierName(getNodeProperty(callee));
156
162
 
157
163
  return objName && propName ? { objName, propName } : null;
158
164
  };
@@ -177,8 +183,8 @@ const extractFireLocalName = (prop: AstNode): string | null => {
177
183
  if (prop.type !== 'Property') {
178
184
  return null;
179
185
  }
180
- const { key } = prop as unknown as { key?: AstNode };
181
- const { value } = prop as unknown as { value?: AstNode };
186
+ const key = getNodeKey(prop);
187
+ const value = getNodeValueNode(prop);
182
188
  const keyName = identifierName(key);
183
189
  if (keyName !== 'fire') {
184
190
  return null;
@@ -193,9 +199,7 @@ const collectFireNamesFromPattern = (
193
199
  pattern: AstNode,
194
200
  names: Set<string>
195
201
  ): void => {
196
- const { properties } = pattern as unknown as {
197
- properties?: readonly AstNode[];
198
- };
202
+ const properties = getNodeProperties(pattern);
199
203
  if (!properties) {
200
204
  return;
201
205
  }
@@ -224,7 +228,7 @@ const extractContextParamName = (blazeBody: AstNode): string | null => {
224
228
  return null;
225
229
  }
226
230
  if (param.type === 'AssignmentPattern') {
227
- const { left } = param as unknown as { left?: AstNode };
231
+ const left = getNodeLeft(param);
228
232
  return identifierName(left);
229
233
  }
230
234
  return identifierName(param);
@@ -364,10 +368,8 @@ const getCtxDestructurePattern = (
364
368
  if (node.type !== 'VariableDeclarator') {
365
369
  return null;
366
370
  }
367
- const { id, init } = node as unknown as {
368
- readonly id?: AstNode;
369
- readonly init?: AstNode;
370
- };
371
+ const id = getNodeId(node);
372
+ const init = getNodeInit(node);
371
373
  if (!id || id.type !== 'ObjectPattern' || !init) {
372
374
  return null;
373
375
  }
@@ -398,11 +400,11 @@ const getCtxDestructurePattern = (
398
400
  */
399
401
  /** Get the top-level statements of a blaze function's BlockStatement body. */
400
402
  const getTopLevelStatements = (body: AstNode): readonly AstNode[] => {
401
- const blockBody = (body as unknown as { body?: AstNode }).body;
403
+ const blockBody = getNodeBodyNode(body);
402
404
  if (!blockBody || blockBody.type !== 'BlockStatement') {
403
405
  return [];
404
406
  }
405
- return (blockBody as unknown as { body?: readonly AstNode[] }).body ?? [];
407
+ return getNodeBodyStatements(blockBody);
406
408
  };
407
409
 
408
410
  /** Collect fire-local names from a single top-level VariableDeclaration. */
@@ -419,13 +421,11 @@ const collectFireNamesFromDeclaration = (
419
421
  // fire('x')` would otherwise be a false positive. Skipping non-const is a
420
422
  // small precision loss (see TSDoc on `collectDestructuredFireNames`) in
421
423
  // exchange for eliminating that class of false positives.
422
- const { kind } = stmt as unknown as { kind?: string };
424
+ const kind = getNodeKind(stmt);
423
425
  if (kind !== 'const') {
424
426
  return;
425
427
  }
426
- const declarations =
427
- (stmt as unknown as { declarations?: readonly AstNode[] }).declarations ??
428
- [];
428
+ const declarations = getNodeDeclarations(stmt) ?? [];
429
429
  for (const decl of declarations) {
430
430
  const pattern = getCtxDestructurePattern(decl, ctxNames);
431
431
  if (pattern) {
@@ -576,16 +576,13 @@ const buildUndeclaredDiagnostic = (
576
576
  trailId: string,
577
577
  signalId: string,
578
578
  filePath: string,
579
- line: number,
580
- softened = false
579
+ line: number
581
580
  ): WardenDiagnostic => ({
582
581
  filePath,
583
582
  line,
584
- message: softened
585
- ? `Trail "${trailId}": ctx.fire('${signalId}') called but '${signalId}' is not declared in fires (may be declared via object-form fires entries)`
586
- : `Trail "${trailId}": ctx.fire('${signalId}') called but '${signalId}' is not declared in fires`,
583
+ message: `Trail "${trailId}": ctx.fire('${signalId}') called but '${signalId}' is not declared in fires`,
587
584
  rule: 'fires-declarations',
588
- severity: softened ? 'warn' : 'error',
585
+ severity: 'error',
589
586
  });
590
587
 
591
588
  const buildStringFireDiagnostic = (
@@ -626,20 +623,13 @@ const reportUndeclared = (
626
623
  trailId: string;
627
624
  filePath: string;
628
625
  line: number;
629
- softened?: boolean;
630
626
  },
631
627
  diagnostics: WardenDiagnostic[]
632
628
  ): void => {
633
629
  for (const id of called) {
634
630
  if (!declared.has(id)) {
635
631
  diagnostics.push(
636
- buildUndeclaredDiagnostic(
637
- ctx.trailId,
638
- id,
639
- ctx.filePath,
640
- ctx.line,
641
- ctx.softened
642
- )
632
+ buildUndeclaredDiagnostic(ctx.trailId, id, ctx.filePath, ctx.line)
643
633
  );
644
634
  }
645
635
  }
@@ -648,12 +638,9 @@ const reportUndeclared = (
648
638
  /**
649
639
  * Emit warning for each declared ID not present in called set.
650
640
  *
651
- * Note: unlike `reportUndeclared`, this function does NOT soften its
652
- * diagnostics when `hasUnresolved` is true. The asymmetry is intentional
653
- * softening only applies to the undeclared direction because unresolved
654
- * Signal-value entries might cover an unknown set of called IDs. In the
655
- * unused direction, a declared string-literal that is never called is
656
- * genuinely unused regardless of whether other entries are unresolved.
641
+ * Unused string-literal declarations remain warnings: a declared string that is
642
+ * never called is visible cleanup debt even when other object-form declarations
643
+ * are unresolved.
657
644
  */
658
645
  const reportUnused = (
659
646
  declared: ReadonlySet<string>,
@@ -708,17 +695,7 @@ const checkTrailDefinition = (
708
695
  );
709
696
 
710
697
  reportStringFireCalls(called.stringIds, ctx, diagnostics);
711
- // When the declared array contains object-form references we can't resolve,
712
- // downgrade "undeclared" diagnostics from error to warn with a disclaimer
713
- // instead of suppressing entirely. The developer still sees genuinely
714
- // undeclared calls, but we can't statically prove the call isn't covered by
715
- // a Signal-value entry the runtime will normalize.
716
- reportUndeclared(
717
- signalValueCalledIds,
718
- declared.ids,
719
- { ...ctx, softened: declared.hasUnresolved },
720
- diagnostics
721
- );
698
+ reportUndeclared(signalValueCalledIds, declared.ids, ctx, diagnostics);
722
699
  reportUnused(declared.ids, called.ids, ctx, diagnostics);
723
700
  };
724
701