DetectionPipeline class exported ✓ 100.0%

Last updated: 2026-03-01T23:35:47.796Z

Metrics

LOC: 179 Complexity: 17 Params: 0 Coverage: 100.0% (48/48 lines, 0x executed)

Signature

class DetectionPipeline

Architecture violations

View all

  • [warning] max-cyclomatic-complexity: 'DetectionPipeline' has cyclomatic complexity 17 (max 10)
  • [warning] max-lines: 'DetectionPipeline' has 179 lines (max 80)

Source Code

export class DetectionPipeline {
  constructor(readonly classifiers: ReadonlyArray<FieldClassifier>) {}

  /**
   * Async variant — prefers `detectAsync` when available on a classifier
   * (e.g. Chrome AI), falling back to the synchronous `detect` for all others.
   *
   * Cross-validation behaviour:
   * When `html-type` produces a result, the pipeline does NOT stop immediately.
   * Instead, the result is held as provisional and the TensorFlow classifier is
   * still executed. If TensorFlow returns a *different* type with confidence ≥
   * HTML_TYPE_CROSS_VALIDATE_THRESHOLD, TensorFlow wins (semantic context beats
   * the structural HTML hint). Otherwise the original html-type result stands.
   *
   * Example: `<input type="date" name="birthDate">` with label "Nascimento" →
   * html-type says "date" (100%), but TensorFlow may say "birthDate" (>50%)
   * based on signals → TensorFlow result is used.
   */
  async runAsync(field: FormField): Promise<PipelineResult> {
    const t0 = performance.now();
    const timings: PipelineResult["timings"] = [];
    const predictions: PipelineResult["predictions"] = [];
    const decisionTrace: string[] = [];

    // Holds the provisional html-type result while we wait for TF cross-validation
    let htmlTypeProvisional: ClassifierResult | null = null;

    for (const classifier of this.classifiers) {
      const ct = performance.now();
      const result = classifier.detectAsync
        ? await classifier.detectAsync(field)
        : classifier.detect(field);
      const classifierMs = performance.now() - ct;
      timings.push({ strategy: classifier.name, durationMs: classifierMs });

      if (result === null) {
        decisionTrace.push(`${classifier.name}: null — skipped`);
      } else if (result.type === "unknown") {
        decisionTrace.push(
          `${classifier.name}: unknown (${(result.confidence * 100).toFixed(0)}%) — skipped`,
        );
        predictions.push({ type: result.type, confidence: result.confidence });
      } else {
        predictions.push({ type: result.type, confidence: result.confidence });

        // html-type: store provisionally and continue to tensorflow for cross-validation
        if (classifier.name === "html-type") {
          htmlTypeProvisional = result;
          decisionTrace.push(
            `${classifier.name}: ${result.type} (${(result.confidence * 100).toFixed(0)}%) — provisional, awaiting tensorflow cross-validation`,
          );
          continue;
        }

        // tensorflow: check whether it should override the provisional html-type result
        if (classifier.name === "tensorflow" && htmlTypeProvisional !== null) {
          if (
            result.type !== htmlTypeProvisional.type &&
            result.confidence >= HTML_TYPE_CROSS_VALIDATE_THRESHOLD
          ) {
            // TensorFlow has a different, confident semantic classification → override
            decisionTrace.push(
              `${classifier.name}: ${result.type} (${(result.confidence * 100).toFixed(0)}%) — overrides html-type (semantic context)`,
            );
            // Fall through to the normal return below
          } else {
            // TensorFlow confirms html-type or is not confident enough → html-type stands
            decisionTrace.push(
              `${classifier.name}: ${result.type} (${(result.confidence * 100).toFixed(0)}%) — html-type confirmed`,
            );
            return {
              ...htmlTypeProvisional,
              method: "html-type",
              durationMs: performance.now() - t0,
              timings,
              predictions,
              decisionTrace,
            };
          }
        }

        decisionTrace.push(
          `${classifier.name}: ${result.type} (${(result.confidence * 100).toFixed(0)}%) — selected`,
        );
        return {
          ...result,
          method: classifier.name,
          durationMs: performance.now() - t0,
          timings,
          predictions,
          decisionTrace,
        };
      }

      // After tensorflow processed (null or unknown): provisional html-type stands
      if (htmlTypeProvisional !== null && classifier.name === "tensorflow") {
        decisionTrace.push(
          `html-type: ${htmlTypeProvisional.type} (100%) — confirmed (tensorflow skipped/unknown)`,
        );
        return {
          ...htmlTypeProvisional,
          method: "html-type",
          durationMs: performance.now() - t0,
          timings,
          predictions,
          decisionTrace,
        };
      }
    }

    // End of pipeline — if html-type was provisional and tensorflow wasn't in the pipeline
    if (htmlTypeProvisional !== null) {
      decisionTrace.push(
        `html-type: ${htmlTypeProvisional.type} (100%) — confirmed (no tensorflow in pipeline)`,
      );
      return {
        ...htmlTypeProvisional,
        method: "html-type",
        durationMs: performance.now() - t0,
        timings,
        predictions,
        decisionTrace,
      };
    }

    decisionTrace.push("html-fallback: unknown — no classifier matched");
    return {
      type: "unknown",
      method: "html-fallback",
      confidence: 0.1,
      durationMs: performance.now() - t0,
      timings,
      predictions,
      decisionTrace,
    };
  }

  /**
   * Returns a new pipeline with classifiers reordered by the given method names.
   * Classifiers not listed are dropped.
   */
  withOrder(names: DetectionMethod[]): DetectionPipeline {
    const ordered = names
      .map((n) => this.classifiers.find((c) => c.name === n))
      .filter((c): c is FieldClassifier => c !== undefined);
    return new DetectionPipeline(ordered);
  }

  /**
   * Returns a new pipeline excluding the specified strategies.
   */
  without(...names: DetectionMethod[]): DetectionPipeline {
    return new DetectionPipeline(
      this.classifiers.filter((c) => !names.includes(c.name)),
    );
  }

  /**
   * Returns a new pipeline with the given classifier appended at the end.
   */
  with(classifier: FieldClassifier): DetectionPipeline {
    return new DetectionPipeline([...this.classifiers, classifier]);
  }

  /**
   * Returns a new pipeline with a classifier inserted before the one with
   * the given name. Useful for injecting a strategy at a specific priority.
   */
  insertBefore(
    beforeName: DetectionMethod,
    classifier: FieldClassifier,
  ): DetectionPipeline {
    const idx = this.classifiers.findIndex((c) => c.name === beforeName);
    if (idx === -1) return this.with(classifier);
    const next = [...this.classifiers];
    next.splice(idx, 0, classifier);
    return new DetectionPipeline(next);
  }
}

Members

Name Kind Visibility Status Signature
runAsync method - runAsync(field: FormField): : Promise<PipelineResult>
insertBefore method - insertBefore( beforeName: DetectionMethod, classifier: FieldClassifier, ): : DetectionPipeline

No outgoing dependencies.

Impact (Incoming)

graph LR DetectionPipeline["DetectionPipeline"] FieldProcessingChain["FieldProcessingChain"] runAsync["runAsync"] stream["stream"] getActiveClassifiers["getActiveClassifiers"] withOrder["withOrder"] without["without"] with["with"] insertBefore["insertBefore"] makeField["makeField"] FieldProcessingChain -->|uses| DetectionPipeline runAsync -.->|instantiates| DetectionPipeline stream -.->|instantiates| DetectionPipeline getActiveClassifiers -->|uses| DetectionPipeline withOrder -.->|instantiates| DetectionPipeline without -.->|instantiates| DetectionPipeline with -.->|instantiates| DetectionPipeline insertBefore -.->|instantiates| DetectionPipeline makeField -->|uses| DetectionPipeline style DetectionPipeline fill:#dbeafe,stroke:#2563eb,stroke-width:2px click DetectionPipeline "c51ffbb1759babe9.html" click FieldProcessingChain "100ca2148a515e25.html" click runAsync "9fb910953d2bd7ba.html" click stream "201dc8fac84b81eb.html" click getActiveClassifiers "94c3286cfdb569c3.html" click withOrder "9e9296596d1d2db5.html" click without "f903396d2ca4f08c.html" click with "1fc816e8cb905ec6.html" click insertBefore "45f163b7d5d25a47.html" click makeField "40cb7e5a6354eaea.html"
SourceType
FieldProcessingChain uses
runAsync instantiates
stream instantiates
getActiveClassifiers uses
withOrder instantiates
without instantiates
with instantiates
insertBefore instantiates
makeField uses