@hcmai/sdk 0.3.5 → 0.3.7
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/dist/index.d.ts +210 -1
- package/dist/index.js +1858 -31
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -1068,18 +1068,18 @@ async function action(http, model, actionName, input) {
|
|
|
1068
1068
|
if (scope === "object" && !input.id) {
|
|
1069
1069
|
throw new Error(`object action ${model}.${actionName} requires id`);
|
|
1070
1070
|
}
|
|
1071
|
-
const
|
|
1071
|
+
const path9 = scope === "object" ? `/api/models/${model}/${input.id}/action/${actionName}` : `/api/models/${model}/action/${actionName}`;
|
|
1072
1072
|
const resp = await (async () => {
|
|
1073
1073
|
if (method === "GET") {
|
|
1074
|
-
return http.get(
|
|
1074
|
+
return http.get(path9, { params: queryParams(input) });
|
|
1075
1075
|
}
|
|
1076
1076
|
if (method === "DELETE") {
|
|
1077
|
-
return http.delete(
|
|
1077
|
+
return http.delete(path9, { params: queryParams(input) });
|
|
1078
1078
|
}
|
|
1079
1079
|
if (method === "PUT") {
|
|
1080
|
-
return http.put(
|
|
1080
|
+
return http.put(path9, scope === "object" ? objectBody(input) : classBody(input));
|
|
1081
1081
|
}
|
|
1082
|
-
return http.post(
|
|
1082
|
+
return http.post(path9, scope === "object" ? objectBody(input) : classBody(input));
|
|
1083
1083
|
})();
|
|
1084
1084
|
return resp.data;
|
|
1085
1085
|
}
|
|
@@ -1812,7 +1812,7 @@ var WsClient = class {
|
|
|
1812
1812
|
});
|
|
1813
1813
|
}
|
|
1814
1814
|
this.ws = new WebSocket(this.opts.endpoint);
|
|
1815
|
-
return new Promise((resolve4,
|
|
1815
|
+
return new Promise((resolve4, reject2) => {
|
|
1816
1816
|
const settle = (fn) => {
|
|
1817
1817
|
off();
|
|
1818
1818
|
fn();
|
|
@@ -1857,7 +1857,7 @@ var WsClient = class {
|
|
|
1857
1857
|
this.ws.on("error", this.persistentOnClose);
|
|
1858
1858
|
} else if (msg.type === "auth_error") {
|
|
1859
1859
|
settle(
|
|
1860
|
-
() =>
|
|
1860
|
+
() => reject2(
|
|
1861
1861
|
new CliError({
|
|
1862
1862
|
code: "WS_AUTH_ERROR" /* WS_AUTH_ERROR */,
|
|
1863
1863
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -1870,10 +1870,10 @@ var WsClient = class {
|
|
|
1870
1870
|
}
|
|
1871
1871
|
};
|
|
1872
1872
|
const onError = (err) => settle(
|
|
1873
|
-
() =>
|
|
1873
|
+
() => reject2(new CliError({ code: "NETWORK_ERROR" /* NETWORK_ERROR */, message: err.message }))
|
|
1874
1874
|
);
|
|
1875
1875
|
const onClose = () => settle(
|
|
1876
|
-
() =>
|
|
1876
|
+
() => reject2(
|
|
1877
1877
|
new CliError({ code: "NETWORK_ERROR" /* NETWORK_ERROR */, message: "WS closed before auth" })
|
|
1878
1878
|
)
|
|
1879
1879
|
);
|
|
@@ -5035,18 +5035,18 @@ async function listTenantMeta(http, opts = {}) {
|
|
|
5035
5035
|
const items = payload?.items ?? payload?.list ?? payload?.records ?? [];
|
|
5036
5036
|
return { items, total: payload?.total, raw: payload };
|
|
5037
5037
|
}
|
|
5038
|
-
async function getTenantMeta(http,
|
|
5039
|
-
const resp = await http.get("/api/tenant-meta/meta", { params: { path:
|
|
5038
|
+
async function getTenantMeta(http, path9) {
|
|
5039
|
+
const resp = await http.get("/api/tenant-meta/meta", { params: { path: path9 } });
|
|
5040
5040
|
const payload = resp.data?.data ?? resp.data;
|
|
5041
5041
|
if (typeof payload === "string") return payload;
|
|
5042
5042
|
return String(payload?.content ?? "");
|
|
5043
5043
|
}
|
|
5044
|
-
async function saveTenantMeta(http,
|
|
5045
|
-
const resp = await http.put("/api/tenant-meta/meta", { content }, { params: { path:
|
|
5044
|
+
async function saveTenantMeta(http, path9, content) {
|
|
5045
|
+
const resp = await http.put("/api/tenant-meta/meta", { content }, { params: { path: path9 } });
|
|
5046
5046
|
return resp.data?.data ?? resp.data;
|
|
5047
5047
|
}
|
|
5048
|
-
async function deleteTenantMeta(http,
|
|
5049
|
-
const resp = await http.delete("/api/tenant-meta/meta", { params: { path:
|
|
5048
|
+
async function deleteTenantMeta(http, path9) {
|
|
5049
|
+
const resp = await http.delete("/api/tenant-meta/meta", { params: { path: path9 } });
|
|
5050
5050
|
return resp.data?.data ?? resp.data;
|
|
5051
5051
|
}
|
|
5052
5052
|
|
|
@@ -5148,14 +5148,14 @@ function resolveSkillInstallOrder(manifest2, targetId) {
|
|
|
5148
5148
|
throw invalid(`\u6280\u80FD\u76EE\u5F55\u4E2D\u4E0D\u5B58\u5728 ${targetId}`);
|
|
5149
5149
|
}
|
|
5150
5150
|
const state = /* @__PURE__ */ new Map();
|
|
5151
|
-
const
|
|
5151
|
+
const path9 = [];
|
|
5152
5152
|
const order = [];
|
|
5153
5153
|
const visit = (id, requiredBy) => {
|
|
5154
5154
|
const current = state.get(id);
|
|
5155
5155
|
if (current === "done") return;
|
|
5156
5156
|
if (current === "visiting") {
|
|
5157
|
-
const cycleStart =
|
|
5158
|
-
const cycle = [...
|
|
5157
|
+
const cycleStart = path9.indexOf(id);
|
|
5158
|
+
const cycle = [...path9.slice(Math.max(0, cycleStart)), id];
|
|
5159
5159
|
throw invalid(`\u6280\u80FD\u4F9D\u8D56\u5B58\u5728\u73AF: ${cycle.join(" -> ")}`);
|
|
5160
5160
|
}
|
|
5161
5161
|
const skill = byId.get(id);
|
|
@@ -5164,11 +5164,11 @@ function resolveSkillInstallOrder(manifest2, targetId) {
|
|
|
5164
5164
|
}
|
|
5165
5165
|
assertSafeSkillId(skill.id);
|
|
5166
5166
|
state.set(id, "visiting");
|
|
5167
|
-
|
|
5167
|
+
path9.push(id);
|
|
5168
5168
|
for (const required of normalizeRequirements(skill.requires, skill.id)) {
|
|
5169
5169
|
visit(required, skill.id);
|
|
5170
5170
|
}
|
|
5171
|
-
|
|
5171
|
+
path9.pop();
|
|
5172
5172
|
state.set(id, "done");
|
|
5173
5173
|
order.push(skill);
|
|
5174
5174
|
};
|
|
@@ -5259,11 +5259,11 @@ async function resetSettingItem(http, domain, namespace, settingKey, revision) {
|
|
|
5259
5259
|
function parseSettingAssignment(raw) {
|
|
5260
5260
|
const eq = raw.indexOf("=");
|
|
5261
5261
|
if (eq <= 0) throw new Error(`--set \u9700\u8981 namespace.key=value \u5F62\u6001\uFF0C\u6536\u5230 "${raw}"`);
|
|
5262
|
-
const
|
|
5262
|
+
const path9 = raw.slice(0, eq).trim();
|
|
5263
5263
|
const value = raw.slice(eq + 1);
|
|
5264
|
-
const dot =
|
|
5265
|
-
if (dot <= 0) throw new Error(`--set \u7684\u952E\u9700\u542B namespace\uFF0C\u5982 security.password.passwordPolicy=complex\uFF0C\u6536\u5230 "${
|
|
5266
|
-
return { namespace:
|
|
5264
|
+
const dot = path9.lastIndexOf(".");
|
|
5265
|
+
if (dot <= 0) throw new Error(`--set \u7684\u952E\u9700\u542B namespace\uFF0C\u5982 security.password.passwordPolicy=complex\uFF0C\u6536\u5230 "${path9}"`);
|
|
5266
|
+
return { namespace: path9.slice(0, dot), settingKey: path9.slice(dot + 1), value };
|
|
5267
5267
|
}
|
|
5268
5268
|
|
|
5269
5269
|
// src/fde/layout.ts
|
|
@@ -5460,6 +5460,11 @@ function manifestDigest(rows) {
|
|
|
5460
5460
|
const lines = rows.map((r) => `${r.name}|${r.size}|${r.lastModified}`).sort();
|
|
5461
5461
|
return sha2562(lines.join("\n"));
|
|
5462
5462
|
}
|
|
5463
|
+
async function computeReconSourceKey(probe) {
|
|
5464
|
+
const out = await probe.get(MODELS_PATH);
|
|
5465
|
+
const p = await probeExtensionSource(probe, modelKeys(out.body));
|
|
5466
|
+
return p.sourceKey || "unknown";
|
|
5467
|
+
}
|
|
5463
5468
|
async function computeFingerprint(probe, opts = {}) {
|
|
5464
5469
|
const ttlDays = opts.ttlDays ?? 30;
|
|
5465
5470
|
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
@@ -5652,11 +5657,11 @@ function classifyStatus(status) {
|
|
|
5652
5657
|
return "bad-response";
|
|
5653
5658
|
}
|
|
5654
5659
|
var CAPABILITIES_HEADER = "x-hcm-capabilities";
|
|
5655
|
-
function buildUrl(
|
|
5660
|
+
function buildUrl(path9, params) {
|
|
5656
5661
|
const entries = Object.entries(params ?? {}).filter(([, v]) => v !== void 0 && v !== null);
|
|
5657
|
-
if (entries.length === 0) return
|
|
5662
|
+
if (entries.length === 0) return path9;
|
|
5658
5663
|
const qs = entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
|
|
5659
|
-
return `${
|
|
5664
|
+
return `${path9}${path9.includes("?") ? "&" : "?"}${qs}`;
|
|
5660
5665
|
}
|
|
5661
5666
|
function lowerHeaders(raw) {
|
|
5662
5667
|
const out = {};
|
|
@@ -5691,20 +5696,21 @@ function httpProbe(http, capabilities) {
|
|
|
5691
5696
|
} catch (e) {
|
|
5692
5697
|
const msg = e instanceof Error ? e.message : String(e);
|
|
5693
5698
|
const local = /Invalid URL|ERR_INVALID_URL|ERR_UNESCAPED_CHARACTERS/i.test(msg);
|
|
5699
|
+
if (!local && isProgrammingError(e)) throw e;
|
|
5694
5700
|
return { status: -1, body: null, error: local ? "bad-response" : "network", headers: {} };
|
|
5695
5701
|
}
|
|
5696
5702
|
}
|
|
5697
5703
|
const headers = { [CAPABILITIES_HEADER]: capabilities };
|
|
5698
5704
|
const probe = {
|
|
5699
5705
|
capabilities,
|
|
5700
|
-
get(
|
|
5706
|
+
get(path9, params) {
|
|
5701
5707
|
return once(
|
|
5702
|
-
() => http.get(buildUrl(
|
|
5708
|
+
() => http.get(buildUrl(path9, params), { headers, validateStatus: () => true })
|
|
5703
5709
|
);
|
|
5704
5710
|
},
|
|
5705
|
-
post(
|
|
5711
|
+
post(path9, body) {
|
|
5706
5712
|
return once(
|
|
5707
|
-
() => http.post(
|
|
5713
|
+
() => http.post(path9, body, {
|
|
5708
5714
|
headers: { ...headers, "content-type": "application/json" },
|
|
5709
5715
|
validateStatus: () => true
|
|
5710
5716
|
})
|
|
@@ -5736,9 +5742,1815 @@ function httpProbe(http, capabilities) {
|
|
|
5736
5742
|
};
|
|
5737
5743
|
return probe;
|
|
5738
5744
|
}
|
|
5745
|
+
function isProgrammingError(e) {
|
|
5746
|
+
if (!(e instanceof Error)) return false;
|
|
5747
|
+
if (e.isAxiosError) return false;
|
|
5748
|
+
return e instanceof TypeError || e instanceof ReferenceError || e instanceof SyntaxError;
|
|
5749
|
+
}
|
|
5739
5750
|
function networkOutcome() {
|
|
5740
5751
|
return { status: -1, body: null, error: "network", headers: {} };
|
|
5741
5752
|
}
|
|
5753
|
+
|
|
5754
|
+
// src/fde/recon/runner.ts
|
|
5755
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
5756
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
|
|
5757
|
+
import * as path8 from "path";
|
|
5758
|
+
|
|
5759
|
+
// src/fde/recon/carry-over.ts
|
|
5760
|
+
var KIT_CARRY_OVER_KEY = "_kitCarryOver";
|
|
5761
|
+
|
|
5762
|
+
// src/fde/recon/notes.ts
|
|
5763
|
+
var NOTE_404 = "HTTP 404\uFF1A\u591A\u4E3A @HiddenFromRegistry \u578B\u6A21\u578B\uFF08\u5982\u5404\u79CD *Index\uFF09\uFF0C\u4EA7\u54C1\u5BF9\u5176\u6052\u8FD4 DATA_NOT_FOUND\u3002\u5C5E\u6B63\u5E38\u5206\u7C7B\u7ED3\u679C\uFF0C\u975E\u6545\u969C";
|
|
5764
|
+
var NOTE_NETWORK = "\u672A\u6536\u5230 HTTP \u54CD\u5E94\uFF08\u6052 status=-1\uFF09\u3002\u{1F534} \u672C\u6876**\u4E0D\u53EA\u88C5\u7F51\u7EDC\u6545\u969C**\uFF1B\u4E0B\u9762\u662F**\u5DF2\u5B9E\u6D4B**\u56DB\u7C7B\uFF0C**\u4E0D\u4FDD\u8BC1\u5217\u5168**\uFF08\u6279\u91CF\u8DEF\u5F84\u515C\u5E95\u63A5\u4F4F\u4E00\u5207\u5F02\u5E38\uFF0C\u672C\u5C42\u4EFB\u4F55\u65B0\u5F02\u5E38\u90FD\u4F1A\u843D\u8FD9\u91CC\uFF09\uFF1A\u2460 modelKey \u542B\u7A7A\u683C/\u63A7\u5236\u5B57\u7B26 \u2192 URL \u7578\u5F62\uFF0C\u5DE5\u5177\u4FA7\u95EE\u9898\uFF1B\u2461 \u7AEF\u70B9\u6F0F\u5199 http:// \u4E14\u5E26\u7AEF\u53E3\uFF08\u5982 localhost:8087\uFF09\u2192\u300Cunknown url type\u300D\uFF0C\u914D\u7F6E\u95EE\u9898\uFF1B\u2462 https + \u81EA\u7B7E/\u5185\u7F51 CA \u2192 TLS \u8BC1\u4E66\u95EE\u9898\uFF0C\u914D CA \u6216\u6539 http\uFF0C\u67E5\u94FE\u8DEF\u662F\u767D\u67E5\uFF1B\u2463 \u5DE5\u5177\u81EA\u8EAB\u5F02\u5E38 \u2192 \u6279\u91CF\u8DEF\u5F84\u515C\u6210\u672C\u6876\uFF0C**\u53BB\u770B stderr \u91CC\u7684\u5806\u6808**\u3002\u90FD\u4E0D\u50CF\u624D\u8F6E\u5230\u67E5 VPN / DNS / \u94FE\u8DEF\u3002\u5360\u6BD4\u4E5F\u662F\u5224\u636E\uFF1A\u6574\u6279\u5168\u843D\u672C\u6876 \u2192 \u7AEF\u70B9/\u8BC1\u4E66/\u7F51\u7EDC\uFF1B\u53EA\u4E2A\u522B\u51E0\u6761 \u2192 \u90A3\u4E2A modelKey \u6216\u5DE5\u5177 bug";
|
|
5765
|
+
var NOTE_BAD_RESPONSE = '\u5206\u7C7B\u6309 status \u8D70\uFF0C**\u4E0D\u4EE3\u8868\u6B63\u6587\u4E00\u5B9A\u4E0D\u662F JSON**\u3002\u6309 status \u5206\u6D41\uFF1Astatus=-1 \u2192 \u8BF7\u6C42**\u6839\u672C\u6CA1\u53D1\u51FA\u53BB**\uFF08\u540C\u6837**\u4E0D\u4FDD\u8BC1\u5217\u5168**\uFF09\uFF1AmodelKey \u542B**\u975E ASCII**\uFF08\u5982\u4E2D\u6587\uFF09\u2192 \u53D1\u9001\u9636\u6BB5\u7F16\u7801\u5931\u8D25\uFF1B\u6216\u7AEF\u70B9\u6F0F\u5199 http:// \u4E14**\u4E0D\u5E26\u7AEF\u53E3**\uFF08\u5982 myserver\uFF09\u2192\u300Cunknown url type\u300D\u3002\u26A0\uFE0F \u542B**\u7A7A\u683C**\u7684 modelKey\u3001\u4EE5\u53CA\u6F0F scheme \u4F46**\u5E26\u7AEF\u53E3**\u7684\u7AEF\u70B9\uFF0C\u843D\u7684\u662F network \u6876\u4E0D\u5728\u8FD9\u91CC\uFF1Bstatus=2xx \u2192 \u670D\u52A1\u7AEF\u8FD4\u56DE\u4E86\u7A7A\u4F53\u3001\u975E JSON \u6B63\u6587\u6216 JSON null\uFF08\u300C\u6CA1\u7ED9\u5230\u6570\u636E\u300D\uFF0C\u975E\u300C\u7A7A\u96C6\u5408\u300D\uFF09\u2014\u2014 \u53EA\u6709\u8FD9\u4E00\u6863\u7684**\u5206\u7C7B\u539F\u56E0**\u662F\u89E3\u6790\u5931\u8D25\uFF1B\u5176\u4F59 status\uFF08400/409/422 \u7B49\u672A\u88AB\u5355\u72EC\u5206\u6863\u7684\u975E 2xx\uFF09\u2192 \u8BCA\u65AD\u5728**\u72B6\u6001\u7801**\u91CC\uFF0C\u6B63\u6587\u5F88\u53EF\u80FD\u662F\u4E00\u4EFD**\u5B8C\u597D**\u7684\u4E1A\u52A1\u9519\u8BEF\u4FE1\u5C01\uFF08{"error":{"code","message","details"},"success":false}\uFF0C\u4EBA\u8BDD\u5728 details \u91CC\uFF1Berror \u8FD8\u53EF\u80FD\u5E26 field\uFF0C\u8BE5\u952E\u56DE\u6E90\u81EA Java DTO\u30012026-08-16 \u5B9E\u6D4B\u4E09\u7AEF\u70B9\u5747\u672A\u51FA\u73B0\uFF09\uFF0C\u503C\u5F97\u770B\u4E00\u773C\uFF1B\u4F46\u672C\u5DE5\u5177\u7684 GET \u8DEF\u5F84\u4E0D\u4FDD\u7559\u975E 2xx \u7684\u6B63\u6587\uFF08\u4EA7\u7269\u91CC\u6CA1\u6709\u5B83\uFF09\uFF0C\u8981\u770B\u8FD9\u6BB5\u4EBA\u8BDD\u5F97\u7167\u539F\u6837\u91CD\u6253\u4E00\u6B21\u8BE5\u8BF7\u6C42\uFF08curl \u5373\u53EF\uFF09\u3002\u4E5F\u53EF\u80FD\u662F\u7F51\u5173/\u53CD\u4EE3\u5410\u7684 HTML \u515C\u5E95\u9875';
|
|
5766
|
+
var UNAVAILABLE_NOTES = {
|
|
5767
|
+
"not-found": NOTE_404,
|
|
5768
|
+
forbidden: "HTTP 403\uFF1A\u5F53\u524D\u8D26\u53F7\u5BF9\u8BE5\u6A21\u578B\u65E0\u6743\u9650\u3002\u4EA7\u7269\u8986\u76D6\u9762\u56E0\u6B64\u53D7\u9650\uFF0C\u987B\u8BB0\u8FDB\u6743\u9650\u753B\u50CF",
|
|
5769
|
+
unauthorized: "HTTP 401\uFF1Atoken \u5931\u6548\u6216\u672A\u8BA4\u8BC1",
|
|
5770
|
+
network: NOTE_NETWORK,
|
|
5771
|
+
"bad-response": NOTE_BAD_RESPONSE,
|
|
5772
|
+
"server-error": "\u670D\u52A1\u7AEF 5xx"
|
|
5773
|
+
};
|
|
5774
|
+
function unclassifiedNote(error) {
|
|
5775
|
+
return `\u672A\u5206\u7C7B\u5931\u8D25\uFF1A\u5206\u7C7B\u4E32 ${JSON.stringify(error)} \u4E0D\u5728\u5DE5\u5177\u7684 note \u8868\u91CC\u3002\u672C\u5C42\u91CC\u7ED9\u8FD9\u4E2A\u53C2\u6570\u5582\u503C\u7684\u662F \`crawlMeta\` \u4E0E \`collectActionGates\` \u4E24\u5904\uFF0C\u4E24\u5904\u4F20\u7684\u90FD\u662F\u540C\u4E00\u8FDB\u7A0B\u91CC\u63A2\u9488\u7ED9\u7684\u5206\u7C7B\u4E32\uFF08\u540C\u4E00\u4EFD\u5B89\u88C5\uFF09\u2014\u2014\u5728\u8FD9\u6761\u94FE\u8DEF\u4E0A\u5B83\u4E0E note \u8868\u672C\u8BE5\u9010\u4E00\u5BF9\u4E0A\u3002\u522B\u7684\u6765\u6E90\u672C\u6A21\u5757**\u6CA1\u6709\u67E5\u8FC7**\uFF1B\u628A\u4E0A\u9762\u8FD9\u4E2A\u5206\u7C7B\u4E32\u539F\u6837\u62A5\u56DE\u6765`;
|
|
5776
|
+
}
|
|
5777
|
+
function noteFor(error) {
|
|
5778
|
+
return UNAVAILABLE_NOTES[error] ?? unclassifiedNote(error);
|
|
5779
|
+
}
|
|
5780
|
+
|
|
5781
|
+
// src/fde/recon/crawl.ts
|
|
5782
|
+
var KIT_PREMISE_KEY = "_kitPremiseCheck";
|
|
5783
|
+
var DEFAULT_META_TYPE = "model";
|
|
5784
|
+
function metaPath(modelKey, metaType = DEFAULT_META_TYPE) {
|
|
5785
|
+
const base = `/api/models/${modelKey}/meta`;
|
|
5786
|
+
return metaType === DEFAULT_META_TYPE ? base : `${base}?type=${metaType}`;
|
|
5787
|
+
}
|
|
5788
|
+
function modelRows(body) {
|
|
5789
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return [];
|
|
5790
|
+
const rows = body.models;
|
|
5791
|
+
if (!Array.isArray(rows)) return [];
|
|
5792
|
+
return rows.filter((m) => Boolean(m) && typeof m === "object" && !Array.isArray(m));
|
|
5793
|
+
}
|
|
5794
|
+
function shapeOf(value) {
|
|
5795
|
+
if (value === null) return "null";
|
|
5796
|
+
if (Array.isArray(value)) return "list";
|
|
5797
|
+
return typeof value === "object" ? "dict" : typeof value;
|
|
5798
|
+
}
|
|
5799
|
+
async function crawlModels(probe) {
|
|
5800
|
+
const out = await probe.get(MODELS_PATH);
|
|
5801
|
+
const healthy = Boolean(out.body) && typeof out.body === "object" && !Array.isArray(out.body);
|
|
5802
|
+
const body = healthy ? { ...out.body } : { totalCount: 0, typeCounts: {}, models: [] };
|
|
5803
|
+
const rows = Array.isArray(body.models) ? body.models : void 0;
|
|
5804
|
+
const premise = {
|
|
5805
|
+
// 前提显式断言:totalCount 与 models 长度不符时,后续任何「共 N 个模型」都不成立。
|
|
5806
|
+
totalCountMatchesListLength: Boolean(healthy && rows !== void 0 && body.totalCount === rows.length),
|
|
5807
|
+
responseShape: shapeOf(out.body),
|
|
5808
|
+
status: out.status,
|
|
5809
|
+
error: out.error,
|
|
5810
|
+
// 撞键与否**恒有值**:没有这个键时「没撞」与「没检查过」在产物里同形。
|
|
5811
|
+
collidedWithServerKey: Object.prototype.hasOwnProperty.call(body, KIT_PREMISE_KEY)
|
|
5812
|
+
};
|
|
5813
|
+
if (premise.collidedWithServerKey) {
|
|
5814
|
+
premise.serverValue = body[KIT_PREMISE_KEY];
|
|
5815
|
+
}
|
|
5816
|
+
body[KIT_PREMISE_KEY] = premise;
|
|
5817
|
+
return body;
|
|
5818
|
+
}
|
|
5819
|
+
async function crawlMeta(probe, modelKeys2, metaType = DEFAULT_META_TYPE) {
|
|
5820
|
+
const paths = /* @__PURE__ */ new Map();
|
|
5821
|
+
for (const key of modelKeys2) paths.set(metaPath(key, metaType), key);
|
|
5822
|
+
const results = probe.getMany ? await probe.getMany([...paths.keys()]) : await sequentialGet(probe, [...paths.keys()]);
|
|
5823
|
+
const metas = {};
|
|
5824
|
+
const unavailable = [];
|
|
5825
|
+
for (const [path9, key] of paths) {
|
|
5826
|
+
const out = results[path9];
|
|
5827
|
+
if (out === void 0) {
|
|
5828
|
+
unavailable.push({
|
|
5829
|
+
modelKey: key,
|
|
5830
|
+
metaType,
|
|
5831
|
+
status: -1,
|
|
5832
|
+
error: "not-crawled",
|
|
5833
|
+
note: "\u5E76\u53D1\u6279\u6B21\u91CC\u6CA1\u6709\u8FD4\u56DE\u8BE5 path"
|
|
5834
|
+
});
|
|
5835
|
+
continue;
|
|
5836
|
+
}
|
|
5837
|
+
const isObject = Boolean(out.body) && typeof out.body === "object" && !Array.isArray(out.body);
|
|
5838
|
+
if (out.error === "ok" && isObject) {
|
|
5839
|
+
metas[key] = out.body;
|
|
5840
|
+
continue;
|
|
5841
|
+
}
|
|
5842
|
+
if (out.error === "ok") {
|
|
5843
|
+
unavailable.push({
|
|
5844
|
+
modelKey: key,
|
|
5845
|
+
metaType,
|
|
5846
|
+
status: out.status,
|
|
5847
|
+
error: "unexpected-shape",
|
|
5848
|
+
note: `HTTP ${out.status} \u6210\u529F\u4F46\u8F7D\u8377\u662F ${shapeOf(out.body)}\uFF0C\u4E0D\u662F meta \u5BF9\u8C61\uFF08\u8F7D\u8377\u5F62\u72B6\u95EE\u9898\uFF0C\u4E0D\u662F\u53D6\u6570\u5931\u8D25\uFF09`,
|
|
5849
|
+
bodyPreview: JSON.stringify(out.body).slice(0, 200)
|
|
5850
|
+
});
|
|
5851
|
+
continue;
|
|
5852
|
+
}
|
|
5853
|
+
unavailable.push({ modelKey: key, metaType, status: out.status, error: out.error, note: noteFor(out.error) });
|
|
5854
|
+
}
|
|
5855
|
+
return { metas, unavailable };
|
|
5856
|
+
}
|
|
5857
|
+
async function sequentialGet(probe, paths) {
|
|
5858
|
+
const out = {};
|
|
5859
|
+
for (const p of paths) out[p] = await probe.get(p);
|
|
5860
|
+
return out;
|
|
5861
|
+
}
|
|
5862
|
+
function reconcileUnavailable(modelsBody, unavailable) {
|
|
5863
|
+
const declared = [
|
|
5864
|
+
...new Set(modelRows(modelsBody).filter((m) => m.error).map((m) => String(m.modelKey)))
|
|
5865
|
+
].sort();
|
|
5866
|
+
const observed = [...new Set(unavailable.map((e) => e.modelKey).filter(Boolean))].sort();
|
|
5867
|
+
const dset = new Set(declared);
|
|
5868
|
+
const oset = new Set(observed);
|
|
5869
|
+
return {
|
|
5870
|
+
declaredUnavailable: declared,
|
|
5871
|
+
observedUnavailable: observed,
|
|
5872
|
+
declaredNotObserved: declared.filter((k) => !oset.has(k)),
|
|
5873
|
+
observedNotDeclared: observed.filter((k) => !dset.has(k)),
|
|
5874
|
+
agrees: declared.length === observed.length && declared.every((k) => oset.has(k)),
|
|
5875
|
+
note: "declaredNotObserved = \u6A21\u578B\u6E05\u5355\u58F0\u660E\u4E0D\u53EF\u7528\u3001\u4F46\u672C\u8F6E\u6CA1\u6709\u5BF9\u5E94\u5931\u8D25\u8BB0\u5F55 \u21D2 \u6F0F\u722C\uFF1BobservedNotDeclared = \u6A21\u578B\u6E05\u5355\u8BF4\u53EF\u7528\u3001\u672C\u8F6E\u5374\u5931\u8D25 \u21D2 \u67E5\u6743\u9650/\u8D85\u65F6/\u7F51\u5173\uFF0C\u4E0D\u662F\u300C\u53D1\u73B0\u4E86\u9690\u85CF\u6A21\u578B\u300D"
|
|
5876
|
+
};
|
|
5877
|
+
}
|
|
5878
|
+
function writeCrawlProducts(layout, modelsBody, metas, unavailable, metaType = DEFAULT_META_TYPE) {
|
|
5879
|
+
const written = {};
|
|
5880
|
+
layout.writeJson("models.json", modelsBody);
|
|
5881
|
+
written.models = "models.json";
|
|
5882
|
+
for (const [key, body] of Object.entries(metas)) {
|
|
5883
|
+
try {
|
|
5884
|
+
layout.writeJson(`meta/${key}.${metaType}.json`, body);
|
|
5885
|
+
} catch (e) {
|
|
5886
|
+
if (!(e instanceof LayoutContainmentError)) throw e;
|
|
5887
|
+
unavailable.push({
|
|
5888
|
+
modelKey: key,
|
|
5889
|
+
metaType,
|
|
5890
|
+
status: 200,
|
|
5891
|
+
error: "unsafe-model-key",
|
|
5892
|
+
note: e.message
|
|
5893
|
+
});
|
|
5894
|
+
}
|
|
5895
|
+
}
|
|
5896
|
+
written.meta = "meta/";
|
|
5897
|
+
layout.writeJson("unavailable.json", {
|
|
5898
|
+
crawled: true,
|
|
5899
|
+
count: unavailable.length,
|
|
5900
|
+
entries: unavailable,
|
|
5901
|
+
reconciliation: reconcileUnavailable(modelsBody, unavailable)
|
|
5902
|
+
});
|
|
5903
|
+
written.unavailable = "unavailable.json";
|
|
5904
|
+
return written;
|
|
5905
|
+
}
|
|
5906
|
+
|
|
5907
|
+
// src/fde/recon/fields.ts
|
|
5908
|
+
var KIT_VALUE_SOURCE = "_kitValueSource";
|
|
5909
|
+
function typeName(v) {
|
|
5910
|
+
if (v === null) return "null";
|
|
5911
|
+
if (Array.isArray(v)) return "list";
|
|
5912
|
+
return typeof v === "object" ? "dict" : typeof v;
|
|
5913
|
+
}
|
|
5914
|
+
function metaFields(body) {
|
|
5915
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
5916
|
+
return { fields: [], problem: `meta \u8F7D\u8377\u4E0D\u662F\u5BF9\u8C61\uFF08${typeName(body)}\uFF09` };
|
|
5917
|
+
}
|
|
5918
|
+
const obj = body;
|
|
5919
|
+
if (!Object.prototype.hasOwnProperty.call(obj, "fields")) return { fields: [] };
|
|
5920
|
+
const fields = obj.fields;
|
|
5921
|
+
if (fields === null) {
|
|
5922
|
+
return { fields: [], problem: "fields \u662F null\uFF08\u670D\u52A1\u7AEF\u663E\u5F0F\u53D1\u4E86 null\uFF0C\u4E0E\u300C\u6CA1\u6709 fields \u952E\u300D\u4E0D\u662F\u4E00\u56DE\u4E8B\uFF09" };
|
|
5923
|
+
}
|
|
5924
|
+
if (!Array.isArray(fields)) return { fields: [], problem: `fields \u4E0D\u662F\u6570\u7EC4\uFF08${typeName(fields)}\uFF09` };
|
|
5925
|
+
return { fields };
|
|
5926
|
+
}
|
|
5927
|
+
function enumRows(field) {
|
|
5928
|
+
const items = field.enums;
|
|
5929
|
+
if (!Array.isArray(items)) return [];
|
|
5930
|
+
const out = [];
|
|
5931
|
+
for (const o of items) {
|
|
5932
|
+
if (o && typeof o === "object" && !Array.isArray(o)) {
|
|
5933
|
+
const rec = o;
|
|
5934
|
+
const value = rec.value;
|
|
5935
|
+
const label = rec.label;
|
|
5936
|
+
if (value !== void 0 && value !== null) {
|
|
5937
|
+
out.push({ value: String(value), label: String(label ?? "") });
|
|
5938
|
+
} else if (label !== void 0 && label !== null) {
|
|
5939
|
+
out.push({ value: String(label), label: String(label), [KIT_VALUE_SOURCE]: "label" });
|
|
5940
|
+
} else {
|
|
5941
|
+
out.push({ value: JSON.stringify(o), label: "", [KIT_VALUE_SOURCE]: "repr:dict" });
|
|
5942
|
+
}
|
|
5943
|
+
continue;
|
|
5944
|
+
}
|
|
5945
|
+
out.push({ value: String(o), label: "", [KIT_VALUE_SOURCE]: `element:${typeName(o)}` });
|
|
5946
|
+
}
|
|
5947
|
+
return out;
|
|
5948
|
+
}
|
|
5949
|
+
function extractFieldRows(metas) {
|
|
5950
|
+
const rows = [];
|
|
5951
|
+
const dropped = [];
|
|
5952
|
+
for (const [model, body] of Object.entries(metas)) {
|
|
5953
|
+
const { fields, problem } = metaFields(body);
|
|
5954
|
+
if (problem) {
|
|
5955
|
+
dropped.push(`${model}: ${problem}\uFF0C\u6574\u4E2A\u6A21\u578B\u6CA1\u6709\u5B57\u6BB5\u8FDB\u7D22\u5F15`);
|
|
5956
|
+
continue;
|
|
5957
|
+
}
|
|
5958
|
+
fields.forEach((f, index) => {
|
|
5959
|
+
const i = index + 1;
|
|
5960
|
+
if (!f || typeof f !== "object" || Array.isArray(f)) {
|
|
5961
|
+
dropped.push(`${model}: \u7B2C ${i} \u4E2A\u5B57\u6BB5\u4E0D\u662F\u5BF9\u8C61\uFF08${typeName(f)}\uFF09`);
|
|
5962
|
+
return;
|
|
5963
|
+
}
|
|
5964
|
+
const rec = f;
|
|
5965
|
+
const key = rec.key;
|
|
5966
|
+
if (typeof key !== "string" || !key) {
|
|
5967
|
+
dropped.push(`${model}: \u7B2C ${i} \u4E2A\u5B57\u6BB5\u6CA1\u6709\u53EF\u7528\u7684 key\uFF08${JSON.stringify(key) ?? String(key)}\uFF09`);
|
|
5968
|
+
return;
|
|
5969
|
+
}
|
|
5970
|
+
rows.push({
|
|
5971
|
+
model,
|
|
5972
|
+
key,
|
|
5973
|
+
// 🔴 中文名的键是 `name`,**不是 `label`**(实测 2676/2676 个字段没有一个带 label)。
|
|
5974
|
+
// 本仓字段命名规范也是「名称字段统一用 name,禁止 label」。
|
|
5975
|
+
name: str(rec.name),
|
|
5976
|
+
type: str(rec.type),
|
|
5977
|
+
// 🔴 必填的键是 `isRequired`,**不是 `required`**(实测 required 出现 0 次)。
|
|
5978
|
+
// 缺省一律显式 false,不用 null——null 会把「没这属性」和「值是假」混成一件事。
|
|
5979
|
+
isRequired: Boolean(rec.isRequired ?? false),
|
|
5980
|
+
isFlex: Boolean(rec.isFlex ?? false),
|
|
5981
|
+
// `columnDefinition` 是实测存在的存储侧信息(14% 的字段有);原计划的 `storage` 键**不存在**。
|
|
5982
|
+
columnDefinition: str(rec.columnDefinition),
|
|
5983
|
+
// 🔴 顾问真正会 grep 的是业务话术:字段级 synonyms 与 description。
|
|
5984
|
+
// 它们稀疏但**恒发**(空就是 [] / "")——「有这一列且为空」与「没这一列」是两个值。
|
|
5985
|
+
synonyms: Array.isArray(rec.synonyms) ? rec.synonyms.filter(Boolean).map(String) : [],
|
|
5986
|
+
description: str(rec.description),
|
|
5987
|
+
enums: enumRows(rec)
|
|
5988
|
+
});
|
|
5989
|
+
});
|
|
5990
|
+
}
|
|
5991
|
+
return { rows, dropped };
|
|
5992
|
+
}
|
|
5993
|
+
function str(v) {
|
|
5994
|
+
return v === void 0 || v === null || v === false ? "" : String(v);
|
|
5995
|
+
}
|
|
5996
|
+
function writeFieldsIndex(layout, rows) {
|
|
5997
|
+
return layout.writeJsonl("index/fields.jsonl", rows);
|
|
5998
|
+
}
|
|
5999
|
+
|
|
6000
|
+
// src/fde/recon/gates.ts
|
|
6001
|
+
var WORDING = "\u672C\u6587\u4EF6\u767B\u8BB0\u7684\u662F**\u663E\u5F0F\u58F0\u660E**\u4E3A\u53EA\u8BFB\u7684\u52A8\u4F5C\u3002kit \u53EA\u80FD\u8D70 RBAC \u88C1\u526A\u540E\u7684 REST\u3001\u89C2\u6D4B\u7A97\u53E3\u6709\u9650\uFF0C\u56E0\u6B64\u4EFB\u4F55\u7ED3\u8BBA\u7684\u53E3\u5F84\u90FD\u662F\u300C\u5728\u58F0\u660E\u5F71\u54CD\u9762\u5185\u672A\u89C2\u6D4B\u5230\u53D8\u5316\u300D\uFF0C\u800C**\u4E0D\u662F**\u300C\u65E0\u526F\u4F5C\u7528\u300D\u3002guard=declared-only \u7684\u6761\u76EE\u8FDE\u670D\u52A1\u7AEF\u9759\u6001\u5B88\u536B\u90FD\u6CA1\u6709\uFF0C\u53EA\u6709\u4E00\u53E5\u58F0\u660E\u3002";
|
|
6002
|
+
var GUARD_ARCH = "declared+arch-guard";
|
|
6003
|
+
var GUARD_DECLARED_ONLY = "declared-only";
|
|
6004
|
+
var ARCH_GUARDED_SOURCE = "java";
|
|
6005
|
+
var REJECT_NOTES = {
|
|
6006
|
+
conflict: "\u7AEF\u70B9\u6807\u4E86 conflict=true\uFF1A\u540C key \u7684\u5019\u9009\u5F7C\u6B64\u4E0D\u4E00\u81F4\u3002\u4E0D\u4E00\u81F4\u5C31\u4E0D\u80FD\u514D\u786E\u8BA4\u6267\u884C \u2014\u2014 \u8FD9\u6B63\u662F\u300C\u4E3A A \u653E\u884C\u3001\u6267\u884C B\u300D\u90A3\u6761\u6D1E",
|
|
6007
|
+
"conflict-undeclared": "\u7AEF\u70B9\u6CA1\u6709\u660E\u786E\u8BF4 conflict=false\uFF08\u952E\u7F3A\u5931\u6216\u4E0D\u662F\u5E03\u5C14\uFF09\u3002fail-closed \u4E0D\u731C\uFF1A\u7F3A\u58F0\u660E\u6309\u6709\u5206\u6B67\u5904\u7406\u3002\u4E0E readOnly \u540C\u4E00\u6761\u7EAA\u5F8B \u2014\u2014 \u7F3A\u952E\u4E0D\u7B49\u4E8E\u300C\u6CA1\u95EE\u9898\u300D",
|
|
6008
|
+
"candidate-not-read-only": "\u805A\u5408\u8BF4\u53EA\u8BFB\uFF0C\u4F46\u81F3\u5C11\u4E00\u4E2A\u5019\u9009\u6CA1\u6709\u663E\u5F0F readOnly=true\u3002\u591A\u5019\u9009\u805A\u5408\u662F all \u4E0D\u662F any\uFF1A\u4E00\u4E2A\u53EA\u8BFB\u5019\u9009\u4E0D\u80FD\u628A\u4F1A\u5199\u5E93\u7684\u52A8\u4F5C\u62AC\u6210\u95F8",
|
|
6009
|
+
"no-candidates": "\u58F0\u660E\u4E86 readOnly=true \u5374\u6CA1\u6709\u4EFB\u4F55\u5019\u9009\u80CC\u4E66\u3002\u6CA1\u6709\u5019\u9009 = \u6CA1\u6709\u4EFB\u4F55\u4E1C\u897F\u53EF\u6838",
|
|
6010
|
+
"malformed-candidate": "\u5019\u9009\u4E0D\u662F\u5BF9\u8C61\uFF08\u7F51\u5173\u6539\u5199 / \u7248\u672C\u6F02\u79FB\uFF1F\uFF09\uFF0C\u65E0\u6CD5\u6838\u5BF9\u5B83\u7684 source \u4E0E readOnly",
|
|
6011
|
+
"malformed-key": "\u52A8\u4F5C\u6CA1\u6709\u53EF\u7528\u7684 key \u2014\u2014 \u6CA1\u6709 key \u5C31\u65E0\u6CD5\u8C03\u7528\uFF0C\u4E5F\u65E0\u6CD5\u4E0E\u4EA7\u54C1\u4FA7\u5BF9\u8D26",
|
|
6012
|
+
"malformed-action": "\u52A8\u4F5C\u6761\u76EE\u4E0D\u662F\u5BF9\u8C61\uFF0C\u65E0\u6CD5\u5224\u8BFB\u5B83\u662F\u4E0D\u662F\u53EA\u8BFB\u95F8"
|
|
6013
|
+
};
|
|
6014
|
+
function actionsPath(modelKey, state = "") {
|
|
6015
|
+
const base = `/api/models/${modelKey}/actions`;
|
|
6016
|
+
if (!state) return base;
|
|
6017
|
+
return `${base}?${new URLSearchParams({ state }).toString()}`;
|
|
6018
|
+
}
|
|
6019
|
+
function isRecord(v) {
|
|
6020
|
+
return Boolean(v) && typeof v === "object" && !Array.isArray(v);
|
|
6021
|
+
}
|
|
6022
|
+
function reject(out, model, key, reason) {
|
|
6023
|
+
out.push({
|
|
6024
|
+
model,
|
|
6025
|
+
key: typeof key === "string" && key ? key : null,
|
|
6026
|
+
reason,
|
|
6027
|
+
note: REJECT_NOTES[reason]
|
|
6028
|
+
});
|
|
6029
|
+
}
|
|
6030
|
+
async function collectActionGates(probe, modelKeys2, state = "") {
|
|
6031
|
+
const keys = [...new Set(modelKeys2)];
|
|
6032
|
+
const paths = /* @__PURE__ */ new Map();
|
|
6033
|
+
for (const k of keys) paths.set(actionsPath(k, state), k);
|
|
6034
|
+
const results = probe.getMany ? await probe.getMany([...paths.keys()]) : await sequentialGet(probe, [...paths.keys()]);
|
|
6035
|
+
const gates = [];
|
|
6036
|
+
const rejected = [];
|
|
6037
|
+
const unavailable = [];
|
|
6038
|
+
const sources = /* @__PURE__ */ new Set();
|
|
6039
|
+
const statesEchoed = /* @__PURE__ */ new Set();
|
|
6040
|
+
let noStateEcho = 0;
|
|
6041
|
+
let answered = 0;
|
|
6042
|
+
for (const [path9, model] of paths) {
|
|
6043
|
+
const out = results[path9];
|
|
6044
|
+
if (out === void 0) {
|
|
6045
|
+
unavailable.push({
|
|
6046
|
+
modelKey: model,
|
|
6047
|
+
status: -1,
|
|
6048
|
+
error: "not-crawled",
|
|
6049
|
+
note: "\u5E76\u53D1\u6279\u6B21\u91CC\u6CA1\u6709\u8FD4\u56DE\u8BE5 path"
|
|
6050
|
+
});
|
|
6051
|
+
continue;
|
|
6052
|
+
}
|
|
6053
|
+
if (out.error !== "ok") {
|
|
6054
|
+
unavailable.push({ modelKey: model, status: out.status, error: out.error, note: noteFor(out.error) });
|
|
6055
|
+
continue;
|
|
6056
|
+
}
|
|
6057
|
+
const body = out.body;
|
|
6058
|
+
if (!isRecord(body) || !Array.isArray(body.actions)) {
|
|
6059
|
+
unavailable.push({
|
|
6060
|
+
modelKey: model,
|
|
6061
|
+
status: out.status,
|
|
6062
|
+
error: "unexpected-shape",
|
|
6063
|
+
note: `HTTP ${out.status} \u6210\u529F\u4F46\u8F7D\u8377\u662F ${body === null ? "null" : Array.isArray(body) ? "list" : typeof body}\u3001\u6216\u5176\u4E2D\u6CA1\u6709 actions \u6570\u7EC4\uFF0C\u65E0\u6CD5\u5224\u8BFB\u8BE5\u6A21\u578B\u7684\u53EA\u8BFB\u95F8\uFF08\u8F7D\u8377\u5F62\u72B6\u95EE\u9898\uFF0C\u4E0D\u662F\u53D6\u6570\u5931\u8D25\uFF09`,
|
|
6064
|
+
bodyPreview: JSON.stringify(body).slice(0, 200)
|
|
6065
|
+
});
|
|
6066
|
+
continue;
|
|
6067
|
+
}
|
|
6068
|
+
answered += 1;
|
|
6069
|
+
const echoed = typeof body.state === "string" ? body.state : null;
|
|
6070
|
+
if (echoed === null) noStateEcho += 1;
|
|
6071
|
+
else statesEchoed.add(echoed);
|
|
6072
|
+
for (const action2 of body.actions) {
|
|
6073
|
+
if (!isRecord(action2)) {
|
|
6074
|
+
reject(rejected, model, null, "malformed-action");
|
|
6075
|
+
continue;
|
|
6076
|
+
}
|
|
6077
|
+
const cands = Array.isArray(action2.candidates) ? action2.candidates : [];
|
|
6078
|
+
for (const c of cands) {
|
|
6079
|
+
if (isRecord(c) && typeof c.source === "string" && c.source) sources.add(c.source);
|
|
6080
|
+
}
|
|
6081
|
+
if (action2.readOnly !== true) continue;
|
|
6082
|
+
const key = action2.key;
|
|
6083
|
+
if (typeof key !== "string" || !key) {
|
|
6084
|
+
reject(rejected, model, key, "malformed-key");
|
|
6085
|
+
continue;
|
|
6086
|
+
}
|
|
6087
|
+
if (action2.conflict !== false) {
|
|
6088
|
+
reject(rejected, model, key, action2.conflict === true ? "conflict" : "conflict-undeclared");
|
|
6089
|
+
continue;
|
|
6090
|
+
}
|
|
6091
|
+
if (cands.length === 0) {
|
|
6092
|
+
reject(rejected, model, key, "no-candidates");
|
|
6093
|
+
continue;
|
|
6094
|
+
}
|
|
6095
|
+
if (!cands.every((c) => isRecord(c))) {
|
|
6096
|
+
reject(rejected, model, key, "malformed-candidate");
|
|
6097
|
+
continue;
|
|
6098
|
+
}
|
|
6099
|
+
if (!cands.every((c) => c.readOnly === true)) {
|
|
6100
|
+
reject(rejected, model, key, "candidate-not-read-only");
|
|
6101
|
+
continue;
|
|
6102
|
+
}
|
|
6103
|
+
const allJava = cands.every((c) => c.source === ARCH_GUARDED_SOURCE);
|
|
6104
|
+
gates.push({
|
|
6105
|
+
model,
|
|
6106
|
+
key,
|
|
6107
|
+
// 缺 label 兜成 null,见 `Gate.label` 上那条注释(undefined 会让键整个消失)。
|
|
6108
|
+
label: typeof action2.label === "string" ? action2.label : null,
|
|
6109
|
+
source: [
|
|
6110
|
+
...new Set(
|
|
6111
|
+
cands.map((c) => c.source).filter((s) => typeof s === "string" && Boolean(s))
|
|
6112
|
+
)
|
|
6113
|
+
].sort(),
|
|
6114
|
+
state: echoed,
|
|
6115
|
+
readOnly: true,
|
|
6116
|
+
conflict: false,
|
|
6117
|
+
candidates: cands,
|
|
6118
|
+
guard: allJava ? GUARD_ARCH : GUARD_DECLARED_ONLY
|
|
6119
|
+
});
|
|
6120
|
+
}
|
|
6121
|
+
}
|
|
6122
|
+
const echoedList = [...statesEchoed].sort();
|
|
6123
|
+
const coverage = {
|
|
6124
|
+
// 🔴 永远是 state-scoped:产品没有「列出可用 state」的端点(实测约 200 个取值),
|
|
6125
|
+
// 「全集」不是一个可兑现的词,所以本产物只覆盖本次请求的那一个 state。
|
|
6126
|
+
scope: "state-scoped",
|
|
6127
|
+
// 端点回显的 state。恰好一个取值时记那个值;多个模型回显不一致时记 null 并把
|
|
6128
|
+
// 全部取值摊在 statesEchoed 里 —— 取最后一个赢家会让产物声称一个并不成立的覆盖面。
|
|
6129
|
+
state: echoedList.length === 1 ? echoedList[0] : null,
|
|
6130
|
+
statesEchoed: echoedList,
|
|
6131
|
+
// 🔴 「回显了空 state」(state="") 与「一个模型都没答上来」是两回事,
|
|
6132
|
+
// 不许靠 null 一个值兼职两种含义。
|
|
6133
|
+
stateObserved: statesEchoed.size > 0,
|
|
6134
|
+
stateEchoAgrees: statesEchoed.size <= 1,
|
|
6135
|
+
stateRequested: state,
|
|
6136
|
+
modelsWithoutStateEcho: noStateEcho,
|
|
6137
|
+
sources: [...sources].sort(),
|
|
6138
|
+
modelsQueried: keys.length,
|
|
6139
|
+
duplicateModelKeys: modelKeys2.length - keys.length,
|
|
6140
|
+
modelsAnswered: answered,
|
|
6141
|
+
modelsUnavailable: unavailable.length,
|
|
6142
|
+
// 前提显式断言:不平就说明有模型既没记成答上来、也没记成失败 —— 「扫了 N 个模型」不成立。
|
|
6143
|
+
accountingBalanced: keys.length === answered + unavailable.length,
|
|
6144
|
+
unavailable,
|
|
6145
|
+
rejected,
|
|
6146
|
+
endpointGates: false,
|
|
6147
|
+
// 普通 Controller 端点闸不在本产物内
|
|
6148
|
+
note: "scope \u6052\u4E3A state-scoped\uFF1Astate \u4E0D\u53EF\u679A\u4E3E\uFF0C\u672C\u4EA7\u7269\u53EA\u8986\u76D6 stateRequested \u8BF7\u6C42\u5230\u7684\u90A3\u4E00\u4E2A\uFF1Bunavailable = \u6CA1\u626B\u6210\u7684\u6A21\u578B\uFF08\u4E0E\u300C\u8FD9\u4E2A\u6A21\u578B\u6CA1\u6709\u53EA\u8BFB\u95F8\u300D\u662F\u4E24\u56DE\u4E8B\uFF09\uFF1Brejected = \u58F0\u660E\u4E86\u53EA\u8BFB\u5374\u88AB fail-closed \u89C4\u5219\u62D2\u6389\u7684\u52A8\u4F5C\uFF0C\u5B83\u4EEC\u4E0D\u662F\u95F8\uFF1Bsources = \u672C\u8F6E\u89C1\u5230\u7684**\u5168\u90E8** action\uFF08\u542B\u5199\u52A8\u4F5C\u3001\u542B\u88AB rejected \u7684\u6761\u76EE\uFF09\u7684\u5019\u9009 source \u53BB\u91CD\uFF0C**\u4E0D\u662F**\u95F8\u7684\u6765\u6E90\uFF1B\u95F8\u81EA\u5DF1\u7684\u6765\u6E90\u770B\u6BCF\u6761 gate \u7684 source \u5B57\u6BB5"
|
|
6149
|
+
};
|
|
6150
|
+
return { gates, coverage };
|
|
6151
|
+
}
|
|
6152
|
+
function writeGates(layout, gates, coverage) {
|
|
6153
|
+
return layout.writeJson("gates.json", {
|
|
6154
|
+
wording: WORDING,
|
|
6155
|
+
coverage,
|
|
6156
|
+
crawled: true,
|
|
6157
|
+
count: gates.length,
|
|
6158
|
+
gates
|
|
6159
|
+
});
|
|
6160
|
+
}
|
|
6161
|
+
|
|
6162
|
+
// src/fde/recon/scc.ts
|
|
6163
|
+
function compareStringArrays(a, b) {
|
|
6164
|
+
for (let i = 0; i < Math.min(a.length, b.length); i += 1) {
|
|
6165
|
+
if (a[i] < b[i]) return -1;
|
|
6166
|
+
if (a[i] > b[i]) return 1;
|
|
6167
|
+
}
|
|
6168
|
+
return a.length - b.length;
|
|
6169
|
+
}
|
|
6170
|
+
function tarjan(nodes, adj) {
|
|
6171
|
+
const index = /* @__PURE__ */ new Map();
|
|
6172
|
+
const low = /* @__PURE__ */ new Map();
|
|
6173
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
6174
|
+
const stack = [];
|
|
6175
|
+
const result = [];
|
|
6176
|
+
let counter = 0;
|
|
6177
|
+
const neighbours = (n) => [...adj.get(n) ?? []].sort();
|
|
6178
|
+
for (const root of nodes) {
|
|
6179
|
+
if (index.has(root)) continue;
|
|
6180
|
+
index.set(root, counter);
|
|
6181
|
+
low.set(root, counter);
|
|
6182
|
+
counter += 1;
|
|
6183
|
+
stack.push(root);
|
|
6184
|
+
onStack.add(root);
|
|
6185
|
+
const work = [
|
|
6186
|
+
{ node: root, it: neighbours(root), pos: 0 }
|
|
6187
|
+
];
|
|
6188
|
+
while (work.length > 0) {
|
|
6189
|
+
const frame = work[work.length - 1];
|
|
6190
|
+
let advanced = false;
|
|
6191
|
+
while (frame.pos < frame.it.length) {
|
|
6192
|
+
const nxt = frame.it[frame.pos];
|
|
6193
|
+
frame.pos += 1;
|
|
6194
|
+
if (!index.has(nxt)) {
|
|
6195
|
+
index.set(nxt, counter);
|
|
6196
|
+
low.set(nxt, counter);
|
|
6197
|
+
counter += 1;
|
|
6198
|
+
stack.push(nxt);
|
|
6199
|
+
onStack.add(nxt);
|
|
6200
|
+
work.push({ node: nxt, it: neighbours(nxt), pos: 0 });
|
|
6201
|
+
advanced = true;
|
|
6202
|
+
break;
|
|
6203
|
+
}
|
|
6204
|
+
if (onStack.has(nxt)) {
|
|
6205
|
+
low.set(frame.node, Math.min(low.get(frame.node), index.get(nxt)));
|
|
6206
|
+
}
|
|
6207
|
+
}
|
|
6208
|
+
if (advanced) continue;
|
|
6209
|
+
work.pop();
|
|
6210
|
+
if (work.length > 0) {
|
|
6211
|
+
const parent = work[work.length - 1].node;
|
|
6212
|
+
low.set(parent, Math.min(low.get(parent), low.get(frame.node)));
|
|
6213
|
+
}
|
|
6214
|
+
if (low.get(frame.node) === index.get(frame.node)) {
|
|
6215
|
+
const comp = [];
|
|
6216
|
+
for (; ; ) {
|
|
6217
|
+
const w = stack.pop();
|
|
6218
|
+
onStack.delete(w);
|
|
6219
|
+
comp.push(w);
|
|
6220
|
+
if (w === frame.node) break;
|
|
6221
|
+
}
|
|
6222
|
+
result.push(comp.sort());
|
|
6223
|
+
}
|
|
6224
|
+
}
|
|
6225
|
+
}
|
|
6226
|
+
return result;
|
|
6227
|
+
}
|
|
6228
|
+
|
|
6229
|
+
// src/fde/recon/topology.ts
|
|
6230
|
+
var AUDIT_FIELD_KEYS = ["createById", "updateById"];
|
|
6231
|
+
var AUDIT_SET = new Set(AUDIT_FIELD_KEYS);
|
|
6232
|
+
var WORDING2 = (
|
|
6233
|
+
// 🔴 这一句必须在最前面:下面每个键都是**在这张裁剪过的图上**算出来的。
|
|
6234
|
+
// 少了它,顾问拿 cycles=[] 会读成「产品里没有互相引用」,而实际是「排掉审计边之后没有」。
|
|
6235
|
+
'\u{1F534} \u5168\u90E8\u7ED3\u8BBA\u90FD\u7B97\u5728**\u6392\u9664 excludedEdges\uFF08\u5BA1\u8BA1\u8FB9\uFF09\u4E4B\u540E**\u7684\u8FB9\u96C6\u4E0A \u2014\u2014 cycles / selfLoops / waves / blockedByCycles \u65E0\u4E00\u4F8B\u5916\uFF1Bwaves = \u6A21\u578B\u7EA7\u62D3\u6251\u5206\u6CE2\uFF0C\u8DD1\u5728\u7F29\u70B9\u56FE\u4E0A\uFF0C\u540C\u4E00\u6CE2\u5185\u90E8\u53EF\u5E76\u884C\uFF0C\u8986\u76D6\u300C\u5168\u90E8\u6A21\u578B \u2212 unordered\u300D\u4E0D\u91CD\u4E0D\u6F0F\uFF1Bwaves[0] \u4E0D\u4F9D\u8D56\u4EFB\u4F55**\u53EF\u5B9A\u5E8F**\u6A21\u578B\uFF1B\u{1F534} \u4F46 waves \u91CC\u6709\u4F4D\u7F6E**\u4E0D\u7B49\u4E8E\u300C\u53EF\u4EE5\u76F4\u63A5\u5BFC\u300D**\uFF1A\u51E1\u662F\u540C\u65F6\u51FA\u73B0\u5728 blockedByCycles \u91CC\u7684\u6A21\u578B\uFF0C\u90FD\u8FD8\u4F9D\u8D56\u7740\u73AF\u6210\u5458\uFF0C**\u5FC5\u987B\u5148\u628A cycles \u6309\u4E24\u6BB5\u5F0F\u89E3\u5F00**\u624D\u80FD\u5BFC \u2014\u2014 \u8BFB waves \u5FC5\u987B\u8FDE blockedByCycles \u4E00\u8D77\u8BFB\uFF1Bcycles = \u5F3A\u8FDE\u901A\u5206\u91CF\uFF08\u22652 \u4E2A\u6A21\u578B\u4E92\u76F8\u4F9D\u8D56\uFF09\uFF0C[] \u8868\u793A**\u7B97\u8FC7\u4E14\u6CA1\u6709**\uFF0C\u4E0D\u662F\u6CA1\u7B97\uFF1BselfLoops = \u81EA\u73AF\u6A21\u578B\uFF08\u5982\u7EC4\u7EC7\u7684 parentId\uFF09\uFF0C\u81EA\u73AF\u4E0D\u8FDB cycles \u2014\u2014 \u5B83\u8BF4\u660E**\u884C\u7EA7**\u6709\u7236\u5B50\u5E8F\uFF0C\u6A21\u578B\u7EA7\u5E76\u4E0D\u963B\u585E\uFF0C\u7167\u6837\u8FDB waves\uFF1Bunordered = \u5377\u8FDB cycles \u56E0\u800C\u5B9A\u4E0D\u51FA\u987A\u5E8F\u7684\u6A21\u578B\uFF0C\u5B83\u4EEC**\u4E0D\u5728** waves \u91CC\uFF0C\u628A\u5B83\u4EEC\u585E\u8FDB waves \u7B49\u4E8E\u5047\u88C5\u5B9A\u5F97\u51FA\u987A\u5E8F\uFF1BblockedByCycles = \u81EA\u5DF1\u4E0D\u5728\u73AF\u91CC\u3001\u4F46\uFF08\u4F20\u9012\u5730\uFF09\u4F9D\u8D56\u67D0\u4E2A\u73AF\u6210\u5458\u7684\u6A21\u578B\uFF1BexcludedEdges = \u6309\u5B57\u6BB5\u540D\u7EA6\u5B9A\uFF08createById / updateById\uFF09\u6392\u9664\u51FA\u4F9D\u8D56\u56FE\u7684\u5BA1\u8BA1\u8FB9\uFF0C\u539F\u6837\u7559\u7740\u53EF\u653E\u56DE\uFF0C\u{1F534} \u8FD9\u6761\u53E3\u5F84\u5728**\u672C\u6B21\u6570\u636E\u4E0A**\u503C\u591A\u5C11\uFF0C\u770B excludedEdges.impact\uFF08\u542B/\u4E0D\u542B\u5BA1\u8BA1\u8FB9\u5404\u73B0\u7B97\u4E00\u904D\uFF0C\u4E0D\u662F\u4EFB\u4F55\u4E00\u4E2A\u79DF\u6237\u7684\u5386\u53F2\u6570\u5B57\uFF09\uFF1BedgesOutsideNodes = \u4E24\u7AEF\u6709\u4E00\u5934\u4E0D\u5728\u672C\u6B21\u6A21\u578B\u540D\u518C\u91CC\u7684\u8FB9\uFF0C\u4E0D\u8FDB\u56FE\u4F46\u4E5F\u4E0D\u9759\u9ED8\u4E22\uFF0C\u540C\u6837\u539F\u6837\u7559\u7740\uFF1Bdangling = \u4EA7\u54C1\u58F0\u660E\u4E86\u5F15\u7528\u3001\u4F46\u8FD9\u4E2A\u5F15\u7528\u7528\u4E0D\u4E86\u7684\u5B57\u6BB5\uFF0C**\u4E24\u6863\uFF0C\u{1F534} \u6743\u5A01\u5224\u636E\u662F reason \u4E0D\u662F toModel**\uFF1Areason=not-in-registry \u65F6 toModel \u662F**\u771F\u7684\u6A21\u578B\u540D**\u3001\u53EA\u662F\u4E0D\u5728 /api/system/models \u540D\u518C\u91CC\uFF08\u5B9E\u6D4B 4 \u4E2A\uFF1AImpactRecord / JobFamily / PerfAssessDimensionInstance / UserModel\uFF09\uFF0C\u7167\u7740\u586B\u4F1A\u62FF\u5230 400\uFF1Breason=malformed-target \u65F6\u4EA7\u54C1\u7ED9\u7684\u76EE\u6807\u5F62\u6001\u5C31\u662F\u574F\u7684\uFF08object={"a":1} / 5 / ["T"] \u4E4B\u7C7B\uFF09\uFF0C\u6B64\u65F6 toModel \u5B58\u7684\u662F **repr(\u539F\u503C)**\u3001\u662F kit \u5408\u6210\u7684\u5B57\u7B26\u4E32\uFF0C**\u4E0D\u662F\u6A21\u578B\u540D** \u2014\u2014 \u62FF\u5B83\u53BB\u67E5\u6A21\u578B\u53EA\u4F1A\u67E5\u5230\u4E00\u4E2A\u4E0D\u5B58\u5728\u7684\u540D\u5B57\uFF0C\u8BE5\u5B57\u6BB5\u6839\u672C\u6CA1\u6709\u53EF\u7528\u7684\u5F15\u7528\u76EE\u6807'
|
|
6236
|
+
);
|
|
6237
|
+
var NOTE = "\u6A21\u578B\u7EA7\u987A\u5E8F \u2260 \u884C\u7EA7\u987A\u5E8F\u3002\u6811\u5F62\u6570\u636E\uFF08\u7EC4\u7EC7\u3001\u5C97\u4F4D\u5E8F\u5217\uFF09\u7684\u884C\u5E8F\u8981\u770B\u540E\u7EED\u7684 work/import-plan.json\uFF1B\u672C\u6587\u4EF6\u53EA\u4FDD\u8BC1\u300C\u6A21\u578B A \u7684\u7B2C\u4E00\u884C\u5199\u4E4B\u524D\uFF0C\u6A21\u578B B \u5DF2\u7ECF\u5199\u5B8C\u300D\u3002waves \u91CC\u6CA1\u6709 cycles/unordered \u7684\u6210\u5458\uFF0C\u6CE2\u6B21\u7F16\u53F7\u4E5F**\u4E0D\u7ED9\u5B83\u4EEC\u7559\u7A7A\u6863** \u2014\u2014 \u6574\u5C42\u90FD\u662F\u73AF\u6210\u5458\u7684\u90A3\u4E00\u5C42\u4E0D\u4F1A\u51FA\u73B0\u5728 waves \u91CC\uFF0C\u6240\u4EE5 waves[0] \u672A\u5FC5\u662F\u300C\u53EF\u4EE5\u7B2C\u4E00\u4E2A\u5BFC\u300D\u7684\u90A3\u4E00\u6279\uFF1A\u5148\u770B\u5B83\u4E0E blockedByCycles \u6709\u6CA1\u6709\u4EA4\u96C6\u3002\u7167 waves \u987A\u5E8F\u6267\u884C\u524D\uFF0C\u5FC5\u987B\u5148\u6309\u4E24\u6BB5\u5F0F\uFF08\u5148\u63D2\u5165\u3001\u5F15\u7528\u5217\u7559\u7A7A\uFF0C\u518D\u56DE\u586B\uFF09\u628A cycles \u5904\u7406\u6389\u3002";
|
|
6238
|
+
var EXCLUDED_NOTE = "\u8FD9\u4E9B\u8FB9\u6309**\u5B57\u6BB5\u540D\u7EA6\u5B9A**\uFF08createById / updateById\uFF09\u6392\u9664\u51FA\u4F9D\u8D56\u56FE\u3002\u{1F534} \u4F9D\u636E\u53EA\u6709\u547D\u540D\u7EA6\u5B9A\uFF0C**\u4E0D\u662F\u4EA7\u54C1\u58F0\u660E**\uFF1A\u5B9E\u6D4B\u5168\u5E93 1476 \u4E2A\u8FD9\u6837\u7684\u5B57\u6BB5\uFF0CisReadonly / autoGenerate / isList / isInfo \u56DB\u4E2A\u6807\u5FD7\u5168\u90E8\u4E3A false\uFF0C\u5143\u6570\u636E\u91CC\u6CA1\u6709\u4EFB\u4F55\u53EF\u7F16\u7A0B\u5224\u636E\u80FD\u8BA4\u51FA\u300C\u8FD9\u662F\u670D\u52A1\u7AEF\u586B\u7684\u5BA1\u8BA1\u5B57\u6BB5\u300D\u3002\uFF08\u65C1\u8BC1\uFF1A\u6709\u4EBA\u5728\u73B0\u7F51 CO_0001 \u7684\u884C\u4E0A\u89C2\u5BDF\u5230 createById \u5168\u662F\u7CFB\u7EDF\u54E8\u5175 UUID 000\u2026001\uFF0C\u5F3A\u70C8\u63D0\u793A\u670D\u52A1\u7AEF\u586B\u5145 \u2014\u2014 \u4F46\u90A3\u662F**\u4ED6\u4EBA\u89C2\u5BDF\u3001\u672C\u5DE5\u5177\u672A\u590D\u6838**\uFF0C\u4E14 **recon \u8FD9\u6761\u8DEF**\u53EA\u8BFB\u4E0D\u5199\uFF0C**\u8BC1\u4E0D\u4E86**\uFF0C\u6240\u4EE5\u8FD9\u91CC\u4E0D\u628A\u5B83\u5F53\u7406\u7531\u3002\uFF09\u6392\u9664\u7684**\u6548\u679C**\u662F\u91CF\u5F97\u5230\u7684\uFF0C\u4E14**\u6BCF\u6B21\u8FD0\u884C\u73B0\u7B97**\uFF1A\u89C1\u540C\u7EA7\u7684 impact\u3002\u{1F534} \u5224\u5B9A\u987A\u5E8F\uFF1A\u5BA1\u8BA1\u5B57\u6BB5**\u5148\u5224**\uFF0C\u65E9\u4E8E\u81EA\u73AF\u5224\u5B9A\u4E0E\u540D\u518C\u5224\u5B9A \u2014\u2014 \u6240\u4EE5\u2460 `User.createById \u2192 User` \u8BB0\u5728\u8FD9\u91CC\u800C\u4E0D\u662F selfLoops\uFF08\u5426\u5219\u7B49\u4E8E\u51ED\u7A7A\u65AD\u8A00\u300CUser \u6709\u884C\u7EA7\u7236\u5B50\u5E8F\u300D\uFF09\uFF1B\u2461 \u4E00\u6761 createById \u6307\u5411\u540D\u518C\u5916\u76EE\u6807\u7684\u8FB9\u4E5F\u8BB0\u5728\u8FD9\u91CC\uFF0C**\u4E0D\u4F1A**\u51FA\u73B0\u5728 edgesOutsideNodes \u91CC\u3002\u{1F534} \u5982\u679C\u67D0\u4E2A\u5BA2\u6237\u73AF\u5883\u771F\u7684\u9700\u8981\u987E\u95EE\u81EA\u5DF1\u586B createById / updateById\uFF0C\u8FD9\u6279\u8FB9\u5C31\u5728\u540C\u7EA7\u7684 edges \u6570\u7EC4\u91CC\uFF0C\u952E\u4E0E refs \u4EA7\u51FA\u7684\u5F15\u7528\u8FB9\u9010\u5B57\u76F8\u540C\uFF08\u4E03\u952E\uFF1AfromModel / fromField / fieldName / toModel / carrier / labelField / labelFieldSource\uFF09\uFF0C\u653E\u56DE\u4F9D\u8D56\u56FE\u5373\u53EF \u2014\u2014 \u6392\u9664\u4E0D\u7B49\u4E8E\u4E22\u5F03\u3002";
|
|
6239
|
+
var OUTSIDE_NOTE = "\u8FB9\u7684\u4E00\u7AEF\u4E0D\u5728\u672C\u6B21\u6A21\u578B\u540D\u518C\u91CC\uFF1A\u4E0D\u8FDB\u4F9D\u8D56\u56FE\uFF08\u8FDB\u4E86\u4F1A\u51ED\u7A7A\u591A\u51FA\u4E00\u4E2A\u4E0D\u5B58\u5728\u7684\u8282\u70B9\uFF09\uFF0C\u4F46\u4E5F\u4E0D\u9759\u9ED8\u4E22 \u2014\u2014 \u9759\u9ED8\u4E22\u7684\u5F62\u6001\u662F edgeCount \u4E0E\u56FE\u91CC\u7528\u5230\u7684\u8FB9\u6570\u5BF9\u4E0D\u4E0A\uFF0C\u800C\u8FD9\u4E2A\u5DEE\u989D\u6CA1\u6709\u4EFB\u4F55\u4EBA\u770B\u5F97\u51FA\u6765\u3002\u540C\u7EA7\u7684 edges \u662F**\u539F\u6837**\u7684\u5F15\u7528\u8FB9\uFF08\u952E\u4E0E refs \u4EA7\u51FA\u7684\u5F15\u7528\u8FB9\u9010\u5B57\u76F8\u540C\uFF0C\u4E03\u952E\uFF09\uFF0C\u54EA\u4E00\u7AEF\u51FA\u754C\uFF0C\u62FF\u5B83\u7684 fromModel / toModel \u4E0E\u6A21\u578B\u540D\u518C\u6BD4\u4E00\u4E0B\u5373\u77E5\u3002\u6309\u7F16\u6392\uFF08modelKeys \u2287 metas \u7684\u952E\uFF0C\u4E14 refEdges \u5DF2\u628A\u300C\u76EE\u6807\u4E0D\u5728\u540D\u518C\u300D\u5206\u6D41\u8FDB dangling\uFF09\u8FD9\u91CC\u6052\u4E3A\u7A7A\uFF0C\u5B9E\u6D4B 0 \u6761\u3002";
|
|
6240
|
+
var REFERENCE_IMPACT = {
|
|
6241
|
+
source: "\u79DF\u6237 13 / 742 \u4E2A\u6A21\u578B / 2923 \u6761\u8FB9\uFF0C2026-08-16 \u5B9E\u6D4B",
|
|
6242
|
+
withAudit: { cycles: 4, cycleMembers: 22, selfLoops: 22, blockedByCycles: 717 },
|
|
6243
|
+
withoutAudit: { cycles: 3, cycleMembers: 7, selfLoops: 21, blockedByCycles: 23 },
|
|
6244
|
+
note: "\u{1F534} \u8FD9\u662F**\u53C2\u8003\u503C\uFF0C\u4E0D\u662F\u672C\u6B21\u7ED3\u679C** \u2014\u2014 \u672C\u6B21\u7684\u4E24\u7EC4\u6570\u5728\u540C\u7EA7\u7684 withAudit / withoutAudit \u91CC\u3002\u5728\u90A3\u4E2A\u79DF\u6237\u4E0A\uFF0C\u542B\u5BA1\u8BA1\u8FB9\u65F6 742 \u4E2A\u6A21\u578B\u6709 717 \u4E2A\uFF0896%\uFF09\u88AB\u73AF\u6321\u4F4F\u3001\u4EA7\u7269\u7B49\u4E8E\u6CA1\u7528\uFF1B\u6392\u9664\u540E\u53EA\u5269 23 \u4E2A\u3002\u522B\u62FF\u8FD9\u7EC4\u6570\u53BB\u89E3\u91CA\u4F60\u624B\u4E0A\u8FD9\u4EFD\u4EA7\u7269\u3002"
|
|
6245
|
+
};
|
|
6246
|
+
function analyze(edges, nodes, nodeSet, excludeAudit) {
|
|
6247
|
+
const deps = new Map(nodes.map((n) => [n, /* @__PURE__ */ new Set()]));
|
|
6248
|
+
const selfLoops = /* @__PURE__ */ new Set();
|
|
6249
|
+
const outside = [];
|
|
6250
|
+
const excluded = [];
|
|
6251
|
+
for (const e of edges) {
|
|
6252
|
+
const a = e.fromModel;
|
|
6253
|
+
const b = e.toModel;
|
|
6254
|
+
if (excludeAudit && AUDIT_SET.has(e.fromField)) {
|
|
6255
|
+
excluded.push(e);
|
|
6256
|
+
continue;
|
|
6257
|
+
}
|
|
6258
|
+
if (!nodeSet.has(a) || !nodeSet.has(b)) {
|
|
6259
|
+
outside.push(e);
|
|
6260
|
+
continue;
|
|
6261
|
+
}
|
|
6262
|
+
if (a === b) {
|
|
6263
|
+
selfLoops.add(a);
|
|
6264
|
+
continue;
|
|
6265
|
+
}
|
|
6266
|
+
deps.get(a).add(b);
|
|
6267
|
+
}
|
|
6268
|
+
const comps = tarjan(nodes, deps);
|
|
6269
|
+
const compOf = /* @__PURE__ */ new Map();
|
|
6270
|
+
comps.forEach((comp, i) => comp.forEach((m) => compOf.set(m, i)));
|
|
6271
|
+
const cyclicComps = new Set(comps.map((c, i) => c.length > 1 ? i : -1).filter((i) => i >= 0));
|
|
6272
|
+
const cycles = [...cyclicComps].map((i) => comps[i]).sort(compareStringArrays);
|
|
6273
|
+
const unordered = [...cyclicComps].flatMap((i) => comps[i]).sort();
|
|
6274
|
+
const cyclic = new Set(unordered);
|
|
6275
|
+
const compDeps = new Map(comps.map((_, i) => [i, /* @__PURE__ */ new Set()]));
|
|
6276
|
+
for (const n of nodes) {
|
|
6277
|
+
const ci = compOf.get(n);
|
|
6278
|
+
for (const d of deps.get(n)) {
|
|
6279
|
+
const cj = compOf.get(d);
|
|
6280
|
+
if (cj !== ci) compDeps.get(ci).add(cj);
|
|
6281
|
+
}
|
|
6282
|
+
}
|
|
6283
|
+
const remaining = new Map([...compDeps].map(([i, v]) => [i, new Set(v)]));
|
|
6284
|
+
const levelOf = /* @__PURE__ */ new Map();
|
|
6285
|
+
let level = 0;
|
|
6286
|
+
for (let round = 0; round <= comps.length; round += 1) {
|
|
6287
|
+
if (remaining.size === 0) break;
|
|
6288
|
+
const ready = [...remaining].filter(([, d]) => d.size === 0).map(([i]) => i);
|
|
6289
|
+
if (ready.length === 0) {
|
|
6290
|
+
const stuck = [...remaining.keys()].map((i) => comps[i]).sort(compareStringArrays);
|
|
6291
|
+
throw new Error(
|
|
6292
|
+
`\u62D3\u6251\u5206\u6CE2\u5361\u4F4F\uFF1A\u7F29\u70B9\u56FE\u91CC\u4ECD\u7136\u5B58\u5728\u73AF\uFF0C\u8BF4\u660E tarjan \u7ED9\u51FA\u7684\u5206\u91CF\u4E0D\u662F\u5F3A\u8FDE\u901A\u5206\u91CF\uFF08\u4E92\u76F8\u4F9D\u8D56\u7684\u6A21\u578B\u6CA1\u88AB\u6536\u8FDB\u540C\u4E00\u4E2A\u5206\u91CF\uFF09\u3002\u5269\u4F59\u5206\u91CF\uFF1A${JSON.stringify(stuck)}\u3002\u4EA7\u7269\u4E0D\u53EF\u4FE1`
|
|
6293
|
+
);
|
|
6294
|
+
}
|
|
6295
|
+
for (const i of ready) {
|
|
6296
|
+
levelOf.set(i, level);
|
|
6297
|
+
remaining.delete(i);
|
|
6298
|
+
}
|
|
6299
|
+
const readySet = new Set(ready);
|
|
6300
|
+
for (const d of remaining.values()) for (const r of readySet) d.delete(r);
|
|
6301
|
+
level += 1;
|
|
6302
|
+
}
|
|
6303
|
+
if (remaining.size > 0) {
|
|
6304
|
+
throw new Error(
|
|
6305
|
+
`\u62D3\u6251\u5206\u6CE2\u5728 ${comps.length + 1} \u8F6E\u5185\u6CA1\u6709\u51FA\u6E05\uFF0C\u5269\u4F59\u5206\u91CF ${JSON.stringify([...remaining.keys()].map((i) => comps[i]).sort(compareStringArrays))}\uFF1B\u5206\u6CE2\u4E0E SCC \u8BA1\u7B97\u4E0D\u4E00\u81F4\uFF0C\u4EA7\u7269\u4E0D\u53EF\u4FE1`
|
|
6306
|
+
);
|
|
6307
|
+
}
|
|
6308
|
+
const blockedComps = /* @__PURE__ */ new Set();
|
|
6309
|
+
for (const i of [...comps.keys()].sort((x, y) => levelOf.get(x) - levelOf.get(y))) {
|
|
6310
|
+
if ([...compDeps.get(i)].some((j) => cyclicComps.has(j) || blockedComps.has(j))) blockedComps.add(i);
|
|
6311
|
+
}
|
|
6312
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
6313
|
+
for (const n of nodes) {
|
|
6314
|
+
if (cyclic.has(n)) continue;
|
|
6315
|
+
const lv = levelOf.get(compOf.get(n));
|
|
6316
|
+
if (!buckets.has(lv)) buckets.set(lv, []);
|
|
6317
|
+
buckets.get(lv).push(n);
|
|
6318
|
+
}
|
|
6319
|
+
const waves = [...buckets.keys()].sort((a, b) => a - b).map((lv) => buckets.get(lv).sort());
|
|
6320
|
+
return {
|
|
6321
|
+
selfLoops: [...selfLoops].sort(),
|
|
6322
|
+
cycles,
|
|
6323
|
+
unordered,
|
|
6324
|
+
waves,
|
|
6325
|
+
blockedByCycles: nodes.filter((m) => !cyclic.has(m) && blockedComps.has(compOf.get(m))).sort(),
|
|
6326
|
+
excluded,
|
|
6327
|
+
outside
|
|
6328
|
+
};
|
|
6329
|
+
}
|
|
6330
|
+
function impactOf(r) {
|
|
6331
|
+
return {
|
|
6332
|
+
cycles: r.cycles.length,
|
|
6333
|
+
cycleMembers: r.unordered.length,
|
|
6334
|
+
selfLoops: r.selfLoops.length,
|
|
6335
|
+
blockedByCycles: r.blockedByCycles.length
|
|
6336
|
+
};
|
|
6337
|
+
}
|
|
6338
|
+
var NO_META_NOTE = "\u8FD9\u4E9B\u6A21\u578B\u5728\u540D\u518C\u91CC\u3001\u4F46**\u672C\u8F6E\u6CA1\u62FF\u5230 meta**\uFF08\u591A\u4E3A HTTP 404 \u7684 `*Index` \u578B\u9690\u85CF\u6A21\u578B\uFF0C\u8BE6\u89C1 unavailable.json\uFF09\u3002\u21D2 \u5B83\u4EEC\u7684**\u51FA\u8FB9\u4E0D\u53EF\u77E5**\uFF1A\u56FE\u91CC\u770B\u7740\u6CA1\u6709\u4F9D\u8D56\uFF0C\u53EA\u662F\u56E0\u4E3A\u6211\u4EEC\u6CA1\u8BFB\u5230\u5B83\u4EEC\u7684\u5B57\u6BB5\u3002\u{1F534} \u5B83\u4EEC\u7167\u6837\u5728 waves \u91CC\uFF08\u5220\u6389\u7B49\u4E8E\u5047\u88C5\u540D\u518C\u91CC\u6CA1\u8FD9\u4E2A\u6A21\u578B\uFF09\uFF0C\u800C waves \u91CC\u300C\u771F\u7684\u65E0\u4F9D\u8D56\u300D\u4E0E\u300C\u51FA\u8FB9\u4E0D\u53EF\u77E5\u300D**\u540C\u5F62** \u2014\u2014 \u7167 waves[0] \u5148\u5BFC\u5B83\u4EEC\uFF0C\u53EF\u80FD\u5BFC\u5230\u4E00\u534A\u624D\u53D1\u73B0\u6709\u4F9D\u8D56\u3002\u8981\u5224\u65AD\u67D0\u4E2A wave \u6210\u5458\u53EF\u4E0D\u53EF\u4FE1\uFF0C**\u5148\u548C\u8FD9\u4EFD\u540D\u5355\u53D6\u4EA4\u96C6**\u3002";
|
|
6339
|
+
var NO_META_UNKNOWN_NOTE = "\u8C03\u7528\u65B9**\u6CA1\u544A\u8BC9**\u672C\u6A21\u5757\u54EA\u4E9B\u6A21\u578B\u62FF\u5230\u4E86 meta\uFF08`modelsWithMeta` \u672A\u63D0\u4F9B\uFF09\u21D2 \u672C\u8F6E\u65E0\u4ECE\u5224\u65AD\u6709\u6CA1\u6709\u300C\u51FA\u8FB9\u4E0D\u53EF\u77E5\u300D\u7684\u8282\u70B9\u3002`null` \u662F\u300C\u4E0D\u77E5\u9053\u300D\uFF0C**\u4E0D\u662F**\u300C\u4E00\u4E2A\u90FD\u6CA1\u6709\u300D\u2014\u2014 \u540E\u8005\u662F `count: 0` + \u7A7A\u5217\u8868\u3002";
|
|
6340
|
+
function buildTopology(edges, dangling, modelKeys2, modelsWithMeta) {
|
|
6341
|
+
const nodes = [...new Set(modelKeys2)].sort();
|
|
6342
|
+
const nodeSet = new Set(nodes);
|
|
6343
|
+
const kept = analyze(edges, nodes, nodeSet, true);
|
|
6344
|
+
const withAudit = analyze(edges, nodes, nodeSet, false);
|
|
6345
|
+
let noMeta;
|
|
6346
|
+
if (modelsWithMeta === void 0 || modelsWithMeta === null) {
|
|
6347
|
+
noMeta = { count: null, models: null, note: NO_META_UNKNOWN_NOTE };
|
|
6348
|
+
} else {
|
|
6349
|
+
const seen = new Set(modelsWithMeta);
|
|
6350
|
+
const missing = nodes.filter((n) => !seen.has(n));
|
|
6351
|
+
noMeta = { count: missing.length, models: missing, note: NO_META_NOTE };
|
|
6352
|
+
}
|
|
6353
|
+
return {
|
|
6354
|
+
crawled: true,
|
|
6355
|
+
wording: WORDING2,
|
|
6356
|
+
nodeCount: nodes.length,
|
|
6357
|
+
// 🔴 「出边不可知」的节点。恒发键:缺席会被读成「这个工具还不懂这一档」,
|
|
6358
|
+
// 而 `null` 与 `[]` 又是两个不同的事实(见上方两条 note)。
|
|
6359
|
+
nodesWithoutMeta: noMeta,
|
|
6360
|
+
// 🔴 边总数按**传入的边**记,不按图里用掉的边记:两者的差额是
|
|
6361
|
+
// excludedEdges + edgesOutsideNodes + 自环,全部摆在同一份产物里才对得上账。
|
|
6362
|
+
edgeCount: edges.length,
|
|
6363
|
+
selfLoops: kept.selfLoops,
|
|
6364
|
+
cycles: kept.cycles,
|
|
6365
|
+
waves: kept.waves,
|
|
6366
|
+
unordered: kept.unordered,
|
|
6367
|
+
blockedByCycles: kept.blockedByCycles,
|
|
6368
|
+
excludedEdges: {
|
|
6369
|
+
count: kept.excluded.length,
|
|
6370
|
+
// 依据写成机器可读的一档,下游要放回去时不必去解析中文。
|
|
6371
|
+
basis: "field-name-convention",
|
|
6372
|
+
fieldKeys: [...AUDIT_FIELD_KEYS],
|
|
6373
|
+
note: EXCLUDED_NOTE,
|
|
6374
|
+
impact: {
|
|
6375
|
+
withAudit: impactOf(withAudit),
|
|
6376
|
+
withoutAudit: impactOf(kept),
|
|
6377
|
+
note: "\u4E24\u7EC4\u6570\u90FD\u662F**\u672C\u6B21\u73B0\u7B97**\u7684\uFF1A\u540C\u4E00\u4EFD\u8FB9\u96C6\u3001\u540C\u4E00\u6BB5\u4EE3\u7801\uFF0C\u4E00\u6B21\u542B\u5BA1\u8BA1\u8FB9\u3001\u4E00\u6B21\u6392\u9664\uFF0C\u5DEE\u989D\u5C31\u662F\u8FD9\u6761\u53E3\u5F84\u5728**\u4F60\u624B\u4E0A\u8FD9\u4EFD\u6570\u636E**\u4E0A\u7684\u4EE3\u4EF7\u3002reference \u662F\u53E6\u4E00\u4E2A\u79DF\u6237\u7684\u53C2\u8003\u503C\uFF0C\u4E0D\u662F\u672C\u6B21\u7ED3\u679C\u3002",
|
|
6378
|
+
reference: REFERENCE_IMPACT
|
|
6379
|
+
},
|
|
6380
|
+
// 原样,键与 refEdges 产出的引用边逐字相同(七键)—— 放得回去
|
|
6381
|
+
edges: kept.excluded
|
|
6382
|
+
},
|
|
6383
|
+
edgesOutsideNodes: {
|
|
6384
|
+
count: kept.outside.length,
|
|
6385
|
+
note: OUTSIDE_NOTE,
|
|
6386
|
+
edges: kept.outside
|
|
6387
|
+
// 原样,同 excludedEdges 一个形状
|
|
6388
|
+
},
|
|
6389
|
+
dangling: [...dangling],
|
|
6390
|
+
note: NOTE
|
|
6391
|
+
};
|
|
6392
|
+
}
|
|
6393
|
+
function writeTopology(layout, topo) {
|
|
6394
|
+
return layout.writeJson("topology.json", topo);
|
|
6395
|
+
}
|
|
6396
|
+
|
|
6397
|
+
// src/fde/recon/mapping.ts
|
|
6398
|
+
var AUDIT_SET2 = new Set(AUDIT_FIELD_KEYS);
|
|
6399
|
+
var MUTATION_PATH = (model) => `/api/models/${model}/mutation-capability`;
|
|
6400
|
+
var MUTATION_OPS = ["create", "update", "delete", "batch_delete"];
|
|
6401
|
+
var NOT_QUERIED = "\u672C\u8F6E\u6CA1\u6709\u67E5\u8BE2\u8BE5\u6A21\u578B\u7684 mutation-capability";
|
|
6402
|
+
var BATCH_LOST = "\u5DF2\u968F\u5E76\u53D1\u6279\u6B21\u53D1\u51FA\uFF0C\u4F46\u6279\u6B21\u7ED3\u679C\u91CC\u6CA1\u6709\u8FD9\u4E2A\u6A21\u578B \u2014\u2014 \u7ED3\u679C\u4E22\u4E86\uFF0C\u65E2\u4E0D\u662F\u670D\u52A1\u7AEF\u62D2\u7B54\u3001\u4E5F\u4E0D\u662F\u672C\u8F6E\u6CA1\u67E5\uFF0C\u91CD\u8DD1\u4E00\u8F6E\u5373\u53EF";
|
|
6403
|
+
var badSection = (t) => `\u4F20\u5165\u7684 mutation \u6BB5\u4E0D\u662F\u5BF9\u8C61\uFF08${t}\uFF09\uFF0C\u65E0\u6CD5\u5224\u8BFB`;
|
|
6404
|
+
var badCell = (op, t) => `\u4F20\u5165\u7684 mutation \u91CC ${op} \u8FD9\u4E00\u9879\u4E0D\u662F\u5BF9\u8C61\uFF08${t}\uFF09\uFF0C\u65E0\u6CD5\u5224\u8BFB`;
|
|
6405
|
+
var missingCell = (op) => `\u4F20\u5165\u7684 mutation \u91CC\u6CA1\u6709 ${op} \u8FD9\u4E00\u9879`;
|
|
6406
|
+
var WORDING3 = 'writable=false \u7684\u4E24\u79CD\u539F\u56E0\uFF1Aderived:<kind>\uFF08\u6D3E\u751F\u5B57\u6BB5\uFF0C\u670D\u52A1\u7AEF\u7B97\u51FA\u6765\u7684\uFF09\u3001readonly\uFF08\u5143\u6570\u636E\u58F0\u660E\u53EA\u8BFB\uFF09\u3002autoGenerate=true \u7684\u5B57\u6BB5\uFF08\u5DE5\u53F7 / \u7EC4\u7EC7 / \u5C97\u4F4D / \u6CD5\u4EBA\u7F16\u7801\u7B49\uFF09**\u4E0D\u662F**\u4E0D\u53EF\u5199\uFF1A\u670D\u52A1\u7AEF\u53EA\u5728\u5B57\u6BB5\u4E3A\u7A7A\u65F6\u751F\u6210\uFF0C\u8C03\u7528\u65B9\u624B\u5DE5\u6307\u5B9A\u7684\u503C\u4F18\u5148 \u2014\u2014 \u7531\u72EC\u7ACB\u6807\u8BB0 `autoGeneratedIfAbsent` \u70B9\u540D\uFF08\u4E0D\u9001\u5219\u751F\u6210\u3001\u9001\u5219\u91C7\u7EB3\uFF09\uFF0C\u4E0D\u518D\u8FDB notWritableReason\u3002\u{1F534} writable=true **\u53EA\u8868\u793A\u5143\u6570\u636E\u6CA1\u58F0\u660E\u8FD9\u4E24\u79CD\u4E0D\u53EF\u5199**\uFF0C\u4E0D\u8868\u793A\u8FD9\u4E2A\u5B57\u6BB5\u8BE5\u7531\u987E\u95EE\u586B \u2014\u2014 \u5B9E\u6D4B createById / updateById \u8FD9 1476 \u4E2A\u5BA1\u8BA1\u5B57\u6BB5\u56DB\u4E2A\u6807\u5FD7\u5168\u662F false\uFF0C\u7167\u6837\u662F writable=true\uFF0C\u5B83\u4EEC\u7531 ref.auditBackref=true \u70B9\u540D\uFF08topology \u628A\u540C\u4E00\u6279\u8FB9\u6392\u9664\u51FA\u4F9D\u8D56\u56FE\uFF0C\u8FD9\u91CC\u4E0D\u6392\u9664\uFF1Amapping \u662F\u5B57\u6BB5\u6E05\u5355\uFF0C\u4E0D\u662F\u4F9D\u8D56\u56FE\uFF09\u3002ref.valueDomain \u53EA\u5728\u76EE\u6807\u662F\u7801\u8868\uFF08DYNAMIC / CO_*\uFF09\u65F6\u7ED9\u51FA\uFF1B\u6307\u5411\u4E1A\u52A1\u6A21\u578B\u65F6\u4E3A null \u2014\u2014 \u5B9E\u6D4B 234 \u4E2A\u58F0\u660E\u51FA\u6765\u7684\u5F15\u7528\u76EE\u6807\u91CC\uFF0C230 \u4E2A\u5728\u540D\u518C\u4E2D\uFF1A\u5176\u4E2D 32 \u4E2A\u662F\u7801\u8868\u3001198 \u4E2A\u662F\u4E1A\u52A1\u6A21\u578B\uFF08User 1564 / OrgUnit 292 / Employee 199 \u2026\uFF09\u8981\u6309\u4E1A\u52A1\u952E\u73B0\u67E5\uFF1B\u53E6\u6709 4 \u4E2A\u60AC\u7A7A\u76EE\u6807\uFF08\u58F0\u660E\u4E86\u5F15\u7528\u4F46\u4E0D\u5728 /api/system/models \u540D\u518C\u91CC\uFF0C\u89C1 topology.json \u7684 dangling\uFF09\u3002valuedomain \u91CC\u6CA1\u6709\u4E1A\u52A1\u6A21\u578B\u7684\u5BF9\u7167\u8868\uFF0C\u6307\u4E00\u4E2A\u4E0D\u5B58\u5728\u7684\u6587\u4EF6\u6BD4\u4E0D\u6307\u66F4\u7CDF\uFF1A\u987E\u95EE\u4F1A\u4EE5\u4E3A\u90A3\u4EFD\u5BF9\u7167\u8868\u53EA\u662F\u8FD8\u6CA1\u751F\u6210\u3002ref.labelFieldSource=unknown \u8868\u793A\u4EA7\u54C1\u6CA1\u58F0\u660E\u663E\u793A\u5B57\u6BB5\u540D\uFF08\u5B9E\u6D4B\u8FDB\u56FE\u7684 2923 \u6761\u8FB9\u91CC 1366 \u6761\u6CA1\u58F0\u660E\uFF09\uFF0C**\u4E0D\u662F**\u300C\u9ED8\u8BA4\u7528 name\u300D\u2014\u2014 \u90A3 1366 \u6761\u6307\u5411 224 \u4E2A\u5728\u518C\u76EE\u6807\uFF0C\u5176\u4E2D 60 \u4E2A\uFF0827%\uFF0C\u76EE\u6807\u6A21\u578B\u53E3\u5F84\uFF09\u8FDE name \u5B57\u6BB5\u90FD\u6CA1\u6709\uFF0C\u6309\u8FB9\u7B97 152 \u6761\uFF0811%\uFF0C\u5B57\u6BB5\u53E3\u5F84\uFF09\u843D\u5728\u5B83\u4EEC\u8EAB\u4E0A\u3002mutation.allowed \u6052\u4E3A\u4E09\u4E2A\u503C\u4E4B\u4E00\uFF1Atrue / false / "unknown"\u3002"unknown" \u8868\u793A\u8BE5\u6A21\u578B\u8FD9\u4E00\u9879\u6CA1\u67E5\u5230\uFF0C**\u4E0D\u662F**\u300C\u5141\u8BB8\u300D\uFF1B\u5177\u4F53\u662F\u54EA\u4E00\u79CD\u6CA1\u67E5\u5230\u770B reason\uFF08\u670D\u52A1\u7AEF\u4FA7\uFF1A\u6CA1\u5E94\u7B54 / \u54CD\u5E94\u7F3A\u8FD9\u4E00\u9879 / allowed \u4E0D\u662F\u5E03\u5C14\uFF1B\u5DE5\u5177\u4FA7\uFF1A\u5E76\u53D1\u6279\u6B21\u91CC\u6CA1\u62FF\u5230\u8FD9\u4E2A\u6A21\u578B\u7684\u7ED3\u679C\uFF1B\u8C03\u7528\u65B9\u4FA7\uFF1A\u672C\u8F6E\u6CA1\u67E5 / \u4F20\u5165\u7684 mutation \u6BB5\u4E0D\u662F\u5BF9\u8C61 / \u4F20\u5165\u7684\u8FD9\u4E00\u9879\u4E0D\u662F\u5BF9\u8C61 / \u4F20\u5165\u7684\u91CC\u6CA1\u6709\u8FD9\u4E00\u9879 \u2014\u2014 \u516B\u79CD\u6210\u56E0\u63AA\u8F9E\u5404\u4E0D\u76F8\u540C\uFF0C\u300C\u4F20\u5165\u7684\u300D\u5F00\u5934\u7684\u662F\u62FC\u8FD9\u4EFD\u57FA\u7EBF\u7684\u4EBA\u641E\u9519\u4E86\uFF0C\u4E0D\u662F\u670D\u52A1\u7AEF\uFF09\u3002enumCount \u6570\u7684\u662F\u8BE5\u5B57\u6BB5\u5019\u9009\u503C\u7684**\u5143\u7D20\u4E2A\u6570**\uFF0C\u4E0E index/fields.jsonl \u91CC\u8BE5\u5B57\u6BB5\u7684 enums \u884C\u6570\u76F8\u7B49\u3002';
|
|
6407
|
+
function isRecord2(v) {
|
|
6408
|
+
return Boolean(v) && typeof v === "object" && !Array.isArray(v);
|
|
6409
|
+
}
|
|
6410
|
+
function typeName2(v) {
|
|
6411
|
+
if (v === null) return "null";
|
|
6412
|
+
if (Array.isArray(v)) return "list";
|
|
6413
|
+
return typeof v;
|
|
6414
|
+
}
|
|
6415
|
+
async function collectMutation(probe, modelKeys2) {
|
|
6416
|
+
const keys = [...new Set(modelKeys2)].sort();
|
|
6417
|
+
const paths = /* @__PURE__ */ new Map();
|
|
6418
|
+
for (const k of keys) paths.set(MUTATION_PATH(k), k);
|
|
6419
|
+
const results = probe.getMany ? await probe.getMany([...paths.keys()]) : await sequentialGet(probe, [...paths.keys()]);
|
|
6420
|
+
const mutation = {};
|
|
6421
|
+
const problems = [];
|
|
6422
|
+
for (const [path9, key] of paths) {
|
|
6423
|
+
const out = results[path9];
|
|
6424
|
+
if (out === void 0) {
|
|
6425
|
+
problems.push(`${key}: mutation-capability ${BATCH_LOST}`);
|
|
6426
|
+
mutation[key] = Object.fromEntries(
|
|
6427
|
+
MUTATION_OPS.map((op) => [op, { allowed: "unknown", reason: BATCH_LOST }])
|
|
6428
|
+
);
|
|
6429
|
+
continue;
|
|
6430
|
+
}
|
|
6431
|
+
const body = out.error === "ok" ? out.body : null;
|
|
6432
|
+
if (!isRecord2(body)) {
|
|
6433
|
+
const reason = `\u672A\u5E94\u7B54\uFF08HTTP ${out.status} / ${out.error}\uFF09`;
|
|
6434
|
+
problems.push(`${key}: mutation-capability ${reason}`);
|
|
6435
|
+
mutation[key] = Object.fromEntries(MUTATION_OPS.map((op) => [op, { allowed: "unknown", reason }]));
|
|
6436
|
+
continue;
|
|
6437
|
+
}
|
|
6438
|
+
const entry = {};
|
|
6439
|
+
for (const op of MUTATION_OPS) {
|
|
6440
|
+
const cell = body[op];
|
|
6441
|
+
if (!isRecord2(cell) || !("allowed" in cell)) {
|
|
6442
|
+
entry[op] = { allowed: "unknown", reason: `\u54CD\u5E94\u91CC\u6CA1\u6709 ${op} \u8FD9\u4E00\u9879` };
|
|
6443
|
+
problems.push(`${key}: mutation-capability \u54CD\u5E94\u7F3A ${op}`);
|
|
6444
|
+
continue;
|
|
6445
|
+
}
|
|
6446
|
+
const allowed = cell.allowed;
|
|
6447
|
+
if (typeof allowed !== "boolean") {
|
|
6448
|
+
entry[op] = { allowed: "unknown", reason: `allowed \u4E0D\u662F\u5E03\u5C14\uFF08${JSON.stringify(allowed)}\uFF09\uFF0C\u65E0\u6CD5\u5224\u8BFB` };
|
|
6449
|
+
problems.push(`${key}: mutation-capability \u7684 ${op} \u9879 allowed \u4E0D\u662F\u5E03\u5C14\uFF08${JSON.stringify(allowed)}\uFF09`);
|
|
6450
|
+
continue;
|
|
6451
|
+
}
|
|
6452
|
+
entry[op] = { allowed, reason: cell.reason ?? null };
|
|
6453
|
+
}
|
|
6454
|
+
mutation[key] = entry;
|
|
6455
|
+
}
|
|
6456
|
+
return { mutation, problems };
|
|
6457
|
+
}
|
|
6458
|
+
function mutationSection(entry) {
|
|
6459
|
+
if (entry === void 0 || entry === null) {
|
|
6460
|
+
return Object.fromEntries(MUTATION_OPS.map((op) => [op, { allowed: "unknown", reason: NOT_QUERIED }]));
|
|
6461
|
+
}
|
|
6462
|
+
if (!isRecord2(entry)) {
|
|
6463
|
+
const reason = badSection(typeName2(entry));
|
|
6464
|
+
return Object.fromEntries(MUTATION_OPS.map((op) => [op, { allowed: "unknown", reason }]));
|
|
6465
|
+
}
|
|
6466
|
+
const section = {};
|
|
6467
|
+
for (const op of MUTATION_OPS) {
|
|
6468
|
+
const cell = entry[op];
|
|
6469
|
+
if (isRecord2(cell)) {
|
|
6470
|
+
section[op] = cell;
|
|
6471
|
+
} else if (op in entry) {
|
|
6472
|
+
section[op] = { allowed: "unknown", reason: badCell(op, typeName2(cell)) };
|
|
6473
|
+
} else {
|
|
6474
|
+
section[op] = { allowed: "unknown", reason: missingCell(op) };
|
|
6475
|
+
}
|
|
6476
|
+
}
|
|
6477
|
+
return section;
|
|
6478
|
+
}
|
|
6479
|
+
function writability(field) {
|
|
6480
|
+
const derived = field.derived;
|
|
6481
|
+
if (derived) {
|
|
6482
|
+
const kind = isRecord2(derived) ? derived.kind : null;
|
|
6483
|
+
return { writable: false, reason: `derived:${typeof kind === "string" && kind ? kind : "unknown"}` };
|
|
6484
|
+
}
|
|
6485
|
+
if (field.isReadonly) return { writable: false, reason: "readonly" };
|
|
6486
|
+
return { writable: true, reason: null };
|
|
6487
|
+
}
|
|
6488
|
+
function buildMapping(metas, edges, mutation, codeTables = []) {
|
|
6489
|
+
const codeSet = new Set(codeTables);
|
|
6490
|
+
const byField = /* @__PURE__ */ new Map();
|
|
6491
|
+
for (const e of edges) byField.set(`${e.fromModel} ${e.fromField}`, e);
|
|
6492
|
+
const sheets = [];
|
|
6493
|
+
for (const modelKey of Object.keys(metas).sort()) {
|
|
6494
|
+
const { fields, problem } = metaFields(metas[modelKey]);
|
|
6495
|
+
const rows = [];
|
|
6496
|
+
for (const f of fields) {
|
|
6497
|
+
if (!isRecord2(f)) continue;
|
|
6498
|
+
const key = f.key;
|
|
6499
|
+
if (typeof key !== "string" || !key) continue;
|
|
6500
|
+
const { writable, reason } = writability(f);
|
|
6501
|
+
const edge = byField.get(`${modelKey} ${key}`);
|
|
6502
|
+
const ref = edge ? {
|
|
6503
|
+
toModel: edge.toModel,
|
|
6504
|
+
carrier: edge.carrier,
|
|
6505
|
+
// labelField / labelFieldSource 原样透传:没声明就是 null + "unknown",
|
|
6506
|
+
// 不许在这里兜底成 "name"(实测 60 个目标模型连 name 字段都没有)。
|
|
6507
|
+
labelField: edge.labelField,
|
|
6508
|
+
labelFieldSource: edge.labelFieldSource,
|
|
6509
|
+
valueDomain: codeSet.has(edge.toModel) ? `valuedomain/${edge.toModel}.json` : null,
|
|
6510
|
+
// 依据只有**字段名约定**,与 topology 同源、口径也同 —— 不是产品声明:
|
|
6511
|
+
// 实测这 1476 个字段的 isReadonly/autoGenerate/isList/isInfo 全为 false。
|
|
6512
|
+
auditBackref: AUDIT_SET2.has(key)
|
|
6513
|
+
} : null;
|
|
6514
|
+
const enums = f.enums;
|
|
6515
|
+
rows.push({
|
|
6516
|
+
key,
|
|
6517
|
+
// 🔴 中文名的键是 `name`,**不是 `label`**;必填的键是 `isRequired`,**不是 `required`**。
|
|
6518
|
+
// 接错不报错也不崩,产物里只是「全是英文键名」或「一个字段都不必填」。
|
|
6519
|
+
name: f.name ?? "",
|
|
6520
|
+
type: f.type ?? "",
|
|
6521
|
+
required: Boolean(f.isRequired),
|
|
6522
|
+
writable,
|
|
6523
|
+
notWritableReason: reason,
|
|
6524
|
+
// 🔴 autoGenerate=true 的字段**可以**由调用方指定 —— 服务端只在字段为空时才生成。
|
|
6525
|
+
// 这是 writable=true 之外的一道**独立标记**,避免把编码当成只读而漏发、导致业务键漂移。
|
|
6526
|
+
autoGeneratedIfAbsent: Boolean(f.autoGenerate),
|
|
6527
|
+
ref,
|
|
6528
|
+
// 🔴 数**全部元素**,不是「形状认得出的那些」。`fields` 侧对非对象元素也逐个产出一行
|
|
6529
|
+
// (带 `_kitValueSource` 认领),这里若只数对象,一个 `enums: ["A","B"]` 的字段
|
|
6530
|
+
// 在 mapping 里就是 enumCount=0 —— 与「这个字段没有候选值」同形,
|
|
6531
|
+
// 而 index/fields.jsonl 里明明有两行。
|
|
6532
|
+
enumCount: Array.isArray(enums) ? enums.length : 0
|
|
6533
|
+
});
|
|
6534
|
+
}
|
|
6535
|
+
sheets.push({
|
|
6536
|
+
model: modelKey,
|
|
6537
|
+
// 「爬过且这个模型确实没有字段」必须与「没爬过」(文件不存在)区分开。
|
|
6538
|
+
crawled: true,
|
|
6539
|
+
wording: WORDING3,
|
|
6540
|
+
mutation: mutationSection(isRecord2(mutation) ? mutation[modelKey] : mutation),
|
|
6541
|
+
fieldCount: rows.length,
|
|
6542
|
+
// 🔴 `null` = 载荷判读正常(fieldCount=0 就是「这个模型确实没字段」);
|
|
6543
|
+
// 非 null = **爬过、但判不出字段**,此时 fieldCount=0 不代表「没字段」。
|
|
6544
|
+
fieldsProblem: problem ?? null,
|
|
6545
|
+
fields: rows
|
|
6546
|
+
});
|
|
6547
|
+
}
|
|
6548
|
+
return sheets;
|
|
6549
|
+
}
|
|
6550
|
+
function allowedIn(sheet, op) {
|
|
6551
|
+
const section = sheet.mutation;
|
|
6552
|
+
const cell = isRecord2(section) ? section[op] : void 0;
|
|
6553
|
+
return isRecord2(cell) ? cell.allowed : "unknown";
|
|
6554
|
+
}
|
|
6555
|
+
var PATH_SEPARATORS = ["/", String.fromCharCode(92)];
|
|
6556
|
+
function unsafeKeyReason(modelKey) {
|
|
6557
|
+
if (typeof modelKey !== "string" || !modelKey) return `modelKey \u4E0D\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\uFF08${JSON.stringify(modelKey)}\uFF09`;
|
|
6558
|
+
if (PATH_SEPARATORS.some((s) => modelKey.includes(s))) {
|
|
6559
|
+
return `modelKey \u542B\u8DEF\u5F84\u5206\u9694\u7B26\uFF0C\u4E0D\u80FD\u5F53\u6587\u4EF6\u540D\uFF1A${JSON.stringify(modelKey)}`;
|
|
6560
|
+
}
|
|
6561
|
+
if (modelKey === "." || modelKey === "..") return `modelKey \u6307\u5411\u76EE\u5F55\u672C\u8EAB\u6216\u4E0A\u7EA7\uFF1A${JSON.stringify(modelKey)}`;
|
|
6562
|
+
if (modelKey.startsWith(".") || modelKey.startsWith("_")) {
|
|
6563
|
+
return `modelKey \u4EE5 . \u6216 _ \u5F00\u5934\uFF0C\u4E0D\u80FD\u5F53\u6587\u4EF6\u540D\uFF1A${JSON.stringify(modelKey)} \u2014\u2014 \u524D\u8005\u4EA7\u51FA ls \u770B\u4E0D\u89C1\u7684\u9690\u85CF\u6587\u4EF6\u3001\u540E\u8005\u4F1A\u9876\u6389\u5DE5\u5177\u81EA\u5DF1\u7684 _index.json`;
|
|
6564
|
+
}
|
|
6565
|
+
return null;
|
|
6566
|
+
}
|
|
6567
|
+
function reasonTally(sheets) {
|
|
6568
|
+
const tally = /* @__PURE__ */ new Map();
|
|
6569
|
+
for (const sheet of sheets) {
|
|
6570
|
+
for (const f of sheet.fields) {
|
|
6571
|
+
const r = f.notWritableReason;
|
|
6572
|
+
if (typeof r === "string" && r) tally.set(r, (tally.get(r) ?? 0) + 1);
|
|
6573
|
+
}
|
|
6574
|
+
}
|
|
6575
|
+
return Object.fromEntries([...tally.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
6576
|
+
}
|
|
6577
|
+
function writeMapping(layout, sheets, opts = {}) {
|
|
6578
|
+
const written = [];
|
|
6579
|
+
const unsafe = [];
|
|
6580
|
+
for (const sheet of sheets) {
|
|
6581
|
+
const modelKey = sheet.model;
|
|
6582
|
+
let reason = unsafeKeyReason(modelKey);
|
|
6583
|
+
if (reason === null) {
|
|
6584
|
+
try {
|
|
6585
|
+
written.push(layout.writeJson(`mapping/${modelKey}.json`, sheet));
|
|
6586
|
+
continue;
|
|
6587
|
+
} catch (err) {
|
|
6588
|
+
if (!(err instanceof LayoutContainmentError)) throw err;
|
|
6589
|
+
reason = err.message;
|
|
6590
|
+
}
|
|
6591
|
+
}
|
|
6592
|
+
unsafe.push({
|
|
6593
|
+
model: typeof modelKey === "string" ? modelKey : JSON.stringify(modelKey),
|
|
6594
|
+
reason,
|
|
6595
|
+
fieldCount: sheet.fieldCount
|
|
6596
|
+
});
|
|
6597
|
+
}
|
|
6598
|
+
written.push(
|
|
6599
|
+
layout.writeJson("mapping/_index.json", {
|
|
6600
|
+
crawled: true,
|
|
6601
|
+
wording: WORDING3,
|
|
6602
|
+
// 🔴 轮次身份。一轮 recon 被打断时,**这一份最容易留在上一轮**(它是编排里最后写的
|
|
6603
|
+
// 几份之一)—— 而那时 `mapping/` 里缺某个模型,与「那个模型的 modelKey 不安全、
|
|
6604
|
+
// 被拒写」同形。`null` = 调用方没告诉我,恒发键。
|
|
6605
|
+
roundId: opts.roundId ?? null,
|
|
6606
|
+
// 🔴 三份台账**恒发键**(null = 调用方没告诉我,[] = 对过账且一条都没有)。
|
|
6607
|
+
// 明细与摘要两处都要有:只进摘要 ⇒ 产物被单独拷走后就没了;只进产物 ⇒
|
|
6608
|
+
// 跑完那一刻看不见。少任何一处都等于没记。
|
|
6609
|
+
problems: opts.problems ?? null,
|
|
6610
|
+
danglingValueDomainPointers: opts.danglingValueDomain ?? null,
|
|
6611
|
+
[KIT_CARRY_OVER_KEY]: opts.carryOver ?? null,
|
|
6612
|
+
modelCount: sheets.length,
|
|
6613
|
+
// 🔴 「有几个模型」与「有几份 sheet 文件」不是同一个数,差额就是下面这一档 ——
|
|
6614
|
+
// 不点名的话,`ls mapping/ | wc -l` 与 modelCount 对不上而没人说得清为什么。
|
|
6615
|
+
sheetsWritten: written.length,
|
|
6616
|
+
unsafeModelKeys: unsafe,
|
|
6617
|
+
notWritableByReason: reasonTally(sheets),
|
|
6618
|
+
// 🔴 判据是 `=== false`,与下面 `mutationUnknown` 的补集判据**配成一个划分**。
|
|
6619
|
+
// 它的牙来自那条**没经过归一的裸 null** 夹具:`!allowed` 对 null 为真,
|
|
6620
|
+
// 于是同一个模型会同时落进 denied 和 unknown,划分不变量当场把它逮住。
|
|
6621
|
+
mutationDenied: Object.fromEntries(
|
|
6622
|
+
MUTATION_OPS.map((op) => [
|
|
6623
|
+
op,
|
|
6624
|
+
sheets.filter((s) => allowedIn(s, op) === false).map((s) => s.model).sort()
|
|
6625
|
+
])
|
|
6626
|
+
),
|
|
6627
|
+
// 与 mutationDenied 同一批数据的另一面:没查到的那些必须也点名,
|
|
6628
|
+
// 否则「不在 denied 里」会被读成「允许」。
|
|
6629
|
+
//
|
|
6630
|
+
// 🔴 判据是**补集**(既不是 true 也不是 false),不是 `=== 'unknown'`。
|
|
6631
|
+
// 差别只在一种输入上,而那种输入是真实可达的:`collectMutation` 归一过的 mutation 里
|
|
6632
|
+
// 只有三值,两种写法等价;但 `writeMapping` **不检查这个前提**,而喂给它的 mutation
|
|
6633
|
+
// 完全可能没经过归一 —— 手拼、断点续跑从磁盘读回、定向重爬合并两轮结果,都是这条路。
|
|
6634
|
+
// 那时一个 `{allowed: null}` 在 `=== 'unknown'` 的写法下两个清单都不进,**在索引里隐身**。
|
|
6635
|
+
// 写成补集,划分在**取值域**上对任何调用方都自证。
|
|
6636
|
+
// 🔴 措辞收窄过一次:补集只管**取值**,管不了 **cell 的形状** —— 未归一的调用方喂
|
|
6637
|
+
// `{create: "yes"}`(cell 不是对象)时原先直接抛。形状那一半由 `mutationSection`
|
|
6638
|
+
// (build 侧)与 `allowedIn`(本函数侧)两道闸补上,两半齐了「对任何调用方都自证」才是句实话。
|
|
6639
|
+
mutationUnknown: Object.fromEntries(
|
|
6640
|
+
MUTATION_OPS.map((op) => [
|
|
6641
|
+
op,
|
|
6642
|
+
sheets.filter((s) => {
|
|
6643
|
+
const a = allowedIn(s, op);
|
|
6644
|
+
return a !== true && a !== false;
|
|
6645
|
+
}).map((s) => s.model).sort()
|
|
6646
|
+
])
|
|
6647
|
+
)
|
|
6648
|
+
})
|
|
6649
|
+
);
|
|
6650
|
+
return written;
|
|
6651
|
+
}
|
|
6652
|
+
|
|
6653
|
+
// src/fde/recon/modelsmd.ts
|
|
6654
|
+
var SCOPE_NOTE = "\u95F8\u7684\u8986\u76D6\u9762\u662F **state-scoped**\uFF1A\u4EA7\u54C1\u91CC state \u4E0D\u53EF\u679A\u4E3E\uFF0C\u672C\u6587\u4EF6\u53EA\u53CD\u6620\u672C\u6B21\u722C\u53D6\u6240\u7528\u7684\u90A3\u4E00\u4E2A state\u3002\u9875\u7B7E\u7C7B\u95EE\u9898\u5FC5\u5148\u95EE state\uFF0C\u4E0D\u8981\u4FE1 base meta\u3002";
|
|
6655
|
+
var NO_GATE = "- \u58F0\u660E\u53EA\u8BFB\u7684\u52A8\u4F5C\uFF1A**\u672A\u53D1\u73B0\u58F0\u660E\u53EA\u8BFB\u7684\u52A8\u4F5C**\uFF08\u672A\u58F0\u660E = \u6309\u5199\u52A8\u4F5C\u5BF9\u5F85\uFF0C\u65E0\u8BBA\u5B83\u53EB preview \u8FD8\u662F check\uFF09";
|
|
6656
|
+
var FIELDS_NOT_CRAWLED = "- \u5B57\u6BB5\u6570\uFF1A\u672A\u722C\u53D6\uFF08\u8BE5\u6A21\u578B meta \u4E0D\u53EF\u7528\uFF0C\u8BE6\u89C1 unavailable.json\uFF09";
|
|
6657
|
+
var UNDECLARED = "\uFF08\u672A\u58F0\u660E\uFF09";
|
|
6658
|
+
var ORPHAN_TITLE = "## \u672A\u5F52\u5C5E\u7684\u53EA\u8BFB\u95F8";
|
|
6659
|
+
function gateDesc(gate) {
|
|
6660
|
+
const label = gate.label || "";
|
|
6661
|
+
const named = `\`${gate.key}\`` + (label ? ` \xB7 ${label}` : "");
|
|
6662
|
+
const source = (gate.source ?? []).join("+") || UNDECLARED;
|
|
6663
|
+
const guard = gate.guard || UNDECLARED;
|
|
6664
|
+
return `${named}\uFF08source=${source}\uFF0Cguard=${guard}\uFF09`;
|
|
6665
|
+
}
|
|
6666
|
+
function renderModelsMd(modelsBody, metas, gates, dropped = []) {
|
|
6667
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
6668
|
+
for (const g of gates) {
|
|
6669
|
+
const list = byModel.get(g.model) ?? [];
|
|
6670
|
+
list.push(g);
|
|
6671
|
+
byModel.set(g.model, list);
|
|
6672
|
+
}
|
|
6673
|
+
const parts = [
|
|
6674
|
+
"# \u6A21\u578B\u7D22\u5F15",
|
|
6675
|
+
"",
|
|
6676
|
+
`\u5171 ${modelsBody.totalCount ?? 0} \u4E2A\u6A21\u578B\uFF1B\u7C7B\u578B\u5206\u5E03 ${JSON.stringify(modelsBody.typeCounts ?? {})}\u3002`,
|
|
6677
|
+
"",
|
|
6678
|
+
SCOPE_NOTE,
|
|
6679
|
+
"",
|
|
6680
|
+
"\u5B57\u6BB5\u4E00\u884C\u4E00\u6761\u5728 `index/fields.jsonl`\uFF08\u6309\u4E2D\u6587\u540D / \u540C\u4E49\u8BCD grep \u5373\u53EF\u53CD\u67E5 model + key\uFF09\u3002",
|
|
6681
|
+
""
|
|
6682
|
+
];
|
|
6683
|
+
if (dropped.length > 0) {
|
|
6684
|
+
parts.push(
|
|
6685
|
+
`> \u26A0\uFE0F \u6709 **${dropped.length}** \u4E2A\u5B57\u6BB5\u6CA1\u80FD\u8FDB \`index/fields.jsonl\`\uFF0C\u9010\u6761\u5982\u4E0B\uFF08\u5B83\u4EEC\u5728\u7D22\u5F15\u91CC\u4E0E\u300C\u4EA7\u54C1\u6CA1\u6709\u8FD9\u4E9B\u5B57\u6BB5\u300D\u540C\u5F62\uFF0C\u6240\u4EE5\u5FC5\u987B\u5728\u8FD9\u91CC\u70B9\u540D\uFF09\uFF1A`
|
|
6686
|
+
);
|
|
6687
|
+
for (const d of dropped) parts.push(`> - ${d}`);
|
|
6688
|
+
parts.push("");
|
|
6689
|
+
}
|
|
6690
|
+
const listed = /* @__PURE__ */ new Set();
|
|
6691
|
+
for (const m of modelEntries(modelsBody)) {
|
|
6692
|
+
const key = String(m.modelKey ?? "");
|
|
6693
|
+
listed.add(key);
|
|
6694
|
+
const name = m.name || "";
|
|
6695
|
+
parts.push(name ? `## ${key} \xB7 ${name}` : `## ${key}`);
|
|
6696
|
+
parts.push("");
|
|
6697
|
+
parts.push(`- \u7C7B\u578B\uFF1A${m.type || UNDECLARED}`);
|
|
6698
|
+
if (!Object.prototype.hasOwnProperty.call(metas, key)) {
|
|
6699
|
+
parts.push(FIELDS_NOT_CRAWLED);
|
|
6700
|
+
} else {
|
|
6701
|
+
const { fields, problem } = metaFields(metas[key]);
|
|
6702
|
+
parts.push(
|
|
6703
|
+
problem === void 0 ? `- \u5B57\u6BB5\u6570\uFF1A${fields.length}` : `- \u5B57\u6BB5\u6570\uFF1A\u65E0\u6CD5\u5224\u8BFB\uFF08${problem}\uFF09\uFF0C\u8BE5\u6A21\u578B\u6CA1\u6709\u5B57\u6BB5\u8FDB\u7D22\u5F15`
|
|
6704
|
+
);
|
|
6705
|
+
}
|
|
6706
|
+
const gs = byModel.get(key) ?? [];
|
|
6707
|
+
if (gs.length > 0) {
|
|
6708
|
+
parts.push("- \u58F0\u660E\u53EA\u8BFB\u7684\u52A8\u4F5C\uFF1A");
|
|
6709
|
+
for (const g of gs) parts.push(` - ${gateDesc(g)}`);
|
|
6710
|
+
} else {
|
|
6711
|
+
parts.push(NO_GATE);
|
|
6712
|
+
}
|
|
6713
|
+
parts.push("");
|
|
6714
|
+
}
|
|
6715
|
+
const orphans = gates.filter((g) => !listed.has(String(g.model ?? "")));
|
|
6716
|
+
if (orphans.length > 0) {
|
|
6717
|
+
parts.push(ORPHAN_TITLE);
|
|
6718
|
+
parts.push("");
|
|
6719
|
+
parts.push(
|
|
6720
|
+
`\u4EE5\u4E0B **${orphans.length}** \u6761\u95F8\u7684\u6A21\u578B\u4E0D\u5728\u672C\u8F6E\u7684\u6A21\u578B\u6E05\u5355\u5185 \u2014\u2014 **\u4E3A\u4EC0\u4E48\u4E0D\u5728\uFF0C\u672C\u6A21\u5757\u6CA1\u6709\u67E5\u8FC7**\u3002\u{1F534} **\u522B\u8BFB\u6210\u300C\u53D1\u73B0\u4E86\u9690\u85CF\u6A21\u578B\u300D**\u3002\u5DF2\u77E5**\u81F3\u5C11**\u4E24\u6761\u8DEF\u957F\u6210\u8FD9\u6837\uFF08\u4E0D\u662F\u5168\u96C6\uFF09\uFF1A\u2460 \u6A21\u578B\u6E05\u5355\u672C\u8EAB\u6CA1\u53D6\u5168\uFF08\u6E05\u5355\u54CD\u5E94\u5F62\u72B6\u5F02\u5E38\u65F6\u4F1A\u56DE\u843D\u6210\u7A7A\u6E05\u5355\uFF09\uFF1B\u2461 \u8FD9\u51E0\u6761\u95F8\u662F**\u6CBF\u7528\u4E0A\u4E00\u8F6E\u4EA7\u7269**\u7684\uFF0C\u800C\u90A3\u4E00\u8F6E\u7684\u540D\u518C\u4E0E\u672C\u8F6E\u4E0D\u540C\uFF08\u89C1 \`gates.json\` \u7684 \`coverage._kitCarryOver\`\uFF09\u3002\u5148\u770B \`models.json\` \u7684 \`_kitPremiseCheck\`\u3002`
|
|
6721
|
+
);
|
|
6722
|
+
parts.push("");
|
|
6723
|
+
for (const g of orphans) parts.push(`- \`${g.model || UNDECLARED}\` \xB7 ${gateDesc(g)}`);
|
|
6724
|
+
parts.push("");
|
|
6725
|
+
}
|
|
6726
|
+
return parts.join("\n");
|
|
6727
|
+
}
|
|
6728
|
+
function writeModelsMd(layout, text) {
|
|
6729
|
+
return layout.writeText("index/models.md", text);
|
|
6730
|
+
}
|
|
6731
|
+
|
|
6732
|
+
// src/fde/recon/pointer-audit.ts
|
|
6733
|
+
import { existsSync as existsSync2 } from "fs";
|
|
6734
|
+
import * as path7 from "path";
|
|
6735
|
+
function isRecord3(v) {
|
|
6736
|
+
return Boolean(v) && typeof v === "object" && !Array.isArray(v);
|
|
6737
|
+
}
|
|
6738
|
+
function auditValueDomainPointers(layout, sheets) {
|
|
6739
|
+
const problems = [];
|
|
6740
|
+
const seen = /* @__PURE__ */ new Map();
|
|
6741
|
+
for (const sheet of sheets) {
|
|
6742
|
+
for (const field of sheet.fields) {
|
|
6743
|
+
const ref = field.ref;
|
|
6744
|
+
const pointer = isRecord3(ref) ? ref.valueDomain : null;
|
|
6745
|
+
if (typeof pointer !== "string" || pointer === "") continue;
|
|
6746
|
+
if (!seen.has(pointer)) {
|
|
6747
|
+
seen.set(pointer, existsSync2(path7.join(layout.dir, pointer)));
|
|
6748
|
+
}
|
|
6749
|
+
if (!seen.get(pointer)) {
|
|
6750
|
+
problems.push(
|
|
6751
|
+
`${sheet.model}.${String(field.key)}: ref.valueDomain \u6307\u5411 ${pointer}\uFF0C\u4F46\u8FD9\u4EFD\u5BF9\u7167\u8868\u6CA1\u6709\u843D\u76D8\uFF08\u60AC\u7A7A\u6307\u9488\uFF1A\u987E\u95EE\u4F1A\u4EE5\u4E3A\u5B83\u53EA\u662F\u8FD8\u6CA1\u751F\u6210\uFF09`
|
|
6752
|
+
);
|
|
6753
|
+
}
|
|
6754
|
+
}
|
|
6755
|
+
}
|
|
6756
|
+
return problems;
|
|
6757
|
+
}
|
|
6758
|
+
|
|
6759
|
+
// src/fde/recon/refs.ts
|
|
6760
|
+
var REF_CARRIERS = ["object", "refer"];
|
|
6761
|
+
function refEdges(metas, knownModels) {
|
|
6762
|
+
const known = new Set(knownModels);
|
|
6763
|
+
const edges = [];
|
|
6764
|
+
const dangling = [];
|
|
6765
|
+
for (const modelKey of Object.keys(metas).sort()) {
|
|
6766
|
+
const { fields } = metaFields(metas[modelKey]);
|
|
6767
|
+
for (const field of fields) {
|
|
6768
|
+
if (!field || typeof field !== "object" || Array.isArray(field)) continue;
|
|
6769
|
+
const rec = field;
|
|
6770
|
+
const fieldKey = rec.key;
|
|
6771
|
+
if (typeof fieldKey !== "string" || !fieldKey) continue;
|
|
6772
|
+
const carrier = REF_CARRIERS.find((c) => rec[c]);
|
|
6773
|
+
if (carrier === void 0) continue;
|
|
6774
|
+
const target = rec[carrier];
|
|
6775
|
+
if (typeof target !== "string") {
|
|
6776
|
+
dangling.push({
|
|
6777
|
+
fromModel: modelKey,
|
|
6778
|
+
fromField: fieldKey,
|
|
6779
|
+
toModel: reprOf(target),
|
|
6780
|
+
carrier,
|
|
6781
|
+
reason: "malformed-target"
|
|
6782
|
+
});
|
|
6783
|
+
continue;
|
|
6784
|
+
}
|
|
6785
|
+
if (!known.has(target)) {
|
|
6786
|
+
dangling.push({ fromModel: modelKey, fromField: fieldKey, toModel: target, carrier, reason: "not-in-registry" });
|
|
6787
|
+
continue;
|
|
6788
|
+
}
|
|
6789
|
+
const label = rec.objectLabel;
|
|
6790
|
+
const declared = typeof label === "string" && label.length > 0;
|
|
6791
|
+
edges.push({
|
|
6792
|
+
fromModel: modelKey,
|
|
6793
|
+
fromField: fieldKey,
|
|
6794
|
+
// 🔴 字段中文名,是「grep 部门 → 落到 {model, key}」这条链上唯一的人话。
|
|
6795
|
+
// 接错成 key 会让产物里全是英文键名——语法完好、不报错,
|
|
6796
|
+
// 所以键集与取值都由「边的键集逐键钉死」那条用例守着。
|
|
6797
|
+
fieldName: typeof rec.name === "string" ? rec.name : "",
|
|
6798
|
+
toModel: target,
|
|
6799
|
+
carrier,
|
|
6800
|
+
// 🔴 没声明就是 null + "unknown",**不许兜底成 "name"**:
|
|
6801
|
+
// 「不知道」必须与「猜出来的名字」可分。
|
|
6802
|
+
// 实测:2926 个 object 字段里只有 1557 个带 objectLabel;
|
|
6803
|
+
// 剩下的指向的目标里 60 个 meta 拿得到但**没有 name 字段**。
|
|
6804
|
+
// 落空时**不报错**——产物里会是一个语法完好、指向不存在列的显示字段名。
|
|
6805
|
+
labelField: declared ? label : null,
|
|
6806
|
+
labelFieldSource: declared ? "declared" : "unknown"
|
|
6807
|
+
});
|
|
6808
|
+
}
|
|
6809
|
+
}
|
|
6810
|
+
const byFrom = (a, b) => a.fromModel.localeCompare(b.fromModel) || a.fromField.localeCompare(b.fromField);
|
|
6811
|
+
edges.sort(byFrom);
|
|
6812
|
+
dangling.sort(byFrom);
|
|
6813
|
+
return { edges, dangling };
|
|
6814
|
+
}
|
|
6815
|
+
function reprOf(v) {
|
|
6816
|
+
if (typeof v === "string") return JSON.stringify(v);
|
|
6817
|
+
if (v === void 0) return "undefined";
|
|
6818
|
+
try {
|
|
6819
|
+
return JSON.stringify(v) ?? String(v);
|
|
6820
|
+
} catch {
|
|
6821
|
+
return String(v);
|
|
6822
|
+
}
|
|
6823
|
+
}
|
|
6824
|
+
|
|
6825
|
+
// src/fde/recon/valuedomain.ts
|
|
6826
|
+
var COMPLETENESS = ["authoritative-complete", "visible-complete", "partial", "unavailable"];
|
|
6827
|
+
var PAGE_SIZE = 500;
|
|
6828
|
+
var DYNAMIC_TYPE = "DYNAMIC";
|
|
6829
|
+
var MAX_REQUESTS_PER_TABLE = 40;
|
|
6830
|
+
var MAX_SECONDS_PER_TABLE = 30;
|
|
6831
|
+
var WORDING4 = "rows = \u672C\u8F6E\u4EE5\u5F53\u524D\u767B\u5F55\u8EAB\u4EFD\u8BFB\u5230\u7684\u7801\u8868\u884C\uFF1B\u6BCF\u884C\u539F\u6837\u4FDD\u7559\u670D\u52A1\u7AEF\u8FD4\u56DE\u7684\u5168\u90E8\u952E\u3002\u5F15\u7528\u5B57\u6BB5\u5B58\u7684\u662F\u8FD9\u91CC\u7684 `id`\uFF0C\u4E0D\u662F `code`\u3002completeness: visible-complete = **\u5F53\u524D\u89D2\u8272\u53EF\u89C1\u8303\u56F4\u5185**\u53D6\u6EE1\uFF08\u4E0D\u662F\u5168\u96C6\uFF0CRBAC \u88C1\u526A\u5148\u4E8E count\uFF09\uFF1Bpartial = \u6CA1\u53D6\u6EE1\u6216\u6709\u9875\u5931\u8D25**\u6216\u89E6\u53D1\u4E86\u5DE5\u5177\u7684\u6BCF\u8868\u4E0A\u9650**\uFF08\u540E\u8005\u770B\u8FD9\u5F20\u8868\u7684 `truncatedBy`\uFF1Anull = \u6CA1\u89E6\u53D1\uFF0C\u5BF9\u8C61 = \u505C\u5728\u7B2C\u51E0\u9875 / \u5DF2\u6536\u591A\u5C11\u884C / \u89E6\u53D1\u7684\u662F\u54EA\u4E2A\u4E0A\u9650 \u2014\u2014 \u90A3\u662F**\u5DE5\u5177**\u7684\u622A\u65AD\uFF0C\u4E0D\u662F\u670D\u52A1\u7AEF\u7684\u6BDB\u75C5\uFF0C\u6539\u4E0A\u9650\u91CD\u8DD1\u5C31\u80FD\u53D6\u5168\uFF09\uFF1Bunavailable = \u8FDE\u8BA1\u6570\u90FD\u6CA1\u8BFB\u5230\uFF08crawled=false\uFF0C\u4E0E\u300C\u8FD9\u5F20\u7801\u8868\u662F\u7A7A\u7684\u300D\u662F\u4E24\u56DE\u4E8B\uFF09\uFF1Bauthoritative-complete = \u9700\u8981\u4EA7\u54C1\u4FA7\u6743\u5A01\u503C\u57DF\u51FA\u53E3\uFF0C**\u672C\u7248\u672C\u6C38\u8FDC\u4E0D\u4F1A\u4EA7\u51FA**\u3002partial / unavailable \u65F6\u7981\u6B62\u636E\u6B64\u751F\u6210\u5199\u811A\u672C\u3002";
|
|
6832
|
+
function listPath(modelKey) {
|
|
6833
|
+
return `/api/models/${modelKey}/list/dsl`;
|
|
6834
|
+
}
|
|
6835
|
+
function isRecord4(v) {
|
|
6836
|
+
return Boolean(v) && typeof v === "object" && !Array.isArray(v);
|
|
6837
|
+
}
|
|
6838
|
+
function readContent(body) {
|
|
6839
|
+
if (!isRecord4(body)) return { rows: null, total: null, nonDict: 0 };
|
|
6840
|
+
const rows = body.content;
|
|
6841
|
+
const total = body.totalElements;
|
|
6842
|
+
if (!Array.isArray(rows)) return { rows: null, total: null, nonDict: 0 };
|
|
6843
|
+
if (typeof total !== "number" || !Number.isInteger(total) || total < 0) {
|
|
6844
|
+
return { rows: null, total: null, nonDict: 0 };
|
|
6845
|
+
}
|
|
6846
|
+
const kept = rows.filter((r) => isRecord4(r));
|
|
6847
|
+
return { rows: kept, total, nonDict: rows.length - kept.length };
|
|
6848
|
+
}
|
|
6849
|
+
async function collectCodeTables(probe, modelEntries2, opts = {}) {
|
|
6850
|
+
const maxRequests = opts.maxRequests ?? MAX_REQUESTS_PER_TABLE;
|
|
6851
|
+
const maxSeconds = opts.maxSeconds ?? MAX_SECONDS_PER_TABLE;
|
|
6852
|
+
const tick = opts.clock ?? (() => performance.now() / 1e3);
|
|
6853
|
+
const post = probe.post;
|
|
6854
|
+
if (!post) throw new Error("\u63A2\u9488\u6CA1\u6709 post \u80FD\u529B\uFF0C\u53D6\u4E0D\u4E86 list/dsl \u2014\u2014 \u8FD9\u662F\u7F16\u6392\u7684\u914D\u7F6E\u9519\uFF0C\u4E0D\u662F\u6570\u636E\u95EE\u9898");
|
|
6855
|
+
const tables = {};
|
|
6856
|
+
const problems = [];
|
|
6857
|
+
const keys = [];
|
|
6858
|
+
for (const entry of modelEntries2) {
|
|
6859
|
+
if (!isRecord4(entry) || entry.type !== DYNAMIC_TYPE) continue;
|
|
6860
|
+
const key = entry.modelKey;
|
|
6861
|
+
if (typeof key !== "string" || !key) {
|
|
6862
|
+
problems.push(`\u540D\u518C\u91CC\u6709\u4E00\u6761 type=DYNAMIC \u7684\u6761\u76EE\u4F46 modelKey \u4E0D\u53EF\u7528\uFF08${JSON.stringify(key)}\uFF09\uFF0C\u5DF2\u8DF3\u8FC7`);
|
|
6863
|
+
continue;
|
|
6864
|
+
}
|
|
6865
|
+
keys.push(key);
|
|
6866
|
+
}
|
|
6867
|
+
for (const key of [...new Set(keys)].sort()) {
|
|
6868
|
+
const path9 = listPath(key);
|
|
6869
|
+
const started = tick();
|
|
6870
|
+
let requests = 0;
|
|
6871
|
+
let truncatedBy = null;
|
|
6872
|
+
const probeOut = await post.call(probe, path9, { page: 0, size: 0 });
|
|
6873
|
+
requests += 1;
|
|
6874
|
+
let total = null;
|
|
6875
|
+
let reason = null;
|
|
6876
|
+
if (probeOut.error !== "ok") {
|
|
6877
|
+
reason = `\u8BA1\u6570\u63A2\u9488\u5931\u8D25\uFF08HTTP ${probeOut.status} / ${probeOut.error}\uFF09`;
|
|
6878
|
+
} else {
|
|
6879
|
+
total = readContent(probeOut.body).total;
|
|
6880
|
+
reason = total !== null ? null : `\u8BA1\u6570\u63A2\u9488\u7B54\u4E86 HTTP ${probeOut.status} \u4F46\u54CD\u5E94\u5F62\u72B6\u4E0D\u5BF9\uFF08content \u4E0D\u662F\u6570\u7EC4 / totalElements \u4E0D\u662F\u975E\u8D1F\u6574\u6570\uFF09`;
|
|
6881
|
+
}
|
|
6882
|
+
if (total === null) {
|
|
6883
|
+
problems.push(`${key}: ${reason}\uFF0C\u672C\u7801\u8868\u6807 unavailable`);
|
|
6884
|
+
tables[key] = {
|
|
6885
|
+
model: key,
|
|
6886
|
+
crawled: false,
|
|
6887
|
+
completeness: "unavailable",
|
|
6888
|
+
totalElements: null,
|
|
6889
|
+
rowsRetrieved: 0,
|
|
6890
|
+
rows: [],
|
|
6891
|
+
probeStatus: probeOut.status,
|
|
6892
|
+
probeError: probeOut.error,
|
|
6893
|
+
// 🔴 恒发键:这一档是「连计数都没读到」,与「取数时撞了上限」是两回事,
|
|
6894
|
+
// 缺键会被读成「这个工具还不懂上限」。
|
|
6895
|
+
truncatedBy: null,
|
|
6896
|
+
wording: WORDING4
|
|
6897
|
+
};
|
|
6898
|
+
continue;
|
|
6899
|
+
}
|
|
6900
|
+
const rows = [];
|
|
6901
|
+
let page = 0;
|
|
6902
|
+
let failed = null;
|
|
6903
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
6904
|
+
let prevPage = null;
|
|
6905
|
+
while (rows.length < total) {
|
|
6906
|
+
const elapsed = tick() - started;
|
|
6907
|
+
if (requests >= maxRequests || elapsed >= maxSeconds) {
|
|
6908
|
+
const which = requests >= maxRequests ? "requests" : "wallclock";
|
|
6909
|
+
truncatedBy = {
|
|
6910
|
+
limit: which,
|
|
6911
|
+
limitValue: which === "requests" ? maxRequests : maxSeconds,
|
|
6912
|
+
requestsSent: requests,
|
|
6913
|
+
elapsedSeconds: Math.round(elapsed * 1e3) / 1e3,
|
|
6914
|
+
stoppedAtPage: page,
|
|
6915
|
+
rowsRetrieved: rows.length,
|
|
6916
|
+
totalElements: total
|
|
6917
|
+
};
|
|
6918
|
+
failed = `\u89E6\u53D1\u5DE5\u5177\u7684\u6BCF\u8868${which === "requests" ? "\u8BF7\u6C42" : "\u5899\u949F"}\u4E0A\u9650\uFF08${which === "requests" ? `\u5DF2\u53D1 ${requests} \u6B21\u8BF7\u6C42` : `\u5DF2\u82B1 ${elapsed.toFixed(3)}s`}\uFF09\uFF0C\u505C\u5728\u7B2C ${page} \u9875\uFF0C\u5DF2\u6536 ${rows.length} \u884C / \u8BA1\u6570 ${total} \u884C\uFF0C\u672C\u8868\u6807 partial\u3002**\u8FD9\u662F\u5DE5\u5177\u7684\u622A\u65AD\uFF0C\u4E0D\u662F\u670D\u52A1\u7AEF\u7684\u6BDB\u75C5** \u2014\u2014 \u8C03\u5927 valuedomain.${which === "requests" ? "MAX_REQUESTS_PER_TABLE" : "MAX_SECONDS_PER_TABLE"} \u6216\u7ED9 collectCodeTables \u4F20 ${which === "requests" ? "maxRequests" : "maxSeconds"} \u540E\u91CD\u8DD1\u5C31\u80FD\u53D6\u5168`;
|
|
6919
|
+
break;
|
|
6920
|
+
}
|
|
6921
|
+
const out = await post.call(probe, path9, { page, size: PAGE_SIZE });
|
|
6922
|
+
requests += 1;
|
|
6923
|
+
if (out.error !== "ok") {
|
|
6924
|
+
failed = `\u7B2C ${page} \u9875\u5931\u8D25\uFF08HTTP ${out.status} / ${out.error}\uFF09`;
|
|
6925
|
+
break;
|
|
6926
|
+
}
|
|
6927
|
+
const { rows: got, total: pageTotal, nonDict } = readContent(out.body);
|
|
6928
|
+
if (got === null) {
|
|
6929
|
+
failed = `\u7B2C ${page} \u9875\u54CD\u5E94\u5F62\u72B6\u4E0D\u5BF9`;
|
|
6930
|
+
break;
|
|
6931
|
+
}
|
|
6932
|
+
const pageIds = new Set(got.filter((r) => typeof r.id === "string").map((r) => r.id));
|
|
6933
|
+
const serialized = JSON.stringify(got);
|
|
6934
|
+
const allIdentifiable = pageIds.size > 0 && pageIds.size === got.length;
|
|
6935
|
+
const noProgress = got.length > 0 && (allIdentifiable && [...pageIds].every((i) => seenIds.has(i)) || pageIds.size === 0 && prevPage !== null && serialized === prevPage);
|
|
6936
|
+
if (pageTotal !== total) {
|
|
6937
|
+
failed = `\u53D6\u6570\u671F\u95F4\u8BA1\u6570\u4ECE ${total} \u53D8\u6210 ${pageTotal}\uFF08\u6709\u4EBA\u5728\u5199\u8FD9\u5F20\u7801\u8868\uFF1F\u672C\u8F6E\u5FEB\u7167\u4E0D\u5B8C\u6574\uFF09`;
|
|
6938
|
+
if (noProgress) failed += "\uFF1B\u540C\u4E00\u9875\u8FD8\u89C2\u5BDF\u5230\u5206\u9875\u6CA1\u6709\u63A8\u8FDB\uFF08\u670D\u52A1\u7AEF\u53EF\u80FD\u4E5F\u5FFD\u7565\u4E86 page \u53C2\u6570\uFF09";
|
|
6939
|
+
break;
|
|
6940
|
+
}
|
|
6941
|
+
if (nonDict) {
|
|
6942
|
+
problems.push(`${key}: \u7B2C ${page} \u9875\u6709 ${nonDict} \u4E2A\u975E\u5BF9\u8C61\u5143\u7D20\uFF0C\u672A\u8BA1\u5165 rows`);
|
|
6943
|
+
}
|
|
6944
|
+
if (got.length === 0) {
|
|
6945
|
+
failed = `\u7B2C ${page} \u9875\u8FD4\u7A7A\u4F46\u8FD8\u5DEE ${total - rows.length} \u884C`;
|
|
6946
|
+
break;
|
|
6947
|
+
}
|
|
6948
|
+
if (noProgress) {
|
|
6949
|
+
failed = `\u7B2C ${page} \u9875\u6CA1\u6709\u5E26\u6765\u4EFB\u4F55\u65B0\u884C\uFF08\u670D\u52A1\u7AEF\u53EF\u80FD\u5FFD\u7565\u4E86 page \u53C2\u6570\uFF09\uFF0C\u5DF2\u7194\u65AD\uFF0C\u6B62\u6B65 ${rows.length} \u884C / \u8BA1\u6570 ${total} \u884C`;
|
|
6950
|
+
break;
|
|
6951
|
+
}
|
|
6952
|
+
for (const i of pageIds) seenIds.add(i);
|
|
6953
|
+
prevPage = serialized;
|
|
6954
|
+
rows.push(...got);
|
|
6955
|
+
page += 1;
|
|
6956
|
+
}
|
|
6957
|
+
const collectedIds = rows.filter((r) => typeof r.id === "string").map((r) => r.id);
|
|
6958
|
+
const duplicated = collectedIds.length - new Set(collectedIds).size;
|
|
6959
|
+
const complete = failed === null && rows.length === total && !duplicated ? "visible-complete" : "partial";
|
|
6960
|
+
if (duplicated) {
|
|
6961
|
+
problems.push(
|
|
6962
|
+
`${key}: \u53D6\u56DE\u7684 ${rows.length} \u884C\u91CC\u6709 ${duplicated} \u884C id \u91CD\u590D\uFF0C\u5206\u9875\u5F88\u53EF\u80FD\u6CA1\u751F\u6548\uFF08\u670D\u52A1\u7AEF\u5FFD\u7565\u4E86 page\uFF1F\uFF09\uFF0C\u884C\u6570\u51D1\u591F totalElements \u4E5F\u4E0D\u7B97\u53D6\u6EE1`
|
|
6963
|
+
);
|
|
6964
|
+
}
|
|
6965
|
+
if (failed) {
|
|
6966
|
+
problems.push(`${key}: ${failed}`);
|
|
6967
|
+
} else if (rows.length !== total) {
|
|
6968
|
+
problems.push(`${key}: \u53D6\u56DE ${rows.length} \u884C\u4F46\u8BA1\u6570\u662F ${total} \u884C`);
|
|
6969
|
+
}
|
|
6970
|
+
tables[key] = {
|
|
6971
|
+
model: key,
|
|
6972
|
+
crawled: true,
|
|
6973
|
+
completeness: complete,
|
|
6974
|
+
totalElements: total,
|
|
6975
|
+
rowsRetrieved: rows.length,
|
|
6976
|
+
rows,
|
|
6977
|
+
probeStatus: probeOut.status,
|
|
6978
|
+
probeError: probeOut.error,
|
|
6979
|
+
// 🔴 两处落点之一(另一处是上面的 `problems`):**产物是被单独拷走的那一半**,
|
|
6980
|
+
// 只进 problems 的话,顾问 `cat valuedomain/CO_X.json` 看到 partial 却查不出是被谁截的。
|
|
6981
|
+
// `null` = 查过了没触发,对象 = 触发了(恒发键)。
|
|
6982
|
+
truncatedBy,
|
|
6983
|
+
wording: WORDING4
|
|
6984
|
+
};
|
|
6985
|
+
}
|
|
6986
|
+
return { tables, problems };
|
|
6987
|
+
}
|
|
6988
|
+
function collectEnumDomains(metas) {
|
|
6989
|
+
const rows = [];
|
|
6990
|
+
for (const modelKey of Object.keys(metas).sort()) {
|
|
6991
|
+
const { fields } = metaFields(metas[modelKey]);
|
|
6992
|
+
for (const field of fields) {
|
|
6993
|
+
if (!isRecord4(field)) continue;
|
|
6994
|
+
const fieldKey = field.key;
|
|
6995
|
+
if (typeof fieldKey !== "string" || !fieldKey) continue;
|
|
6996
|
+
const enums = field.enums;
|
|
6997
|
+
if (!Array.isArray(enums) || enums.length === 0) continue;
|
|
6998
|
+
const kept = enums.filter(isRecord4).map((e) => ({ ...e }));
|
|
6999
|
+
rows.push({
|
|
7000
|
+
model: modelKey,
|
|
7001
|
+
field: fieldKey,
|
|
7002
|
+
fieldName: typeof field.name === "string" && field.name || "",
|
|
7003
|
+
count: kept.length,
|
|
7004
|
+
enums: kept,
|
|
7005
|
+
// 🔴 非对象元素进不了 `enums`(那一列的契约是「每项都是对象」),但也不许无痕消失
|
|
7006
|
+
// —— 否则「这个字段的枚举少了一项」这件事没有任何出处。
|
|
7007
|
+
// 实测 3179 条全是对象,这一档从未观测到;**恒发空列表**而不是条件出键:
|
|
7008
|
+
// 「有这一列且为空」与「没这一列」是两个值,后者读起来像「没检查过」。
|
|
7009
|
+
nonDictElements: enums.filter((e) => !isRecord4(e)).map((e) => JSON.stringify(e) ?? String(e))
|
|
7010
|
+
});
|
|
7011
|
+
}
|
|
7012
|
+
}
|
|
7013
|
+
return rows;
|
|
7014
|
+
}
|
|
7015
|
+
var RESERVED_FILENAMES = ["_index.json", "_enums.jsonl"];
|
|
7016
|
+
var PATH_SEPARATORS2 = ["/", String.fromCharCode(92)];
|
|
7017
|
+
function unsafeTableKeyReason(key) {
|
|
7018
|
+
if (PATH_SEPARATORS2.some((s) => key.includes(s))) {
|
|
7019
|
+
return `modelKey \u542B\u8DEF\u5F84\u5206\u9694\u7B26\uFF0C\u843D\u76D8\u4F1A\u5199\u51FA valuedomain/ \u5B50\u76EE\u5F55\uFF1A${JSON.stringify(key)}`;
|
|
7020
|
+
}
|
|
7021
|
+
if (key.startsWith(".")) return `modelKey \u4EE5 . \u5F00\u5934\uFF0C\u4F1A\u4EA7\u51FA ls \u770B\u4E0D\u89C1\u7684\u9690\u85CF\u6587\u4EF6\uFF1A${JSON.stringify(key)}`;
|
|
7022
|
+
if (key.startsWith("_")) {
|
|
7023
|
+
return `modelKey \u4EE5 _ \u5F00\u5934\uFF0C\u4E0E\u672C\u76EE\u5F55\u5DE5\u5177\u81EA\u5DF1\u7684\u4FDD\u7559\u6587\u4EF6\u540D\uFF08${RESERVED_FILENAMES.join("\u3001")}\uFF09\u540C\u4E00\u547D\u540D\u7A7A\u95F4\uFF0C\u4F1A\u9876\u6389\u5B83\u4EEC\uFF1A${JSON.stringify(key)}`;
|
|
7024
|
+
}
|
|
7025
|
+
return null;
|
|
7026
|
+
}
|
|
7027
|
+
function writeValuedomain(layout, codeTables, enums, opts = {}) {
|
|
7028
|
+
const written = [];
|
|
7029
|
+
const writeProblems = [];
|
|
7030
|
+
for (const key of Object.keys(codeTables).sort()) {
|
|
7031
|
+
const reason = unsafeTableKeyReason(key);
|
|
7032
|
+
if (reason !== null) {
|
|
7033
|
+
writeProblems.push(`${key}: ${reason}\uFF0C\u5DF2\u62D2\u7EDD`);
|
|
7034
|
+
continue;
|
|
7035
|
+
}
|
|
7036
|
+
try {
|
|
7037
|
+
written.push(layout.writeJson(`valuedomain/${key}.json`, codeTables[key]));
|
|
7038
|
+
} catch (err) {
|
|
7039
|
+
if (!(err instanceof LayoutContainmentError)) throw err;
|
|
7040
|
+
writeProblems.push(`${key}: \u843D\u76D8\u88AB\u62D2\uFF0C\u672C\u7801\u8868\u6CA1\u6709\u4EA7\u7269\u6587\u4EF6\uFF08${err.message}\uFF09`);
|
|
7041
|
+
}
|
|
7042
|
+
}
|
|
7043
|
+
written.push(layout.writeJsonl("valuedomain/_enums.jsonl", enums));
|
|
7044
|
+
written.push(
|
|
7045
|
+
layout.writeJson("valuedomain/_index.json", {
|
|
7046
|
+
crawled: true,
|
|
7047
|
+
wording: WORDING4,
|
|
7048
|
+
// 🔴 轮次身份。这份索引会被**单独拷走**,而一次中断会让它留在上一轮 ——
|
|
7049
|
+
// 那时 `valuedomain/` 里缺某张码表与「那张表真的没有」同形。
|
|
7050
|
+
// `null` = 调用方没告诉我(与 identity / modelEntriesSeen 同口径),恒发键。
|
|
7051
|
+
roundId: opts.roundId ?? null,
|
|
7052
|
+
// 🔴 `identity || {}` 会把「调用方没传身份」和「身份是空的」压成同一个值,
|
|
7053
|
+
// 而它们的下一步不同(前者去补工具的登录,后者去问服务端为什么不给 user)。
|
|
7054
|
+
identity: isRecord4(opts.identity) ? opts.identity : null,
|
|
7055
|
+
codeTableCount: Object.keys(codeTables).length,
|
|
7056
|
+
enumFieldCount: enums.length,
|
|
7057
|
+
// 🔴 名册里一共看到几条。上游名册加载失败给出空数组时,产物会与
|
|
7058
|
+
// 「名册 742 条里一条 DYNAMIC 都没有」**逐字节同形**。
|
|
7059
|
+
// `null` = 调用方没告诉我(与 identity 同口径),`0` = 名册确实是空的。
|
|
7060
|
+
modelEntriesSeen: opts.modelEntriesSeen ?? null,
|
|
7061
|
+
// 🔴 定向重爬披露。这份产物**会被单独拷走**,而 `_enums.jsonl` 里非重爬模型的枚举
|
|
7062
|
+
// 来自**沿用的 meta** —— 不在这份索引里说一声,拿走 `valuedomain/` 的人就会把整册
|
|
7063
|
+
// 都当成本轮观测到的。**恒发键**:缺席不许兼职「本轮是全量」。
|
|
7064
|
+
[KIT_CARRY_OVER_KEY]: opts.carryOver ?? null,
|
|
7065
|
+
// 🔴 四档全列,产不出来的那一档给空数组而不是缺键:下游据这份索引决定放不放行写入,
|
|
7066
|
+
// 缺键会被读成「这个工具还不懂这一档」,空数组才是「懂,且本轮一个都没有」。
|
|
7067
|
+
byCompleteness: Object.fromEntries(
|
|
7068
|
+
COMPLETENESS.map((c) => [
|
|
7069
|
+
c,
|
|
7070
|
+
Object.entries(codeTables).filter(([, v]) => v.completeness === c).map(([k]) => k).sort()
|
|
7071
|
+
])
|
|
7072
|
+
),
|
|
7073
|
+
// 🔴 `byCompleteness` 里有名字、这里却列着的表 = 爬到了但没落盘。
|
|
7074
|
+
// 两者一起看才知道「找不到某张码表的文件」是哪一种原因。
|
|
7075
|
+
writeProblems,
|
|
7076
|
+
// 🔴 **取数期**的台账,与上面那份**落盘期**的不是一回事,合并会把「有这张表但内容残」
|
|
7077
|
+
// 和「压根没有这份文件」压成一句话。明细与摘要两处都要有,缺一份都等于没记。
|
|
7078
|
+
// `null` = 调用方没告诉我,`[]` = 对过账且一条都没有。
|
|
7079
|
+
problems: opts.problems ?? null
|
|
7080
|
+
})
|
|
7081
|
+
);
|
|
7082
|
+
return { written, writeProblems };
|
|
7083
|
+
}
|
|
7084
|
+
|
|
7085
|
+
// src/fde/recon/runner.ts
|
|
7086
|
+
var TargetModelNotFound = class extends Error {
|
|
7087
|
+
constructor(message) {
|
|
7088
|
+
super(message);
|
|
7089
|
+
this.name = "TargetModelNotFound";
|
|
7090
|
+
}
|
|
7091
|
+
};
|
|
7092
|
+
var CARRY_NOTE = "mode=targeted \u65F6\u672C\u8F6E\u53EA\u91CD\u722C\u4E86 modelsRecrawled \u91CC\u7684\u6A21\u578B\uFF1BmodelsCarriedOver \u7684\u5B57\u6BB5\u4E0E\u53EA\u8BFB\u95F8\u662F\u4ECE\u4E0A\u4E00\u8F6E\u843D\u76D8\u7684\u4EA7\u7269\u91CC**\u8BFB\u56DE\u6765\u6CBF\u7528**\u7684\uFF0C\u4E0D\u662F\u8FD9\u4E00\u8F6E\u89C2\u6D4B\u5230\u7684\u3002coverage \u7684\u5176\u4F59\u7EDF\u8BA1\u91CF\uFF08modelsQueried / modelsAnswered / unavailable / rejected \u2026\uFF09\u53EA\u63CF\u8FF0**\u672C\u8F6E**\uFF0C\u4E0D\u542B\u6CBF\u7528\u90E8\u5206 \u2014\u2014 \u522B\u62FF\u5B83\u4EEC\u53BB\u89E3\u91CA gates \u7684\u603B\u6761\u6570\u3002mode=full \u65F6\u4E24\u4E2A\u5217\u8868\u90FD\u4E3A\u7A7A\uFF0C\u8FD9\u662F\u300C\u5168\u91CF\u722C\u8FC7\u300D\u800C\u4E0D\u662F\u300C\u6CA1\u722C\u300D\u3002 | topology / valuedomain / mapping \u4E09\u4EFD\u4EA7\u7269\u5728 mode=targeted \u65F6**\u7167\u65E7\u6574\u4F53\u91CD\u7B97**\uFF0C\u4F46\u91CD\u7B97\u7528\u7684\u8F93\u5165\u5206\u4E24\u534A\uFF0C\u522B\u6DF7\u7740\u8BFB\uFF1A\u2460 **\u73B0\u6253\u670D\u52A1\u7AEF**\u7684\u90A3\u534A\uFF08\u7801\u8868 rows\u3001mutation-capability\uFF09\u8986\u76D6\u5168\u90E8\u6A21\u578B\uFF0C**\u5168\u90E8\u662F\u672C\u8F6E\u89C2\u6D4B**\uFF1B\u2461 **\u5403 meta** \u7684\u90A3\u534A\uFF08topology \u7684\u8FB9\u96C6\u3001mapping \u7684\u5B57\u6BB5\u6E05\u5355\u3001valuedomain/_enums.jsonl\uFF09\u5BF9 modelsCarriedOver \u91CC\u7684\u6A21\u578B\u7528\u7684\u662F**\u6CBF\u7528\u7684 meta**\u3002topologyRecomputed=false \u8BF4\u7684\u6B63\u662F \u2461\uFF1A\u8FD9\u5F20\u56FE\u8FD9\u4E00\u8F6E**\u7B97\u8FC7**\uFF0C\u53EA\u662F\u5B83\u7684\u8FB9\u6709\u4E00\u90E8\u5206\u6765\u81EA\u4E0A\u4E00\u8F6E\u89C2\u6D4B\u7684 meta \u2014\u2014 **\u4E0D\u662F**\u300C\u6CA1\u7B97\u300D\u3002mode=full \u65F6\u5B83\u4E3A true\u3002";
|
|
7093
|
+
var KIT_CRAWLED_AT_FILE = "_kitCrawledAt.json";
|
|
7094
|
+
var CRAWLED_AT_NOTE = "models[<\u6A21\u578B>] = \u8BE5\u6A21\u578B\u7684 meta **\u6700\u540E\u4E00\u6B21\u771F\u6B63\u88AB\u722C\u4E0B\u6765**\u7684\u65F6\u523B\uFF08UTC\uFF09\u3002\u5B9A\u5411\u91CD\u722C\u53EA\u66F4\u65B0\u672C\u8F6E\u91CD\u722C\u7684\u90A3\u4E9B\uFF0C\u6CBF\u7528\u7684\u4FDD\u6301\u539F\u503C\u4E0D\u52A8 \u2014\u2014 \u8FD9\u4EFD\u6587\u4EF6\u6B63\u662F\u300C\u8FD9\u4EFD\u57FA\u7EBF\u6709\u591A\u8001\u300D\u7684\u552F\u4E00\u53EF\u9760\u4F9D\u636E\uFF1A\u6587\u4EF6 mtime \u4F1A\u88AB cp/\u6253\u5305/rsync \u6539\u5199\uFF0C\u5B83\u4E0D\u4F1A\u3002\u503C\u4E3A null = \u8FD9\u4E2A\u6A21\u578B\u662F\u6CBF\u7528\u6765\u7684\u3001\u800C\u4EA7\u51FA\u5B83\u7684\u90A3\u4E00\u8F6E**\u6CA1\u6709\u8BB0\u5F55\u65F6\u95F4\u6233**\uFF08\u57FA\u7EBF\u7531\u66F4\u65E9\u7248\u672C\u7684 kit \u4EA7\u51FA\uFF09\u2014\u2014 \u4E0E\u300C\u521A\u722C\u7684\u300D\u662F\u4E24\u4EF6\u4E8B\uFF0C\u4E0D\u8BB8\u5408\u5E76\u3002";
|
|
7095
|
+
var ROUND_STAMPED_PRODUCTS = [
|
|
7096
|
+
"topology.json",
|
|
7097
|
+
"valuedomain/_index.json",
|
|
7098
|
+
"mapping/_index.json"
|
|
7099
|
+
];
|
|
7100
|
+
var ROUND_STATES = ["complete", "mixed", "unknown"];
|
|
7101
|
+
var ROUND_EXIT_CODES = { complete: 0, mixed: 1, unknown: 2 };
|
|
7102
|
+
var GREP_RECIPE_HEAD = `grep -H -e '"roundId"' -e '"productsWrittenThisRound"'`;
|
|
7103
|
+
var ROUND_SUBCOMMAND = "hcm delivery recon round";
|
|
7104
|
+
var ROUND_NOTE = `roundId = \u8FD9\u4E00\u8F6E recon \u7684\u8EAB\u4EFD\uFF08\u8D77\u8DD1\u65F6\u523B + \u968F\u673A\u540E\u7F00\uFF0C\u53EF\u6392\u5E8F\u3001\u540C\u79D2\u4E24\u8F6E\u4E5F\u4E0D\u4F1A\u649E\uFF09\u3002productsWrittenThisRound = \u672C\u8F6E**\u771F\u6B63\u843D\u76D8**\u7684\u4EA7\u7269\u76F8\u5BF9\u8DEF\u5F84 \u2014\u2014 \u5224\u636E\u662F\u6587\u4EF6\u771F\u7684\u5199\u51FA\u6765\u4E86\uFF0C\u4E0D\u662F\u300C\u672C\u8F6E\u6253\u7B97\u5199\u300D\uFF1B\`null\` \u8868\u793A\u8FD9\u4E00\u8F6E\u6CA1\u8DD1\u5230\u5199\u8FD9\u4EFD\u53F0\u8D26\u90A3\u4E00\u6B65\uFF08\u4E2D\u65AD / \u65AD\u7F51 / \u8FDB\u7A0B\u88AB\u6740\uFF09\uFF0C\u4E0E \`[]\`\uFF08\u8DD1\u5230\u4E86\u3001\u4E00\u4EFD\u90FD\u6CA1\u5199\uFF09\u662F\u4E24\u4EF6\u4E8B\u3002topology.json / valuedomain/_index.json / mapping/_index.json \u5404\u81EA\u5E26\u4E00\u4EFD\u540C\u540D\u7684 roundId\uFF1A\u5B83\u4EEC\u4E0E\u8FD9\u91CC\u5BF9\u4E0D\u4E0A\uFF0C\u5C31\u8BF4\u660E\u90A3\u51E0\u4EFD\u4EA7\u7269\u662F**\u522B\u7684\u8F6E\u6B21**\u7559\u4E0B\u7684 \u2014\u2014 \u6B64\u65F6 \`mapping/\` \u91CC\u300C\u6CA1\u6709\u67D0\u4E2A\u6A21\u578B\u300D\u53EF\u80FD\u53EA\u662F\u672C\u8F6E\u6CA1\u5199\u5230\u90A3\u91CC\uFF0C\u4E0D\u662F\u4EA7\u54C1\u7684\u7ED3\u8BBA\u3002 | \u600E\u4E48\u5F53\u573A\u5224\uFF08\u96F6\u7F51\u7EDC\uFF09\uFF1A\u2460 \u8089\u773C\u5BF9\u8D26\uFF0C**\u5728\u4EA7\u7269\u76EE\u5F55\u91CC**\u6267\u884C \`${GREP_RECIPE_HEAD} ${KIT_CRAWLED_AT_FILE} ${ROUND_STAMPED_PRODUCTS.join(" ")}\`\u3002\u8BFB\u6CD5\u6709\u4E09\u79CD\uFF1A**\u56DB\u4E2A roundId \u5BF9\u4E0D\u9F50** = \u6DF7\u88C5\uFF08\u90A3\u51E0\u4EFD\u4EA7\u7269\u662F\u522B\u7684\u8F6E\u6B21\u7559\u4E0B\u7684\uFF09\uFF1B**\u56DB\u4E2A\u90FD\u5BF9\u9F50\u3001\u4F46 productsWrittenThisRound \u662F \`null\`** = \u8FD9\u4E00\u8F6E**\u6CA1\u8DD1\u5B8C**\uFF08\u5199\u5B8C\u6700\u540E\u4E00\u4EFD\u4EA7\u7269\u4E4B\u524D\u5C31\u65AD\u4E86\uFF0C\u771F\u673A\u5B9E\u6D4B\u5B58\u5728\u8FD9\u4E00\u6863\uFF0C\u522B\u628A\u300C\u5BF9\u9F50\u300D\u8BFB\u6210\u300C\u6CA1\u95EE\u9898\u300D\uFF09\uFF1B**\u4E00\u884C\u90FD grep \u4E0D\u5230** = \u8FD9\u4EFD\u57FA\u7EBF\u662F\u66F4\u65E9\u7248\u672C\u7684 kit \u722C\u7684\uFF0C\u5224\u4E0D\u4E86\u3002\u2461 \u8BA9\u5DE5\u5177\u8BF4\u4EBA\u8BDD\uFF08complete / mixed / unknown \u4E09\u6863 + \u6307\u540D\u9053\u59D3\u8BF4\u51FA\u54EA\u4EFD\u4EA7\u7269\u65E7\u5230\u54EA\u4E00\u8F6E\uFF09\uFF1A\u5728\u4EA7\u7269\u76EE\u5F55\u91CC\u6267\u884C \`${ROUND_SUBCOMMAND} --dir . --output table\`\uFF1B\u9000\u51FA\u7801 0=complete / 1=mixed / 2=unknown\uFF0C\u5199\u8FDB\u811A\u672C\u5F53\u524D\u7F6E\u95F8\u4E5F\u53EF\u4EE5\u3002\u26A0\uFE0F \u5B83\u8981 \`@hcmai/cli\` \u88C5\u8FC7\uFF08\`npx @hcmai/cli\` \u4EA6\u53EF\uFF09\uFF1B\u88C5\u4E0D\u4E86\u5C31\u8D70 \u2460\uFF0Cgrep \u662F\u4EFB\u4F55\u673A\u5668\u4E0A\u90FD\u6709\u7684\u90A3\u6761\u8DEF\u3002\u5224\u636E\u672C\u4F53\u662F \`@hcmai/sdk\` \u7684 \`inspectRound()\` / \`renderRound()\`\uFF0C\u53EA\u8BFB\u6587\u4EF6\u3001\u96F6\u7F51\u7EDC\uFF0C\u4EA7\u7269\u88AB\u5355\u72EC\u62F7\u8D70\u540E\u7167\u6837\u76F4\u63A5\u8C03\u5F97\u52A8\u3002`;
|
|
7105
|
+
var TOPOLOGY_RECOMPUTE_NOTE = `recomputedThisRound=true\uFF1A\u672C\u8F6E\u662F\u5168\u91CF\u722C\u53D6\uFF0C\u56FE\u91CC\u6BCF\u4E00\u6761\u8FB9\u90FD\u7B97\u81EA**\u8FD9\u4E00\u8F6E**\u53D6\u56DE\u7684 meta\u3002recomputedThisRound=false\uFF1A\u672C\u8F6E\u662F\u5B9A\u5411\u91CD\u722C\uFF08--model\uFF09\uFF0C\u8FD9\u5F20\u56FE**\u7167\u6837\u6574\u4F53\u91CD\u7B97\u4E86** \u2014\u2014 \u8282\u70B9\u53D6\u81EA\u672C\u8F6E\u7684\u6A21\u578B\u540D\u518C\uFF0C\u8FB9\u96C6\u4E5F\u6574\u4F53\u91CD\u7B97 \u2014\u2014 \u4F46 modelsRecrawledThisRound \u4E4B\u5916\u7684\u6A21\u578B\uFF0C\u5B83\u4EEC\u7684\u8FB9\u6765\u81EA**\u4E0A\u4E00\u8F6E\u843D\u76D8\u7684 meta**\u3002\u6240\u4EE5 false \u7684\u610F\u601D\u662F\u300C\u7B97\u8FC7\uFF0C\u90E8\u5206\u8F93\u5165\u662F\u65E7\u7684\u300D\uFF0C**\u4E0D\u662F**\u300C\u8FD9\u4E00\u8F6E\u6CA1\u7B97\u300D\u3002\u6CBF\u7528\u4E86\u54EA\u4E9B\u6A21\u578B\u3001\u4E0A\u4E00\u8F6E\u662F\u4EC0\u4E48\u65F6\u5019\u722C\u7684\uFF1A\u89C1 gates.json \u7684 coverage.${KIT_CARRY_OVER_KEY} \u4E0E\u4EA7\u7269\u6839\u7684 ${KIT_CRAWLED_AT_FILE}\uFF1Bmapping/_index.json \u4E0E valuedomain/_index.json \u4E5F\u5404\u5E26\u4E00\u4EFD\u540C\u540D\u7684 ${KIT_CARRY_OVER_KEY}\uFF08\u9876\u5C42\uFF09\u3002`;
|
|
7106
|
+
function two(n) {
|
|
7107
|
+
return String(n).padStart(2, "0");
|
|
7108
|
+
}
|
|
7109
|
+
function roundStamp(d) {
|
|
7110
|
+
return `${d.getUTCFullYear()}${two(d.getUTCMonth() + 1)}${two(d.getUTCDate())}T${two(d.getUTCHours())}${two(d.getUTCMinutes())}${two(d.getUTCSeconds())}Z`;
|
|
7111
|
+
}
|
|
7112
|
+
function isoStamp(d) {
|
|
7113
|
+
return d.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
7114
|
+
}
|
|
7115
|
+
function parseRoundStamp(stamp) {
|
|
7116
|
+
const m = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(stamp);
|
|
7117
|
+
if (!m) return null;
|
|
7118
|
+
const [, y, mo, d, h, mi, s] = m;
|
|
7119
|
+
return new Date(Date.UTC(+y, +mo - 1, +d, +h, +mi, +s));
|
|
7120
|
+
}
|
|
7121
|
+
function newRoundId(started) {
|
|
7122
|
+
return `${roundStamp(started)}-${randomBytes2(4).toString("hex")}`;
|
|
7123
|
+
}
|
|
7124
|
+
function isRecord5(v) {
|
|
7125
|
+
return Boolean(v) && typeof v === "object" && !Array.isArray(v);
|
|
7126
|
+
}
|
|
7127
|
+
function readJsonSoft(file) {
|
|
7128
|
+
let text;
|
|
7129
|
+
try {
|
|
7130
|
+
text = readFileSync3(file, "utf8");
|
|
7131
|
+
} catch (err) {
|
|
7132
|
+
if (err?.code === "ENOENT") return void 0;
|
|
7133
|
+
throw err;
|
|
7134
|
+
}
|
|
7135
|
+
try {
|
|
7136
|
+
return JSON.parse(text);
|
|
7137
|
+
} catch {
|
|
7138
|
+
return void 0;
|
|
7139
|
+
}
|
|
7140
|
+
}
|
|
7141
|
+
function readCrawledAt(layout) {
|
|
7142
|
+
const body = readJsonSoft(path8.join(layout.dir, KIT_CRAWLED_AT_FILE));
|
|
7143
|
+
const models = isRecord5(body) ? body.models : null;
|
|
7144
|
+
return isRecord5(models) ? models : {};
|
|
7145
|
+
}
|
|
7146
|
+
function writeCrawledAt(layout, freshKeys, carriedKeys, roundId, started) {
|
|
7147
|
+
const previous = readCrawledAt(layout);
|
|
7148
|
+
const now = isoStamp(started);
|
|
7149
|
+
const models = {};
|
|
7150
|
+
for (const key of carriedKeys) {
|
|
7151
|
+
const prev = previous[key];
|
|
7152
|
+
models[key] = typeof prev === "string" ? prev : null;
|
|
7153
|
+
}
|
|
7154
|
+
for (const key of freshKeys) models[key] = now;
|
|
7155
|
+
const sorted = {};
|
|
7156
|
+
for (const key of Object.keys(models).sort()) sorted[key] = models[key] ?? null;
|
|
7157
|
+
layout.writeJson(KIT_CRAWLED_AT_FILE, {
|
|
7158
|
+
note: CRAWLED_AT_NOTE,
|
|
7159
|
+
roundNote: ROUND_NOTE,
|
|
7160
|
+
roundId,
|
|
7161
|
+
roundStartedAt: now,
|
|
7162
|
+
// 🔴 这一刻**必然**是 null:本轮还没写完,台账要等到最后一份产物落盘才知道。
|
|
7163
|
+
// 恒发键(GC3):`null` = 没跑到那一步,`[]`/清单 = 跑到了。
|
|
7164
|
+
// ⚠️ 正因为每一轮起手都会把它重置成 null,它才能当「这一轮跑完了没有」的见证 ——
|
|
7165
|
+
// 上一轮留下的清单不会冒充本轮的。
|
|
7166
|
+
productsWrittenThisRound: null,
|
|
7167
|
+
models: sorted
|
|
7168
|
+
});
|
|
7169
|
+
return sorted;
|
|
7170
|
+
}
|
|
7171
|
+
function finishRound(layout, models, roundId, started, products) {
|
|
7172
|
+
const written = [...new Set(products.filter((rel) => rel && existsSync3(path8.join(layout.dir, rel))))].sort();
|
|
7173
|
+
layout.writeJson(KIT_CRAWLED_AT_FILE, {
|
|
7174
|
+
note: CRAWLED_AT_NOTE,
|
|
7175
|
+
roundNote: ROUND_NOTE,
|
|
7176
|
+
roundId,
|
|
7177
|
+
roundStartedAt: isoStamp(started),
|
|
7178
|
+
productsWrittenThisRound: written,
|
|
7179
|
+
models
|
|
7180
|
+
});
|
|
7181
|
+
return written;
|
|
7182
|
+
}
|
|
7183
|
+
function relOf(layout, p) {
|
|
7184
|
+
const rel = path8.relative(layout.dir, p);
|
|
7185
|
+
if (!rel || rel.startsWith("..")) return p;
|
|
7186
|
+
return rel.split(path8.sep).join("/");
|
|
7187
|
+
}
|
|
7188
|
+
function productRoundId(root, rel) {
|
|
7189
|
+
const p = path8.join(root, rel);
|
|
7190
|
+
if (!existsSync3(p)) return ["missing", null];
|
|
7191
|
+
const body = readJsonSoft(p);
|
|
7192
|
+
if (body === void 0 || !isRecord5(body)) return ["unreadable", null];
|
|
7193
|
+
const rid = body.roundId;
|
|
7194
|
+
if (typeof rid !== "string" || !rid) return ["unstamped", null];
|
|
7195
|
+
return ["stamped", rid];
|
|
7196
|
+
}
|
|
7197
|
+
function inspectRound(root) {
|
|
7198
|
+
const raw = readJsonSoft(path8.join(root, KIT_CRAWLED_AT_FILE));
|
|
7199
|
+
const ledger = isRecord5(raw) ? raw : {};
|
|
7200
|
+
const rid = ledger.roundId;
|
|
7201
|
+
const roundId = typeof rid === "string" && rid ? rid : null;
|
|
7202
|
+
const rawWritten = ledger.productsWrittenThisRound;
|
|
7203
|
+
const written = Array.isArray(rawWritten) ? rawWritten : null;
|
|
7204
|
+
let ledgerMissing = null;
|
|
7205
|
+
if (written !== null) {
|
|
7206
|
+
const gone = written.filter((rel) => typeof rel === "string" && !existsSync3(path8.join(root, rel))).sort();
|
|
7207
|
+
ledgerMissing = { checked: written.length, count: gone.length, files: gone };
|
|
7208
|
+
}
|
|
7209
|
+
const products = [];
|
|
7210
|
+
for (const rel of ROUND_STAMPED_PRODUCTS) {
|
|
7211
|
+
const [readState, pid] = productRoundId(root, rel);
|
|
7212
|
+
const state2 = readState === "stamped" ? roundId && pid === roundId ? "this-round" : "older-round" : readState;
|
|
7213
|
+
products.push({ path: rel, state: state2, roundId: pid });
|
|
7214
|
+
}
|
|
7215
|
+
let state;
|
|
7216
|
+
let finished;
|
|
7217
|
+
if (roundId === null && products.every((p) => p.roundId === null)) {
|
|
7218
|
+
state = "unknown";
|
|
7219
|
+
finished = null;
|
|
7220
|
+
} else {
|
|
7221
|
+
finished = written !== null;
|
|
7222
|
+
state = roundId && finished && !ledgerMissing?.count && products.every((p) => p.state === "this-round") ? "complete" : "mixed";
|
|
7223
|
+
}
|
|
7224
|
+
return {
|
|
7225
|
+
state,
|
|
7226
|
+
roundId,
|
|
7227
|
+
roundStartedAt: ledger.roundStartedAt ?? null,
|
|
7228
|
+
finished,
|
|
7229
|
+
productsWrittenThisRound: written !== null ? written.length : null,
|
|
7230
|
+
ledgerFilesMissing: ledgerMissing,
|
|
7231
|
+
products,
|
|
7232
|
+
dir: root,
|
|
7233
|
+
note: ROUND_NOTE
|
|
7234
|
+
};
|
|
7235
|
+
}
|
|
7236
|
+
var LEDGER_MISSING_WORD = "\u672C\u8F6E\u5199\u8FC7\u3001\u73B0\u5728\u5374**\u627E\u4E0D\u5230\u4E86**\uFF08\u62F7\u8D1D / \u88C1\u526A\u65F6\u6F0F\u5E26\uFF0C\u6216\u4E8B\u540E\u88AB\u5220\uFF09";
|
|
7237
|
+
var PRODUCT_STATE_WORDS = {
|
|
7238
|
+
"older-round": "\u662F**\u522B\u7684\u8F6E\u6B21**\u7559\u4E0B\u7684",
|
|
7239
|
+
unstamped: "\u5728\u76D8\u4E0A\uFF0C\u4F46**\u6CA1\u6709\u8F6E\u6B21\u6233**\uFF08\u66F4\u65E9\u7248\u672C\u7684 kit \u5199\u7684\uFF09",
|
|
7240
|
+
missing: "**\u4E0D\u5728\u76D8\u4E0A**",
|
|
7241
|
+
unreadable: "\u5728\u76D8\u4E0A\u4F46**\u8BFB\u4E0D\u51FA JSON**"
|
|
7242
|
+
};
|
|
7243
|
+
function roundAge(roundId, other) {
|
|
7244
|
+
if (typeof roundId !== "string" || typeof other !== "string") return "";
|
|
7245
|
+
const a = parseRoundStamp(roundId.split("-")[0] ?? "");
|
|
7246
|
+
const b = parseRoundStamp(other.split("-")[0] ?? "");
|
|
7247
|
+
if (!a || !b) return "";
|
|
7248
|
+
const seconds = Math.trunc((a.getTime() - b.getTime()) / 1e3);
|
|
7249
|
+
if (seconds <= 0) return "";
|
|
7250
|
+
if (seconds < 60) return `\uFF0C\u6BD4\u672C\u8F6E\u65E9 ${seconds}s`;
|
|
7251
|
+
return `\uFF0C\u6BD4\u672C\u8F6E\u65E9 ${Math.floor(seconds / 3600)}h${two(Math.floor(seconds % 3600 / 60))}m`;
|
|
7252
|
+
}
|
|
7253
|
+
function renderRound(result) {
|
|
7254
|
+
const lines = [`\u57FA\u7EBF\uFF1A${result.dir}`];
|
|
7255
|
+
if (result.state === "unknown") {
|
|
7256
|
+
lines.push(
|
|
7257
|
+
"\u72B6\u6001\uFF1Aunknown \u2014\u2014 \u8FD9\u4EFD\u57FA\u7EBF\u91CC**\u4E00\u4E2A roundId \u90FD\u6CA1\u6709**\uFF08\u66F4\u65E9\u7248\u672C\u7684 kit \u722C\u7684\uFF09\u3002",
|
|
7258
|
+
"\u21D2 \u5224\u4E0D\u4E86\u5B83\u662F\u4E0D\u662F\u4E00\u8F6E\u5B8C\u6574\u4EA7\u7269\u3002**\u522B\u628A\u5B83\u5F53 complete\uFF0C\u4E5F\u522B\u5F53 mixed** \u2014\u2014\u91CD\u8DD1\u4E00\u6B21 recon \u624D\u4F1A\u5E26\u4E0A\u8F6E\u6B21\u8EAB\u4EFD\u3002"
|
|
7259
|
+
);
|
|
7260
|
+
return `${lines.join("\n")}
|
|
7261
|
+
`;
|
|
7262
|
+
}
|
|
7263
|
+
lines.push(
|
|
7264
|
+
`\u672C\u8F6E roundId\uFF1A${result.roundId ?? "\uFF08\u65E0\uFF09"}\uFF08\u8D77\u8DD1 ${result.roundStartedAt ?? "\u672A\u77E5"}\uFF09`
|
|
7265
|
+
);
|
|
7266
|
+
if (result.state === "complete") {
|
|
7267
|
+
lines.push(
|
|
7268
|
+
`\u72B6\u6001\uFF1Acomplete \u2014\u2014 ${result.products.length} \u4EFD\u5E26\u6233\u7684\u4EA7\u7269\u540C\u5C5E\u8FD9\u4E00\u8F6E\uFF0C\u672C\u8F6E\u5171\u843D\u76D8 ${result.productsWrittenThisRound} \u4E2A\u6587\u4EF6\u3002`,
|
|
7269
|
+
`\u26A0\uFE0F complete \u8BF4\u7684\u662F\u300C**\u540C\u4E00\u8F6E**\u300D\uFF0C\u4E0D\u662F\u300C\u5168\u91CF\u722C\u8FC7\u300D\uFF1A\u5B9A\u5411\u91CD\u722C\uFF08\`--model X\`\uFF09\u540C\u6837\u4F1A\u628A\u4E09\u4EFD\u4EA7\u7269\u6574\u4F53\u91CD\u5199\uFF0C\u7167\u6837\u662F complete\u3002\u5168\u91CF\u4E0E\u5426\u770B gates.json \u7684 \`coverage.${KIT_CARRY_OVER_KEY}\`\u3002`
|
|
7270
|
+
);
|
|
7271
|
+
return `${lines.join("\n")}
|
|
7272
|
+
`;
|
|
7273
|
+
}
|
|
7274
|
+
lines.push("\u72B6\u6001\uFF1Amixed \u2014\u2014 \u8FD9\u4EFD\u57FA\u7EBF**\u4E0D\u662F\u4E00\u8F6E\u5B8C\u6574\u8DD1\u51FA\u6765\u7684**\uFF1A");
|
|
7275
|
+
for (const p of result.products) {
|
|
7276
|
+
if (p.state === "this-round") continue;
|
|
7277
|
+
let detail = PRODUCT_STATE_WORDS[p.state];
|
|
7278
|
+
if (p.state === "older-round") {
|
|
7279
|
+
detail += `\uFF08roundId ${p.roundId}${roundAge(result.roundId, p.roundId)}\uFF09`;
|
|
7280
|
+
}
|
|
7281
|
+
lines.push(` \xB7 ${p.path} ${detail}`);
|
|
7282
|
+
}
|
|
7283
|
+
if (result.finished === false) {
|
|
7284
|
+
lines.push(
|
|
7285
|
+
" \xB7 \u672C\u8F6E\u7684 productsWrittenThisRound \u6CA1\u6709\u843D\u76D8 \u21D2 \u8FD9\u4E00\u8F6E**\u6CA1\u8DD1\u5B8C**\uFF08Ctrl-C / \u65AD\u7F51 / \u8FDB\u7A0B\u88AB\u6740\u90FD\u4F1A\u8FD9\u6837\uFF09"
|
|
7286
|
+
);
|
|
7287
|
+
}
|
|
7288
|
+
const gone = result.ledgerFilesMissing;
|
|
7289
|
+
if (gone?.count) {
|
|
7290
|
+
const shown = gone.files.slice(0, 5).join("\u3001");
|
|
7291
|
+
const more = gone.count > 5 ? "\uFF08\u4EC5\u5217\u524D 5 \u4E2A\uFF0C\u5B8C\u6574\u540D\u5355\u89C1 ledgerFilesMissing.files\uFF09" : "";
|
|
7292
|
+
lines.push(
|
|
7293
|
+
` \xB7 \u53F0\u8D26\u70B9\u540D\u7684 ${gone.checked} \u4EFD\u4EA7\u7269\u91CC\uFF0C\u6709 **${gone.count}** \u4EFD${LEDGER_MISSING_WORD}\uFF1A${shown}${more}`
|
|
7294
|
+
);
|
|
7295
|
+
}
|
|
7296
|
+
lines.push(
|
|
7297
|
+
"\u21D2 \u{1F534} \u522B\u62FF\u8FD9\u4E9B\u4EA7\u7269\u91CC\u300C\u6CA1\u6709\u67D0\u4E2A\u6A21\u578B\u300D\u5F53\u7ED3\u8BBA \u2014\u2014 \u90A3\u53EF\u80FD\u53EA\u662F**\u672C\u8F6E\u6CA1\u5199\u5230\u90A3\u91CC**\uFF0C\u4E0E\u300C\u4EA7\u54C1\u91CC\u771F\u7684\u6CA1\u6709\u300D\u540C\u5F62\u3002\u91CD\u8DD1\u4E00\u6B21\u5B8C\u6574 recon\u3002"
|
|
7298
|
+
);
|
|
7299
|
+
return `${lines.join("\n")}
|
|
7300
|
+
`;
|
|
7301
|
+
}
|
|
7302
|
+
function collectCarryOver(layout, freshMetas) {
|
|
7303
|
+
const problems = [];
|
|
7304
|
+
const carriedMetas = {};
|
|
7305
|
+
const metaDir = path8.join(layout.dir, "meta");
|
|
7306
|
+
const suffix = `.${DEFAULT_META_TYPE}.json`;
|
|
7307
|
+
let names = [];
|
|
7308
|
+
try {
|
|
7309
|
+
names = readdirSync(metaDir).filter((n) => n.endsWith(suffix)).sort();
|
|
7310
|
+
} catch (err) {
|
|
7311
|
+
if (err?.code !== "ENOENT") throw err;
|
|
7312
|
+
}
|
|
7313
|
+
for (const name of names) {
|
|
7314
|
+
const key = name.slice(0, -suffix.length);
|
|
7315
|
+
if (key in freshMetas) continue;
|
|
7316
|
+
const file = path8.join(metaDir, name);
|
|
7317
|
+
let body;
|
|
7318
|
+
try {
|
|
7319
|
+
body = JSON.parse(readFileSync3(file, "utf8"));
|
|
7320
|
+
} catch (e) {
|
|
7321
|
+
problems.push(`${name}: \u8BFB\u4E0D\u56DE\u4E0A\u4E00\u8F6E\u7684 meta\uFF08${e.message}\uFF09\uFF0C\u8BE5\u6A21\u578B\u8FD9\u6B21\u4E0D\u8FDB\u7D22\u5F15`);
|
|
7322
|
+
continue;
|
|
7323
|
+
}
|
|
7324
|
+
if (isRecord5(body)) carriedMetas[key] = body;
|
|
7325
|
+
else {
|
|
7326
|
+
problems.push(
|
|
7327
|
+
`${name}: \u4E0A\u4E00\u8F6E\u7684 meta \u8F7D\u8377\u662F ${Array.isArray(body) ? "list" : body === null ? "null" : typeof body} \u4E0D\u662F\u5BF9\u8C61\uFF0C\u8BE5\u6A21\u578B\u8FD9\u6B21\u4E0D\u8FDB\u7D22\u5F15`
|
|
7328
|
+
);
|
|
7329
|
+
}
|
|
7330
|
+
}
|
|
7331
|
+
const carriedGates = [];
|
|
7332
|
+
const oldGates = readJsonSoft(path8.join(layout.dir, "gates.json"));
|
|
7333
|
+
if (oldGates === void 0) {
|
|
7334
|
+
problems.push("gates.json: \u6CA1\u6709\u53EF\u6CBF\u7528\u7684\u4E0A\u4E00\u8F6E\u95F8\u8868\uFF08\u4E0D\u5B58\u5728\u6216\u8BFB\u4E0D\u51FA JSON\uFF09");
|
|
7335
|
+
}
|
|
7336
|
+
const oldList = isRecord5(oldGates) && Array.isArray(oldGates.gates) ? oldGates.gates : [];
|
|
7337
|
+
for (const g of oldList) {
|
|
7338
|
+
if (isRecord5(g) && !(String(g.model) in freshMetas)) carriedGates.push(g);
|
|
7339
|
+
}
|
|
7340
|
+
return { carriedMetas, carriedGates, problems };
|
|
7341
|
+
}
|
|
7342
|
+
function carryOverBanner(carryOver) {
|
|
7343
|
+
if (carryOver.mode !== "targeted") return "";
|
|
7344
|
+
return `> \u26A0\uFE0F **\u672C\u8F6E\u662F\u5B9A\u5411\u91CD\u722C\uFF08\`--model ${carryOver.onlyModel}\`\uFF09\uFF0C\u4E0D\u662F\u5168\u91CF\u3002**
|
|
7345
|
+
> \u8FD9\u4E00\u8F6E\u771F\u6B63\u722C\u7684\u53EA\u6709\uFF1A${carryOver.modelsRecrawled.join(", ") || "\uFF08\u65E0\uFF09"}\uFF1B
|
|
7346
|
+
> \u53E6\u6709 **${carryOver.modelsCarriedOver.length}** \u4E2A\u6A21\u578B\u7684\u5B57\u6BB5\u4E0E **${carryOver.gatesCarriedOver}** \u6761\u53EA\u8BFB\u95F8\u662F**\u6CBF\u7528\u4E0A\u4E00\u8F6E\u4EA7\u7269**\uFF0C\u4E0D\u662F\u8FD9\u4E00\u8F6E\u89C2\u6D4B\u5230\u7684\u3002
|
|
7347
|
+
` + carryOver.problems.map((p) => `> - \u26A0\uFE0F ${p}
|
|
7348
|
+
`).join("") + "\n";
|
|
7349
|
+
}
|
|
7350
|
+
async function runRecon(probe, layout, opts = {}) {
|
|
7351
|
+
const state = opts.state ?? "";
|
|
7352
|
+
const onlyModel = opts.onlyModel ?? null;
|
|
7353
|
+
const started = /* @__PURE__ */ new Date();
|
|
7354
|
+
const roundId = newRoundId(started);
|
|
7355
|
+
const modelsBody = await crawlModels(probe);
|
|
7356
|
+
let keys = modelKeys(modelsBody);
|
|
7357
|
+
if (onlyModel) {
|
|
7358
|
+
keys = keys.filter((k) => k === onlyModel);
|
|
7359
|
+
if (keys.length === 0) {
|
|
7360
|
+
const total = Array.isArray(modelsBody.models) ? modelsBody.models.length : 0;
|
|
7361
|
+
throw new TargetModelNotFound(
|
|
7362
|
+
`--model '${onlyModel}' \u5728\u6A21\u578B\u6E05\u5355\u91CC\u4E00\u4E2A\u90FD\u6CA1\u5339\u914D\u4E0A\uFF08\u6E05\u5355\u5171 ${total} \u4E2A\u6A21\u578B\uFF09\u3002\u5DF2\u62D2\u7EDD\u5199\u76D8\uFF0C\u73B0\u6709\u57FA\u7EBF\u539F\u6837\u4FDD\u7559\u3002\u8BF7\u6838\u5BF9\u6A21\u578B\u540D\u5927\u5C0F\u5199\u4E0E\u62FC\u5199\uFF0C\u6216\u5148\u8DD1\u4E00\u6B21\u4E0D\u5E26 --model \u7684\u5168\u91CF recon\u3002`
|
|
7363
|
+
);
|
|
7364
|
+
}
|
|
7365
|
+
}
|
|
7366
|
+
const { metas: freshMetas, unavailable } = await crawlMeta(probe, keys);
|
|
7367
|
+
const gateResult = await collectActionGates(probe, keys, state);
|
|
7368
|
+
const carried = onlyModel ? collectCarryOver(layout, freshMetas) : { carriedMetas: {}, carriedGates: [], problems: [] };
|
|
7369
|
+
const metas = { ...carried.carriedMetas, ...freshMetas };
|
|
7370
|
+
const gates = [...carried.carriedGates, ...gateResult.gates];
|
|
7371
|
+
const { rows, dropped } = extractFieldRows(metas);
|
|
7372
|
+
const carryOver = {
|
|
7373
|
+
mode: onlyModel ? "targeted" : "full",
|
|
7374
|
+
onlyModel,
|
|
7375
|
+
modelsRecrawled: Object.keys(freshMetas).sort(),
|
|
7376
|
+
modelsCarriedOver: Object.keys(carried.carriedMetas).sort(),
|
|
7377
|
+
gatesCarriedOver: carried.carriedGates.length,
|
|
7378
|
+
problems: carried.problems,
|
|
7379
|
+
// 🔴 整图产物的诚实标注。语义见 CARRY_NOTE 与 topology.json 的 recomputedNote:
|
|
7380
|
+
// false ≠「没算」,而是「算过、边集里有一部分输入是沿用的 meta」。
|
|
7381
|
+
topologyRecomputed: !onlyModel,
|
|
7382
|
+
note: CARRY_NOTE
|
|
7383
|
+
};
|
|
7384
|
+
const coverage = {
|
|
7385
|
+
...gateResult.coverage,
|
|
7386
|
+
[KIT_CARRY_OVER_KEY]: carryOver
|
|
7387
|
+
};
|
|
7388
|
+
const crawledAt = writeCrawledAt(
|
|
7389
|
+
layout,
|
|
7390
|
+
Object.keys(freshMetas).sort(),
|
|
7391
|
+
Object.keys(carried.carriedMetas).sort(),
|
|
7392
|
+
roundId,
|
|
7393
|
+
started
|
|
7394
|
+
);
|
|
7395
|
+
const crawlWritten = writeCrawlProducts(layout, modelsBody, freshMetas, unavailable);
|
|
7396
|
+
const gatesPath = writeGates(layout, gates, coverage);
|
|
7397
|
+
const fieldsPath = writeFieldsIndex(layout, rows);
|
|
7398
|
+
const mdPath = writeModelsMd(
|
|
7399
|
+
layout,
|
|
7400
|
+
carryOverBanner(carryOver) + renderModelsMd(modelsBody, metas, gates, dropped)
|
|
7401
|
+
);
|
|
7402
|
+
const rejectedMetas = new Set(
|
|
7403
|
+
unavailable.filter((e) => e?.error === "unsafe-model-key").map((e) => e.modelKey)
|
|
7404
|
+
);
|
|
7405
|
+
const products = [
|
|
7406
|
+
crawlWritten.models,
|
|
7407
|
+
crawlWritten.unavailable,
|
|
7408
|
+
KIT_CRAWLED_AT_FILE,
|
|
7409
|
+
relOf(layout, gatesPath),
|
|
7410
|
+
relOf(layout, fieldsPath),
|
|
7411
|
+
relOf(layout, mdPath)
|
|
7412
|
+
];
|
|
7413
|
+
for (const k of Object.keys(freshMetas).sort()) {
|
|
7414
|
+
if (!rejectedMetas.has(k)) products.push(`meta/${k}.${DEFAULT_META_TYPE}.json`);
|
|
7415
|
+
}
|
|
7416
|
+
const known = /* @__PURE__ */ new Set([...Object.keys(metas), ...modelKeys(modelsBody)]);
|
|
7417
|
+
const { edges, dangling } = refEdges(metas, known);
|
|
7418
|
+
const topo = buildTopology(edges, dangling, [...known].sort(), Object.keys(metas).sort());
|
|
7419
|
+
topo.recomputedThisRound = !onlyModel;
|
|
7420
|
+
topo.modelsRecrawledThisRound = Object.keys(freshMetas).sort();
|
|
7421
|
+
topo.recomputedNote = TOPOLOGY_RECOMPUTE_NOTE;
|
|
7422
|
+
topo.roundId = roundId;
|
|
7423
|
+
products.push(relOf(layout, writeTopology(layout, topo)));
|
|
7424
|
+
const entries = modelEntries(modelsBody);
|
|
7425
|
+
const { tables: codeTables, problems: vdProblems } = await collectCodeTables(probe, entries);
|
|
7426
|
+
const enumRows2 = collectEnumDomains(metas);
|
|
7427
|
+
const { written: vdWritten, writeProblems: vdWriteProblems } = writeValuedomain(
|
|
7428
|
+
layout,
|
|
7429
|
+
codeTables,
|
|
7430
|
+
enumRows2,
|
|
7431
|
+
{
|
|
7432
|
+
identity: opts.identity,
|
|
7433
|
+
// `modelEntriesSeen` 不许省:名册加载失败给出空 list 时,产物会与「名册里一条
|
|
7434
|
+
// DYNAMIC 都没有」逐字节同形(GC3)。null 是「调用方没告诉我」,这里我们知道。
|
|
7435
|
+
modelEntriesSeen: entries.length,
|
|
7436
|
+
carryOver,
|
|
7437
|
+
roundId,
|
|
7438
|
+
// 取数期台账也要进产物:与落盘期的 writeProblems 分两个键,
|
|
7439
|
+
// 两侧现在都是「摘要给条数 + 产物给明细」。
|
|
7440
|
+
problems: vdProblems
|
|
7441
|
+
}
|
|
7442
|
+
);
|
|
7443
|
+
const writtenPaths = new Set(vdWritten);
|
|
7444
|
+
const codeTableFiles = new Set(
|
|
7445
|
+
Object.keys(codeTables).filter(
|
|
7446
|
+
(k) => unsafeTableKeyReason(k) === null && writtenPaths.has(path8.join(layout.dir, "valuedomain", `${k}.json`))
|
|
7447
|
+
)
|
|
7448
|
+
);
|
|
7449
|
+
const { mutation, problems: mutProblems } = await collectMutation(probe, Object.keys(metas).sort());
|
|
7450
|
+
const sheets = buildMapping(metas, edges, mutation, codeTableFiles);
|
|
7451
|
+
const danglingPointers = auditValueDomainPointers(layout, sheets);
|
|
7452
|
+
products.push(...vdWritten.map((p) => relOf(layout, p)));
|
|
7453
|
+
const mappingPaths = writeMapping(layout, sheets, {
|
|
7454
|
+
problems: mutProblems,
|
|
7455
|
+
danglingValueDomain: danglingPointers,
|
|
7456
|
+
carryOver,
|
|
7457
|
+
roundId
|
|
7458
|
+
});
|
|
7459
|
+
products.push(...mappingPaths.map((p) => relOf(layout, p)));
|
|
7460
|
+
const sheetsWritten = Math.max(mappingPaths.length - 1, 0);
|
|
7461
|
+
let previous = readJsonSoft(path8.join(layout.dir, "fingerprint.json"));
|
|
7462
|
+
if (!isRecord5(previous)) previous = null;
|
|
7463
|
+
const current = await computeFingerprint(probe);
|
|
7464
|
+
products.push(relOf(layout, layout.writeJson("fingerprint.json", current)));
|
|
7465
|
+
const drift = previous === null ? null : compareFingerprint(previous, current);
|
|
7466
|
+
const productsWritten = finishRound(layout, crawledAt, roundId, started, products);
|
|
7467
|
+
const roundResult = inspectRound(layout.dir);
|
|
7468
|
+
const byCompleteness = {};
|
|
7469
|
+
for (const c of COMPLETENESS) {
|
|
7470
|
+
byCompleteness[c] = Object.values(codeTables).filter((v) => v.completeness === c).length;
|
|
7471
|
+
}
|
|
7472
|
+
return {
|
|
7473
|
+
driftSinceLastRecon: drift,
|
|
7474
|
+
modelsQueried: keys.length,
|
|
7475
|
+
metasCrawled: Object.keys(freshMetas).length,
|
|
7476
|
+
unavailable: unavailable.length,
|
|
7477
|
+
gates: gates.length,
|
|
7478
|
+
fieldRows: rows.length,
|
|
7479
|
+
// 🔴 被丢掉的字段必须有落点。摘要给条数,models.md 给明细 ——
|
|
7480
|
+
// 「记下来了但没人看得见」等于没记。
|
|
7481
|
+
fieldsDropped: dropped.length,
|
|
7482
|
+
// 🔴 索引里有多少行不是这一轮爬的,必须能一眼看出来 —— 否则定向重爬产出的
|
|
7483
|
+
// 基线与全量基线在摘要里同形。
|
|
7484
|
+
carryOver,
|
|
7485
|
+
// 🔴 时间戳未知的模型数必须能一眼看到:它是「沿用了但不知道多老」的落点,
|
|
7486
|
+
// 与「刚爬的」不许合并(GC3)。
|
|
7487
|
+
crawledAtUnknown: Object.values(crawledAt).filter((v) => v === null).length,
|
|
7488
|
+
// 🔴 三段统计一律**带上自己的 problems / 差额**,不许只报好数字(no silent caps):
|
|
7489
|
+
// 明细在各自的产物文件里,摘要给条数 + 指向文件,两处缺一份都等于没记
|
|
7490
|
+
// (只进摘要 ⇒ 产物被单独拷走后就没了;只进产物 ⇒ 跑完那一刻看不见)。
|
|
7491
|
+
topology: {
|
|
7492
|
+
nodes: topo.nodeCount,
|
|
7493
|
+
edges: topo.edgeCount,
|
|
7494
|
+
cycles: topo.cycles.length,
|
|
7495
|
+
selfLoops: topo.selfLoops.length,
|
|
7496
|
+
unordered: topo.unordered.length,
|
|
7497
|
+
blockedByCycles: topo.blockedByCycles.length,
|
|
7498
|
+
// 三个「没进依赖图」的口子都要报数:edgeCount 与图里用掉的边数的差额
|
|
7499
|
+
// 就是它们仨,不报的话没人说得清差在哪。
|
|
7500
|
+
dangling: topo.dangling.length,
|
|
7501
|
+
// 🔴 「出边不可知」的节点数(接线后恒是数字,不再是 null)。明细在 topology.json
|
|
7502
|
+
// 的 nodesWithoutMeta.models 里,摘要给条数 —— 两处缺一份都等于没记。
|
|
7503
|
+
nodesWithoutMeta: topo.nodesWithoutMeta.count,
|
|
7504
|
+
excludedEdges: topo.excludedEdges.count,
|
|
7505
|
+
edgesOutsideNodes: topo.edgesOutsideNodes.count,
|
|
7506
|
+
recomputedThisRound: topo.recomputedThisRound,
|
|
7507
|
+
file: "topology.json"
|
|
7508
|
+
},
|
|
7509
|
+
valuedomain: {
|
|
7510
|
+
codeTableCount: Object.keys(codeTables).length,
|
|
7511
|
+
enumFieldCount: enumRows2.length,
|
|
7512
|
+
// 四档全列(含产不出来的那一档),缺键会被读成「这个 kit 还不懂这一档」。
|
|
7513
|
+
byCompleteness,
|
|
7514
|
+
// 🔴 两份台账**不许合并**:problems 是取数时的(爬不到 / 没取满 / 分页可疑),
|
|
7515
|
+
// writeProblems 是落盘时的(modelKey 当不了文件名)。前者「有这张表但内容残」,
|
|
7516
|
+
// 后者「压根没有这份文件」—— 下一步动作完全不同。
|
|
7517
|
+
problems: vdProblems,
|
|
7518
|
+
writeProblems: vdWriteProblems,
|
|
7519
|
+
file: "valuedomain/_index.json"
|
|
7520
|
+
},
|
|
7521
|
+
mapping: {
|
|
7522
|
+
modelCount: sheets.length,
|
|
7523
|
+
sheetsWritten,
|
|
7524
|
+
// 差额恒有值:`ls mapping/ | wc -l` 与 modelCount 对不上时,这里先给出数,
|
|
7525
|
+
// 名字在 mapping/_index.json 的 unsafeModelKeys 里。
|
|
7526
|
+
sheetsNotWritten: sheets.length - sheetsWritten,
|
|
7527
|
+
problems: mutProblems,
|
|
7528
|
+
// 🔴 悬空的 valueDomain 指针。空列表 = **对过账且一条都没有**,
|
|
7529
|
+
// 不是「没对过」(GC3)—— 这份对账是 codeTableFiles 那条推导的独立复核。
|
|
7530
|
+
danglingValueDomainPointers: danglingPointers,
|
|
7531
|
+
file: "mapping/_index.json"
|
|
7532
|
+
},
|
|
7533
|
+
// 🔴 轮次身份 + 本轮台账的条数。产物与摘要两处落点:明细在 `_kitCrawledAt.json` 里,
|
|
7534
|
+
// 摘要给结论与条数。
|
|
7535
|
+
round: {
|
|
7536
|
+
roundId,
|
|
7537
|
+
state: roundResult.state,
|
|
7538
|
+
productsWritten: productsWritten.length,
|
|
7539
|
+
file: KIT_CRAWLED_AT_FILE,
|
|
7540
|
+
// 中断过的基线不会走到这里,所以必须告诉顾问**事后**怎么问同一个问题。
|
|
7541
|
+
// ⚠️ 这里写的是**今天真能敲**的那条,且**两个模式都要 grep**:只看 roundId
|
|
7542
|
+
// 会把「四行对齐」读成「没问题」,而真机实测存在「对齐但没跑完」那一档。
|
|
7543
|
+
// 🔴 命令头与 roundNote 同源(`GREP_RECIPE_HEAD`),不抄第二份。
|
|
7544
|
+
recheckWith: `cd ${layout.dir} && ${GREP_RECIPE_HEAD} ${KIT_CRAWLED_AT_FILE} ${ROUND_STAMPED_PRODUCTS.join(" ")}`,
|
|
7545
|
+
// 子命令形态(零网络)—— 它给的是 complete/mixed/unknown 三档人话 + 退出码,
|
|
7546
|
+
// 比 grep 多一层判读。🔴 命令名与 roundNote 的 ② 同源(`ROUND_SUBCOMMAND`),
|
|
7547
|
+
// 不抄第二份:抄两份的表现是「产物教的命令与摘要教的不一样」。
|
|
7548
|
+
recheckCommand: `${ROUND_SUBCOMMAND} --dir ${layout.dir} --output table`
|
|
7549
|
+
},
|
|
7550
|
+
coverage,
|
|
7551
|
+
dir: layout.dir
|
|
7552
|
+
};
|
|
7553
|
+
}
|
|
5742
7554
|
export {
|
|
5743
7555
|
AUTH_INFO_PATH,
|
|
5744
7556
|
BLIND_SPOTS,
|
|
@@ -5751,6 +7563,9 @@ export {
|
|
|
5751
7563
|
ExitCode,
|
|
5752
7564
|
GATE_COMPONENTS,
|
|
5753
7565
|
HcmClient,
|
|
7566
|
+
KIT_CARRY_OVER_KEY,
|
|
7567
|
+
KIT_CRAWLED_AT_FILE,
|
|
7568
|
+
LEDGER_MISSING_WORD,
|
|
5754
7569
|
LayoutContainmentError,
|
|
5755
7570
|
MANIFEST_PATH,
|
|
5756
7571
|
MAX_MANIFEST_PAGES,
|
|
@@ -5765,9 +7580,15 @@ export {
|
|
|
5765
7580
|
MODELS_PATH,
|
|
5766
7581
|
PASSWORD_CHANGE_REQUIRED,
|
|
5767
7582
|
PROACTIVE_REFRESH_MARGIN_SECONDS,
|
|
7583
|
+
PRODUCT_STATE_WORDS,
|
|
7584
|
+
ROUND_EXIT_CODES,
|
|
7585
|
+
ROUND_STAMPED_PRODUCTS,
|
|
7586
|
+
ROUND_STATES,
|
|
7587
|
+
ROUND_SUBCOMMAND,
|
|
5768
7588
|
ReconLayout,
|
|
5769
7589
|
RefStore,
|
|
5770
7590
|
SDK_VERSION,
|
|
7591
|
+
TargetModelNotFound,
|
|
5771
7592
|
TokenStore,
|
|
5772
7593
|
VERSION_PATH,
|
|
5773
7594
|
WsClient,
|
|
@@ -5794,6 +7615,7 @@ export {
|
|
|
5794
7615
|
clearModelCache,
|
|
5795
7616
|
compareFingerprint,
|
|
5796
7617
|
computeFingerprint,
|
|
7618
|
+
computeReconSourceKey,
|
|
5797
7619
|
conversationStateFile,
|
|
5798
7620
|
create,
|
|
5799
7621
|
createConversation,
|
|
@@ -5849,6 +7671,7 @@ export {
|
|
|
5849
7671
|
inferEnvName,
|
|
5850
7672
|
inferMiniWritableTemplateOptionsFromModel,
|
|
5851
7673
|
initMiniAppProject,
|
|
7674
|
+
inspectRound,
|
|
5852
7675
|
isDelegationToolName,
|
|
5853
7676
|
isServerSlidingSession,
|
|
5854
7677
|
listEnvs,
|
|
@@ -5871,6 +7694,7 @@ export {
|
|
|
5871
7694
|
modelEntries,
|
|
5872
7695
|
modelKeys,
|
|
5873
7696
|
needsRefresh,
|
|
7697
|
+
newRoundId,
|
|
5874
7698
|
normalizeReferences,
|
|
5875
7699
|
oneShot,
|
|
5876
7700
|
parseConfirmToolName,
|
|
@@ -5891,6 +7715,7 @@ export {
|
|
|
5891
7715
|
readPullManifest,
|
|
5892
7716
|
refreshToken,
|
|
5893
7717
|
remove,
|
|
7718
|
+
renderRound,
|
|
5894
7719
|
replHistoryFile,
|
|
5895
7720
|
resetSettingItem,
|
|
5896
7721
|
resolveActiveEnv,
|
|
@@ -5900,9 +7725,11 @@ export {
|
|
|
5900
7725
|
resolveChironBase,
|
|
5901
7726
|
resolveRefs,
|
|
5902
7727
|
resolveSkillInstallOrder,
|
|
7728
|
+
roundAge,
|
|
5903
7729
|
runDoctor,
|
|
5904
7730
|
runImport,
|
|
5905
7731
|
runMiniSmokeChecks,
|
|
7732
|
+
runRecon,
|
|
5906
7733
|
safeSegment,
|
|
5907
7734
|
saveConversationState,
|
|
5908
7735
|
saveEnv,
|