Migrate remaining legacy operator workflows

This commit is contained in:
2026-07-12 05:39:27 +09:00
parent ffb8f43c19
commit a01836a2d7
132 changed files with 20566 additions and 720 deletions

View File

@@ -13,9 +13,11 @@
const MAXIMUM_QUERY_LENGTH = 64;
const MAXIMUM_WEB_SAFE_INTEGER = Number.MAX_SAFE_INTEGER;
const requestIdPattern = /^[A-Za-z0-9_-]{1,128}$/;
const themeCodePattern = /^[0-9]{8}$/;
const newThemeCodePattern = /^[0-9]{8}$/;
const existingThemeCodePattern = /^[0-9]{3,8}$/;
const expertCodePattern = /^[0-9]{4}$/;
const stockCodePattern = /^[A-Za-z0-9]{1,32}$/;
const stockMarkets = Object.freeze(["kospi", "kosdaq", "nxt-kospi", "nxt-kosdaq"]);
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const unsafeTextPattern = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}\uFFFD]/u;
@@ -44,6 +46,12 @@
})
});
const stockMasterContract = Object.freeze({
request: "search-stocks",
result: "stock-search-results",
error: "stock-search-error"
});
function hasExactKeys(value, keys) {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
const actual = Object.keys(value).sort();
@@ -95,7 +103,7 @@
if (!hasExactKeys(value, ["market", "session", "themeCode", "themeTitle"])) return null;
const market = normalizeMarket(value.market);
if (!market || !normalizeSession(value.session, market) ||
!themeCodePattern.test(value.themeCode) ||
!existingThemeCodePattern.test(value.themeCode) ||
!isSafeCanonicalText(value.themeTitle, 128) ||
(market === "nxt" && /\(NXT\)/i.test(value.themeTitle))) return null;
return Object.freeze({
@@ -141,7 +149,7 @@
function normalizeThemeDraft(value) {
if (!hasExactKeys(value, ["market", "themeCode", "themeTitle", "items"])) return null;
const market = normalizeMarket(value.market);
if (!market || !themeCodePattern.test(value.themeCode) ||
if (!market || !existingThemeCodePattern.test(value.themeCode) ||
!isSafeCanonicalText(value.themeTitle, 128) ||
(market === "nxt" && /\(NXT\)/i.test(value.themeTitle))) return null;
const items = normalizeThemeItems(value.items, market);
@@ -250,6 +258,75 @@
return Object.freeze({ requestId: requireRequestId(requestId) });
}
function createStockMasterSearchRequest(requestId, query, maximumResults = MAXIMUM_RESULTS) {
requireRequestId(requestId);
const normalizedQuery = normalizeQuery(query);
if (!normalizedQuery) throw new TypeError("A non-empty stock-master search query is required.");
if (!Number.isInteger(maximumResults) || maximumResults < 1 ||
maximumResults > MAXIMUM_RESULTS) {
throw new RangeError("Stock-master results must be limited from 1 through 500.");
}
return Object.freeze({ requestId, query: normalizedQuery, maximumResults });
}
function normalizeStockMasterIdentity(value) {
if (!hasExactKeys(value, ["market", "source", "name", "code"]) ||
!stockMarkets.includes(value.market) || !stockCodePattern.test(value.code) ||
!isSafeCanonicalText(value.name, 200)) return null;
const expectedSource = value.market === "kospi" || value.market === "kosdaq"
? "oracle"
: "mariaDb";
if (value.source !== expectedSource) return null;
return Object.freeze({
market: value.market,
source: value.source,
name: value.name,
code: value.code
});
}
function normalizeStockMasterSearchResult(value, expectedRequest) {
if (!hasExactKeys(value, [
"requestId", "query", "retrievedAt", "totalRowCount", "truncated", "results"
]) || !expectedRequest || value.requestId !== expectedRequest.requestId ||
value.query !== expectedRequest.query || !requestIdPattern.test(value.requestId) ||
normalizeQuery(value.query) !== value.query ||
!isSafeCanonicalText(value.retrievedAt, 64) ||
!Number.isFinite(Date.parse(value.retrievedAt)) ||
!Number.isInteger(value.totalRowCount) || value.totalRowCount < 0 ||
value.truncated !== false || !Array.isArray(value.results) ||
value.results.length !== value.totalRowCount ||
value.results.length > expectedRequest.maximumResults) return null;
const results = [];
const identities = new Set();
const names = new Set();
for (const raw of value.results) {
const item = normalizeStockMasterIdentity(raw);
const identity = item && `${item.market}\u0000${item.source}\u0000${item.code}\u0000${item.name}`;
if (!item || identities.has(identity) || names.has(item.name)) return null;
identities.add(identity);
names.add(item.name);
results.push(item);
}
return Object.freeze({
requestId: value.requestId,
query: value.query,
retrievedAt: value.retrievedAt,
totalRowCount: value.totalRowCount,
truncated: false,
results: Object.freeze(results)
});
}
function normalizeStockMasterSearchError(value, expectedRequest) {
if (!hasExactKeys(value, ["requestId", "query", "message"]) || !expectedRequest ||
value.requestId !== expectedRequest.requestId || value.query !== expectedRequest.query ||
!requestIdPattern.test(value.requestId) || normalizeQuery(value.query) !== value.query ||
!isSafeCanonicalText(value.message, 512)) return null;
return Object.freeze({ ...value });
}
function normalizeThemeCatalogSelection(value) {
if (!hasExactKeys(value, ["market", "session", "themeCode", "themeTitle", "source"])) return null;
const identity = normalizeThemeIdentity({
@@ -392,7 +469,7 @@
}
function createBlankThemeDraft(market, themeCode) {
if (!normalizeMarket(market) || !themeCodePattern.test(themeCode)) {
if (!normalizeMarket(market) || !newThemeCodePattern.test(themeCode)) {
throw new TypeError("A typed market and suggested eight-digit theme code are required.");
}
return { market, themeCode, themeTitle: "", items: [] };
@@ -427,7 +504,9 @@
function createThemeCreateRequest(requestId, draftValue) {
requireRequestId(requestId);
const draft = cloneThemeDraft(draftValue);
if (!draft) throw new TypeError("A complete theme draft is required.");
if (!draft || !newThemeCodePattern.test(draft.themeCode)) {
throw new TypeError("A complete theme draft with a new eight-digit code is required.");
}
return Object.freeze({ requestId, draft });
}
@@ -550,7 +629,7 @@
if (!hasExactKeys(value, [
"requestId", "market", "themeCode", "canMutate", "writeQuarantined"
]) || !requestIdPattern.test(value.requestId) || !normalizeMarket(value.market) ||
!themeCodePattern.test(value.themeCode) || typeof value.canMutate !== "boolean" ||
!newThemeCodePattern.test(value.themeCode) || typeof value.canMutate !== "boolean" ||
typeof value.writeQuarantined !== "boolean" ||
(value.writeQuarantined && value.canMutate)) return null;
if (expectedRequest && (value.requestId !== expectedRequest.requestId ||
@@ -591,7 +670,8 @@
]) || !requestIdPattern.test(value.requestId) ||
(value.entity !== "theme" && value.entity !== "expert") ||
!mutationOperations.has(value.operation) ||
!(value.entity === "theme" ? themeCodePattern : expertCodePattern).test(value.identityCode) ||
!(value.entity === "theme" ? existingThemeCodePattern : expertCodePattern)
.test(value.identityCode) ||
!uuidPattern.test(value.operationId) || !isSafeCanonicalText(value.committedAt, 64) ||
!Number.isFinite(Date.parse(value.committedAt)) || value.writeQuarantined !== false) return null;
if (expected && ["requestId", "entity", "operation", "identityCode"]
@@ -721,6 +801,7 @@
return {
bridgeContract,
stockMasterContract,
limits: Object.freeze({
maximumThemeItems: MAXIMUM_THEME_ITEMS,
maximumRecommendations: MAXIMUM_RECOMMENDATIONS,
@@ -736,6 +817,10 @@
createSchemaPreflightRequest,
createThemeNextCodeRequest,
createExpertNextCodeRequest,
createStockMasterSearchRequest,
normalizeStockMasterIdentity,
normalizeStockMasterSearchResult,
normalizeStockMasterSearchError,
normalizeThemeCatalogSelection,
normalizeExpertCatalogSelection,
createThemeEditDraft,