Skip to content

fix(config): focus-manifest validator never warns on unknown top-level fields, unlike selfhost config-lint #5929

Description

@JSONbored

Context

There are two independent validators for the same .loopover.yml/focus-manifest file shape in this
repo, and only one of them catches unrecognized top-level keys (e.g. a typo'd gates: instead of
gate:, or preferedLabels: instead of preferredLabels:):

  1. src/selfhost/config-lint.ts (lintManifestText, wired up via scripts/loopover-config-lint.ts
    as npm run selfhost:config-lint -- [path]) maintains an explicit TOP_LEVEL_FIELDS allowlist and
    an unknownTopLevelWarnings() helper that diffs the parsed manifest's keys against it, emitting
    "Manifest contains unknown top-level field(s): <name>." for anything not recognized (plus a
    dedicated migration message for retired fields via RETIRED_FIELD_MIGRATION_WARNINGS).

  2. src/services/focus-manifest-validation.ts (buildFocusManifestValidation) — the validator behind
    POST /v1/validate/focus-manifest, which is what loopover-mcp validate-config --file <path>
    and the loopover_validate_config MCP tool actually call (per printValidateConfigHelp() in
    packages/loopover-mcp/bin/loopover-mcp.js: "Mirrors the loopover_validate_config MCP tool and
    POST /v1/validate/focus-manifest"). This validator calls parseFocusManifestContent /
    parseFocusManifest in packages/loopover-engine/src/focus-manifest.ts, which reads only the
    known fields directly off the parsed record (record.wantedPaths, record.gate, record.review,
    ...) and never inspects Object.keys(record) for anything unrecognized. The only "nothing
    recognized" signal is the catch-all warning emitted when every field is empty/default — a
    manifest with a correct wantedPaths: but a typo'd gates: block (instead of gate:) still has
    at least one recognized, non-default field, so that catch-all never fires either. The gate:
    config is silently dropped with zero warnings and status: "ok".

So a contributor who runs the documented, contributor-facing validation path
(loopover-mcp validate-config --file .loopover.yml) gets a clean "ok" for a manifest that the
self-host-only npm run selfhost:config-lint tool would correctly flag as having an unknown field.
The detection logic already exists and is tested (src/selfhost/config-lint.ts); it's just not
reachable from the validator contributors are actually told to use.

Requirements

  • Extract unknownTopLevelWarnings (and its TOP_LEVEL_FIELDS/RETIRED_FIELD_MIGRATION_WARNINGS
    allowlist) from src/selfhost/config-lint.ts into a location both src/selfhost/config-lint.ts
    and src/services/focus-manifest-validation.ts can import (e.g. into
    packages/loopover-engine/src/focus-manifest.ts alongside parseFocusManifestContent, since that
    is already the shared source of truth for the manifest shape, or a new small shared module — pick
    whichever keeps src/selfhost/config-lint.ts's existing behavior byte-identical).
  • Wire the unknown-field warnings into buildFocusManifestValidation in
    src/services/focus-manifest-validation.ts, added to its warnings array alongside the existing
    manifest.warnings, and reflected in resolveValidationStatus (an unknown-field warning should
    produce status: "warn", matching how every other warning already downgrades status from "ok").
  • Do not change src/selfhost/config-lint.ts's own output/behavior (lintManifestText must keep
    returning byte-identical results for every existing test case) — this is a reuse/dedup, not a
    rewrite of the self-host tool.
  • Keep the allowlist as the single source of truth: if a new top-level manifest field is ever added,
    updating one list should fix unknown-field detection in both places (today's four-copies-per-field
    pattern documented for review.fields doesn't apply here since this is about the top-level key
    list, not the review.fields sub-block, but the same "don't let it drift" principle applies).

Deliverables

  • Shared unknown-top-level-field detection extracted from src/selfhost/config-lint.ts into a
    module importable by both src/selfhost/config-lint.ts and
    src/services/focus-manifest-validation.ts.
  • buildFocusManifestValidation includes an "unknown top-level field(s)"-style warning (and
    status: "warn") for a manifest containing a key outside the known field set.
  • Regression test in test/unit/ covering buildFocusManifestValidation (or wherever its
    existing tests live) for a manifest with a typo'd top-level key (e.g. gates: instead of
    gate:), asserting the new warning appears and status !== "ok".
  • Existing src/selfhost/config-lint.ts test suite passes unchanged (proving no behavior drift
    in the self-host tool during extraction).

Test Coverage Requirements

99%+ Codecov patch coverage (branch-counted) on every changed line in
src/services/focus-manifest-validation.ts, src/selfhost/config-lint.ts, and the new shared
module — including both the "known fields only" and "has an unknown field" branches.

Expected Outcome

loopover-mcp validate-config --file .loopover.yml (and the loopover_validate_config MCP tool and
POST /v1/validate/focus-manifest route it calls) now reports the same "unknown top-level field"
warning that npm run selfhost:config-lint already reports for the same input, closing the gap
between the two validators for the one file format they both parse.

Links & Resources

  • src/selfhost/config-lint.ts — existing TOP_LEVEL_FIELDS, unknownTopLevelWarnings(),
    RETIRED_FIELD_MIGRATION_WARNINGS.
  • src/services/focus-manifest-validation.tsbuildFocusManifestValidation,
    resolveValidationStatus.
  • packages/loopover-engine/src/focus-manifest.tsparseFocusManifest (~L3062),
    parseFocusManifestContent (~L3119), the likely home for a shared allowlist.
  • packages/loopover-mcp/bin/loopover-mcp.jsprintValidateConfigHelp() (~L2035),
    validateConfigCli (~L2048) — the contributor-facing entrypoint this fixes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.help wantedExtra attention is needed

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions