@knights-eye-chess/ai-analysis 0.0.0-stage → 0.1.0-dev.14
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.
- package/LICENSE +235 -0
- package/LICENSING.md +26 -0
- package/NOTICE +5 -0
- package/NPM-PUBLISHING.md +24 -0
- package/README.md +141 -2
- package/SOURCE-ORIGIN.json +16 -0
- package/THIRD_PARTY_NOTICES.md +18 -0
- package/build.mjs +22 -0
- package/dist/answer-pipeline.test.mjs +375 -0
- package/dist/chunk-PLKNJRPF.js +10662 -0
- package/dist/chunk-XQDZJOYD.js +2515 -0
- package/dist/index.js +146 -0
- package/dist/owner-tests/chess-reasoning-compact-search-preparation.test.mjs +963 -0
- package/dist/owner-tests/mechanism-encoding.test.mjs +529 -0
- package/dist/owner-tests/move-mechanism-facts.test.mjs +396 -0
- package/dist/owner-tests/pipeline-host-preparation.test.mjs +10556 -0
- package/dist/owner-tests/position-preparation.test.mjs +625 -0
- package/dist/owner-tests/preparation-consolidation.test.mjs +1052 -0
- package/dist/owner-tests/prose-reference-admission.test.mjs +1770 -0
- package/dist/owner-tests/score-comparison-notices.test.mjs +240 -0
- package/dist/owner-tests/simple-engine-presentation.test.mjs +233 -0
- package/dist/owner-tests/tool-input-loop-guard.test.mjs +78 -0
- package/dist/research.js +2274 -0
- package/dist/runtime-closure.json +103 -0
- package/dist/shared-runtime-closure.json +39 -0
- package/dist/shared.js +152 -0
- package/dist/types/ai-request-failure.d.ts +14 -0
- package/dist/types/ai-request-limits.d.ts +4 -0
- package/dist/types/ai-routing-diagnostics.d.ts +14 -0
- package/dist/types/chess-reasoning/ablation-adaptive-evidence-loop.d.ts +127 -0
- package/dist/types/chess-reasoning/ablation-adaptive-runtime.d.ts +178 -0
- package/dist/types/chess-reasoning/ablation-direct-synthesis.d.ts +1030 -0
- package/dist/types/chess-reasoning/ablation-evidence-bundle.d.ts +185 -0
- package/dist/types/chess-reasoning/ablation-evidence-capture.d.ts +37 -0
- package/dist/types/chess-reasoning/ablation-lean-direct-runtime.d.ts +193 -0
- package/dist/types/chess-reasoning/ablation-model-facing-evidence.d.ts +38 -0
- package/dist/types/chess-reasoning/ablation-transport-correction.d.ts +13 -0
- package/dist/types/chess-reasoning/ai-sdk-runtime-support.d.ts +93 -0
- package/dist/types/chess-reasoning/answer-host-contracts.d.ts +59 -0
- package/dist/types/chess-reasoning/answer-model-connector.d.ts +68 -0
- package/dist/types/chess-reasoning/answer-pipeline-exports.d.ts +98 -0
- package/dist/types/chess-reasoning/answer-pipeline.d.ts +41 -0
- package/dist/types/chess-reasoning/answer-reference-validation.d.ts +40 -0
- package/dist/types/chess-reasoning/answer-tool-host.d.ts +8 -0
- package/dist/types/chess-reasoning/branch-expansion-policy.d.ts +13 -0
- package/dist/types/chess-reasoning/branch-expansion.d.ts +39 -0
- package/dist/types/chess-reasoning/causal-claim-checks.d.ts +21 -0
- package/dist/types/chess-reasoning/chess-reference-dictionary.d.ts +13 -0
- package/dist/types/chess-reasoning/classification-question-target.d.ts +22 -0
- package/dist/types/chess-reasoning/classification-reassessment.d.ts +31 -0
- package/dist/types/chess-reasoning/compact-search-preparation.d.ts +7 -0
- package/dist/types/chess-reasoning/concept-registry.d.ts +99 -0
- package/dist/types/chess-reasoning/conversation-context.d.ts +12 -0
- package/dist/types/chess-reasoning/engine-search-failure.d.ts +7 -0
- package/dist/types/chess-reasoning/evidence-citation-normalization.d.ts +9 -0
- package/dist/types/chess-reasoning/evidence-licenses.d.ts +57 -0
- package/dist/types/chess-reasoning/evidence-selector-contract.d.ts +5 -0
- package/dist/types/chess-reasoning/existing-analysis-context.d.ts +104 -0
- package/dist/types/chess-reasoning/fact-observation-provider.d.ts +15 -0
- package/dist/types/chess-reasoning/fact-provider.d.ts +111 -0
- package/dist/types/chess-reasoning/fresh-engine-pv-witnesses.d.ts +67 -0
- package/dist/types/chess-reasoning/historical-prose.d.ts +23 -0
- package/dist/types/chess-reasoning/idea-graph.d.ts +34 -0
- package/dist/types/chess-reasoning/initial-search-plan.d.ts +37 -0
- package/dist/types/chess-reasoning/lossless-evidence-sharing.d.ts +18 -0
- package/dist/types/chess-reasoning/matched-comparison.d.ts +10 -0
- package/dist/types/chess-reasoning/material-accounting.d.ts +80 -0
- package/dist/types/chess-reasoning/material-format.d.ts +4 -0
- package/dist/types/chess-reasoning/mechanical-fact-provider.d.ts +33 -0
- package/dist/types/chess-reasoning/mechanical-ray-facts.d.ts +19 -0
- package/dist/types/chess-reasoning/mechanism-presentation.d.ts +9 -0
- package/dist/types/chess-reasoning/move-mechanism-facts.d.ts +45 -0
- package/dist/types/chess-reasoning/native-answer-connector.d.ts +4 -0
- package/dist/types/chess-reasoning/opening-source-contracts.d.ts +54 -0
- package/dist/types/chess-reasoning/opening-source-credits.d.ts +16 -0
- package/dist/types/chess-reasoning/opponent-threat-preparation.d.ts +7 -0
- package/dist/types/chess-reasoning/piece-identities.d.ts +11 -0
- package/dist/types/chess-reasoning/poc-contracts.d.ts +1026 -0
- package/dist/types/chess-reasoning/poc-hypotheses.d.ts +74 -0
- package/dist/types/chess-reasoning/poc-move-witnesses.d.ts +13 -0
- package/dist/types/chess-reasoning/poc-observations.d.ts +23 -0
- package/dist/types/chess-reasoning/poc-piece-refs.d.ts +12 -0
- package/dist/types/chess-reasoning/poc-plan-model.d.ts +45 -0
- package/dist/types/chess-reasoning/poc-semantic-text.d.ts +8 -0
- package/dist/types/chess-reasoning/poc-tools.d.ts +81 -0
- package/dist/types/chess-reasoning/portable-answer-runtime.d.ts +291 -0
- package/dist/types/chess-reasoning/position-analysis-contracts.d.ts +31 -0
- package/dist/types/chess-reasoning/position-analysis-scope.d.ts +3 -0
- package/dist/types/chess-reasoning/preparation-consolidation.d.ts +21 -0
- package/dist/types/chess-reasoning/production-observation-provider.d.ts +16 -0
- package/dist/types/chess-reasoning/question-routing.d.ts +326 -0
- package/dist/types/chess-reasoning/recorded-main-line.d.ts +37 -0
- package/dist/types/chess-reasoning/recorded-position-tools.d.ts +11 -0
- package/dist/types/chess-reasoning/recorded-reference-context.d.ts +13 -0
- package/dist/types/chess-reasoning/recorded-selected-classification.d.ts +36 -0
- package/dist/types/chess-reasoning/reference-limits.d.ts +2 -0
- package/dist/types/chess-reasoning/research-exports.d.ts +197 -0
- package/dist/types/chess-reasoning/saved-score-presentation.d.ts +14 -0
- package/dist/types/chess-reasoning/score-comparison-notices.d.ts +33 -0
- package/dist/types/chess-reasoning/server-simple-policy.d.ts +31 -0
- package/dist/types/chess-reasoning/simple-agent-config.d.ts +73 -0
- package/dist/types/chess-reasoning/simple-board-analysis.d.ts +85 -0
- package/dist/types/chess-reasoning/simple-compact-evidence.d.ts +7 -0
- package/dist/types/chess-reasoning/simple-completion.d.ts +23 -0
- package/dist/types/chess-reasoning/simple-engine-presentation.d.ts +3 -0
- package/dist/types/chess-reasoning/simple-evidence-preparation.d.ts +17 -0
- package/dist/types/chess-reasoning/simple-evidence-view.d.ts +17 -0
- package/dist/types/chess-reasoning/simple-explanation-coverage.d.ts +90 -0
- package/dist/types/chess-reasoning/simple-fact-delivery.d.ts +43 -0
- package/dist/types/chess-reasoning/simple-fallback-premise.d.ts +5 -0
- package/dist/types/chess-reasoning/simple-prose-references.d.ts +108 -0
- package/dist/types/chess-reasoning/simple-runtime-version.d.ts +1 -0
- package/dist/types/chess-reasoning/stable.d.ts +2 -0
- package/dist/types/chess-reasoning/tool-input-loop-guard.d.ts +5 -0
- package/dist/types/chess-reasoning/types.d.ts +180 -0
- package/dist/types/chess-reasoning/visible-support-lines.d.ts +12 -0
- package/dist/types/chess-reasoning/wikibooks-index.d.ts +30 -0
- package/dist/types/chess-reasoning/wikibooks-tool-context.d.ts +10 -0
- package/dist/types/player-perspective.d.ts +16 -0
- package/dist/types/shared-exports.d.ts +22 -0
- package/docs/API.md +82 -0
- package/docs/OWNERSHIP-ADOPTION.md +35 -0
- package/docs/OWNERSHIP-DRIFT.json +55 -0
- package/docs/RESEARCH-API.md +28 -0
- package/licenses/third-party/INVENTORY.json +202 -0
- package/licenses/third-party/ai-7.0.85/LICENSE +13 -0
- package/licenses/third-party/ai-sdk--gateway-4.0.69/LICENSE +13 -0
- package/licenses/third-party/ai-sdk--provider-4.0.9/LICENSE +13 -0
- package/licenses/third-party/ai-sdk--provider-utils-5.0.34/APACHE-2.0.txt +73 -0
- package/licenses/third-party/ai-sdk--provider-utils-5.0.34/src/to-json-schema/zod3-to-json-schema/LICENSE +16 -0
- package/licenses/third-party/chess.js-1.4.0/LICENSE +23 -0
- package/licenses/third-party/eventsource-parser-3.1.1/LICENSE +21 -0
- package/licenses/third-party/json-schema-0.4.0/LICENSE +195 -0
- package/licenses/third-party/standard-schema--spec-1.1.0/LICENSE +21 -0
- package/licenses/third-party/types--json-schema-7.0.15/LICENSE +21 -0
- package/licenses/third-party/types--node-22.19.19/LICENSE +21 -0
- package/licenses/third-party/undici-7.30.0/LICENSE +21 -0
- package/licenses/third-party/undici-7.30.0/lib/web/fetch/LICENSE +21 -0
- package/licenses/third-party/undici-types-6.21.0/LICENSE +21 -0
- package/licenses/third-party/vercel--oidc-3.2.0/LICENSE +202 -0
- package/licenses/third-party/workflow--serde-4.1.0/APACHE-2.0.txt +73 -0
- package/licenses/third-party/workflow--serde-4.1.0/LICENSE.md +3 -0
- package/licenses/third-party/zod-4.6.5/LICENSE +21 -0
- package/npm-release.json +5 -0
- package/package.json +72 -4
- package/scripts/check-release-tooling.mjs +22 -0
- package/scripts/pack.mjs +68 -0
- package/scripts/publish-reviewed.mjs +72 -0
- package/scripts/release-tooling.json +13 -0
- package/scripts/update-dependency.mjs +44 -0
- package/scripts/verify-release.mjs +52 -0
- package/src/ai-request-failure.ts +34 -0
- package/src/ai-request-limits.ts +4 -0
- package/src/ai-routing-diagnostics.ts +33 -0
- package/src/chess-reasoning/ablation-adaptive-evidence-loop.ts +515 -0
- package/src/chess-reasoning/ablation-adaptive-runtime.ts +924 -0
- package/src/chess-reasoning/ablation-direct-synthesis.ts +1128 -0
- package/src/chess-reasoning/ablation-evidence-bundle.ts +726 -0
- package/src/chess-reasoning/ablation-evidence-capture.ts +513 -0
- package/src/chess-reasoning/ablation-lean-direct-runtime.ts +670 -0
- package/src/chess-reasoning/ablation-model-facing-evidence.ts +212 -0
- package/src/chess-reasoning/ablation-transport-correction.ts +106 -0
- package/src/chess-reasoning/ai-sdk-runtime-support.ts +381 -0
- package/src/chess-reasoning/answer-host-contracts.ts +10 -0
- package/src/chess-reasoning/answer-model-connector.ts +83 -0
- package/src/chess-reasoning/answer-pipeline-exports.ts +98 -0
- package/src/chess-reasoning/answer-pipeline.ts +195 -0
- package/src/chess-reasoning/answer-reference-validation.ts +124 -0
- package/src/chess-reasoning/answer-tool-host.ts +10 -0
- package/src/chess-reasoning/branch-expansion-policy.ts +32 -0
- package/src/chess-reasoning/branch-expansion.ts +80 -0
- package/src/chess-reasoning/causal-claim-checks.ts +256 -0
- package/src/chess-reasoning/chess-reference-dictionary.ts +21 -0
- package/src/chess-reasoning/classification-question-target.ts +30 -0
- package/src/chess-reasoning/classification-reassessment.ts +34 -0
- package/src/chess-reasoning/compact-search-preparation.ts +125 -0
- package/src/chess-reasoning/concept-registry.ts +275 -0
- package/src/chess-reasoning/conversation-context.ts +29 -0
- package/src/chess-reasoning/engine-search-failure.ts +11 -0
- package/src/chess-reasoning/evidence-citation-normalization.ts +15 -0
- package/src/chess-reasoning/evidence-licenses.ts +166 -0
- package/src/chess-reasoning/evidence-selector-contract.ts +5 -0
- package/src/chess-reasoning/existing-analysis-context.ts +544 -0
- package/src/chess-reasoning/fact-observation-provider.ts +134 -0
- package/src/chess-reasoning/fact-provider.ts +250 -0
- package/src/chess-reasoning/fresh-engine-pv-witnesses.ts +128 -0
- package/src/chess-reasoning/historical-prose.ts +116 -0
- package/src/chess-reasoning/idea-graph.ts +224 -0
- package/src/chess-reasoning/initial-search-plan.ts +115 -0
- package/src/chess-reasoning/lossless-evidence-sharing.ts +135 -0
- package/src/chess-reasoning/matched-comparison.ts +40 -0
- package/src/chess-reasoning/material-accounting.ts +55 -0
- package/src/chess-reasoning/material-format.ts +47 -0
- package/src/chess-reasoning/mechanical-fact-provider.ts +1602 -0
- package/src/chess-reasoning/mechanical-ray-facts.ts +123 -0
- package/src/chess-reasoning/mechanism-presentation.ts +94 -0
- package/src/chess-reasoning/move-mechanism-facts.ts +185 -0
- package/src/chess-reasoning/native-answer-connector.ts +13 -0
- package/src/chess-reasoning/opening-source-contracts.ts +47 -0
- package/src/chess-reasoning/opening-source-credits.ts +53 -0
- package/src/chess-reasoning/opponent-threat-preparation.ts +98 -0
- package/src/chess-reasoning/piece-identities.ts +159 -0
- package/src/chess-reasoning/poc-contracts.ts +1097 -0
- package/src/chess-reasoning/poc-hypotheses.ts +759 -0
- package/src/chess-reasoning/poc-move-witnesses.ts +170 -0
- package/src/chess-reasoning/poc-observations.ts +611 -0
- package/src/chess-reasoning/poc-piece-refs.ts +62 -0
- package/src/chess-reasoning/poc-plan-model.ts +345 -0
- package/src/chess-reasoning/poc-semantic-text.ts +10 -0
- package/src/chess-reasoning/poc-tools.ts +978 -0
- package/src/chess-reasoning/portable-answer-runtime.ts +684 -0
- package/src/chess-reasoning/position-analysis-contracts.ts +88 -0
- package/src/chess-reasoning/position-analysis-scope.ts +20 -0
- package/src/chess-reasoning/preparation-consolidation.ts +91 -0
- package/src/chess-reasoning/production-observation-provider.ts +41 -0
- package/src/chess-reasoning/question-routing.ts +332 -0
- package/src/chess-reasoning/recorded-main-line.ts +55 -0
- package/src/chess-reasoning/recorded-position-tools.ts +78 -0
- package/src/chess-reasoning/recorded-reference-context.ts +44 -0
- package/src/chess-reasoning/recorded-selected-classification.ts +54 -0
- package/src/chess-reasoning/reference-limits.ts +2 -0
- package/src/chess-reasoning/research-exports.ts +197 -0
- package/src/chess-reasoning/saved-score-presentation.ts +6 -0
- package/src/chess-reasoning/score-comparison-notices.ts +115 -0
- package/src/chess-reasoning/server-simple-policy.ts +75 -0
- package/src/chess-reasoning/simple-agent-config.ts +58 -0
- package/src/chess-reasoning/simple-board-analysis.ts +57 -0
- package/src/chess-reasoning/simple-compact-evidence.ts +79 -0
- package/src/chess-reasoning/simple-completion.ts +37 -0
- package/src/chess-reasoning/simple-engine-presentation.ts +95 -0
- package/src/chess-reasoning/simple-evidence-preparation.ts +32 -0
- package/src/chess-reasoning/simple-evidence-view.ts +69 -0
- package/src/chess-reasoning/simple-explanation-coverage.ts +118 -0
- package/src/chess-reasoning/simple-fact-delivery.ts +52 -0
- package/src/chess-reasoning/simple-fallback-premise.ts +65 -0
- package/src/chess-reasoning/simple-prose-references.ts +528 -0
- package/src/chess-reasoning/simple-runtime-version.ts +2 -0
- package/src/chess-reasoning/stable.ts +18 -0
- package/src/chess-reasoning/tool-input-loop-guard.ts +38 -0
- package/src/chess-reasoning/types.ts +195 -0
- package/src/chess-reasoning/visible-support-lines.ts +44 -0
- package/src/chess-reasoning/wikibooks-index.ts +56 -0
- package/src/chess-reasoning/wikibooks-tool-context.ts +91 -0
- package/src/player-perspective.ts +41 -0
- package/src/shared-exports.ts +23 -0
- package/test/answer-pipeline.test.mjs +375 -0
- package/test/owner/chess-reasoning-compact-search-preparation.test.mjs +111 -0
- package/test/owner/mechanism-encoding.test.mjs +78 -0
- package/test/owner/move-mechanism-facts.test.mjs +105 -0
- package/test/owner/pipeline-host-preparation.test.mjs +115 -0
- package/test/owner/position-preparation.test.mjs +22 -0
- package/test/owner/preparation-consolidation.test.mjs +74 -0
- package/test/owner/prose-reference-admission.test.mjs +26 -0
- package/test/owner/score-comparison-notices.test.mjs +116 -0
- package/test/owner/simple-engine-presentation.test.mjs +50 -0
- package/test/owner/tool-input-loop-guard.test.mjs +25 -0
- package/test-types.mts +23 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,1602 @@
|
|
|
1
|
+
import { Chess, SQUARES, type Color, type Move, type PieceSymbol, type Square } from "chess.js";
|
|
2
|
+
import { PARTICIPANT_ROLES, REFERENCE_DOMAINS } from "./concept-registry";
|
|
3
|
+
import {
|
|
4
|
+
CHESS_FACT_RESULT_VERSION,
|
|
5
|
+
CHESS_FACT_SCHEMA_VERSION,
|
|
6
|
+
CHESS_FACT_REQUEST_VERSION,
|
|
7
|
+
validateChessFactRecord,
|
|
8
|
+
validateChessFactRequest,
|
|
9
|
+
type ChessFactAuthority,
|
|
10
|
+
type ChessFactCapability,
|
|
11
|
+
type ChessFactCoverage,
|
|
12
|
+
type ChessFactPhase,
|
|
13
|
+
type ChessFactProvider,
|
|
14
|
+
type ChessFactRecord,
|
|
15
|
+
type ChessFactRequest,
|
|
16
|
+
type ChessFactResult,
|
|
17
|
+
} from "./fact-provider";
|
|
18
|
+
import { reasoningContextHash, validateReasoningPositionContext, type ReasoningPositionContext } from "./poc-contracts";
|
|
19
|
+
import { createPocIdentityTimeline, pieceIdAt } from "./poc-piece-refs";
|
|
20
|
+
import { applyIdentityMove } from "./piece-identities";
|
|
21
|
+
import { exchangeRayAlignmentWitnesses } from "./mechanical-ray-facts";
|
|
22
|
+
import { stableHash } from "./stable";
|
|
23
|
+
import type { IdentityState, RegistryKey, SemanticParticipant, SemanticValue } from "./types";
|
|
24
|
+
|
|
25
|
+
export const MECHANICAL_CHESS_FACT_PROVIDER_ID = "knights-eye/mechanical-chess-facts" as const;
|
|
26
|
+
export const MECHANICAL_CHESS_FACT_PROVIDER_VERSION = "mechanical-chess-facts/18" as const;
|
|
27
|
+
|
|
28
|
+
const MAX_FACT_SUMMARY_CHARACTERS = 1_000;
|
|
29
|
+
|
|
30
|
+
const PIECE_NAMES: Record<PieceSymbol, string> = {
|
|
31
|
+
p: "pawn",
|
|
32
|
+
n: "knight",
|
|
33
|
+
b: "bishop",
|
|
34
|
+
r: "rook",
|
|
35
|
+
q: "queen",
|
|
36
|
+
k: "king",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const NOMINAL_VALUES: Record<PieceSymbol, number> = { p: 1, n: 3, b: 3, r: 5, q: 9, k: 0 };
|
|
40
|
+
|
|
41
|
+
const CAPABILITIES: readonly ChessFactCapability[] = Object.freeze([
|
|
42
|
+
{ kind: "castling.king_rook_transition", phases: ["transition"], authorities: ["legal-exact"], coverageDomains: ["target-move"], expandable: true, limitations: [] },
|
|
43
|
+
{ kind: "king.check_state", phases: ["before", "after"], authorities: ["legal-exact"], coverageDomains: ["side-to-move-check-state"], expandable: true, limitations: [] },
|
|
44
|
+
{ kind: "king.checkers_after", phases: ["after"], authorities: ["legal-exact"], coverageDomains: ["checking-pieces-after"], expandable: true, limitations: [] },
|
|
45
|
+
{ kind: "king.legal_moves", phases: ["after"], authorities: ["legal-exact"], coverageDomains: ["legal-king-moves-after"], expandable: true, limitations: ["This is exhaustive for king moves, not for every legal response."] },
|
|
46
|
+
{ kind: "king.geometry_after", phases: ["after"], authorities: ["legal-exact"], coverageDomains: ["king-square-geometry-after"], expandable: true, limitations: ["Distance and alignment are exact geometry, not an opposition or zugzwang label."] },
|
|
47
|
+
{ kind: "king_rook.castling_history", phases: ["path"], authorities: ["legal-exact"], coverageDomains: ["prior-castling-events"], expandable: true, limitations: ["A prior castling event does not by itself establish current king safety or the purpose of the target move."] },
|
|
48
|
+
{ kind: "line.occupied_ray_alignment_after_exchange", phases: ["path"], authorities: ["legal-exact"], coverageDomains: ["new-opposing-slider-ray-alignments-with-higher-value-rear-piece-after-one-legal-exchange-witness"], expandable: true, limitations: ["An exact newly created occupied ray does not by itself establish a pin, tactic, forced continuation, or favorable exchange."] },
|
|
49
|
+
{ kind: "material.inventory", phases: ["after"], authorities: ["legal-exact"], coverageDomains: ["after-position-piece-inventory"], expandable: true, limitations: [] },
|
|
50
|
+
{ kind: "material.nominal_delta", phases: ["transition"], authorities: ["deterministic-derived"], coverageDomains: ["target-move-material"], expandable: true, limitations: ["Nominal values are arithmetic, not an engine evaluation or exchange proof."] },
|
|
51
|
+
{ kind: "move.blocks_check_line", phases: ["transition"], authorities: ["legal-exact"], coverageDomains: ["target-move-check-resolution"], expandable: true, limitations: [] },
|
|
52
|
+
{ kind: "move.capture_check_delta", phases: ["transition"], authorities: ["legal-exact"], coverageDomains: ["target-move-capture-check"], expandable: true, limitations: [] },
|
|
53
|
+
{ kind: "move.capture_response_availability_after", phases: ["after"], authorities: ["deterministic-derived"], coverageDomains: ["target-capture-pre-capture-geometric-defenders-capturing-side-blocked-slider-rays-and-legal-same-square-recaptures-after"], expandable: true, limitations: ["This exhaustively records immediate legal same-square recaptures of an admitted target capture, pre-capture geometric defenders with none, and aligned sliders of the capturing side whose occupied ray prevents that capture. It is not SEE, a safety claim, or a conclusion about exchange quality, intent, or strategic relevance."] },
|
|
54
|
+
{ kind: "move.legal_transition", phases: ["transition"], authorities: ["legal-exact"], coverageDomains: ["target-move"], expandable: true, limitations: [] },
|
|
55
|
+
{ kind: "move.legal_recaptures_after", phases: ["after"], authorities: ["deterministic-derived"], coverageDomains: ["legal-captures-of-target-mover-after"], expandable: true, limitations: ["Immediate same-square replies and nominal material arithmetic are not SEE, engine evaluation, or proof of longer-term exchange soundness."] },
|
|
56
|
+
{ kind: "move.unblocks_line", phases: ["transition"], authorities: ["legal-exact"], coverageDomains: ["target-move-slider-lines"], expandable: true, limitations: [] },
|
|
57
|
+
{ kind: "piece.absolute_pin", phases: ["after"], authorities: ["legal-exact"], coverageDomains: ["side-to-move-absolute-pins-after"], expandable: true, limitations: ["Pins are emitted only when the side to move is not already in check."] },
|
|
58
|
+
{ kind: "piece.attack_delta", phases: ["transition"], authorities: ["deterministic-derived"], coverageDomains: ["moved-piece-geometric-attacks"], expandable: true, limitations: ["Attack geometry is not exchange safety or a claim that pressure is decisive."] },
|
|
59
|
+
{ kind: "piece.legal_moves", phases: ["after"], authorities: ["legal-exact"], coverageDomains: ["legal-moves-from-attacked-piece"], expandable: true, limitations: ["Emitted for occupied opponent pieces newly attacked by the target move."] },
|
|
60
|
+
{ kind: "pawn.advance_state", phases: ["transition"], authorities: ["deterministic-derived"], coverageDomains: ["target-pawn-advance"], expandable: true, limitations: ["Forward and diagonal square geometry does not prove that a future pawn move will remain legal after the opponent replies; passed-pawn geometry and distance do not prove promotion or a winning result."] },
|
|
61
|
+
{ kind: "position.legal_moves", phases: ["after"], authorities: ["legal-exact"], coverageDomains: ["all-legal-moves-after"], expandable: true, limitations: [] },
|
|
62
|
+
{ kind: "position.legal_move_count", phases: ["after"], authorities: ["legal-exact"], coverageDomains: ["all-legal-moves-after"], expandable: true, limitations: [] },
|
|
63
|
+
{ kind: "square.friendly_nonpawn_reachers_after", phases: ["after"], authorities: ["deterministic-derived"], coverageDomains: ["target-destination-friendly-nonpawn-reachers"], expandable: true, limitations: ["Movement geometry plus friendly occupancy does not establish that the occupied square is strategically desirable for the other piece."] },
|
|
64
|
+
{ kind: "square.target_destination_attackers_after", phases: ["after"], authorities: ["deterministic-derived"], coverageDomains: ["target-move-destination-attack-map-after"], expandable: true, limitations: ["Complete post-move destination attack map, including pawns and pinned pieces; not safety, SEE, or strategy."] },
|
|
65
|
+
{ kind: "square.same_square_exchange_witnesses", phases: ["path"], authorities: ["deterministic-derived"], coverageDomains: ["changed-square-exchange-witnesses"], expandable: true, limitations: ["These are legal same-square capture witnesses with nominal material arithmetic, not SEE, forced, best, or exhaustive over moves away from the square."] },
|
|
66
|
+
{ kind: "square.attacker_delta", phases: ["transition"], authorities: ["deterministic-derived"], coverageDomains: ["single-square-attacker-set-transition"], expandable: true, limitations: ["Geometric attackers include pinned pieces and do not establish exchange safety or strategic importance. Legal entry/capture witnesses establish only the recorded replay, not whether the entry is strategically desirable."] },
|
|
67
|
+
{ kind: "square.attackers_delta_inventory", phases: ["transition"], authorities: ["deterministic-derived"], coverageDomains: ["all-square-attacker-sets-transition"], expandable: true, limitations: ["Panoramic inventory; use the per-square records for concise model presentation."] },
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
type CandidateRecord = ChessFactRecord & { priority: number; budgetProtected?: boolean };
|
|
71
|
+
|
|
72
|
+
/** A replayable immediate-capture response fact in the ordinary fact contract. */
|
|
73
|
+
export type AdmittedCaptureResponseAvailabilityFact = ChessFactRecord;
|
|
74
|
+
|
|
75
|
+
export type AdmittedCaptureResponseAvailabilityInput = {
|
|
76
|
+
/** The target-move context that owns stable identities and the record hash. */
|
|
77
|
+
context: ReasoningPositionContext;
|
|
78
|
+
/** Must be the hash of `context`; callers cannot attach a path fact elsewhere. */
|
|
79
|
+
contextHash: string;
|
|
80
|
+
/** Which exact target-context phase the supplied line starts from. */
|
|
81
|
+
start: "before" | "after";
|
|
82
|
+
/** An admitted, fully declared line whose final move must be a legal capture. */
|
|
83
|
+
movesUci: readonly string[];
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function bytes(value: unknown) {
|
|
87
|
+
return new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function opposite(color: Color): Color {
|
|
91
|
+
return color === "w" ? "b" : "w";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function colorName(color: Color): string {
|
|
95
|
+
return color === "w" ? "White" : "Black";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function moveUci(move: { from: string; to: string; promotion?: string }) {
|
|
99
|
+
return `${move.from}${move.to}${move.promotion || ""}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function squareCoordinates(square: Square) {
|
|
103
|
+
return { file: square.charCodeAt(0) - 97, rank: Number(square[1]) - 1 };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function squaresBetween(from: Square, to: Square): Square[] {
|
|
107
|
+
const left = squareCoordinates(from);
|
|
108
|
+
const right = squareCoordinates(to);
|
|
109
|
+
const fileDelta = right.file - left.file;
|
|
110
|
+
const rankDelta = right.rank - left.rank;
|
|
111
|
+
const aligned = fileDelta === 0 || rankDelta === 0 || Math.abs(fileDelta) === Math.abs(rankDelta);
|
|
112
|
+
if (!aligned) return [];
|
|
113
|
+
const fileStep = Math.sign(fileDelta);
|
|
114
|
+
const rankStep = Math.sign(rankDelta);
|
|
115
|
+
const squares: Square[] = [];
|
|
116
|
+
let file = left.file + fileStep;
|
|
117
|
+
let rank = left.rank + rankStep;
|
|
118
|
+
while (file !== right.file || rank !== right.rank) {
|
|
119
|
+
squares.push(`${String.fromCharCode(97 + file)}${rank + 1}` as Square);
|
|
120
|
+
file += fileStep;
|
|
121
|
+
rank += rankStep;
|
|
122
|
+
}
|
|
123
|
+
return squares;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function attackedSquares(board: Chess, origin: Square, color: Color): Square[] {
|
|
127
|
+
return SQUARES.filter((square) => board.attackers(square, color).includes(origin));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function stableAttackers(board: Chess, state: IdentityState, square: Square, color: Color) {
|
|
131
|
+
return board.attackers(square, color).map((origin) => ({
|
|
132
|
+
pieceId: pieceIdAt(state, origin),
|
|
133
|
+
currentSquare: origin,
|
|
134
|
+
pieceType: board.get(origin)?.type || null,
|
|
135
|
+
})).filter((item): item is { pieceId: string; currentSquare: Square; pieceType: PieceSymbol } => Boolean(item.pieceId && item.pieceType));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function sliderReachesSquare(pieceType: PieceSymbol, from: Square, to: Square) {
|
|
139
|
+
const left = squareCoordinates(from);
|
|
140
|
+
const right = squareCoordinates(to);
|
|
141
|
+
const fileDistance = Math.abs(right.file - left.file);
|
|
142
|
+
const rankDistance = Math.abs(right.rank - left.rank);
|
|
143
|
+
if (fileDistance === 0 && rankDistance === 0) return false;
|
|
144
|
+
const orthogonal = fileDistance === 0 || rankDistance === 0;
|
|
145
|
+
const diagonal = fileDistance === rankDistance;
|
|
146
|
+
return pieceType === "r" ? orthogonal : pieceType === "b" ? diagonal : pieceType === "q" ? orthogonal || diagonal : false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function blockedSliderRecaptureRays(
|
|
150
|
+
board: Chess,
|
|
151
|
+
state: IdentityState,
|
|
152
|
+
targetSquare: Square,
|
|
153
|
+
color: Color,
|
|
154
|
+
) {
|
|
155
|
+
return SQUARES.flatMap((sliderSquare) => {
|
|
156
|
+
const slider = board.get(sliderSquare);
|
|
157
|
+
if (!slider || slider.color !== color || !sliderReachesSquare(slider.type, sliderSquare, targetSquare)) return [];
|
|
158
|
+
const between = squaresBetween(sliderSquare, targetSquare);
|
|
159
|
+
const occupiedBlockingSquares = between.flatMap((square) => {
|
|
160
|
+
const blocker = board.get(square);
|
|
161
|
+
const pieceId = pieceIdAt(state, square);
|
|
162
|
+
return blocker && pieceId ? [{
|
|
163
|
+
pieceId,
|
|
164
|
+
currentSquare: square,
|
|
165
|
+
pieceType: blocker.type,
|
|
166
|
+
color: blocker.color,
|
|
167
|
+
}] : [];
|
|
168
|
+
});
|
|
169
|
+
const pieceId = pieceIdAt(state, sliderSquare);
|
|
170
|
+
if (!pieceId || !occupiedBlockingSquares.length) return [];
|
|
171
|
+
return [{
|
|
172
|
+
pieceId,
|
|
173
|
+
currentSquare: sliderSquare,
|
|
174
|
+
pieceType: slider.type,
|
|
175
|
+
ray: [sliderSquare, ...between, targetSquare],
|
|
176
|
+
occupiedBlockingSquares,
|
|
177
|
+
}];
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function attackerSetChanges(
|
|
182
|
+
before: Chess,
|
|
183
|
+
after: Chess,
|
|
184
|
+
timeline: ReturnType<typeof createPocIdentityTimeline>,
|
|
185
|
+
) {
|
|
186
|
+
return (["w", "b"] as Color[]).flatMap((color) => SQUARES.flatMap((square) => {
|
|
187
|
+
const beforeAttackers = stableAttackers(before, timeline.before, square, color);
|
|
188
|
+
const afterAttackers = stableAttackers(after, timeline.after, square, color);
|
|
189
|
+
const beforeIds = new Set(beforeAttackers.map((item) => item.pieceId));
|
|
190
|
+
const afterIds = new Set(afterAttackers.map((item) => item.pieceId));
|
|
191
|
+
const lost = beforeAttackers.filter((item) => !afterIds.has(item.pieceId));
|
|
192
|
+
const gained = afterAttackers.filter((item) => !beforeIds.has(item.pieceId));
|
|
193
|
+
if (!lost.length && !gained.length) return [];
|
|
194
|
+
return [{ square, color, beforeAttackers, afterAttackers, lost, gained }];
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function inventory(board: Chess, state: IdentityState) {
|
|
199
|
+
return board.board().flat().filter(Boolean).map((piece) => ({
|
|
200
|
+
pieceId: pieceIdAt(state, piece!.square),
|
|
201
|
+
color: piece!.color,
|
|
202
|
+
type: piece!.type,
|
|
203
|
+
square: piece!.square,
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function nominalMaterial(board: Chess, color: Color) {
|
|
208
|
+
return board.board().flat().filter((piece) => piece?.color === color).reduce((total, piece) => total + NOMINAL_VALUES[piece!.type], 0);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function nominalMaterialBalance(board: Chess, color: Color) {
|
|
212
|
+
return nominalMaterial(board, color) - nominalMaterial(board, opposite(color));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function applyVerboseMove(board: Chess, move: Pick<Move, "from" | "to" | "promotion">) {
|
|
216
|
+
return board.move({ from: move.from, to: move.to, promotion: move.promotion });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function immediateCaptureOutcomes(fenAfter: string, captures: Move[]) {
|
|
220
|
+
const baseline = new Chess(fenAfter);
|
|
221
|
+
return captures.map((capture) => {
|
|
222
|
+
const afterCapture = new Chess(fenAfter);
|
|
223
|
+
applyVerboseMove(afterCapture, capture);
|
|
224
|
+
const baselineBalance = nominalMaterialBalance(baseline, capture.color);
|
|
225
|
+
const immediateSameSquareRecaptures = afterCapture.moves({ verbose: true })
|
|
226
|
+
.filter((reply) => reply.to === capture.to && Boolean(reply.captured))
|
|
227
|
+
.map((reply) => {
|
|
228
|
+
const afterReply = new Chess(afterCapture.fen());
|
|
229
|
+
applyVerboseMove(afterReply, reply);
|
|
230
|
+
return {
|
|
231
|
+
uci: moveUci(reply),
|
|
232
|
+
san: reply.san,
|
|
233
|
+
nominalBalanceDeltaForCapturingSide: nominalMaterialBalance(afterReply, capture.color) - baselineBalance,
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
return {
|
|
237
|
+
capture: { uci: moveUci(capture), san: capture.san },
|
|
238
|
+
capturingColor: capture.color,
|
|
239
|
+
nominalBalanceDeltaAfterCapture: nominalMaterialBalance(afterCapture, capture.color) - baselineBalance,
|
|
240
|
+
immediateSameSquareRecaptures,
|
|
241
|
+
minimumNominalBalanceDeltaAfterImmediateSameSquareRecapture: immediateSameSquareRecaptures.length
|
|
242
|
+
? Math.min(...immediateSameSquareRecaptures.map((reply) => reply.nominalBalanceDeltaForCapturingSide))
|
|
243
|
+
: null,
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function pseudoCaptureKingExposure(
|
|
249
|
+
board: Chess,
|
|
250
|
+
from: Square,
|
|
251
|
+
to: Square,
|
|
252
|
+
color: Color,
|
|
253
|
+
) {
|
|
254
|
+
const defender = board.get(from);
|
|
255
|
+
if (!defender || defender.color !== color) return null;
|
|
256
|
+
const replay = new Chess(board.fen());
|
|
257
|
+
replay.remove(from);
|
|
258
|
+
replay.remove(to);
|
|
259
|
+
replay.put({ type: defender.type, color: defender.color }, to);
|
|
260
|
+
const kingSquare = replay.findPiece({ type: "k", color })[0];
|
|
261
|
+
if (!kingSquare) return null;
|
|
262
|
+
const checkerSquares = replay.attackers(kingSquare, opposite(color));
|
|
263
|
+
if (!checkerSquares.length) return null;
|
|
264
|
+
return {
|
|
265
|
+
kind: "own-king-exposure",
|
|
266
|
+
kingSquare,
|
|
267
|
+
checkerSquares,
|
|
268
|
+
checkingLines: checkerSquares.map((checkerSquare) => ({
|
|
269
|
+
checkerSquare,
|
|
270
|
+
ray: [checkerSquare, ...squaresBetween(checkerSquare, kingSquare), kingSquare],
|
|
271
|
+
})),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function absolutePinRayConstraints(
|
|
276
|
+
board: Chess,
|
|
277
|
+
defenderSquare: Square,
|
|
278
|
+
color: Color,
|
|
279
|
+
) {
|
|
280
|
+
if (board.isCheck()) return [];
|
|
281
|
+
const kingSquare = board.findPiece({ type: "k", color })[0];
|
|
282
|
+
if (!kingSquare) return [];
|
|
283
|
+
const removed = new Chess(board.fen());
|
|
284
|
+
removed.remove(defenderSquare);
|
|
285
|
+
return removed.attackers(kingSquare, opposite(color)).flatMap((pinnerSquare) => {
|
|
286
|
+
const pinner = removed.get(pinnerSquare);
|
|
287
|
+
if (!pinner || !new Set<PieceSymbol>(["b", "r", "q"]).has(pinner.type)
|
|
288
|
+
|| !squaresBetween(pinnerSquare, kingSquare).includes(defenderSquare)) return [];
|
|
289
|
+
return [{
|
|
290
|
+
kind: "absolute-pin-ray",
|
|
291
|
+
pinnerSquare,
|
|
292
|
+
pinnerType: pinner.type,
|
|
293
|
+
kingSquare,
|
|
294
|
+
ray: [pinnerSquare, ...squaresBetween(pinnerSquare, kingSquare), kingSquare],
|
|
295
|
+
}];
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function sameSquareLineOutcomes(fenAfter: string, lines: ReturnType<typeof sameSquareExchangeWitnesses>) {
|
|
300
|
+
return lines.map((line) => {
|
|
301
|
+
const baseline = new Chess(fenAfter);
|
|
302
|
+
const board = new Chess(fenAfter);
|
|
303
|
+
let firstCapturingColor: Color | null = null;
|
|
304
|
+
let baselineBalance = 0;
|
|
305
|
+
const nominalBalanceDeltasAfterEachPly: number[] = [];
|
|
306
|
+
for (const item of line) {
|
|
307
|
+
const legal = board.moves({ verbose: true }).find((move) => moveUci(move) === item.uci);
|
|
308
|
+
if (!legal) throw new Error(`Same-square witness contains an illegal move: ${item.uci}`);
|
|
309
|
+
if (!firstCapturingColor && legal.captured) {
|
|
310
|
+
firstCapturingColor = legal.color;
|
|
311
|
+
baselineBalance = nominalMaterialBalance(baseline, legal.color);
|
|
312
|
+
}
|
|
313
|
+
applyVerboseMove(board, legal);
|
|
314
|
+
nominalBalanceDeltasAfterEachPly.push(firstCapturingColor
|
|
315
|
+
? nominalMaterialBalance(board, firstCapturingColor) - baselineBalance
|
|
316
|
+
: 0);
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
movesUci: line.map((item) => item.uci),
|
|
320
|
+
firstCapturingColor,
|
|
321
|
+
nominalBalanceDeltasAfterEachPly,
|
|
322
|
+
finalNominalBalanceDeltaForFirstCapturingSide: firstCapturingColor
|
|
323
|
+
? nominalMaterialBalance(board, firstCapturingColor) - baselineBalance
|
|
324
|
+
: null,
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function kingDistance(left: Square, right: Square) {
|
|
330
|
+
const a = squareCoordinates(left);
|
|
331
|
+
const b = squareCoordinates(right);
|
|
332
|
+
return {
|
|
333
|
+
file: Math.abs(a.file - b.file),
|
|
334
|
+
rank: Math.abs(a.rank - b.rank),
|
|
335
|
+
chebyshev: Math.max(Math.abs(a.file - b.file), Math.abs(a.rank - b.rank)),
|
|
336
|
+
manhattan: Math.abs(a.file - b.file) + Math.abs(a.rank - b.rank),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function isPassedPawn(board: Chess, square: Square, color: Color) {
|
|
341
|
+
const location = squareCoordinates(square);
|
|
342
|
+
const opponent = opposite(color);
|
|
343
|
+
const blockers = board.findPiece({ type: "p", color: opponent }).filter((candidate) => {
|
|
344
|
+
const target = squareCoordinates(candidate);
|
|
345
|
+
const ahead = color === "w" ? target.rank > location.rank : target.rank < location.rank;
|
|
346
|
+
return ahead && Math.abs(target.file - location.file) <= 1;
|
|
347
|
+
});
|
|
348
|
+
return { passed: blockers.length === 0, opposingPawnBlockers: blockers };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function priorCastlingEvents(context: ReasoningPositionContext) {
|
|
352
|
+
const board = context.historyStartFen ? new Chess(context.historyStartFen) : new Chess();
|
|
353
|
+
return context.historyUci.flatMap((uci, plyIndex) => {
|
|
354
|
+
const move = board.move({ from: uci.slice(0, 2), to: uci.slice(2, 4), promotion: uci.slice(4) || undefined });
|
|
355
|
+
if (!move || (!move.isKingsideCastle() && !move.isQueensideCastle())) return [];
|
|
356
|
+
const rank = move.color === "w" ? "1" : "8";
|
|
357
|
+
return [{
|
|
358
|
+
plyIndex,
|
|
359
|
+
color: move.color,
|
|
360
|
+
uci,
|
|
361
|
+
san: move.san,
|
|
362
|
+
king: { from: move.from, to: move.to },
|
|
363
|
+
rook: {
|
|
364
|
+
from: `${move.isKingsideCastle() ? "h" : "a"}${rank}`,
|
|
365
|
+
to: `${move.isKingsideCastle() ? "f" : "d"}${rank}`,
|
|
366
|
+
},
|
|
367
|
+
}];
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function sameSquareExchangeWitnesses(fenAfter: string, square: Square) {
|
|
372
|
+
const root = new Chess(fenAfter);
|
|
373
|
+
const entries = root.moves({ verbose: true }).filter((move) => move.to === square);
|
|
374
|
+
return entries.flatMap((entry) => {
|
|
375
|
+
const afterEntry = new Chess(fenAfter);
|
|
376
|
+
afterEntry.move({ from: entry.from, to: entry.to, promotion: entry.promotion });
|
|
377
|
+
const captures = afterEntry.moves({ verbose: true }).filter((move) => move.to === square && Boolean(move.captured));
|
|
378
|
+
return captures.flatMap((capture) => {
|
|
379
|
+
const afterCapture = new Chess(afterEntry.fen());
|
|
380
|
+
afterCapture.move({ from: capture.from, to: capture.to, promotion: capture.promotion });
|
|
381
|
+
const recaptures = afterCapture.moves({ verbose: true }).filter((move) => move.to === square && Boolean(move.captured));
|
|
382
|
+
const prefix = [
|
|
383
|
+
{ uci: moveUci(entry), san: entry.san },
|
|
384
|
+
{ uci: moveUci(capture), san: capture.san },
|
|
385
|
+
];
|
|
386
|
+
return recaptures.length
|
|
387
|
+
? recaptures.map((recapture) => [...prefix, { uci: moveUci(recapture), san: recapture.san }])
|
|
388
|
+
: [prefix];
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function participantForSquare(
|
|
394
|
+
state: IdentityState,
|
|
395
|
+
square: Square,
|
|
396
|
+
role: RegistryKey,
|
|
397
|
+
): { participant: SemanticParticipant; location: Record<string, SemanticValue> } | null {
|
|
398
|
+
const pieceId = pieceIdAt(state, square);
|
|
399
|
+
if (!pieceId) return null;
|
|
400
|
+
return {
|
|
401
|
+
participant: { role, ref: { domain: REFERENCE_DOMAINS.PIECE, id: pieceId } },
|
|
402
|
+
location: { pieceId, currentSquare: square },
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function targetMoveParticipants(
|
|
407
|
+
timeline: ReturnType<typeof createPocIdentityTimeline>,
|
|
408
|
+
from: Square,
|
|
409
|
+
to: Square,
|
|
410
|
+
uci: string,
|
|
411
|
+
): { participants: SemanticParticipant[]; locations: Record<string, SemanticValue>[]; afterParticipants: SemanticParticipant[] } {
|
|
412
|
+
const actorBefore = participantForSquare(timeline.before, from, PARTICIPANT_ROLES.ACTOR);
|
|
413
|
+
const actorAfter = participantForSquare(timeline.after, to, PARTICIPANT_ROLES.ACTOR);
|
|
414
|
+
const moveParticipant = { role: PARTICIPANT_ROLES.ACTOR, ref: { domain: REFERENCE_DOMAINS.MOVE, id: uci } };
|
|
415
|
+
const originParticipant = { role: "origin", ref: { domain: REFERENCE_DOMAINS.SQUARE, id: from } };
|
|
416
|
+
const destinationParticipant = { role: PARTICIPANT_ROLES.DESTINATION, ref: { domain: REFERENCE_DOMAINS.SQUARE, id: to } };
|
|
417
|
+
// Promotion retires the pawn identity and creates a distinct piece identity.
|
|
418
|
+
// Retain both ends of that transition instead of placing the retired pawn on
|
|
419
|
+
// the promotion square, and let after-only facts select the resulting piece.
|
|
420
|
+
const actorAfterAddsIdentity = actorAfter && actorAfter.participant.ref.id !== actorBefore?.participant.ref.id;
|
|
421
|
+
return {
|
|
422
|
+
participants: [
|
|
423
|
+
moveParticipant,
|
|
424
|
+
...(actorBefore ? [actorBefore.participant] : []),
|
|
425
|
+
...(actorAfterAddsIdentity ? [actorAfter.participant] : []),
|
|
426
|
+
originParticipant,
|
|
427
|
+
destinationParticipant,
|
|
428
|
+
],
|
|
429
|
+
locations: [
|
|
430
|
+
...(actorBefore ? [{ ...actorBefore.location, phase: "before" }] : []),
|
|
431
|
+
...(actorAfter ? [{ ...actorAfter.location, phase: "after" }] : []),
|
|
432
|
+
],
|
|
433
|
+
afterParticipants: [moveParticipant, ...(actorAfter ? [actorAfter.participant] : []), destinationParticipant],
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function phaseState(
|
|
438
|
+
timeline: ReturnType<typeof createPocIdentityTimeline>,
|
|
439
|
+
phase: "before" | "after",
|
|
440
|
+
) {
|
|
441
|
+
return phase === "before" ? timeline.before : timeline.after;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function createRecord(input: {
|
|
445
|
+
contextHash: string;
|
|
446
|
+
kind: string;
|
|
447
|
+
phase: ChessFactPhase;
|
|
448
|
+
authority: ChessFactAuthority;
|
|
449
|
+
priority: number;
|
|
450
|
+
/**
|
|
451
|
+
* One direct target-move fact may be retained ahead of ordinary presentation
|
|
452
|
+
* budgeting. This is deliberately limited to exact destination geometry;
|
|
453
|
+
* it prevents a fact-cap from silently removing the board state needed to
|
|
454
|
+
* inspect an immediate capture, without making any safety conclusion.
|
|
455
|
+
*/
|
|
456
|
+
budgetProtected?: boolean;
|
|
457
|
+
summary: string;
|
|
458
|
+
participants: SemanticParticipant[];
|
|
459
|
+
data: Record<string, SemanticValue>;
|
|
460
|
+
pieceLocations: Record<string, SemanticValue>[];
|
|
461
|
+
coverage: ChessFactCoverage;
|
|
462
|
+
replay: Record<string, SemanticValue> | null;
|
|
463
|
+
derivedFromIds?: string[];
|
|
464
|
+
}): CandidateRecord {
|
|
465
|
+
const summaryTruncated = input.summary.length > MAX_FACT_SUMMARY_CHARACTERS;
|
|
466
|
+
const summary = summaryTruncated
|
|
467
|
+
? `${input.summary.slice(0, MAX_FACT_SUMMARY_CHARACTERS - 1).trimEnd()}…`
|
|
468
|
+
: input.summary;
|
|
469
|
+
const data = {
|
|
470
|
+
...input.data,
|
|
471
|
+
pieceLocations: input.pieceLocations,
|
|
472
|
+
...(summaryTruncated ? { summaryTruncated: true } : {}),
|
|
473
|
+
};
|
|
474
|
+
const identity = {
|
|
475
|
+
provider: MECHANICAL_CHESS_FACT_PROVIDER_ID,
|
|
476
|
+
version: MECHANICAL_CHESS_FACT_PROVIDER_VERSION,
|
|
477
|
+
contextHash: input.contextHash,
|
|
478
|
+
kind: input.kind,
|
|
479
|
+
phase: input.phase,
|
|
480
|
+
data,
|
|
481
|
+
};
|
|
482
|
+
const record: CandidateRecord = {
|
|
483
|
+
schemaVersion: CHESS_FACT_SCHEMA_VERSION,
|
|
484
|
+
id: `fact:${stableHash(identity)}`,
|
|
485
|
+
contextHash: input.contextHash,
|
|
486
|
+
kind: input.kind,
|
|
487
|
+
phase: input.phase,
|
|
488
|
+
authority: input.authority,
|
|
489
|
+
summary,
|
|
490
|
+
participants: input.participants,
|
|
491
|
+
data,
|
|
492
|
+
coverage: input.coverage,
|
|
493
|
+
provenance: {
|
|
494
|
+
provider: MECHANICAL_CHESS_FACT_PROVIDER_ID,
|
|
495
|
+
version: MECHANICAL_CHESS_FACT_PROVIDER_VERSION,
|
|
496
|
+
method: input.authority === "legal-exact" ? "chess.js-legal-state" : "chess.js-deterministic-delta",
|
|
497
|
+
derivedFromIds: input.derivedFromIds || [],
|
|
498
|
+
},
|
|
499
|
+
replay: input.replay,
|
|
500
|
+
priority: input.priority,
|
|
501
|
+
...(input.budgetProtected ? { budgetProtected: true } : {}),
|
|
502
|
+
};
|
|
503
|
+
const validated = { ...record };
|
|
504
|
+
delete (validated as Partial<CandidateRecord>).priority;
|
|
505
|
+
delete (validated as Partial<CandidateRecord>).budgetProtected;
|
|
506
|
+
validateChessFactRecord(validated);
|
|
507
|
+
return record;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Derive the immediate legal recapture availability after a capture already
|
|
512
|
+
* admitted by a replayable line. This never discovers a continuation: the
|
|
513
|
+
* caller supplies every UCI move, and the result is rooted at the declared
|
|
514
|
+
* target-context phase. Stable identities are replayed along that exact
|
|
515
|
+
* prefix so a later capture can still name the original piece accurately.
|
|
516
|
+
*/
|
|
517
|
+
export function deriveAdmittedCaptureResponseAvailabilityFact(
|
|
518
|
+
input: AdmittedCaptureResponseAvailabilityInput,
|
|
519
|
+
): AdmittedCaptureResponseAvailabilityFact {
|
|
520
|
+
validateReasoningPositionContext(input.context);
|
|
521
|
+
const rootContextHash = reasoningContextHash(input.context);
|
|
522
|
+
if (input.contextHash !== rootContextHash) {
|
|
523
|
+
throw new Error("Admitted capture response fact contextHash does not match its root context");
|
|
524
|
+
}
|
|
525
|
+
if (input.start !== "before" && input.start !== "after") {
|
|
526
|
+
throw new Error("Admitted capture response fact start must be before or after");
|
|
527
|
+
}
|
|
528
|
+
if (!Array.isArray(input.movesUci) || input.movesUci.length === 0) {
|
|
529
|
+
throw new Error("Admitted capture response fact requires a non-empty admitted UCI line");
|
|
530
|
+
}
|
|
531
|
+
if (input.movesUci.some((uci) => !/^[a-h][1-8][a-h][1-8][qrbn]?$/.test(uci))) {
|
|
532
|
+
throw new Error("Admitted capture response fact line contains invalid UCI");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const rootTimeline = createPocIdentityTimeline(input.context);
|
|
536
|
+
const replayFen = input.start === "before" ? input.context.fenBefore : input.context.fenAfter;
|
|
537
|
+
const board = new Chess(replayFen);
|
|
538
|
+
let state = input.start === "before" ? rootTimeline.before : rootTimeline.after;
|
|
539
|
+
if (state.fen !== board.fen()) throw new Error("Admitted capture response fact identity start does not match replay phase");
|
|
540
|
+
|
|
541
|
+
let beforeCapture: Chess | null = null;
|
|
542
|
+
let beforeCaptureState: IdentityState | null = null;
|
|
543
|
+
let capture: Move | null = null;
|
|
544
|
+
for (const [index, uci] of input.movesUci.entries()) {
|
|
545
|
+
const legal = board.moves({ verbose: true }).find((move) => moveUci(move) === uci);
|
|
546
|
+
if (!legal) throw new Error(`Admitted capture response fact line is illegal at ply ${index}: ${uci}`);
|
|
547
|
+
if (index === input.movesUci.length - 1) {
|
|
548
|
+
beforeCapture = new Chess(board.fen());
|
|
549
|
+
beforeCaptureState = state;
|
|
550
|
+
capture = legal;
|
|
551
|
+
}
|
|
552
|
+
let nextState: IdentityState;
|
|
553
|
+
try {
|
|
554
|
+
nextState = applyIdentityMove(state, {
|
|
555
|
+
from: legal.from,
|
|
556
|
+
to: legal.to,
|
|
557
|
+
promotion: legal.promotion,
|
|
558
|
+
}).state;
|
|
559
|
+
} catch {
|
|
560
|
+
throw new Error(`Admitted capture response fact stable-identity replay failed at ply ${index}: ${uci}`);
|
|
561
|
+
}
|
|
562
|
+
applyVerboseMove(board, legal);
|
|
563
|
+
if (nextState.fen !== board.fen()) throw new Error(`Admitted capture response fact identity replay mismatch at ply ${index}: ${uci}`);
|
|
564
|
+
state = nextState;
|
|
565
|
+
}
|
|
566
|
+
if (!capture || !beforeCapture || !beforeCaptureState) throw new Error("Admitted capture response fact failed to replay its final move");
|
|
567
|
+
if (!capture.captured) throw new Error("Admitted capture response fact final move must be a capture");
|
|
568
|
+
|
|
569
|
+
const afterCapture = board;
|
|
570
|
+
const afterCaptureState = state;
|
|
571
|
+
const from = capture.from as Square;
|
|
572
|
+
const to = capture.to as Square;
|
|
573
|
+
const moverColor = capture.color;
|
|
574
|
+
const captureTimeline = { before: beforeCaptureState, after: afterCaptureState } as ReturnType<typeof createPocIdentityTimeline>;
|
|
575
|
+
const target = targetMoveParticipants(captureTimeline, from, to, moveUci(capture));
|
|
576
|
+
const captureSquare = (capture.isEnPassant() ? `${to[0]}${from[1]}` : to) as Square;
|
|
577
|
+
const captured = participantForSquare(beforeCaptureState, captureSquare, PARTICIPANT_ROLES.TARGET);
|
|
578
|
+
if (captured) {
|
|
579
|
+
target.participants.push(captured.participant);
|
|
580
|
+
target.locations.push({ ...captured.location, phase: "before" });
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const movedPieceAfter = afterCapture.get(to);
|
|
584
|
+
if (!movedPieceAfter) throw new Error("Admitted capture response fact final capture has no mover on its destination");
|
|
585
|
+
const legalRecaptures = afterCapture.moves({ verbose: true }).filter((reply) => (
|
|
586
|
+
reply.to === to && reply.captured === movedPieceAfter.type
|
|
587
|
+
));
|
|
588
|
+
const defenderColor = opposite(moverColor);
|
|
589
|
+
const preCaptureGeometricDefenders = stableAttackers(beforeCapture, beforeCaptureState, to, defenderColor);
|
|
590
|
+
const preCaptureBlockedSliderCapturers = blockedSliderRecaptureRays(beforeCapture, beforeCaptureState, to, moverColor);
|
|
591
|
+
const responseSquares = [
|
|
592
|
+
...preCaptureGeometricDefenders.map((defender) => defender.currentSquare),
|
|
593
|
+
...preCaptureBlockedSliderCapturers.flatMap((slider) => [
|
|
594
|
+
slider.currentSquare,
|
|
595
|
+
...slider.occupiedBlockingSquares.map((blocker) => blocker.currentSquare),
|
|
596
|
+
]),
|
|
597
|
+
].filter((square, index, all) => all.indexOf(square) === index);
|
|
598
|
+
const responseParticipants = responseSquares.flatMap((square) => {
|
|
599
|
+
const participant = participantForSquare(beforeCaptureState, square, PARTICIPANT_ROLES.RESOURCE);
|
|
600
|
+
return participant ? [{ square, participant }] : [];
|
|
601
|
+
});
|
|
602
|
+
const defenders = preCaptureGeometricDefenders.map((defender) => {
|
|
603
|
+
const legalSameSquareRecaptures = legalRecaptures.filter((reply) => (
|
|
604
|
+
pieceIdAt(afterCaptureState, reply.from as Square) === defender.pieceId
|
|
605
|
+
));
|
|
606
|
+
const ownKingExposure = pseudoCaptureKingExposure(afterCapture, defender.currentSquare, to, defenderColor);
|
|
607
|
+
const exclusionConstraints = legalSameSquareRecaptures.length
|
|
608
|
+
? []
|
|
609
|
+
: [
|
|
610
|
+
...absolutePinRayConstraints(afterCapture, defender.currentSquare, defenderColor),
|
|
611
|
+
...(ownKingExposure ? [ownKingExposure] : []),
|
|
612
|
+
];
|
|
613
|
+
return {
|
|
614
|
+
...defender,
|
|
615
|
+
legalSameSquareRecaptures: legalSameSquareRecaptures.map((reply) => ({
|
|
616
|
+
uci: moveUci(reply),
|
|
617
|
+
san: reply.san,
|
|
618
|
+
from: reply.from,
|
|
619
|
+
to: reply.to,
|
|
620
|
+
})),
|
|
621
|
+
exclusionConstraints,
|
|
622
|
+
};
|
|
623
|
+
});
|
|
624
|
+
const unavailableGeometricDefenders = defenders.filter((defender) => defender.legalSameSquareRecaptures.length === 0);
|
|
625
|
+
const responsePieceLocations: Record<string, SemanticValue>[] = responseParticipants.flatMap(({ square, participant }) => [
|
|
626
|
+
{ pieceId: participant.participant.ref.id, currentSquare: square, phase: "before" },
|
|
627
|
+
{ pieceId: participant.participant.ref.id, currentSquare: square, phase: "after" },
|
|
628
|
+
]);
|
|
629
|
+
const responseLocations = [...target.locations, ...responsePieceLocations]
|
|
630
|
+
.filter((item, index, all) => all.findIndex((candidate) => candidate.pieceId === item.pieceId && candidate.currentSquare === item.currentSquare && candidate.phase === item.phase) === index);
|
|
631
|
+
const isDirectTargetCapture = input.start === "before"
|
|
632
|
+
&& input.movesUci.length === 1
|
|
633
|
+
&& input.movesUci[0] === input.context.moveUci;
|
|
634
|
+
const record = createRecord({
|
|
635
|
+
contextHash: input.contextHash,
|
|
636
|
+
kind: "move.capture_response_availability_after",
|
|
637
|
+
phase: "after",
|
|
638
|
+
authority: "deterministic-derived",
|
|
639
|
+
priority: unavailableGeometricDefenders.length || preCaptureBlockedSliderCapturers.length ? 95 : legalRecaptures.length ? 84 : 72,
|
|
640
|
+
summary: `After the admitted capture ${capture.san} on ${to}, legal same-square recaptures are ${legalRecaptures.map((reply) => reply.san).join(", ") || "none"}. Pre-capture geometric defenders of ${to} with no legal same-square recapture are ${unavailableGeometricDefenders.map((defender) => defender.currentSquare).join(", ") || "none"}. Pre-capture aligned sliders of the capturing side blocked by occupied rays are ${preCaptureBlockedSliderCapturers.map((slider) => slider.currentSquare).join(", ") || "none"}. This records legal availability and exact ray geometry only; it does not establish safety, exchange quality, intent, or strategic relevance.`,
|
|
641
|
+
participants: [...target.participants, ...responseParticipants.map((item) => item.participant.participant)]
|
|
642
|
+
.filter((item, index, all) => all.findIndex((candidate) => candidate.role === item.role && candidate.ref.domain === item.ref.domain && candidate.ref.id === item.ref.id) === index),
|
|
643
|
+
data: {
|
|
644
|
+
capture: {
|
|
645
|
+
uci: moveUci(capture),
|
|
646
|
+
san: capture.san,
|
|
647
|
+
from,
|
|
648
|
+
to,
|
|
649
|
+
capturedSquare: captureSquare,
|
|
650
|
+
capturedType: capture.captured,
|
|
651
|
+
},
|
|
652
|
+
defenderColor,
|
|
653
|
+
preCaptureGeometricDefenders: defenders,
|
|
654
|
+
preCaptureBlockedSliderCapturers,
|
|
655
|
+
legalSameSquareRecaptures: legalRecaptures.map((reply) => ({
|
|
656
|
+
uci: moveUci(reply),
|
|
657
|
+
san: reply.san,
|
|
658
|
+
from: reply.from,
|
|
659
|
+
to: reply.to,
|
|
660
|
+
moverPieceId: pieceIdAt(afterCaptureState, reply.from as Square),
|
|
661
|
+
})),
|
|
662
|
+
unavailableGeometricDefenderPieceIds: unavailableGeometricDefenders.map((defender) => defender.pieceId),
|
|
663
|
+
...(isDirectTargetCapture ? {} : { admittedReplay: { start: input.start, movesUci: [...input.movesUci] } }),
|
|
664
|
+
},
|
|
665
|
+
pieceLocations: responseLocations,
|
|
666
|
+
coverage: {
|
|
667
|
+
domain: "target-capture-pre-capture-geometric-defenders-capturing-side-blocked-slider-rays-and-legal-same-square-recaptures-after",
|
|
668
|
+
exhaustive: true,
|
|
669
|
+
covered: [
|
|
670
|
+
moveUci(capture),
|
|
671
|
+
...preCaptureGeometricDefenders.map((defender) => defender.pieceId),
|
|
672
|
+
...preCaptureBlockedSliderCapturers.flatMap((slider) => [slider.pieceId, ...slider.occupiedBlockingSquares.map((blocker) => blocker.pieceId)]),
|
|
673
|
+
...legalRecaptures.map(moveUci),
|
|
674
|
+
],
|
|
675
|
+
unresolved: [],
|
|
676
|
+
},
|
|
677
|
+
replay: {
|
|
678
|
+
fen: replayFen,
|
|
679
|
+
movesUci: [...input.movesUci],
|
|
680
|
+
linesUci: legalRecaptures.length
|
|
681
|
+
? legalRecaptures.map((reply) => [...input.movesUci, moveUci(reply)])
|
|
682
|
+
: [[...input.movesUci]],
|
|
683
|
+
},
|
|
684
|
+
});
|
|
685
|
+
const fact = { ...record };
|
|
686
|
+
Reflect.deleteProperty(fact, "priority");
|
|
687
|
+
Reflect.deleteProperty(fact, "budgetProtected");
|
|
688
|
+
return fact;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function checkRecord(input: {
|
|
692
|
+
board: Chess;
|
|
693
|
+
phase: "before" | "after";
|
|
694
|
+
timeline: ReturnType<typeof createPocIdentityTimeline>;
|
|
695
|
+
contextHash: string;
|
|
696
|
+
fen: string;
|
|
697
|
+
}): CandidateRecord {
|
|
698
|
+
const color = input.board.turn();
|
|
699
|
+
const kingSquare = input.board.findPiece({ type: "k", color })[0];
|
|
700
|
+
if (!kingSquare) throw new Error("Mechanical fact provider requires both kings");
|
|
701
|
+
const checkerSquares = input.board.attackers(kingSquare, opposite(color));
|
|
702
|
+
const state = phaseState(input.timeline, input.phase);
|
|
703
|
+
const king = participantForSquare(state, kingSquare, PARTICIPANT_ROLES.TARGET);
|
|
704
|
+
const checkers = checkerSquares.flatMap((square) => {
|
|
705
|
+
const participant = participantForSquare(state, square, PARTICIPANT_ROLES.ACTOR);
|
|
706
|
+
return participant ? [participant] : [];
|
|
707
|
+
});
|
|
708
|
+
return createRecord({
|
|
709
|
+
contextHash: input.contextHash,
|
|
710
|
+
kind: "king.check_state",
|
|
711
|
+
phase: input.phase,
|
|
712
|
+
authority: "legal-exact",
|
|
713
|
+
priority: input.phase === "before" && checkerSquares.length ? 99 : input.phase === "after" && checkerSquares.length ? 97 : 52,
|
|
714
|
+
summary: checkerSquares.length
|
|
715
|
+
? `${color === "w" ? "White" : "Black"} is in check; the checking piece${checkerSquares.length === 1 ? " is" : "s are"} on ${checkerSquares.join(", ")}.`
|
|
716
|
+
: `${color === "w" ? "White" : "Black"}, the side to move, is not in check.`,
|
|
717
|
+
participants: [...(king ? [king.participant] : []), ...checkers.map((item) => item.participant)],
|
|
718
|
+
data: {
|
|
719
|
+
sideToMove: color,
|
|
720
|
+
kingSquare,
|
|
721
|
+
inCheck: checkerSquares.length > 0,
|
|
722
|
+
checkerSquares,
|
|
723
|
+
checkingLines: checkerSquares.map((square) => ({ checker: square, between: squaresBetween(square, kingSquare) })),
|
|
724
|
+
},
|
|
725
|
+
pieceLocations: [...(king ? [{ ...king.location, phase: input.phase }] : []), ...checkers.map((item) => ({ ...item.location, phase: input.phase }))],
|
|
726
|
+
coverage: { domain: `${input.phase}-side-to-move-check-state`, exhaustive: true, covered: [kingSquare, ...checkerSquares], unresolved: [] },
|
|
727
|
+
replay: { fen: input.fen },
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function pinRecord(input: {
|
|
732
|
+
board: Chess;
|
|
733
|
+
timeline: ReturnType<typeof createPocIdentityTimeline>;
|
|
734
|
+
contextHash: string;
|
|
735
|
+
fen: string;
|
|
736
|
+
}): CandidateRecord {
|
|
737
|
+
const color = input.board.turn();
|
|
738
|
+
const enemy = opposite(color);
|
|
739
|
+
const kingSquare = input.board.findPiece({ type: "k", color })[0];
|
|
740
|
+
if (!kingSquare) throw new Error("Mechanical fact provider requires both kings");
|
|
741
|
+
const pins: Array<Record<string, SemanticValue>> = [];
|
|
742
|
+
const participants: SemanticParticipant[] = [];
|
|
743
|
+
const pieceLocations: Record<string, SemanticValue>[] = [];
|
|
744
|
+
if (!input.board.isCheck()) {
|
|
745
|
+
for (const square of input.board.findPiece({ color, type: "p" })
|
|
746
|
+
.concat(input.board.findPiece({ color, type: "n" }), input.board.findPiece({ color, type: "b" }), input.board.findPiece({ color, type: "r" }), input.board.findPiece({ color, type: "q" }))) {
|
|
747
|
+
const removed = new Chess(input.fen);
|
|
748
|
+
removed.remove(square);
|
|
749
|
+
const pinners = removed.attackers(kingSquare, enemy).filter((origin) => {
|
|
750
|
+
const piece = removed.get(origin);
|
|
751
|
+
return piece && new Set<PieceSymbol>(["b", "r", "q"]).has(piece.type) && squaresBetween(origin, kingSquare).includes(square);
|
|
752
|
+
});
|
|
753
|
+
for (const pinnerSquare of pinners) {
|
|
754
|
+
const pinned = participantForSquare(input.timeline.after, square, PARTICIPANT_ROLES.TARGET);
|
|
755
|
+
const pinner = participantForSquare(input.timeline.after, pinnerSquare, PARTICIPANT_ROLES.ACTOR);
|
|
756
|
+
const king = participantForSquare(input.timeline.after, kingSquare, PARTICIPANT_ROLES.GOAL);
|
|
757
|
+
const piece = input.board.get(square);
|
|
758
|
+
if (!pinned || !pinner || !king || !piece) continue;
|
|
759
|
+
const moves = input.board.moves({ square, verbose: true });
|
|
760
|
+
pins.push({ pinnedSquare: square, pinnedType: piece.type, pinnerSquare, pinnerType: input.board.get(pinnerSquare)!.type, kingSquare, ray: [pinnerSquare, ...squaresBetween(pinnerSquare, kingSquare), kingSquare], legalMovesUci: moves.map((legalMove) => `${legalMove.from}${legalMove.to}${legalMove.promotion || ""}`) });
|
|
761
|
+
participants.push(pinner.participant, pinned.participant, king.participant);
|
|
762
|
+
pieceLocations.push({ ...pinner.location, phase: "after" }, { ...pinned.location, phase: "after" }, { ...king.location, phase: "after" });
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
const uniqueParticipants = participants.filter((item, index, all) => all.findIndex((candidate) => candidate.role === item.role && candidate.ref.domain === item.ref.domain && candidate.ref.id === item.ref.id) === index);
|
|
767
|
+
const uniqueLocations = pieceLocations.filter((item, index, all) => all.findIndex((candidate) => candidate.pieceId === item.pieceId && candidate.currentSquare === item.currentSquare) === index);
|
|
768
|
+
const exhaustive = !input.board.isCheck();
|
|
769
|
+
return createRecord({
|
|
770
|
+
contextHash: input.contextHash,
|
|
771
|
+
kind: "piece.absolute_pin",
|
|
772
|
+
phase: "after",
|
|
773
|
+
authority: "legal-exact",
|
|
774
|
+
priority: pins.length ? 89 : 45,
|
|
775
|
+
summary: exhaustive
|
|
776
|
+
? pins.length
|
|
777
|
+
? `${pins.length} absolute pin${pins.length === 1 ? " is" : "s are"} present for the side to move after the target move; the recorded legal moves show whether a pinned piece can capture its pinner or remain on the ray.`
|
|
778
|
+
: "No absolute pin is present for the side to move after the target move."
|
|
779
|
+
: "Absolute pins are not enumerated while the side to move is already in check.",
|
|
780
|
+
participants: uniqueParticipants,
|
|
781
|
+
data: { sideToMove: color, kingSquare, pins },
|
|
782
|
+
pieceLocations: uniqueLocations,
|
|
783
|
+
coverage: { domain: "side-to-move-absolute-pins-after", exhaustive, covered: pins.map((pin) => String(pin.pinnedSquare)), unresolved: exhaustive ? [] : ["side-is-in-check"] },
|
|
784
|
+
replay: { fen: input.fen },
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
export class MechanicalChessFactProvider implements ChessFactProvider {
|
|
789
|
+
readonly id = MECHANICAL_CHESS_FACT_PROVIDER_ID;
|
|
790
|
+
readonly version = MECHANICAL_CHESS_FACT_PROVIDER_VERSION;
|
|
791
|
+
|
|
792
|
+
capabilities(): readonly ChessFactCapability[] {
|
|
793
|
+
return CAPABILITIES;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
collect(requestInput: ChessFactRequest, signal?: AbortSignal): ChessFactResult {
|
|
797
|
+
const request = validateChessFactRequest(requestInput);
|
|
798
|
+
if (signal?.aborted) throw signal.reason || new Error("Chess fact request cancelled");
|
|
799
|
+
validateReasoningPositionContext(request.context);
|
|
800
|
+
const startedAt = Date.now();
|
|
801
|
+
const contextHash = reasoningContextHash(request.context);
|
|
802
|
+
const before = new Chess(request.context.fenBefore);
|
|
803
|
+
const moverColor = before.turn();
|
|
804
|
+
const move = before.move({
|
|
805
|
+
from: request.context.moveUci.slice(0, 2),
|
|
806
|
+
to: request.context.moveUci.slice(2, 4),
|
|
807
|
+
promotion: request.context.moveUci.slice(4) || undefined,
|
|
808
|
+
});
|
|
809
|
+
if (!move || before.fen() !== new Chess(request.context.fenAfter).fen()) throw new Error("Mechanical fact provider context transition mismatch");
|
|
810
|
+
const after = before;
|
|
811
|
+
const beforeBoard = new Chess(request.context.fenBefore);
|
|
812
|
+
const timeline = createPocIdentityTimeline(request.context);
|
|
813
|
+
const from = move.from as Square;
|
|
814
|
+
const to = move.to as Square;
|
|
815
|
+
const target = targetMoveParticipants(timeline, from, to, request.context.moveUci);
|
|
816
|
+
let capturedPiece: { pieceId: string; color: Color; pieceType: PieceSymbol; square: Square } | null = null;
|
|
817
|
+
if (move.captured) {
|
|
818
|
+
const captureSquare = (move.isEnPassant() ? `${to[0]}${from[1]}` : to) as Square;
|
|
819
|
+
const captured = participantForSquare(timeline.before, captureSquare, PARTICIPANT_ROLES.TARGET);
|
|
820
|
+
if (captured) {
|
|
821
|
+
const identity = timeline.before.pieces[captured.participant.ref.id];
|
|
822
|
+
if (identity) capturedPiece = { pieceId: identity.id, color: identity.color, pieceType: identity.type, square: captureSquare };
|
|
823
|
+
target.participants.push(captured.participant);
|
|
824
|
+
target.locations.push({ ...captured.location, phase: "before" });
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
const candidates: CandidateRecord[] = [];
|
|
828
|
+
const push = (record: CandidateRecord) => candidates.push(record);
|
|
829
|
+
|
|
830
|
+
push(createRecord({
|
|
831
|
+
contextHash,
|
|
832
|
+
kind: "move.legal_transition",
|
|
833
|
+
phase: "transition",
|
|
834
|
+
authority: "legal-exact",
|
|
835
|
+
priority: 100,
|
|
836
|
+
summary: `${move.san} is legal and moves the ${PIECE_NAMES[move.piece]} from ${from} to ${to}${move.captured ? ` while capturing a ${PIECE_NAMES[move.captured]}` : ""}.`,
|
|
837
|
+
participants: target.participants,
|
|
838
|
+
data: { uci: request.context.moveUci, san: move.san, from, to, moverColor, pieceType: move.piece, capturedType: move.captured || null, capturedPiece, promotion: move.promotion || null },
|
|
839
|
+
pieceLocations: target.locations,
|
|
840
|
+
coverage: { domain: "target-move", exhaustive: true, covered: [request.context.moveUci], unresolved: [] },
|
|
841
|
+
replay: { fen: request.context.fenBefore, movesUci: [request.context.moveUci] },
|
|
842
|
+
}));
|
|
843
|
+
|
|
844
|
+
const beforeCheck = checkRecord({ board: beforeBoard, phase: "before", timeline, contextHash, fen: request.context.fenBefore });
|
|
845
|
+
const afterCheck = checkRecord({ board: after, phase: "after", timeline, contextHash, fen: request.context.fenAfter });
|
|
846
|
+
push(beforeCheck);
|
|
847
|
+
push(afterCheck);
|
|
848
|
+
|
|
849
|
+
const beforeKingSquare = beforeBoard.findPiece({ type: "k", color: moverColor })[0];
|
|
850
|
+
const beforeCheckers = beforeKingSquare ? beforeBoard.attackers(beforeKingSquare, opposite(moverColor)) : [];
|
|
851
|
+
if (move.piece !== "k" && beforeKingSquare && beforeCheckers.length && !after.isAttacked(after.findPiece({ type: "k", color: moverColor })[0], opposite(moverColor))) {
|
|
852
|
+
for (const checkerSquare of beforeCheckers.filter((square) => squaresBetween(square, beforeKingSquare).includes(to))) {
|
|
853
|
+
// This transition is performed by the moved piece. The checker is the
|
|
854
|
+
// piece whose checking line is affected by the block, so it is a
|
|
855
|
+
// target rather than a second actor sharing the mover's destination.
|
|
856
|
+
// Keeping that distinction here prevents consumers from inferring
|
|
857
|
+
// that both pieces travel to the single destination square.
|
|
858
|
+
const checker = participantForSquare(timeline.before, checkerSquare, PARTICIPANT_ROLES.TARGET);
|
|
859
|
+
const king = participantForSquare(timeline.before, beforeKingSquare, PARTICIPANT_ROLES.TARGET);
|
|
860
|
+
push(createRecord({
|
|
861
|
+
contextHash,
|
|
862
|
+
kind: "move.blocks_check_line",
|
|
863
|
+
phase: "transition",
|
|
864
|
+
authority: "legal-exact",
|
|
865
|
+
priority: 100,
|
|
866
|
+
summary: `${move.san} places the moved piece on ${to} between the checker on ${checkerSquare} and the king on ${beforeKingSquare}; the resulting position is not check.`,
|
|
867
|
+
participants: [...target.participants, ...(checker ? [checker.participant] : []), ...(king ? [king.participant] : [])],
|
|
868
|
+
data: { checkerSquare, kingSquare: beforeKingSquare, blockSquare: to, line: [checkerSquare, ...squaresBetween(checkerSquare, beforeKingSquare), beforeKingSquare], checkResolved: true },
|
|
869
|
+
pieceLocations: [...target.locations, ...(checker ? [{ ...checker.location, phase: "before" }] : []), ...(king ? [{ ...king.location, phase: "before" }] : [])],
|
|
870
|
+
coverage: { domain: "target-move-check-resolution", exhaustive: true, covered: [request.context.moveUci, checkerSquare, beforeKingSquare], unresolved: [] },
|
|
871
|
+
replay: { fen: request.context.fenBefore, movesUci: [request.context.moveUci] },
|
|
872
|
+
derivedFromIds: [beforeCheck.id, afterCheck.id],
|
|
873
|
+
}));
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
push(createRecord({
|
|
878
|
+
contextHash,
|
|
879
|
+
kind: "move.capture_check_delta",
|
|
880
|
+
phase: "transition",
|
|
881
|
+
authority: "legal-exact",
|
|
882
|
+
priority: after.isCheck() ? 98 : 88,
|
|
883
|
+
summary: `${move.san}${move.captured ? ` captures a ${PIECE_NAMES[move.captured]} on ${to}` : " does not capture"}${after.isCheck() ? " and gives check" : " and does not give check"}.`,
|
|
884
|
+
participants: target.participants,
|
|
885
|
+
data: { uci: request.context.moveUci, capture: Boolean(move.captured), capturedType: move.captured || null, capturedPiece, givesCheck: after.isCheck() },
|
|
886
|
+
pieceLocations: target.locations,
|
|
887
|
+
coverage: { domain: "target-move-capture-check", exhaustive: true, covered: [request.context.moveUci], unresolved: [] },
|
|
888
|
+
replay: { fen: request.context.fenBefore, movesUci: [request.context.moveUci] },
|
|
889
|
+
}));
|
|
890
|
+
|
|
891
|
+
// A target move's destination is the one square where an immediate
|
|
892
|
+
// capture/recapture question is intrinsically about the target move. Keep
|
|
893
|
+
// its complete attack map as a compact, deterministic fact even when the
|
|
894
|
+
// ordinary presentation budget is tight. This deliberately reports only
|
|
895
|
+
// geometry: chess.js attack maps include pinned pieces, so neither set is
|
|
896
|
+
// a conclusion about legal capture, SEE, or the safety of the mover.
|
|
897
|
+
const destinationFriendlyDefenders = stableAttackers(after, timeline.after, to, moverColor);
|
|
898
|
+
const destinationOpposingAttackers = stableAttackers(after, timeline.after, to, opposite(moverColor));
|
|
899
|
+
const movedPieceId = pieceIdAt(timeline.after, to);
|
|
900
|
+
push(createRecord({
|
|
901
|
+
contextHash,
|
|
902
|
+
kind: "square.target_destination_attackers_after",
|
|
903
|
+
phase: "after",
|
|
904
|
+
authority: "deterministic-derived",
|
|
905
|
+
priority: 101,
|
|
906
|
+
budgetProtected: true,
|
|
907
|
+
summary: `After ${move.san}, the post-move attack map on ${to} has friendly ${destinationFriendlyDefenders.map((item) => item.currentSquare).join(", ") || "none"} and opposing ${destinationOpposingAttackers.map((item) => item.currentSquare).join(", ") || "none"}. Geometry only; not a safety, exchange, or strategic conclusion.`,
|
|
908
|
+
participants: target.participants,
|
|
909
|
+
data: {
|
|
910
|
+
square: to,
|
|
911
|
+
occupant: movedPieceId ? { pieceId: movedPieceId, currentSquare: to, color: moverColor, pieceType: move.piece } : null,
|
|
912
|
+
friendlyDefenderSet: destinationFriendlyDefenders,
|
|
913
|
+
opposingAttackerSet: destinationOpposingAttackers,
|
|
914
|
+
attackMapScope: "geometry-only-including-pinned-pieces",
|
|
915
|
+
},
|
|
916
|
+
pieceLocations: target.locations,
|
|
917
|
+
coverage: {
|
|
918
|
+
domain: "target-move-destination-attack-map-after",
|
|
919
|
+
exhaustive: true,
|
|
920
|
+
covered: [to],
|
|
921
|
+
unresolved: [],
|
|
922
|
+
},
|
|
923
|
+
replay: { fen: request.context.fenAfter },
|
|
924
|
+
}));
|
|
925
|
+
|
|
926
|
+
const afterLegalMoves = after.moves({ verbose: true });
|
|
927
|
+
const movedPieceAfter = after.get(to);
|
|
928
|
+
const legalRecaptures = movedPieceAfter
|
|
929
|
+
? afterLegalMoves.filter((reply) => reply.to === to && reply.captured === movedPieceAfter.type)
|
|
930
|
+
: [];
|
|
931
|
+
if (move.captured && movedPieceAfter) {
|
|
932
|
+
const captureResponse = deriveAdmittedCaptureResponseAvailabilityFact({
|
|
933
|
+
context: request.context,
|
|
934
|
+
contextHash,
|
|
935
|
+
start: "before",
|
|
936
|
+
movesUci: [request.context.moveUci],
|
|
937
|
+
});
|
|
938
|
+
const unavailable = captureResponse.data.unavailableGeometricDefenderPieceIds;
|
|
939
|
+
const blockedSliderCapturers = captureResponse.data.preCaptureBlockedSliderCapturers;
|
|
940
|
+
const replies = captureResponse.data.legalSameSquareRecaptures;
|
|
941
|
+
push({
|
|
942
|
+
...captureResponse,
|
|
943
|
+
priority: Array.isArray(unavailable) && unavailable.length
|
|
944
|
+
|| Array.isArray(blockedSliderCapturers) && blockedSliderCapturers.length
|
|
945
|
+
? 95
|
|
946
|
+
: Array.isArray(replies) && replies.length
|
|
947
|
+
? 84
|
|
948
|
+
: 72,
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
const captureOutcomes = immediateCaptureOutcomes(request.context.fenAfter, legalRecaptures);
|
|
952
|
+
const allCapturesLoseAfterImmediateSameSquareRecapture = captureOutcomes.length > 0 && captureOutcomes.every((outcome) => (
|
|
953
|
+
outcome.immediateSameSquareRecaptures.length > 0
|
|
954
|
+
&& outcome.minimumNominalBalanceDeltaAfterImmediateSameSquareRecapture !== null
|
|
955
|
+
&& outcome.minimumNominalBalanceDeltaAfterImmediateSameSquareRecapture < 0
|
|
956
|
+
));
|
|
957
|
+
const recaptureParticipants = legalRecaptures.flatMap((reply) => {
|
|
958
|
+
const participant = participantForSquare(timeline.after, reply.from as Square, PARTICIPANT_ROLES.ACTOR);
|
|
959
|
+
return participant ? [participant] : [];
|
|
960
|
+
});
|
|
961
|
+
push(createRecord({
|
|
962
|
+
contextHash,
|
|
963
|
+
kind: "move.legal_recaptures_after",
|
|
964
|
+
phase: "after",
|
|
965
|
+
authority: "deterministic-derived",
|
|
966
|
+
// A legal capture of the moved piece which is immediately answered on
|
|
967
|
+
// the same square is direct target-move evidence. It stays near the
|
|
968
|
+
// top of normal deterministic ordering, while still making no SEE or
|
|
969
|
+
// longer-sequence safety claim.
|
|
970
|
+
priority: legalRecaptures.length && captureOutcomes.some((outcome) => outcome.immediateSameSquareRecaptures.length)
|
|
971
|
+
? 100
|
|
972
|
+
: legalRecaptures.length ? (allCapturesLoseAfterImmediateSameSquareRecapture ? 72 : 96) : 42,
|
|
973
|
+
summary: legalRecaptures.length
|
|
974
|
+
? `The piece that moved to ${to} can be captured immediately by ${legalRecaptures.map((reply) => reply.san).join(", ")}.${captureOutcomes.map((outcome) => outcome.immediateSameSquareRecaptures.length
|
|
975
|
+
? ` After ${outcome.capture.san}, immediate same-square replies are ${outcome.immediateSameSquareRecaptures.map((reply) => `${reply.san} (capturing side nominal balance delta ${reply.nominalBalanceDeltaForCapturingSide})`).join(", ")}.`
|
|
976
|
+
: ` No immediate same-square reply follows ${outcome.capture.san}.`).join("")} This is nominal arithmetic, not SEE or proof of longer-term exchange soundness.`
|
|
977
|
+
: `The side to move has no immediate legal capture of the piece on ${to}. This does not establish that the piece is safe in a longer sequence.`,
|
|
978
|
+
participants: [...target.afterParticipants, ...recaptureParticipants.map((item) => item.participant)],
|
|
979
|
+
data: {
|
|
980
|
+
targetSquare: to,
|
|
981
|
+
targetPieceType: movedPieceAfter?.type || null,
|
|
982
|
+
targetPieceColor: movedPieceAfter?.color || null,
|
|
983
|
+
moves: legalRecaptures.map((reply) => ({
|
|
984
|
+
uci: moveUci(reply),
|
|
985
|
+
san: reply.san,
|
|
986
|
+
moverPieceId: pieceIdAt(timeline.after, reply.from as Square),
|
|
987
|
+
from: reply.from,
|
|
988
|
+
to: reply.to,
|
|
989
|
+
capturedType: reply.captured || null,
|
|
990
|
+
})),
|
|
991
|
+
captureOutcomes,
|
|
992
|
+
allCapturesLoseAfterImmediateSameSquareRecapture,
|
|
993
|
+
},
|
|
994
|
+
pieceLocations: [...target.locations.filter((location) => location.phase === "after"), ...recaptureParticipants.map((item) => ({ ...item.location, phase: "after" }))],
|
|
995
|
+
coverage: { domain: "legal-captures-of-target-mover-after", exhaustive: true, covered: legalRecaptures.map(moveUci), unresolved: [] },
|
|
996
|
+
replay: {
|
|
997
|
+
fen: request.context.fenAfter,
|
|
998
|
+
linesUci: captureOutcomes.flatMap((outcome) => outcome.immediateSameSquareRecaptures.length
|
|
999
|
+
? outcome.immediateSameSquareRecaptures.map((reply) => [outcome.capture.uci, reply.uci])
|
|
1000
|
+
: [[outcome.capture.uci]]),
|
|
1001
|
+
},
|
|
1002
|
+
}));
|
|
1003
|
+
|
|
1004
|
+
if (after.isCheck()) {
|
|
1005
|
+
const checkedColor = after.turn();
|
|
1006
|
+
const kingSquare = after.findPiece({ type: "k", color: checkedColor })[0];
|
|
1007
|
+
const checkerSquares = after.attackers(kingSquare, opposite(checkedColor));
|
|
1008
|
+
const participants = checkerSquares.flatMap((square) => {
|
|
1009
|
+
const item = participantForSquare(timeline.after, square, PARTICIPANT_ROLES.ACTOR);
|
|
1010
|
+
return item ? [item] : [];
|
|
1011
|
+
});
|
|
1012
|
+
const king = participantForSquare(timeline.after, kingSquare, PARTICIPANT_ROLES.TARGET);
|
|
1013
|
+
push(createRecord({
|
|
1014
|
+
contextHash,
|
|
1015
|
+
kind: "king.checkers_after",
|
|
1016
|
+
phase: "after",
|
|
1017
|
+
authority: "legal-exact",
|
|
1018
|
+
priority: 97,
|
|
1019
|
+
summary: `After ${move.san}, ${checkerSquares.length} piece${checkerSquares.length === 1 ? " checks" : "s check"} the king on ${kingSquare}: ${checkerSquares.join(", ")}.`,
|
|
1020
|
+
participants: [...participants.map((item) => item.participant), ...(king ? [king.participant] : [])],
|
|
1021
|
+
data: { kingSquare, checkerSquares, checkingLines: checkerSquares.map((square) => ({ checker: square, between: squaresBetween(square, kingSquare) })) },
|
|
1022
|
+
pieceLocations: [...participants.map((item) => ({ ...item.location, phase: "after" })), ...(king ? [{ ...king.location, phase: "after" }] : [])],
|
|
1023
|
+
coverage: { domain: "checking-pieces-after", exhaustive: true, covered: [kingSquare, ...checkerSquares], unresolved: [] },
|
|
1024
|
+
replay: { fen: request.context.fenAfter },
|
|
1025
|
+
}));
|
|
1026
|
+
const kingMoves = after.moves({ square: kingSquare, verbose: true });
|
|
1027
|
+
push(createRecord({
|
|
1028
|
+
contextHash,
|
|
1029
|
+
kind: "king.legal_moves",
|
|
1030
|
+
phase: "after",
|
|
1031
|
+
authority: "legal-exact",
|
|
1032
|
+
priority: 94,
|
|
1033
|
+
summary: `The checked king on ${kingSquare} has ${kingMoves.length} legal king move${kingMoves.length === 1 ? "" : "s"}: ${kingMoves.map((item) => item.san).join(", ") || "none"}.`,
|
|
1034
|
+
participants: king ? [king.participant] : [],
|
|
1035
|
+
data: { kingSquare, movesUci: kingMoves.map((item) => `${item.from}${item.to}${item.promotion || ""}`), movesSan: kingMoves.map((item) => item.san) },
|
|
1036
|
+
pieceLocations: king ? [{ ...king.location, phase: "after" }] : [],
|
|
1037
|
+
coverage: { domain: "legal-king-moves-after", exhaustive: true, covered: kingMoves.map((item) => `${item.from}${item.to}${item.promotion || ""}`), unresolved: [] },
|
|
1038
|
+
replay: { fen: request.context.fenAfter },
|
|
1039
|
+
}));
|
|
1040
|
+
for (const checkerSquare of checkerSquares.filter((square) => square !== to && squaresBetween(square, kingSquare).includes(from))) {
|
|
1041
|
+
const checker = participantForSquare(timeline.after, checkerSquare, PARTICIPANT_ROLES.ACTOR);
|
|
1042
|
+
push(createRecord({
|
|
1043
|
+
contextHash,
|
|
1044
|
+
kind: "move.unblocks_line",
|
|
1045
|
+
phase: "transition",
|
|
1046
|
+
authority: "legal-exact",
|
|
1047
|
+
priority: 95,
|
|
1048
|
+
summary: `Moving from ${from} clears the line from the ${PIECE_NAMES[after.get(checkerSquare)!.type]} on ${checkerSquare} to the king on ${kingSquare}.`,
|
|
1049
|
+
participants: [...target.participants, ...(checker ? [checker.participant] : []), ...(king ? [king.participant] : [])],
|
|
1050
|
+
data: { vacatedSquare: from, checkerSquare, kingSquare, line: [checkerSquare, ...squaresBetween(checkerSquare, kingSquare), kingSquare] },
|
|
1051
|
+
pieceLocations: [...target.locations, ...(checker ? [{ ...checker.location, phase: "after" }] : []), ...(king ? [{ ...king.location, phase: "after" }] : [])],
|
|
1052
|
+
coverage: { domain: "target-move-slider-lines", exhaustive: true, covered: [from, checkerSquare, kingSquare], unresolved: [] },
|
|
1053
|
+
replay: { fen: request.context.fenBefore, movesUci: [request.context.moveUci] },
|
|
1054
|
+
}));
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const whiteKingSquare = after.findPiece({ type: "k", color: "w" })[0];
|
|
1059
|
+
const blackKingSquare = after.findPiece({ type: "k", color: "b" })[0];
|
|
1060
|
+
if (!whiteKingSquare || !blackKingSquare) throw new Error("Mechanical fact provider requires both kings");
|
|
1061
|
+
const whiteKing = participantForSquare(timeline.after, whiteKingSquare, PARTICIPANT_ROLES.ACTOR);
|
|
1062
|
+
const blackKing = participantForSquare(timeline.after, blackKingSquare, PARTICIPANT_ROLES.TARGET);
|
|
1063
|
+
const distance = kingDistance(whiteKingSquare, blackKingSquare);
|
|
1064
|
+
const betweenKings = squaresBetween(whiteKingSquare, blackKingSquare);
|
|
1065
|
+
push(createRecord({
|
|
1066
|
+
contextHash,
|
|
1067
|
+
kind: "king.geometry_after",
|
|
1068
|
+
phase: "after",
|
|
1069
|
+
authority: "legal-exact",
|
|
1070
|
+
priority: distance.chebyshev <= 2 ? 90 : 24,
|
|
1071
|
+
summary: `After ${move.san}, the kings are on ${whiteKingSquare} and ${blackKingSquare}; their Chebyshev distance is ${distance.chebyshev}, it is ${colorName(after.turn())}'s turn, and aligned squares between them are ${betweenKings.join(", ") || "none"}.`,
|
|
1072
|
+
participants: [...(whiteKing ? [whiteKing.participant] : []), ...(blackKing ? [blackKing.participant] : [])],
|
|
1073
|
+
data: { whiteKingSquare, blackKingSquare, sideToMove: after.turn(), distance, alignedBetween: betweenKings },
|
|
1074
|
+
pieceLocations: [...(whiteKing ? [{ ...whiteKing.location, phase: "after" }] : []), ...(blackKing ? [{ ...blackKing.location, phase: "after" }] : [])],
|
|
1075
|
+
coverage: { domain: "king-square-geometry-after", exhaustive: true, covered: [whiteKingSquare, blackKingSquare], unresolved: [] },
|
|
1076
|
+
replay: { fen: request.context.fenAfter },
|
|
1077
|
+
}));
|
|
1078
|
+
|
|
1079
|
+
const castlingEvents = priorCastlingEvents(request.context);
|
|
1080
|
+
const castlingColors = new Set(castlingEvents.map((event) => event.color));
|
|
1081
|
+
push(createRecord({
|
|
1082
|
+
contextHash,
|
|
1083
|
+
kind: "king_rook.castling_history",
|
|
1084
|
+
phase: "path",
|
|
1085
|
+
authority: "legal-exact",
|
|
1086
|
+
priority: castlingEvents.length ? 88 : 18,
|
|
1087
|
+
summary: castlingEvents.length
|
|
1088
|
+
? `Before ${move.san}, the recorded path contains ${castlingEvents.length} castling event${castlingEvents.length === 1 ? "" : "s"}: ${castlingEvents.map((event) => `${colorName(event.color)} ${event.san} on ply ${event.plyIndex + 1}`).join("; ")}.`
|
|
1089
|
+
: `The recorded path before ${move.san} contains no castling event.`,
|
|
1090
|
+
participants: [
|
|
1091
|
+
...(castlingColors.has("w") && whiteKing ? [whiteKing.participant] : []),
|
|
1092
|
+
...(castlingColors.has("b") && blackKing ? [blackKing.participant] : []),
|
|
1093
|
+
],
|
|
1094
|
+
data: { events: castlingEvents },
|
|
1095
|
+
pieceLocations: [
|
|
1096
|
+
...(castlingColors.has("w") && whiteKing ? [{ ...whiteKing.location, phase: "after" }] : []),
|
|
1097
|
+
...(castlingColors.has("b") && blackKing ? [{ ...blackKing.location, phase: "after" }] : []),
|
|
1098
|
+
],
|
|
1099
|
+
coverage: { domain: "prior-castling-events", exhaustive: true, covered: castlingEvents.map((event) => String(event.plyIndex)), unresolved: [] },
|
|
1100
|
+
replay: { fen: request.context.historyStartFen || new Chess().fen(), movesUci: request.context.historyUci },
|
|
1101
|
+
}));
|
|
1102
|
+
|
|
1103
|
+
const beforeAttacks = attackedSquares(beforeBoard, from, moverColor);
|
|
1104
|
+
const afterAttacks = attackedSquares(after, to, moverColor);
|
|
1105
|
+
const gained = afterAttacks.filter((square) => !beforeAttacks.includes(square));
|
|
1106
|
+
const lost = beforeAttacks.filter((square) => !afterAttacks.includes(square));
|
|
1107
|
+
const retained = afterAttacks.filter((square) => beforeAttacks.includes(square));
|
|
1108
|
+
const occupiedTargets = afterAttacks.filter((square) => after.get(square)?.color === opposite(moverColor));
|
|
1109
|
+
const targetPieces = occupiedTargets.flatMap((square) => {
|
|
1110
|
+
const item = participantForSquare(timeline.after, square, PARTICIPANT_ROLES.TARGET);
|
|
1111
|
+
return item ? [{ square, ...item }] : [];
|
|
1112
|
+
});
|
|
1113
|
+
push(createRecord({
|
|
1114
|
+
contextHash,
|
|
1115
|
+
kind: "piece.attack_delta",
|
|
1116
|
+
phase: "transition",
|
|
1117
|
+
authority: "deterministic-derived",
|
|
1118
|
+
priority: occupiedTargets.length ? 92 : 72,
|
|
1119
|
+
summary: `After ${move.san}, the moved ${PIECE_NAMES[move.piece]} attacks ${afterAttacks.join(", ") || "no squares"}; gained attacks are ${gained.join(", ") || "none"}, and occupied opponent targets are ${occupiedTargets.join(", ") || "none"}.`,
|
|
1120
|
+
participants: [...target.participants, ...targetPieces.map((item) => item.participant)],
|
|
1121
|
+
data: { from, to, beforeAttacks, afterAttacks, gained, lost, retained, occupiedOpponentTargets: occupiedTargets },
|
|
1122
|
+
pieceLocations: [...target.locations, ...targetPieces.map((item) => ({ ...item.location, phase: "after" }))],
|
|
1123
|
+
coverage: { domain: "moved-piece-geometric-attacks", exhaustive: true, covered: afterAttacks, unresolved: [] },
|
|
1124
|
+
replay: { fen: request.context.fenBefore, movesUci: [request.context.moveUci] },
|
|
1125
|
+
}));
|
|
1126
|
+
|
|
1127
|
+
const friendlyOrigins = after.attackers(to, moverColor).filter((origin) => after.get(origin)?.type !== "p");
|
|
1128
|
+
const friendlyAttackers = friendlyOrigins.flatMap((origin) => {
|
|
1129
|
+
const item = participantForSquare(timeline.after, origin, PARTICIPANT_ROLES.RESOURCE);
|
|
1130
|
+
const piece = after.get(origin);
|
|
1131
|
+
return item && piece ? [{ origin, pieceType: piece.type, ...item }] : [];
|
|
1132
|
+
});
|
|
1133
|
+
push(createRecord({
|
|
1134
|
+
contextHash,
|
|
1135
|
+
kind: "square.friendly_nonpawn_reachers_after",
|
|
1136
|
+
phase: "after",
|
|
1137
|
+
authority: "deterministic-derived",
|
|
1138
|
+
priority: friendlyAttackers.some((item) => item.pieceType === "n") ? 91 : friendlyAttackers.length ? 64 : 20,
|
|
1139
|
+
summary: `${to} is occupied by the moved ${colorName(moverColor).toLowerCase()} ${PIECE_NAMES[move.piece]} after ${move.san}; friendly non-pawn pieces whose movement geometry reaches ${to} are ${friendlyAttackers.map((item) => `${PIECE_NAMES[item.pieceType]} on ${item.origin}`).join(" and ") || "none"}. Friendly occupancy makes entry onto ${to} illegal while it remains, but does not establish whether that square is strategically desirable for the other piece.`,
|
|
1140
|
+
participants: [...target.participants, ...friendlyAttackers.map((item) => item.participant)],
|
|
1141
|
+
data: { square: to, occupantPieceId: pieceIdAt(timeline.after, to), occupantType: move.piece, color: moverColor, friendlyNonPawnReachers: friendlyAttackers.map((item) => ({ pieceId: item.participant.ref.id, currentSquare: item.origin, pieceType: item.pieceType })), entryIllegalWhileFriendlyOccupied: true },
|
|
1142
|
+
pieceLocations: [...target.locations, ...friendlyAttackers.map((item) => ({ ...item.location, phase: "after" }))],
|
|
1143
|
+
coverage: { domain: "target-destination-friendly-nonpawn-reachers", exhaustive: true, covered: [to, ...friendlyOrigins], unresolved: [] },
|
|
1144
|
+
replay: { fen: request.context.fenAfter },
|
|
1145
|
+
}));
|
|
1146
|
+
|
|
1147
|
+
const attackerChanges = attackerSetChanges(beforeBoard, after, timeline);
|
|
1148
|
+
const retainedAfterLoss = attackerChanges.filter((change) => change.lost.length && change.afterAttackers.length);
|
|
1149
|
+
const attackPieceLocations = attackerChanges
|
|
1150
|
+
.flatMap((change) => [...change.beforeAttackers.map((item) => ({ pieceId: item.pieceId, currentSquare: item.currentSquare, phase: "before" })), ...change.afterAttackers.map((item) => ({ pieceId: item.pieceId, currentSquare: item.currentSquare, phase: "after" }))])
|
|
1151
|
+
.filter((item, index, all) => all.findIndex((candidate) => candidate.pieceId === item.pieceId && candidate.currentSquare === item.currentSquare && candidate.phase === item.phase) === index);
|
|
1152
|
+
for (const change of attackerChanges) {
|
|
1153
|
+
const entries = afterLegalMoves.filter((reply) => reply.to === change.square);
|
|
1154
|
+
const entryResponseWitnesses = entries.map((entry) => {
|
|
1155
|
+
const afterEntry = new Chess(request.context.fenAfter);
|
|
1156
|
+
applyVerboseMove(afterEntry, entry);
|
|
1157
|
+
const captureMoves = afterEntry.moves({ verbose: true })
|
|
1158
|
+
.filter((reply) => reply.to === change.square && Boolean(reply.captured));
|
|
1159
|
+
const captureOutcomes = new Map(immediateCaptureOutcomes(afterEntry.fen(), captureMoves)
|
|
1160
|
+
.map((outcome) => [outcome.capture.uci, outcome]));
|
|
1161
|
+
const immediateSameSquareCaptures = captureMoves.map((reply) => {
|
|
1162
|
+
const outcome = captureOutcomes.get(moveUci(reply));
|
|
1163
|
+
return {
|
|
1164
|
+
uci: moveUci(reply),
|
|
1165
|
+
san: reply.san,
|
|
1166
|
+
from: reply.from,
|
|
1167
|
+
to: reply.to,
|
|
1168
|
+
moverPieceId: pieceIdAt(timeline.after, reply.from as Square),
|
|
1169
|
+
capturedPieceId: pieceIdAt(timeline.after, entry.from as Square),
|
|
1170
|
+
nominalBalanceDeltaAfterCapture: outcome?.nominalBalanceDeltaAfterCapture ?? null,
|
|
1171
|
+
immediateSameSquareRecaptures: outcome?.immediateSameSquareRecaptures || [],
|
|
1172
|
+
...(outcome?.minimumNominalBalanceDeltaAfterImmediateSameSquareRecapture == null
|
|
1173
|
+
? {}
|
|
1174
|
+
: { minimumNominalBalanceDeltaAfterImmediateSameSquareRecapture: outcome.minimumNominalBalanceDeltaAfterImmediateSameSquareRecapture }),
|
|
1175
|
+
};
|
|
1176
|
+
});
|
|
1177
|
+
return {
|
|
1178
|
+
entry: {
|
|
1179
|
+
uci: moveUci(entry),
|
|
1180
|
+
san: entry.san,
|
|
1181
|
+
from: entry.from,
|
|
1182
|
+
to: entry.to,
|
|
1183
|
+
moverPieceId: pieceIdAt(timeline.after, entry.from as Square),
|
|
1184
|
+
},
|
|
1185
|
+
immediateSameSquareCaptures,
|
|
1186
|
+
};
|
|
1187
|
+
});
|
|
1188
|
+
const entryResponseText = entryResponseWitnesses.flatMap((witness) => witness.immediateSameSquareCaptures
|
|
1189
|
+
.map((reply) => {
|
|
1190
|
+
const recaptures = reply.immediateSameSquareRecaptures.map((recapture) => `${recapture.san} (${recapture.nominalBalanceDeltaForCapturingSide})`);
|
|
1191
|
+
return `${witness.entry.san} ${reply.san}${recaptures.length ? ` ${recaptures.join(" or ")}` : ""} (first capturer nominal delta ${recaptures.length ? reply.minimumNominalBalanceDeltaAfterImmediateSameSquareRecapture : reply.nominalBalanceDeltaAfterCapture}; ${recaptures.length ? "after immediate recapture" : "no immediate recapture"})`;
|
|
1192
|
+
}));
|
|
1193
|
+
const entryResponseReplayLines: string[][] = entryResponseWitnesses.flatMap((witness) => witness.immediateSameSquareCaptures.length
|
|
1194
|
+
? witness.immediateSameSquareCaptures.flatMap((reply) => reply.immediateSameSquareRecaptures.length
|
|
1195
|
+
? reply.immediateSameSquareRecaptures.map((recapture) => [request.context.moveUci, witness.entry.uci, reply.uci, recapture.uci])
|
|
1196
|
+
: [[request.context.moveUci, witness.entry.uci, reply.uci]])
|
|
1197
|
+
: [[request.context.moveUci, witness.entry.uci]]);
|
|
1198
|
+
const beforeOccupant = beforeBoard.get(change.square);
|
|
1199
|
+
const afterOccupant = after.get(change.square);
|
|
1200
|
+
const beforeOccupantId = beforeOccupant ? pieceIdAt(timeline.before, change.square) : null;
|
|
1201
|
+
const afterOccupantId = afterOccupant ? pieceIdAt(timeline.after, change.square) : null;
|
|
1202
|
+
const occupiedOpponentLostAttack = change.lost.length > 0
|
|
1203
|
+
&& beforeOccupant?.color === opposite(change.color)
|
|
1204
|
+
&& afterOccupantId === beforeOccupantId;
|
|
1205
|
+
const newlyContestedLegalEntry = change.gained.length > 0 && entries.length > 0;
|
|
1206
|
+
const beforeText = change.beforeAttackers.map((item) => `${PIECE_NAMES[item.pieceType]} on ${item.currentSquare}`).join(" and ") || "none";
|
|
1207
|
+
const afterText = change.afterAttackers.map((item) => `${PIECE_NAMES[item.pieceType]} on ${item.currentSquare}`).join(" and ") || "none";
|
|
1208
|
+
const referenced: Array<{ participant: SemanticParticipant; location: Record<string, SemanticValue> }> = [
|
|
1209
|
+
...change.beforeAttackers.flatMap((item) => {
|
|
1210
|
+
const participant = participantForSquare(timeline.before, item.currentSquare, PARTICIPANT_ROLES.RESOURCE);
|
|
1211
|
+
return participant ? [{ participant: participant.participant, location: { ...participant.location, phase: "before" } }] : [];
|
|
1212
|
+
}),
|
|
1213
|
+
...change.afterAttackers.flatMap((item) => {
|
|
1214
|
+
const participant = participantForSquare(timeline.after, item.currentSquare, PARTICIPANT_ROLES.RESOURCE);
|
|
1215
|
+
return participant ? [{ participant: participant.participant, location: { ...participant.location, phase: "after" } }] : [];
|
|
1216
|
+
}),
|
|
1217
|
+
];
|
|
1218
|
+
if (occupiedOpponentLostAttack) {
|
|
1219
|
+
const affectedBefore = participantForSquare(timeline.before, change.square, PARTICIPANT_ROLES.TARGET);
|
|
1220
|
+
const affectedAfter = participantForSquare(timeline.after, change.square, PARTICIPANT_ROLES.TARGET);
|
|
1221
|
+
if (affectedBefore) referenced.push({ participant: affectedBefore.participant, location: { ...affectedBefore.location, phase: "before" } });
|
|
1222
|
+
if (affectedAfter) referenced.push({ participant: affectedAfter.participant, location: { ...affectedAfter.location, phase: "after" } });
|
|
1223
|
+
}
|
|
1224
|
+
const participants = [...target.participants, ...referenced.map((item) => item.participant)]
|
|
1225
|
+
.filter((item, index, all) => all.findIndex((candidate) => candidate.role === item.role && candidate.ref.domain === item.ref.domain && candidate.ref.id === item.ref.id) === index);
|
|
1226
|
+
const locations = [...target.locations, ...referenced.map((item) => item.location)]
|
|
1227
|
+
.filter((item, index, all) => all.findIndex((candidate) => candidate.pieceId === item.pieceId && candidate.currentSquare === item.currentSquare && candidate.phase === item.phase) === index);
|
|
1228
|
+
const remainsAfterLoss = change.lost.length > 0 && change.afterAttackers.length > 0;
|
|
1229
|
+
push(createRecord({
|
|
1230
|
+
contextHash,
|
|
1231
|
+
kind: "square.attacker_delta",
|
|
1232
|
+
phase: "transition",
|
|
1233
|
+
authority: "deterministic-derived",
|
|
1234
|
+
priority: occupiedOpponentLostAttack
|
|
1235
|
+
? 98
|
|
1236
|
+
: remainsAfterLoss && entries.length
|
|
1237
|
+
? 94
|
|
1238
|
+
: newlyContestedLegalEntry
|
|
1239
|
+
// A newly available legal entry paired with an exact immediate
|
|
1240
|
+
// capture witness is direct, replayable board information. It
|
|
1241
|
+
// remains below occupied-piece protection, but ahead of broad
|
|
1242
|
+
// descriptive deltas so a tight budget can retain the concrete
|
|
1243
|
+
// entry/capture chain without inferring its strategic value.
|
|
1244
|
+
? entryResponseWitnesses.some((witness) => witness.immediateSameSquareCaptures.length) ? 96 : 91
|
|
1245
|
+
: remainsAfterLoss
|
|
1246
|
+
? 88
|
|
1247
|
+
: entries.length
|
|
1248
|
+
? 82
|
|
1249
|
+
: 68,
|
|
1250
|
+
summary: `${colorName(change.color)} attackers of ${change.square} change from ${beforeText} to ${afterText}.${occupiedOpponentLostAttack && beforeOccupant ? ` The ${colorName(beforeOccupant.color).toLowerCase()} ${PIECE_NAMES[beforeOccupant.type]} remains on ${change.square} and is no longer attacked by the removed attacker set.` : ""}${entries.length ? ` ${colorName(after.turn())} has ${entries.length} legal move${entries.length === 1 ? "" : "s"} to ${change.square}: ${entries.map((reply) => reply.san).join(", ")}.` : ""}${entryResponseText.length ? ` Exact immediate captures back onto that entry square include ${entryResponseText.join(", ")}.` : ""}`,
|
|
1251
|
+
participants,
|
|
1252
|
+
data: {
|
|
1253
|
+
...change,
|
|
1254
|
+
remainsAttackedAfterLoss: remainsAfterLoss,
|
|
1255
|
+
occupiedOpponentLostAttack,
|
|
1256
|
+
affectedOccupiedPiece: occupiedOpponentLostAttack && beforeOccupant ? {
|
|
1257
|
+
pieceId: beforeOccupantId,
|
|
1258
|
+
color: beforeOccupant.color,
|
|
1259
|
+
pieceType: beforeOccupant.type,
|
|
1260
|
+
square: change.square,
|
|
1261
|
+
} : null,
|
|
1262
|
+
newlyContestedLegalEntry,
|
|
1263
|
+
sideToMoveLegalEntries: entries.map((reply) => ({ uci: moveUci(reply), san: reply.san, from: reply.from, to: reply.to, moverPieceId: pieceIdAt(timeline.after, reply.from as Square) })),
|
|
1264
|
+
entryResponseWitnesses,
|
|
1265
|
+
entryResponseCoverage: {
|
|
1266
|
+
exhaustive: true,
|
|
1267
|
+
coveredEntryMoves: entries.map((reply) => moveUci(reply)),
|
|
1268
|
+
depth: "entry-capture-immediate-same-square-recapture",
|
|
1269
|
+
unresolved: ["later continuations", "off-square replies after the entry"],
|
|
1270
|
+
},
|
|
1271
|
+
},
|
|
1272
|
+
pieceLocations: locations,
|
|
1273
|
+
coverage: { domain: `attacker-set-${change.color}-${change.square}-transition`, exhaustive: true, covered: [change.square], unresolved: [] },
|
|
1274
|
+
replay: {
|
|
1275
|
+
fen: request.context.fenBefore,
|
|
1276
|
+
movesUci: [request.context.moveUci],
|
|
1277
|
+
...(entryResponseReplayLines.length ? { linesUci: entryResponseReplayLines } : {}),
|
|
1278
|
+
},
|
|
1279
|
+
}));
|
|
1280
|
+
const exchangeWitnesses = sameSquareExchangeWitnesses(request.context.fenAfter, change.square);
|
|
1281
|
+
if (exchangeWitnesses.length) {
|
|
1282
|
+
const lineOutcomes = sameSquareLineOutcomes(request.context.fenAfter, exchangeWitnesses);
|
|
1283
|
+
push(createRecord({
|
|
1284
|
+
contextHash,
|
|
1285
|
+
kind: "square.same_square_exchange_witnesses",
|
|
1286
|
+
phase: "path",
|
|
1287
|
+
authority: "deterministic-derived",
|
|
1288
|
+
priority: lineOutcomes.every((outcome) => outcome.finalNominalBalanceDeltaForFirstCapturingSide !== null && outcome.finalNominalBalanceDeltaForFirstCapturingSide < 0)
|
|
1289
|
+
? 70
|
|
1290
|
+
: exchangeWitnesses.some((line) => line.length >= 3) ? 93 : 78,
|
|
1291
|
+
summary: `Legal same-square exchange witnesses on ${change.square} after ${move.san} include ${exchangeWitnesses.map((line, index) => `${line.map((item) => item.san).join(" ")} (final nominal balance delta for the first capturing side ${lineOutcomes[index].finalNominalBalanceDeltaForFirstCapturingSide ?? "not applicable"})`).join("; ")}. These lines are not asserted to be forced or best, and nominal arithmetic is not SEE.`,
|
|
1292
|
+
participants: target.participants,
|
|
1293
|
+
data: {
|
|
1294
|
+
square: change.square,
|
|
1295
|
+
lines: exchangeWitnesses,
|
|
1296
|
+
lineOutcomes,
|
|
1297
|
+
attackerLossRetainedAndLegalEntry: remainsAfterLoss && entries.length > 0,
|
|
1298
|
+
},
|
|
1299
|
+
pieceLocations: target.locations,
|
|
1300
|
+
coverage: { domain: `same-square-exchange-witnesses-${change.square}`, exhaustive: true, covered: exchangeWitnesses.map((line) => line.map((item) => item.uci).join(" ")), unresolved: [] },
|
|
1301
|
+
replay: { fen: request.context.fenAfter, linesUci: exchangeWitnesses.map((line) => line.map((item) => item.uci)) },
|
|
1302
|
+
}));
|
|
1303
|
+
for (const witness of exchangeRayAlignmentWitnesses(request.context.fenAfter, timeline.after, exchangeWitnesses)) {
|
|
1304
|
+
push(createRecord({
|
|
1305
|
+
contextHash,
|
|
1306
|
+
kind: "line.occupied_ray_alignment_after_exchange",
|
|
1307
|
+
phase: "path",
|
|
1308
|
+
authority: "legal-exact",
|
|
1309
|
+
// Exact geometry in a hypothetical exchange line remains useful
|
|
1310
|
+
// on demand, but is less salient than the target move's direct
|
|
1311
|
+
// changes regardless of the rear piece's nominal value.
|
|
1312
|
+
priority: witness.rear.pieceType === "q" || witness.rear.pieceType === "k" ? 58 : 52,
|
|
1313
|
+
summary: `The legal witness ${witness.movesUci.join(" ")} newly creates an exact occupied ray from ${colorName(witness.slider.color)} ${PIECE_NAMES[witness.slider.pieceType]} on ${witness.slider.currentSquare} through ${colorName(witness.front.color)} ${PIECE_NAMES[witness.front.pieceType]} on ${witness.front.currentSquare} to ${colorName(witness.rear.color)} ${PIECE_NAMES[witness.rear.pieceType]} on ${witness.rear.currentSquare}. This geometry alone does not establish tactical meaning, force, or exchange quality.`,
|
|
1314
|
+
participants: [
|
|
1315
|
+
{ role: PARTICIPANT_ROLES.ACTOR, ref: { domain: REFERENCE_DOMAINS.PIECE, id: witness.slider.pieceId } },
|
|
1316
|
+
{ role: PARTICIPANT_ROLES.TARGET, ref: { domain: REFERENCE_DOMAINS.PIECE, id: witness.front.pieceId } },
|
|
1317
|
+
{ role: PARTICIPANT_ROLES.TARGET, ref: { domain: REFERENCE_DOMAINS.PIECE, id: witness.rear.pieceId } },
|
|
1318
|
+
],
|
|
1319
|
+
data: {
|
|
1320
|
+
movesUci: witness.movesUci,
|
|
1321
|
+
finalFen: witness.finalFen,
|
|
1322
|
+
sliderPieceId: witness.slider.pieceId,
|
|
1323
|
+
sliderSquare: witness.slider.currentSquare,
|
|
1324
|
+
sliderPieceType: witness.slider.pieceType,
|
|
1325
|
+
frontPieceId: witness.front.pieceId,
|
|
1326
|
+
frontSquare: witness.front.currentSquare,
|
|
1327
|
+
frontPieceType: witness.front.pieceType,
|
|
1328
|
+
rearPieceId: witness.rear.pieceId,
|
|
1329
|
+
rearSquare: witness.rear.currentSquare,
|
|
1330
|
+
rearPieceType: witness.rear.pieceType,
|
|
1331
|
+
ray: witness.ray,
|
|
1332
|
+
},
|
|
1333
|
+
pieceLocations: [
|
|
1334
|
+
{ pieceId: witness.slider.pieceId, currentSquare: witness.slider.currentSquare, phase: "path-final" },
|
|
1335
|
+
{ pieceId: witness.front.pieceId, currentSquare: witness.front.currentSquare, phase: "path-final" },
|
|
1336
|
+
{ pieceId: witness.rear.pieceId, currentSquare: witness.rear.currentSquare, phase: "path-final" },
|
|
1337
|
+
],
|
|
1338
|
+
coverage: {
|
|
1339
|
+
domain: `new-opposing-slider-ray-alignments-with-higher-value-rear-piece-after-${witness.movesUci.join("-")}`,
|
|
1340
|
+
exhaustive: true,
|
|
1341
|
+
covered: [`${witness.slider.currentSquare}:${witness.front.currentSquare}:${witness.rear.currentSquare}`],
|
|
1342
|
+
unresolved: [],
|
|
1343
|
+
},
|
|
1344
|
+
replay: { fen: request.context.fenAfter, movesUci: witness.movesUci },
|
|
1345
|
+
}));
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
push(createRecord({
|
|
1350
|
+
contextHash,
|
|
1351
|
+
kind: "square.attackers_delta_inventory",
|
|
1352
|
+
phase: "transition",
|
|
1353
|
+
authority: "deterministic-derived",
|
|
1354
|
+
priority: 22,
|
|
1355
|
+
summary: `Across all squares, ${attackerChanges.length} attacker set${attackerChanges.length === 1 ? " changes" : "s change"}; ${retainedAfterLoss.length} square${retainedAfterLoss.length === 1 ? " remains" : "s remain"} attacked after losing at least one attacker. This is geometry, not a claim of strategic importance.`,
|
|
1356
|
+
participants: target.participants,
|
|
1357
|
+
data: { moverColor, changes: attackerChanges, retainedAfterLoss: retainedAfterLoss.map((change) => change.square) },
|
|
1358
|
+
pieceLocations: [...target.locations, ...attackPieceLocations],
|
|
1359
|
+
coverage: { domain: "all-square-attacker-sets-transition", exhaustive: true, covered: [...SQUARES], unresolved: [] },
|
|
1360
|
+
replay: { fen: request.context.fenBefore, movesUci: [request.context.moveUci] },
|
|
1361
|
+
}));
|
|
1362
|
+
|
|
1363
|
+
for (const attackedSquare of occupiedTargets) {
|
|
1364
|
+
const attackedPiece = after.get(attackedSquare);
|
|
1365
|
+
const participant = participantForSquare(timeline.after, attackedSquare, PARTICIPANT_ROLES.TARGET);
|
|
1366
|
+
if (!attackedPiece || !participant || attackedPiece.color !== after.turn()) continue;
|
|
1367
|
+
const legalMoves = after.moves({ square: attackedSquare, verbose: true });
|
|
1368
|
+
push(createRecord({
|
|
1369
|
+
contextHash,
|
|
1370
|
+
kind: "piece.legal_moves",
|
|
1371
|
+
phase: "after",
|
|
1372
|
+
authority: "legal-exact",
|
|
1373
|
+
priority: 84,
|
|
1374
|
+
summary: `The attacked ${PIECE_NAMES[attackedPiece.type]} on ${attackedSquare} has ${legalMoves.length} legal move${legalMoves.length === 1 ? "" : "s"}: ${legalMoves.map((item) => item.san).join(", ") || "none"}.`,
|
|
1375
|
+
participants: [participant.participant],
|
|
1376
|
+
data: { origin: attackedSquare, pieceType: attackedPiece.type, movesUci: legalMoves.map((item) => `${item.from}${item.to}${item.promotion || ""}`), movesSan: legalMoves.map((item) => item.san) },
|
|
1377
|
+
pieceLocations: [{ ...participant.location, phase: "after" }],
|
|
1378
|
+
coverage: { domain: `legal-moves-from-${attackedSquare}`, exhaustive: true, covered: legalMoves.map((item) => `${item.from}${item.to}${item.promotion || ""}`), unresolved: [] },
|
|
1379
|
+
replay: { fen: request.context.fenAfter },
|
|
1380
|
+
}));
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
if (move.isKingsideCastle() || move.isQueensideCastle()) {
|
|
1384
|
+
const rank = moverColor === "w" ? "1" : "8";
|
|
1385
|
+
const rookFrom = `${move.isKingsideCastle() ? "h" : "a"}${rank}` as Square;
|
|
1386
|
+
const rookTo = `${move.isKingsideCastle() ? "f" : "d"}${rank}` as Square;
|
|
1387
|
+
const rookBefore = participantForSquare(timeline.before, rookFrom, PARTICIPANT_ROLES.RESOURCE);
|
|
1388
|
+
const rookAfter = participantForSquare(timeline.after, rookTo, PARTICIPANT_ROLES.RESOURCE);
|
|
1389
|
+
push(createRecord({
|
|
1390
|
+
contextHash,
|
|
1391
|
+
kind: "castling.king_rook_transition",
|
|
1392
|
+
phase: "transition",
|
|
1393
|
+
authority: "legal-exact",
|
|
1394
|
+
priority: 98,
|
|
1395
|
+
summary: `${move.san} moves the king from ${from} to ${to} and the rook from ${rookFrom} to ${rookTo} in one legal move.`,
|
|
1396
|
+
participants: [...target.participants, ...(rookBefore ? [rookBefore.participant] : [])],
|
|
1397
|
+
data: { king: { from, to }, rook: { from: rookFrom, to: rookTo }, side: move.isKingsideCastle() ? "king" : "queen" },
|
|
1398
|
+
pieceLocations: [...target.locations, ...(rookBefore ? [{ pieceId: rookBefore.participant.ref.id, currentSquare: rookFrom, phase: "before" }, { pieceId: rookAfter?.participant.ref.id || rookBefore.participant.ref.id, currentSquare: rookTo, phase: "after" }] : [])],
|
|
1399
|
+
coverage: { domain: "target-move", exhaustive: true, covered: [request.context.moveUci, rookFrom, rookTo], unresolved: [] },
|
|
1400
|
+
replay: { fen: request.context.fenBefore, movesUci: [request.context.moveUci] },
|
|
1401
|
+
}));
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
push(pinRecord({ board: after, timeline, contextHash, fen: request.context.fenAfter }));
|
|
1405
|
+
|
|
1406
|
+
if (move.piece === "p" && !move.isPromotion()) {
|
|
1407
|
+
const pawnState = isPassedPawn(after, to, moverColor);
|
|
1408
|
+
const toRank = Number(to[1]);
|
|
1409
|
+
const promotionSquare = `${to[0]}${moverColor === "w" ? "8" : "1"}` as Square;
|
|
1410
|
+
const ranksRemaining = moverColor === "w" ? 8 - toRank : toRank - 1;
|
|
1411
|
+
const forwardRank = toRank + (moverColor === "w" ? 1 : -1);
|
|
1412
|
+
const forwardSquare = forwardRank >= 1 && forwardRank <= 8 ? `${to[0]}${forwardRank}` as Square : null;
|
|
1413
|
+
const captureSquares = [-1, 1].flatMap((fileDelta) => {
|
|
1414
|
+
const file = to.charCodeAt(0) - 97 + fileDelta;
|
|
1415
|
+
return file >= 0 && file <= 7 && forwardRank >= 1 && forwardRank <= 8
|
|
1416
|
+
? [`${String.fromCharCode(97 + file)}${forwardRank}` as Square]
|
|
1417
|
+
: [];
|
|
1418
|
+
});
|
|
1419
|
+
const squareOccupant = (square: Square | null) => {
|
|
1420
|
+
if (!square) return null;
|
|
1421
|
+
const piece = after.get(square);
|
|
1422
|
+
return piece ? { color: piece.color, pieceType: piece.type } : null;
|
|
1423
|
+
};
|
|
1424
|
+
const opponentKingSquare = moverColor === "w" ? blackKingSquare : whiteKingSquare;
|
|
1425
|
+
const opponentKingDistance = kingDistance(opponentKingSquare, promotionSquare).chebyshev;
|
|
1426
|
+
push(createRecord({
|
|
1427
|
+
contextHash,
|
|
1428
|
+
kind: "pawn.advance_state",
|
|
1429
|
+
phase: "transition",
|
|
1430
|
+
authority: "deterministic-derived",
|
|
1431
|
+
// A target pawn's immediate forward and capture geometry is direct
|
|
1432
|
+
// transition evidence. Keep it alongside other target-move facts in
|
|
1433
|
+
// constrained collections without treating the geometry as a future
|
|
1434
|
+
// plan or promotion result.
|
|
1435
|
+
priority: 97,
|
|
1436
|
+
summary: `After ${move.san}, the pawn is on ${to}. Its next forward square is ${forwardSquare || "none"} (${forwardSquare ? squareOccupant(forwardSquare) ? "occupied" : "empty" : "off board"}); its diagonal capture squares are ${captureSquares.map((square) => `${square} (${squareOccupant(square) ? "occupied" : "empty"})`).join(" and ") || "none"}. It has ${ranksRemaining} rank${ranksRemaining === 1 ? "" : "s"} remaining to ${promotionSquare}; ${pawnState.opposingPawnBlockers.length ? `opposing pawns ahead on its file or adjacent files are ${pawnState.opposingPawnBlockers.join(", ")}` : "no opposing pawn is ahead on its file or either adjacent file"}. This geometry does not assert that a future advance stays legal after the opponent replies.`,
|
|
1437
|
+
participants: target.participants,
|
|
1438
|
+
data: {
|
|
1439
|
+
from,
|
|
1440
|
+
to,
|
|
1441
|
+
color: moverColor,
|
|
1442
|
+
forwardSquare,
|
|
1443
|
+
forwardSquareOccupant: squareOccupant(forwardSquare),
|
|
1444
|
+
captureSquares,
|
|
1445
|
+
captureSquareOccupants: captureSquares.map((square) => ({ square, occupant: squareOccupant(square) })),
|
|
1446
|
+
promotionSquare,
|
|
1447
|
+
ranksRemaining,
|
|
1448
|
+
hasNoOpposingPawnAheadOnSameOrAdjacentFile: pawnState.passed,
|
|
1449
|
+
opposingPawnBlockers: pawnState.opposingPawnBlockers,
|
|
1450
|
+
opponentKingSquare,
|
|
1451
|
+
opponentKingDistanceToPromotionSquare: opponentKingDistance,
|
|
1452
|
+
sideToMove: after.turn(),
|
|
1453
|
+
},
|
|
1454
|
+
pieceLocations: target.locations,
|
|
1455
|
+
coverage: { domain: "target-pawn-advance", exhaustive: true, covered: [request.context.moveUci, to, promotionSquare], unresolved: [] },
|
|
1456
|
+
replay: { fen: request.context.fenBefore, movesUci: [request.context.moveUci] },
|
|
1457
|
+
}));
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
push(createRecord({
|
|
1461
|
+
contextHash,
|
|
1462
|
+
kind: "material.inventory",
|
|
1463
|
+
phase: "after",
|
|
1464
|
+
authority: "legal-exact",
|
|
1465
|
+
priority: 30,
|
|
1466
|
+
summary: "The complete after-position piece inventory is recorded without assigning strategic meaning.",
|
|
1467
|
+
participants: target.afterParticipants,
|
|
1468
|
+
data: { pieces: inventory(after, timeline.after) },
|
|
1469
|
+
pieceLocations: inventory(after, timeline.after).flatMap((piece) => piece.pieceId ? [{ pieceId: piece.pieceId, currentSquare: piece.square, phase: "after" }] : []),
|
|
1470
|
+
coverage: { domain: "after-position-piece-inventory", exhaustive: true, covered: ["all-occupied-squares"], unresolved: [] },
|
|
1471
|
+
replay: { fen: request.context.fenAfter },
|
|
1472
|
+
}));
|
|
1473
|
+
|
|
1474
|
+
const beforeWhite = nominalMaterial(beforeBoard, "w");
|
|
1475
|
+
const beforeBlack = nominalMaterial(beforeBoard, "b");
|
|
1476
|
+
const afterWhite = nominalMaterial(after, "w");
|
|
1477
|
+
const afterBlack = nominalMaterial(after, "b");
|
|
1478
|
+
push(createRecord({
|
|
1479
|
+
contextHash,
|
|
1480
|
+
kind: "material.nominal_delta",
|
|
1481
|
+
phase: "transition",
|
|
1482
|
+
authority: "deterministic-derived",
|
|
1483
|
+
priority: move.captured || move.promotion ? 86 : 28,
|
|
1484
|
+
summary: `Nominal material changes from White ${beforeWhite}/Black ${beforeBlack} to White ${afterWhite}/Black ${afterBlack}; this arithmetic is not an engine evaluation.`,
|
|
1485
|
+
participants: target.participants,
|
|
1486
|
+
data: { values: NOMINAL_VALUES, before: { white: beforeWhite, black: beforeBlack }, after: { white: afterWhite, black: afterBlack }, delta: { white: afterWhite - beforeWhite, black: afterBlack - beforeBlack } },
|
|
1487
|
+
pieceLocations: target.locations,
|
|
1488
|
+
coverage: { domain: "target-move-material", exhaustive: true, covered: [request.context.moveUci], unresolved: [] },
|
|
1489
|
+
replay: { fen: request.context.fenBefore, movesUci: [request.context.moveUci] },
|
|
1490
|
+
}));
|
|
1491
|
+
|
|
1492
|
+
push(createRecord({
|
|
1493
|
+
contextHash,
|
|
1494
|
+
kind: "position.legal_move_count",
|
|
1495
|
+
phase: "after",
|
|
1496
|
+
authority: "legal-exact",
|
|
1497
|
+
priority: 20,
|
|
1498
|
+
summary: `The side to move after ${move.san} has ${afterLegalMoves.length} legal moves.`,
|
|
1499
|
+
participants: [],
|
|
1500
|
+
data: { sideToMove: after.turn(), legalMoveCount: afterLegalMoves.length },
|
|
1501
|
+
pieceLocations: [],
|
|
1502
|
+
coverage: { domain: "all-legal-moves-after", exhaustive: true, covered: ["all-legal-moves"], unresolved: [] },
|
|
1503
|
+
replay: { fen: request.context.fenAfter },
|
|
1504
|
+
}));
|
|
1505
|
+
|
|
1506
|
+
const replyPieceLocations = afterLegalMoves.map((reply) => ({
|
|
1507
|
+
pieceId: pieceIdAt(timeline.after, reply.from as Square),
|
|
1508
|
+
currentSquare: reply.from,
|
|
1509
|
+
phase: "after",
|
|
1510
|
+
})).filter((item): item is { pieceId: string; currentSquare: Square; phase: string } => Boolean(item.pieceId))
|
|
1511
|
+
.filter((item, index, all) => all.findIndex((candidate) => candidate.pieceId === item.pieceId && candidate.currentSquare === item.currentSquare) === index);
|
|
1512
|
+
push(createRecord({
|
|
1513
|
+
contextHash,
|
|
1514
|
+
kind: "position.legal_moves",
|
|
1515
|
+
phase: "after",
|
|
1516
|
+
authority: "legal-exact",
|
|
1517
|
+
priority: after.isCheck() || afterLegalMoves.length <= 4 ? 92 : 26,
|
|
1518
|
+
summary: `${colorName(after.turn())} has exactly ${afterLegalMoves.length} legal move${afterLegalMoves.length === 1 ? "" : "s"} after ${move.san}: ${afterLegalMoves.map((reply) => reply.san).join(", ") || "none"}.`,
|
|
1519
|
+
participants: [],
|
|
1520
|
+
data: { sideToMove: after.turn(), moves: afterLegalMoves.map((reply) => ({ uci: moveUci(reply), san: reply.san, moverPieceId: pieceIdAt(timeline.after, reply.from as Square), from: reply.from, to: reply.to, capture: reply.captured || null, promotion: reply.promotion || null })) },
|
|
1521
|
+
pieceLocations: replyPieceLocations,
|
|
1522
|
+
coverage: { domain: "all-legal-moves-after", exhaustive: true, covered: afterLegalMoves.map(moveUci), unresolved: [] },
|
|
1523
|
+
replay: { fen: request.context.fenAfter },
|
|
1524
|
+
}));
|
|
1525
|
+
|
|
1526
|
+
if (signal?.aborted) throw signal.reason || new Error("Chess fact request cancelled");
|
|
1527
|
+
if (Date.now() - startedAt > request.budgets.maxWallTimeMs) throw new Error("Chess fact request exceeded its wall-time budget");
|
|
1528
|
+
const requested = new Set(request.requestedKinds);
|
|
1529
|
+
const ordered = [...candidates].sort((left, right) => right.priority - left.priority || left.kind.localeCompare(right.kind) || left.id.localeCompare(right.id));
|
|
1530
|
+
const selected = requested.size ? ordered.filter((record) => requested.has(record.kind)) : ordered;
|
|
1531
|
+
const omitted: ChessFactResult["omitted"] = requested.size
|
|
1532
|
+
? ordered.filter((record) => !requested.has(record.kind)).map((record) => ({ kind: record.kind, reason: "not-requested", recordId: record.id }))
|
|
1533
|
+
: [];
|
|
1534
|
+
const records: ChessFactRecord[] = [];
|
|
1535
|
+
let resultBytes = bytes({ schemaVersion: CHESS_FACT_RESULT_VERSION, contextHash, records: [], capabilities: CAPABILITIES, omitted: [], unavailableKinds: [], limitations: [] });
|
|
1536
|
+
// A caller asking for a named subset owns that subset. For the default
|
|
1537
|
+
// complete target-move collection, reserve the first ordinary budget slot
|
|
1538
|
+
// for the one protected destination geometry record. The protection never
|
|
1539
|
+
// expands maxRecords or maxBytes: if the caller's byte cap cannot express
|
|
1540
|
+
// that required target-move fact, fail explicitly rather than returning a
|
|
1541
|
+
// silently incomplete geometry set.
|
|
1542
|
+
const budgetProtected = requested.size ? [] : selected.filter((record) => record.budgetProtected);
|
|
1543
|
+
const ordinary = selected.filter((record) => !record.budgetProtected);
|
|
1544
|
+
for (const candidate of budgetProtected) {
|
|
1545
|
+
const record = { ...candidate } as Partial<CandidateRecord>;
|
|
1546
|
+
delete record.priority;
|
|
1547
|
+
delete record.budgetProtected;
|
|
1548
|
+
const fact = record as ChessFactRecord;
|
|
1549
|
+
const recordBytes = bytes(fact);
|
|
1550
|
+
if (records.length >= request.budgets.maxRecords) {
|
|
1551
|
+
throw new Error("target-destination-geometry-budget-invariant: maxRecords cannot reserve the required destination attack-map record");
|
|
1552
|
+
}
|
|
1553
|
+
if (resultBytes + recordBytes > request.budgets.maxBytes) {
|
|
1554
|
+
throw new Error("target-destination-geometry-budget-invariant: maxBytes cannot contain the required destination attack-map record");
|
|
1555
|
+
}
|
|
1556
|
+
records.push(fact);
|
|
1557
|
+
resultBytes += recordBytes;
|
|
1558
|
+
}
|
|
1559
|
+
for (const candidate of ordinary) {
|
|
1560
|
+
const record = { ...candidate } as Partial<CandidateRecord>;
|
|
1561
|
+
delete record.priority;
|
|
1562
|
+
delete record.budgetProtected;
|
|
1563
|
+
const fact = record as ChessFactRecord;
|
|
1564
|
+
const recordBytes = bytes(fact);
|
|
1565
|
+
if (records.length >= request.budgets.maxRecords || resultBytes + recordBytes > request.budgets.maxBytes) {
|
|
1566
|
+
omitted.push({ kind: fact.kind, reason: "budget", recordId: fact.id });
|
|
1567
|
+
} else {
|
|
1568
|
+
records.push(fact);
|
|
1569
|
+
resultBytes += recordBytes;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
const availableKinds = new Set(CAPABILITIES.map((capability) => capability.kind));
|
|
1573
|
+
return {
|
|
1574
|
+
schemaVersion: CHESS_FACT_RESULT_VERSION,
|
|
1575
|
+
contextHash,
|
|
1576
|
+
records,
|
|
1577
|
+
capabilities: [...CAPABILITIES],
|
|
1578
|
+
omitted,
|
|
1579
|
+
unavailableKinds: [...requested].filter((kind) => !availableKinds.has(kind)).map((kind) => ({ kind, reason: "The mechanical provider does not implement this registry kind." })),
|
|
1580
|
+
limitations: [
|
|
1581
|
+
"Mechanical browser provider slice; no semantic plans, classifications, engine evaluation, SEE, or exchange-safety claims are emitted.",
|
|
1582
|
+
"Geometric attack deltas do not establish that a target is tactically loose, hanging, or safely capturable.",
|
|
1583
|
+
],
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
export function createMechanicalFactRequest(
|
|
1589
|
+
context: ReasoningPositionContext,
|
|
1590
|
+
overrides: Partial<ChessFactRequest["budgets"]> = {},
|
|
1591
|
+
): ChessFactRequest {
|
|
1592
|
+
return {
|
|
1593
|
+
schemaVersion: CHESS_FACT_REQUEST_VERSION,
|
|
1594
|
+
requestId: `mechanical-facts:${reasoningContextHash(context)}`,
|
|
1595
|
+
context,
|
|
1596
|
+
requestedKinds: [],
|
|
1597
|
+
// This helper is used by offline experiments and deterministic regression
|
|
1598
|
+
// fixtures. Live browser requests carry the run's explicit budget through
|
|
1599
|
+
// ChessFactObservationProvider instead of inheriting this default.
|
|
1600
|
+
budgets: { maxRecords: 64, maxBytes: 160_000, maxWallTimeMs: 5_000, ...overrides },
|
|
1601
|
+
};
|
|
1602
|
+
}
|