Type generated WoW talent legality keys as strings (#4)
This commit is contained in:
1 parent
8a7e44cc14
commit
1622f0893c
3 files changed
+243
-1
No files matched your search
@@ -0,0 +1,46 @@
|
||||
# WoW talent-gate Set typing — 2026-10-09
|
||||
|
||||
This narrow source correction addresses shared-source issue [#4](https://git.whoagland.com/phenom/healer-man/issues/4), coordinated with native issue [#236](https://git.whoagland.com/Codex/healer-man-godot/issues/236). It changes only the inferred TypeScript type of the legal talent-node ID set in `src/game/wow335AbilityCatalog.ts`:
|
||||
|
||||
```ts
|
||||
const SOURCE_WOW335_TALENT_NODE_IDS = new Set<string>(WOW335_TALENT_NODES.map((node) => node.id));
|
||||
```
|
||||
|
||||
The runtime membership check and its inputs are unchanged. The explicit `string` type lets the imported talent-node ID pass strict `Set.has` checking without changing any legal node, exemption, ability rank, rotation, or gameplay value.
|
||||
|
||||
## Before and after
|
||||
|
||||
- Baseline catalog SHA-256: `6a7cf9b934026ec4b5beb0de3fe64eda4872efad561d63d25b2c911676d249a4`
|
||||
- Corrected catalog SHA-256: `3ea007068d8a8b6cd0c49d2d4b780cf176dc567e23fe20537e40f49fded14314`
|
||||
- The focused strict baseline emitted exactly one expected `TS2345` at `wow335AbilityCatalog.ts(2605,75)`, on `SOURCE_WOW335_TALENT_NODE_IDS.has(chain.talentNodeId)`.
|
||||
- The corrected source passed strict TypeScript 5.8.3 checking with zero diagnostics.
|
||||
- Three focused regressions passed: cold native import followed by talent preload, legal talent gates plus source-only-node exemptions, and equality with the full legal talent-node set. The targeted Vitest invocation selected these three tests; 41 other tests in that file were skipped.
|
||||
|
||||
The review run is recorded in `../diagnostics/strict-types/after-regression-tool-final2.json`. The sparse authoritative mirror strict check is recorded in `../diagnostics/strict-types/actual-mirror-types-final.json`; that run used TypeScript 5.8.3 with zero diagnostics and skipped runtime tests because the mirror does not contain the runtime fixture/dependencies. The baseline proof is `../diagnostics/strict-types/baseline-regression-tool.json`.
|
||||
|
||||
## Reproduction checker
|
||||
|
||||
`../scripts/check-wow-talent-gate-types.mjs` checks the catalog and its imported TypeScript dependency graph in strict mode. It can also run the three focused regressions when the project root has `vite.config.ts` and the cold-start fixture, and the toolchain root provides TypeScript, Vite, and Vitest.
|
||||
|
||||
From the source review checkout, run both type and runtime checks with:
|
||||
|
||||
```sh
|
||||
node scripts/check-wow-talent-gate-types.mjs \
|
||||
--project-root /path/to/source-review \
|
||||
--toolchain-root /path/to/source-review
|
||||
```
|
||||
|
||||
For a sparse source mirror with an external dependency tree, point the roots separately and omit only the runtime tests:
|
||||
|
||||
```sh
|
||||
node /path/to/source-mirror/scripts/check-wow-talent-gate-types.mjs \
|
||||
--project-root /path/to/source-mirror \
|
||||
--toolchain-root /path/to/toolchain \
|
||||
--skip-runtime
|
||||
```
|
||||
|
||||
`--expect baseline` is available to verify the recorded pre-fix `TS2345`; the default `--expect fixed` requires a clean strict check. The checker is focused evidence, not a full project build, runtime gameplay validation, balance certification, or GPU validation.
|
||||
|
||||
## Scope record
|
||||
|
||||
Coordinator: `/root`, session `01a11cd3-fa7c-7510-ba8f-bc7a3d0310a6`. The Set typing correction was the source-owned change; independent strict-type and focused-regression validation was performed by the validation worker. All workers used GPT-6 Luna with xhigh reasoning for this scoped work.
|
||||
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createRequire } from "node:module";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
function parseArgs(args) {
|
||||
const result = {
|
||||
projectRoot: process.cwd(),
|
||||
toolchainRoot: null,
|
||||
expectation: "fixed",
|
||||
skipRuntime: false,
|
||||
};
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const argument = args[index];
|
||||
if (argument === "--project-root") result.projectRoot = args[++index];
|
||||
else if (argument.startsWith("--project-root=")) result.projectRoot = argument.slice("--project-root=".length);
|
||||
else if (argument === "--toolchain-root") result.toolchainRoot = args[++index];
|
||||
else if (argument.startsWith("--toolchain-root=")) result.toolchainRoot = argument.slice("--toolchain-root=".length);
|
||||
else if (argument === "--expect") result.expectation = args[++index];
|
||||
else if (argument.startsWith("--expect=")) result.expectation = argument.slice("--expect=".length);
|
||||
else if (argument === "--skip-runtime") result.skipRuntime = true;
|
||||
else if (argument === "--help" || argument === "-h") {
|
||||
console.log("Usage: node scripts/check-wow-talent-gate-types.mjs [--project-root DIR] [--toolchain-root DIR] [--expect baseline|fixed] [--skip-runtime]");
|
||||
process.exit(0);
|
||||
} else {
|
||||
throw new Error(`Unknown argument: ${argument}`);
|
||||
}
|
||||
}
|
||||
assert.ok(["baseline", "fixed"].includes(result.expectation), `invalid --expect value: ${result.expectation}`);
|
||||
result.projectRoot = path.resolve(result.projectRoot);
|
||||
result.toolchainRoot = path.resolve(result.toolchainRoot ?? result.projectRoot);
|
||||
return result;
|
||||
}
|
||||
|
||||
function typecheckTalentCatalog(projectRoot, toolchainRoot) {
|
||||
const requireFromToolchain = createRequire(path.join(toolchainRoot, "package.json"));
|
||||
const ts = requireFromToolchain("typescript");
|
||||
const catalogPath = path.join(projectRoot, "src", "game", "wow335AbilityCatalog.ts");
|
||||
const viteClientPathCandidates = [
|
||||
path.join(projectRoot, "node_modules", "vite", "client.d.ts"),
|
||||
path.join(toolchainRoot, "node_modules", "vite", "client.d.ts"),
|
||||
];
|
||||
const viteClientPath = viteClientPathCandidates.find(existsSync);
|
||||
assert.ok(viteClientPath, "strict catalog check requires Vite's client.d.ts in the project or toolchain dependency tree");
|
||||
const configPath = path.join(projectRoot, "tsconfig.app.json");
|
||||
let parsedOptions;
|
||||
if (existsSync(configPath)) {
|
||||
const configText = readFileSync(configPath, "utf8");
|
||||
const parsedConfig = ts.parseConfigFileTextToJson(configPath, configText);
|
||||
assert.equal(parsedConfig.error, undefined, `could not parse ${configPath}`);
|
||||
const parsed = ts.parseJsonConfigFileContent(parsedConfig.config, ts.sys, projectRoot, undefined, configPath);
|
||||
assert.equal(parsed.errors.length, 0, `could not resolve ${configPath}: ${parsed.errors.map((error) => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("; ")}`);
|
||||
parsedOptions = parsed.options;
|
||||
} else {
|
||||
parsedOptions = {
|
||||
target: ts.ScriptTarget.ES2022,
|
||||
module: ts.ModuleKind.ESNext,
|
||||
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
||||
jsx: ts.JsxEmit.ReactJSX,
|
||||
resolveJsonModule: true,
|
||||
allowImportingTsExtensions: true,
|
||||
paths: {},
|
||||
};
|
||||
}
|
||||
|
||||
const shimDirectory = mkdtempSync(path.join(os.tmpdir(), "wow-talent-gate-types-"));
|
||||
const aliasDeclarationPath = path.join(shimDirectory, "dungeonAssetModules.d.ts");
|
||||
writeFileSync(aliasDeclarationPath, [
|
||||
'declare module "@dungeonAssetModules" {',
|
||||
' export const dungeonModules: Readonly<Record<string, string>>;',
|
||||
'}',
|
||||
'',
|
||||
].join("\n"));
|
||||
|
||||
try {
|
||||
const compilerOptions = {
|
||||
...parsedOptions,
|
||||
strict: true,
|
||||
noEmit: true,
|
||||
skipLibCheck: true,
|
||||
incremental: false,
|
||||
composite: false,
|
||||
types: ["node"],
|
||||
baseUrl: projectRoot,
|
||||
typeRoots: [
|
||||
path.join(projectRoot, "node_modules", "@types"),
|
||||
path.join(toolchainRoot, "node_modules", "@types"),
|
||||
],
|
||||
paths: { ...(parsedOptions.paths ?? {}) },
|
||||
};
|
||||
delete compilerOptions.paths["@dungeonAssetModules"];
|
||||
const rootNames = [catalogPath, viteClientPath, aliasDeclarationPath];
|
||||
const program = ts.createProgram({ rootNames, options: compilerOptions });
|
||||
const diagnostics = ts.getPreEmitDiagnostics(program).filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error);
|
||||
const simplified = diagnostics.map((diagnostic) => {
|
||||
const location = diagnostic.file && diagnostic.start !== undefined
|
||||
? diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start)
|
||||
: undefined;
|
||||
const sourceLine = diagnostic.file && location
|
||||
? diagnostic.file.text.split(/\r?\n/u)[location.line]
|
||||
: undefined;
|
||||
return {
|
||||
code: diagnostic.code,
|
||||
file: diagnostic.file ? path.relative(projectRoot, diagnostic.file.fileName).split(path.sep).join("/") : null,
|
||||
line: location ? location.line + 1 : null,
|
||||
message: ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
|
||||
sourceLine: sourceLine?.trim() ?? null,
|
||||
};
|
||||
});
|
||||
return { typescriptVersion: ts.version, diagnostics: simplified };
|
||||
} finally {
|
||||
rmSync(shimDirectory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function runTalentRuntimeRegressions(projectRoot, toolchainRoot) {
|
||||
assert.ok(existsSync(path.join(projectRoot, "vite.config.ts")), "focused runtime checks require vite.config.ts; use --skip-runtime for a source-only mirror without it");
|
||||
assert.ok(existsSync(path.join(projectRoot, "src", "game", "wow335AbilityCatalog.cold-start.fixture.ts")), "focused runtime checks require the cold-start fixture; use --skip-runtime when it is not present");
|
||||
const requireFromToolchain = createRequire(path.join(toolchainRoot, "package.json"));
|
||||
const vitestPath = requireFromToolchain.resolve("vitest/vitest.mjs");
|
||||
const testPath = path.join("src", "game", "wow335AbilityCatalog.test.ts");
|
||||
const matchingTests = [
|
||||
"preserves talent gates across a cold native import and a later talent preload",
|
||||
"uses legal talent gates for learned builds and leaves source-only nodes unlocked",
|
||||
"uses the same legal talent-node set as the full catalog",
|
||||
];
|
||||
const testPattern = matchingTests.join("|");
|
||||
try {
|
||||
const output = execFileSync(process.execPath, [
|
||||
vitestPath,
|
||||
"run",
|
||||
testPath,
|
||||
"--config",
|
||||
"vite.config.ts",
|
||||
"--maxWorkers=1",
|
||||
"--testTimeout=15000",
|
||||
"--testNamePattern",
|
||||
testPattern,
|
||||
], {
|
||||
cwd: projectRoot,
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
maxBuffer: 8 * 1024 * 1024,
|
||||
});
|
||||
const passedMatch = output.match(/Tests\s+(\d+)\s+passed/u);
|
||||
assert.ok(passedMatch, `Vitest did not report a passing test count:\n${output}`);
|
||||
assert.equal(Number(passedMatch[1]), matchingTests.length, `expected all focused talent gate tests to run:\n${output}`);
|
||||
return { passed: Number(passedMatch[1]), tests: matchingTests, output: output.trim() };
|
||||
} catch (error) {
|
||||
const stdout = error.stdout?.toString() ?? "";
|
||||
const stderr = error.stderr?.toString() ?? "";
|
||||
throw new Error(`focused talent runtime tests failed\n${stdout}\n${stderr}`, { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const projectRoot = args.projectRoot;
|
||||
const toolchainRoot = args.toolchainRoot;
|
||||
const catalogPath = path.join(projectRoot, "src", "game", "wow335AbilityCatalog.ts");
|
||||
const catalogText = readFileSync(catalogPath, "utf8");
|
||||
assert.match(catalogText, /SOURCE_WOW335_TALENT_NODE_IDS\.has\(chain\.talentNodeId\)/u, "the actual source catalog must retain the legal-node gate");
|
||||
|
||||
const typecheck = typecheckTalentCatalog(projectRoot, toolchainRoot);
|
||||
const expectedDiagnostic = typecheck.diagnostics.length === 1
|
||||
&& typecheck.diagnostics[0].code === 2345
|
||||
&& typecheck.diagnostics[0].file === "src/game/wow335AbilityCatalog.ts"
|
||||
&& typecheck.diagnostics[0].sourceLine?.includes("SOURCE_WOW335_TALENT_NODE_IDS.has(chain.talentNodeId)");
|
||||
if (args.expectation === "baseline") {
|
||||
assert.ok(expectedDiagnostic, `expected exactly the pre-fix TS2345 at the actual talent Set gate; got ${JSON.stringify(typecheck.diagnostics, null, 2)}`);
|
||||
} else {
|
||||
assert.deepEqual(typecheck.diagnostics, [], `the corrected catalog and its focused dependency graph must type-check cleanly`);
|
||||
}
|
||||
|
||||
const runtime = args.skipRuntime ? null : runTalentRuntimeRegressions(projectRoot, toolchainRoot);
|
||||
const result = {
|
||||
projectRoot,
|
||||
toolchainRoot,
|
||||
catalogSha256: createHash("sha256").update(catalogText).digest("hex"),
|
||||
expectation: args.expectation,
|
||||
typecheck: {
|
||||
typescriptVersion: typecheck.typescriptVersion,
|
||||
strict: true,
|
||||
diagnostics: typecheck.diagnostics,
|
||||
result: expectedDiagnostic ? "expected-baseline-ts2345" : "clean",
|
||||
},
|
||||
runtime,
|
||||
limits: [
|
||||
"Strict-checks the actual catalog and its imported TypeScript dependency graph, with only the bundler's dungeon asset glob import given its declared Readonly<Record<string, string>> type.",
|
||||
"Runs only the three legal talent gate, source-only exemption, and cold/warm preload regressions; it does not run a full project build or balance sweep.",
|
||||
],
|
||||
};
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
@@ -43,7 +43,7 @@ import { WOW335_HUNTER_DETERRENCE_PARENT_AURA_EFFECTS } from "./wow335Deterrence
|
||||
import { WOW335_DETERRENCE_SOURCE_SIGNATURES } from "./wow335Deterrence";
|
||||
|
||||
// This generated source list is available synchronously when the abilities below are constructed.
|
||||
const SOURCE_WOW335_TALENT_NODE_IDS = new Set(WOW335_TALENT_NODES.map((node) => node.id));
|
||||
const SOURCE_WOW335_TALENT_NODE_IDS = new Set<string>(WOW335_TALENT_NODES.map((node) => node.id));
|
||||
|
||||
export const BASE_CLASS_STARTER_SOURCE_LEVEL = 10;
|
||||
|
||||
|
||||
Reference in new issue
Block a user