Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions scripts/check-docs-drift.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,50 @@ function extractTypeLiteralFieldEntries(text: string, typeName: string): TypeLit
const fieldMatch = /^\s*([a-zA-Z_][a-zA-Z0-9_]*)\??:\s*(.+);\s*$/.exec(line);
if (fieldMatch) fields.push({ name: fieldMatch[1]!, typeText: fieldMatch[2]!.trim() });
}
// Intersection half: `{ ... } & Record<KeyUnion, ValueType>`. Brace-counting above stops at the object
// literal's own `}`, so the Record's keys -- the actual feature/experimental keys of
// FocusManifestFeaturesConfig / FocusManifestExperimentalConfig -- sit entirely outside it and were never
// seen. Resolve each KeyUnion member (inline `"a" | "b"` literals, or a locally-declared string-literal union
// type like ConvergedFeatureKey) to its own field entry. Generalizes to any `{...} & Record<...>` (#8656).
// Only THIS declaration's tail (up to its terminating `;`), so a `& Record<...>` from a later type in the
// file is never mis-attributed here. `^[^;]*` always matches (even with no trailing `;`), so no branch.
const intersection = /^[^;]*/.exec(text.slice(index))![0];
for (const recordMatch of intersection.matchAll(/&\s*Record<\s*([^,]+?)\s*,\s*([^>]+?)>/g)) {
const valueText = recordMatch[2]!.trim();
for (const key of resolveRecordKeyLiterals(recordMatch[1]!, text)) {
fields.push({ name: key, typeText: valueText });
}
}
return fields;
}

function stringLiteralsIn(source: string): string[] {
return [...source.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]!);
}

/** Resolve a `Record<K, V>` key operand `K` to its string-literal members, so each becomes a documented field:
* - inline `"a" | "b"` literals are taken directly;
* - a bare identifier is looked up as a locally-declared `export type K = ...`, resolving either a direct
* string-literal union (`"a" | "b"`) or the `(typeof SOME_ARRAY)[number]` indirection over a `const
* SOME_ARRAY = ["a", "b", ...] as const` string array -- the shape ConvergedFeatureKey / ExperimentalPluginKey
* actually use.
* Returns [] for a non-literal key (e.g. `Record<string, V>`), so no field names are ever invented. */
function resolveRecordKeyLiterals(keyText: string, text: string): string[] {
const inlineLiterals = stringLiteralsIn(keyText);
if (inlineLiterals.length > 0) return inlineLiterals;
const identifier = keyText.trim();
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) return [];
const aliasMatch = new RegExp(`export type ${identifier}\\s*=\\s*([^;]+);`).exec(text);
if (!aliasMatch) return [];
const aliasBody = aliasMatch[1]!;
const directUnion = stringLiteralsIn(aliasBody);
if (directUnion.length > 0) return directUnion;
const typeofArray = /\(\s*typeof\s+([A-Za-z_][A-Za-z0-9_]*)\s*\)\s*\[\s*number\s*\]/.exec(aliasBody);
if (!typeofArray) return [];
const arrayMatch = new RegExp(`const ${typeofArray[1]}\\s*=\\s*\\[([^\\]]*)\\]`).exec(text);
return arrayMatch ? stringLiteralsIn(arrayMatch[1]!) : [];
}

/** Every top-level field name DECLARED directly on the `RepositorySettings` object-literal type in src/types.ts
* (#4617) -- the FULL ~100-field surface, not just the `*GateMode` subset `extractGateModeFields` above
* targets. Unlike that regex (which matches a NAME SHAPE anywhere in the file), this is anchored on the type's
Expand Down
48 changes: 48 additions & 0 deletions test/unit/check-docs-drift-script.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import {
checkDocsDrift,
Expand Down Expand Up @@ -216,6 +217,53 @@ describe("check-docs-drift script", () => {
it("returns [] when FocusManifest has no declaration in the text", () => {
expect(extractFocusManifestFields("export type SomethingElse = { x: string };")).toEqual([]);
});

it("resolves a `{ ... } & Record<K, V>` intersection half's keys to their own leaves (#8656)", () => {
const fixture = `
export type FocusManifest = {
present: boolean;
features: FeaturesConfig;
experimental: ExperimentalConfig;
named: NamedConfig;
numeric: NumericConfig;
loose: LooseConfig;
missing: MissingConfig;
broken: BrokenConfig;
};
export type FeaturesConfig = { present: boolean } & Record<"newKey", boolean | null>;
export const ARR_KEYS = ["gamma", "delta"] as const;
export type ArrKey = (typeof ARR_KEYS)[number];
export type ExperimentalConfig = { present: boolean } & Record<ArrKey, boolean | null>;
export type NamedKeys = "alpha" | "beta";
export type NamedConfig = { present: boolean } & Record<NamedKeys, boolean | null>;
export type NumKey = number;
export type NumericConfig = { present: boolean } & Record<NumKey, boolean | null>;
export type LooseConfig = { present: boolean } & Record<string, boolean | null>;
export type MissingArrKey = (typeof MISSING_ARR)[number];
export type MissingConfig = { present: boolean } & Record<MissingArrKey, boolean | null>;
export type BrokenConfig = { present: boolean } & Record<123, boolean | null>;
`;
// Inline literal (features.newKey), a locally-declared `(typeof ARR)[number]` const-array union
// (experimental.gamma/delta), and a direct string-literal union (named.alpha/beta) each resolve to leaves.
// A `Record<number|string>` or an unresolvable key (NumKey/LooseConfig/MissingConfig/BrokenConfig) invents
// no field names.
expect(extractFocusManifestFields(fixture)).toEqual([
"features.newKey",
"experimental.gamma",
"experimental.delta",
"named.alpha",
"named.beta",
]);
});

it("surfaces the real FocusManifestFeaturesConfig / FocusManifestExperimentalConfig Record keys against the live source (#8656)", () => {
const source = readFileSync("packages/loopover-engine/src/focus-manifest.ts", "utf8");
const fields = extractFocusManifestFields(source);
for (const key of ["rag", "reputation", "safety", "grounding", "e2eTests", "screenshots", "improvementSignal", "amsReputationBridge"]) {
expect(fields, `features.${key}`).toContain(`features.${key}`);
}
expect(fields).toContain("experimental.gittensor");
});
});

describe("checkDocsDrift", () => {
Expand Down