@hcmai/sdk 0.3.6 → 0.3.8
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 +673 -1
- package/dist/index.js +3058 -50
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -177,25 +177,25 @@ var TokenStore = class _TokenStore {
|
|
|
177
177
|
throw e;
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
|
-
async setAccessToken(
|
|
180
|
+
async setAccessToken(token2) {
|
|
181
181
|
if (await this.canUseKeychain()) {
|
|
182
182
|
const keytar = await loadKeytar();
|
|
183
|
-
if (!keytar) return this.writeTokenFile("access",
|
|
183
|
+
if (!keytar) return this.writeTokenFile("access", token2);
|
|
184
184
|
const account = `${this.keychainAccountPrefix()}/access`;
|
|
185
|
-
await keytar.setPassword(SERVICE_PREFIX, account,
|
|
185
|
+
await keytar.setPassword(SERVICE_PREFIX, account, token2);
|
|
186
186
|
return `keychain:${SERVICE_PREFIX}/${account}`;
|
|
187
187
|
}
|
|
188
|
-
return this.writeTokenFile("access",
|
|
188
|
+
return this.writeTokenFile("access", token2);
|
|
189
189
|
}
|
|
190
|
-
async setRefreshToken(
|
|
190
|
+
async setRefreshToken(token2) {
|
|
191
191
|
if (await this.canUseKeychain()) {
|
|
192
192
|
const keytar = await loadKeytar();
|
|
193
|
-
if (!keytar) return this.writeTokenFile("refresh",
|
|
193
|
+
if (!keytar) return this.writeTokenFile("refresh", token2);
|
|
194
194
|
const account = `${this.keychainAccountPrefix()}/refresh`;
|
|
195
|
-
await keytar.setPassword(SERVICE_PREFIX, account,
|
|
195
|
+
await keytar.setPassword(SERVICE_PREFIX, account, token2);
|
|
196
196
|
return `keychain:${SERVICE_PREFIX}/${account}`;
|
|
197
197
|
}
|
|
198
|
-
return this.writeTokenFile("refresh",
|
|
198
|
+
return this.writeTokenFile("refresh", token2);
|
|
199
199
|
}
|
|
200
200
|
async getAccessToken() {
|
|
201
201
|
if (process.env.HCM_PAT) return process.env.HCM_PAT;
|
|
@@ -258,10 +258,10 @@ var TokenStore = class _TokenStore {
|
|
|
258
258
|
return false;
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
|
-
async writeTokenFile(kind,
|
|
261
|
+
async writeTokenFile(kind, token2) {
|
|
262
262
|
const file = `${this.metaFile()}.${kind}`;
|
|
263
263
|
await fs.mkdir(this.dataDir(), { recursive: true });
|
|
264
|
-
await fs.writeFile(file,
|
|
264
|
+
await fs.writeFile(file, token2, { mode: 384 });
|
|
265
265
|
if (kind === "access") {
|
|
266
266
|
console.warn(`[hcm] WARNING: OS keychain unavailable. Token saved to ${file} (0600).`);
|
|
267
267
|
}
|
|
@@ -308,14 +308,14 @@ async function loginPassword(input) {
|
|
|
308
308
|
},
|
|
309
309
|
{ headers: { "Content-Type": "application/json" } }
|
|
310
310
|
);
|
|
311
|
-
const { token, expiresIn, user } = resp.data;
|
|
312
|
-
if (!
|
|
311
|
+
const { token: token2, expiresIn, user } = resp.data;
|
|
312
|
+
if (!token2) {
|
|
313
313
|
throw new Error("Login response missing token");
|
|
314
314
|
}
|
|
315
315
|
const ttlSeconds = expiresIn ?? DEFAULT_SESSION_TTL_SECONDS;
|
|
316
316
|
const expiresAt = new Date(Date.now() + ttlSeconds * 1e3).toISOString();
|
|
317
317
|
const store = new TokenStore(input.profile);
|
|
318
|
-
const accessTokenRef = await store.setAccessToken(
|
|
318
|
+
const accessTokenRef = await store.setAccessToken(token2);
|
|
319
319
|
const record = {
|
|
320
320
|
scheme: "password",
|
|
321
321
|
accessTokenRef,
|
|
@@ -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 path12 = 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(path12, { params: queryParams(input) });
|
|
1075
1075
|
}
|
|
1076
1076
|
if (method === "DELETE") {
|
|
1077
|
-
return http.delete(
|
|
1077
|
+
return http.delete(path12, { params: queryParams(input) });
|
|
1078
1078
|
}
|
|
1079
1079
|
if (method === "PUT") {
|
|
1080
|
-
return http.put(
|
|
1080
|
+
return http.put(path12, scope === "object" ? objectBody(input) : classBody(input));
|
|
1081
1081
|
}
|
|
1082
|
-
return http.post(
|
|
1082
|
+
return http.post(path12, scope === "object" ? objectBody(input) : classBody(input));
|
|
1083
1083
|
})();
|
|
1084
1084
|
return resp.data;
|
|
1085
1085
|
}
|
|
@@ -1690,14 +1690,14 @@ async function loginPairing(input) {
|
|
|
1690
1690
|
{ code: input.code },
|
|
1691
1691
|
{ headers: { "Content-Type": "application/json" } }
|
|
1692
1692
|
);
|
|
1693
|
-
const { token, expiresIn, user } = resp.data;
|
|
1694
|
-
if (!
|
|
1693
|
+
const { token: token2, expiresIn, user } = resp.data;
|
|
1694
|
+
if (!token2) {
|
|
1695
1695
|
throw new Error("Pairing exchange response missing token");
|
|
1696
1696
|
}
|
|
1697
1697
|
const ttlSeconds = expiresIn ?? DEFAULT_SESSION_TTL_SECONDS;
|
|
1698
1698
|
const expiresAt = new Date(Date.now() + ttlSeconds * 1e3).toISOString();
|
|
1699
1699
|
const store = new TokenStore(input.profile);
|
|
1700
|
-
const accessTokenRef = await store.setAccessToken(
|
|
1700
|
+
const accessTokenRef = await store.setAccessToken(token2);
|
|
1701
1701
|
const record = {
|
|
1702
1702
|
scheme: "pairing",
|
|
1703
1703
|
accessTokenRef,
|
|
@@ -1730,10 +1730,10 @@ function detectPasswordChangeChallenge(err) {
|
|
|
1730
1730
|
const body = err?.response?.data;
|
|
1731
1731
|
const error = body?.error ?? body;
|
|
1732
1732
|
if (error?.code !== PASSWORD_CHANGE_REQUIRED) return null;
|
|
1733
|
-
const
|
|
1734
|
-
if (!
|
|
1733
|
+
const token2 = error?.details?.changePasswordToken;
|
|
1734
|
+
if (!token2) return null;
|
|
1735
1735
|
return {
|
|
1736
|
-
changePasswordToken: String(
|
|
1736
|
+
changePasswordToken: String(token2),
|
|
1737
1737
|
expiresInSeconds: Number(error?.details?.expiresInSeconds) || void 0
|
|
1738
1738
|
};
|
|
1739
1739
|
}
|
|
@@ -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, path12) {
|
|
5039
|
+
const resp = await http.get("/api/tenant-meta/meta", { params: { path: path12 } });
|
|
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, path12, content) {
|
|
5045
|
+
const resp = await http.put("/api/tenant-meta/meta", { content }, { params: { path: path12 } });
|
|
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, path12) {
|
|
5049
|
+
const resp = await http.delete("/api/tenant-meta/meta", { params: { path: path12 } });
|
|
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 path12 = [];
|
|
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 = path12.indexOf(id);
|
|
5158
|
+
const cycle = [...path12.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
|
+
path12.push(id);
|
|
5168
5168
|
for (const required of normalizeRequirements(skill.requires, skill.id)) {
|
|
5169
5169
|
visit(required, skill.id);
|
|
5170
5170
|
}
|
|
5171
|
-
|
|
5171
|
+
path12.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 path12 = 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 = path12.lastIndexOf(".");
|
|
5265
|
+
if (dot <= 0) throw new Error(`--set \u7684\u952E\u9700\u542B namespace\uFF0C\u5982 security.password.passwordPolicy=complex\uFF0C\u6536\u5230 "${path12}"`);
|
|
5266
|
+
return { namespace: path12.slice(0, dot), settingKey: path12.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(path12, 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 path12;
|
|
5658
5663
|
const qs = entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
|
|
5659
|
-
return `${
|
|
5664
|
+
return `${path12}${path12.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(path12, params) {
|
|
5701
5707
|
return once(
|
|
5702
|
-
() => http.get(buildUrl(
|
|
5708
|
+
() => http.get(buildUrl(path12, params), { headers, validateStatus: () => true })
|
|
5703
5709
|
);
|
|
5704
5710
|
},
|
|
5705
|
-
post(
|
|
5711
|
+
post(path12, body) {
|
|
5706
5712
|
return once(
|
|
5707
|
-
() => http.post(
|
|
5713
|
+
() => http.post(path12, body, {
|
|
5708
5714
|
headers: { ...headers, "content-type": "application/json" },
|
|
5709
5715
|
validateStatus: () => true
|
|
5710
5716
|
})
|
|
@@ -5736,23 +5742,2970 @@ 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 [path12, key] of paths) {
|
|
5826
|
+
const out = results[path12];
|
|
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 [path12, model] of paths) {
|
|
6043
|
+
const out = results[path12];
|
|
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 [path12, key] of paths) {
|
|
6423
|
+
const out = results[path12];
|
|
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 path12 = listPath(key);
|
|
6869
|
+
const started = tick();
|
|
6870
|
+
let requests = 0;
|
|
6871
|
+
let truncatedBy = null;
|
|
6872
|
+
const probeOut = await post.call(probe, path12, { 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, path12, { 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
|
+
}
|
|
7554
|
+
|
|
7555
|
+
// src/fde/gate0/aclass.ts
|
|
7556
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
7557
|
+
import * as path9 from "path";
|
|
7558
|
+
|
|
7559
|
+
// src/fde/gate0/mapping-artifact.ts
|
|
7560
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
7561
|
+
var MAPPING_ARTIFACT_VERSION = 1;
|
|
7562
|
+
var MappingArtifactError = class extends Error {
|
|
7563
|
+
constructor(reason, message) {
|
|
7564
|
+
super(`[${reason}] ${message}`);
|
|
7565
|
+
this.reason = reason;
|
|
7566
|
+
this.message = message;
|
|
7567
|
+
this.name = "MappingArtifactError";
|
|
7568
|
+
}
|
|
7569
|
+
reason;
|
|
7570
|
+
message;
|
|
7571
|
+
};
|
|
7572
|
+
function isRecord6(value) {
|
|
7573
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7574
|
+
}
|
|
7575
|
+
function readBusinessKeyFields(file) {
|
|
7576
|
+
let text;
|
|
7577
|
+
try {
|
|
7578
|
+
text = readFileSync4(file, "utf8");
|
|
7579
|
+
} catch (e) {
|
|
7580
|
+
const code = e?.code;
|
|
7581
|
+
if (code === "ENOENT") throw new MappingArtifactError("not-found", `\u627E\u4E0D\u5230\u6620\u5C04\u5DE5\u4EF6 ${file}`);
|
|
7582
|
+
throw new MappingArtifactError("unreadable", `${file} \u8BFB\u4E0D\u52A8\uFF08${String(e)}\uFF09`);
|
|
7583
|
+
}
|
|
7584
|
+
if (text.charCodeAt(0) === 65279) text = text.slice(1);
|
|
7585
|
+
let body;
|
|
7586
|
+
try {
|
|
7587
|
+
body = JSON.parse(text);
|
|
7588
|
+
} catch (e) {
|
|
7589
|
+
throw new MappingArtifactError("not-json", `${file} \u4E0D\u662F\u5408\u6CD5 JSON\uFF1A${String(e)}`);
|
|
7590
|
+
}
|
|
7591
|
+
if (!isRecord6(body)) {
|
|
7592
|
+
throw new MappingArtifactError("bad-shape", `${file} \u7684\u6839\u4E0D\u662F\u5BF9\u8C61`);
|
|
7593
|
+
}
|
|
7594
|
+
if (!("version" in body)) {
|
|
7595
|
+
throw new MappingArtifactError("version-missing", `${file} \u6CA1\u6709 version \u5B57\u6BB5`);
|
|
7596
|
+
}
|
|
7597
|
+
if (body.version !== MAPPING_ARTIFACT_VERSION) {
|
|
7598
|
+
throw new MappingArtifactError(
|
|
7599
|
+
"unsupported-version",
|
|
7600
|
+
`${file} \u7684 version \u662F ${JSON.stringify(body.version)}\uFF0C\u672C\u5DE5\u5177\u53EA\u8BA4 ${MAPPING_ARTIFACT_VERSION} \u2014\u2014\u300C\u5C3D\u91CF\u6309\u8001\u7248\u672C\u8BFB\u300D\u4F1A\u628A\u65B0\u8BED\u4E49\u9759\u9ED8\u4E22\u6389`
|
|
7601
|
+
);
|
|
7602
|
+
}
|
|
7603
|
+
const models = body.models;
|
|
7604
|
+
if (!Array.isArray(models)) {
|
|
7605
|
+
throw new MappingArtifactError(
|
|
7606
|
+
"bad-shape",
|
|
7607
|
+
`${file} \u7684 models \u5E94\u5F53\u662F\u4E00\u4E2A\u6570\u7EC4\uFF0C\u5B9E\u9645\u662F ${models === null ? "null" : typeof models}`
|
|
7608
|
+
);
|
|
7609
|
+
}
|
|
7610
|
+
if (models.length === 0) {
|
|
7611
|
+
throw new MappingArtifactError(
|
|
7612
|
+
"empty",
|
|
7613
|
+
`${file} \u7684 models \u662F\u7A7A\u7684 \u2014\u2014 \u7A7A\u5DE5\u4EF6\u8DD1\u5B8C\u4EC0\u4E48\u4E5F\u4E0D\u4F1A\u53D1\u751F\uFF0C\u800C\u987E\u95EE\u4F1A\u4EE5\u4E3A\u300C\u5BFC\u8FC7\u4E86\u300D`
|
|
7614
|
+
);
|
|
7615
|
+
}
|
|
7616
|
+
return models.map((raw, i) => {
|
|
7617
|
+
if (!isRecord6(raw)) {
|
|
7618
|
+
throw new MappingArtifactError("bad-shape", `${file} \u7684 models[${i}] \u4E0D\u662F\u5BF9\u8C61`);
|
|
7619
|
+
}
|
|
7620
|
+
const businessKey = raw.businessKey;
|
|
7621
|
+
if (businessKey === void 0 || businessKey === null) return [];
|
|
7622
|
+
if (!isRecord6(businessKey)) {
|
|
7623
|
+
throw new MappingArtifactError("bad-shape", `${file} \u7684 models[${i}].businessKey \u4E0D\u662F\u5BF9\u8C61`);
|
|
7624
|
+
}
|
|
7625
|
+
const fields = businessKey.fields;
|
|
7626
|
+
if (fields === void 0 || fields === null) return [];
|
|
7627
|
+
if (!Array.isArray(fields)) {
|
|
7628
|
+
throw new MappingArtifactError(
|
|
7629
|
+
"bad-shape",
|
|
7630
|
+
`${file} \u7684 models[${i}].businessKey.fields \u4E0D\u662F\u6570\u7EC4`
|
|
7631
|
+
);
|
|
7632
|
+
}
|
|
7633
|
+
return fields.filter((f) => typeof f === "string" && f !== "");
|
|
7634
|
+
});
|
|
7635
|
+
}
|
|
7636
|
+
|
|
7637
|
+
// src/fde/gate0/aclass.ts
|
|
7638
|
+
var ACLASS_KINDS = [
|
|
7639
|
+
"fieldKey",
|
|
7640
|
+
"fieldLabel",
|
|
7641
|
+
"enumValue",
|
|
7642
|
+
"modelName",
|
|
7643
|
+
"codeValue",
|
|
7644
|
+
"naturalKey",
|
|
7645
|
+
"caliber"
|
|
7646
|
+
];
|
|
7647
|
+
var FIELDS_INDEX = ["index", "fields.jsonl"];
|
|
7648
|
+
var ENUM_MIRROR = ["valuedomain", "_enums.jsonl"];
|
|
7649
|
+
var MODEL_ROSTER = ["models.json"];
|
|
7650
|
+
var VALUE_DOMAIN_DIR = "valuedomain";
|
|
7651
|
+
var ENUM_ELEMENT_KEYS = ["value", "label", "key"];
|
|
7652
|
+
var MAPPING_ARTIFACT_HINT = "work/mapping.json";
|
|
7653
|
+
var CLASS_SCOPE = "class";
|
|
7654
|
+
var ENTRY_SCOPE = "entry";
|
|
7655
|
+
var SCOPES = [CLASS_SCOPE, ENTRY_SCOPE];
|
|
7656
|
+
var REASON_UNREAD = "unread";
|
|
7657
|
+
var REASON_EMPTY = "empty";
|
|
7658
|
+
var CLASS_REASONS = [REASON_UNREAD, REASON_EMPTY];
|
|
7659
|
+
function unguardedKinds(problems) {
|
|
7660
|
+
const out = /* @__PURE__ */ new Set();
|
|
7661
|
+
for (const problem of problems) {
|
|
7662
|
+
if (!SCOPES.includes(problem.scope)) {
|
|
7663
|
+
throw new Error(
|
|
7664
|
+
`\u53F0\u8D26\u7684 scope \u53EA\u6709 ${SCOPES.join("/")} \u4E24\u4E2A\u53D6\u503C\uFF0C\u62FF\u5230\u7684\u662F ${JSON.stringify(problem.scope)}\uFF08${problem.message}\uFF09`
|
|
7665
|
+
);
|
|
7666
|
+
}
|
|
7667
|
+
if (problem.scope === CLASS_SCOPE) for (const kind of problem.kinds) out.add(kind);
|
|
7668
|
+
}
|
|
7669
|
+
return out;
|
|
7670
|
+
}
|
|
7671
|
+
function unguardedWord(kinds) {
|
|
7672
|
+
return kinds.join("/");
|
|
7673
|
+
}
|
|
7674
|
+
function token(value) {
|
|
7675
|
+
if (typeof value === "string") return value === "" ? null : value;
|
|
7676
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
7677
|
+
return null;
|
|
7678
|
+
}
|
|
7679
|
+
function scalarsWithin(value) {
|
|
7680
|
+
if (Array.isArray(value)) return value.flatMap(scalarsWithin);
|
|
7681
|
+
if (typeof value === "object" && value !== null) {
|
|
7682
|
+
return Object.values(value).flatMap(scalarsWithin);
|
|
7683
|
+
}
|
|
7684
|
+
const one = token(value);
|
|
7685
|
+
return one === null ? [] : [one];
|
|
7686
|
+
}
|
|
7687
|
+
function readText(file, kinds, scope, problems) {
|
|
7688
|
+
try {
|
|
7689
|
+
return readFileSync5(file, "utf8");
|
|
7690
|
+
} catch (e) {
|
|
7691
|
+
const code = e?.code;
|
|
7692
|
+
const name = path9.basename(file);
|
|
7693
|
+
problems.push({
|
|
7694
|
+
scope,
|
|
7695
|
+
reason: REASON_UNREAD,
|
|
7696
|
+
kinds,
|
|
7697
|
+
message: code === "ENOENT" ? `\u7F3A\u4EA7\u7269 ${name} \u21D2 ${unguardedWord(kinds)} \u8FD9\u51E0\u7C7B\u6B64\u523B\u65E0\u4EBA\u5B88` : `\u4EA7\u7269 ${name} \u8BFB\u4E0D\u52A8\uFF08${String(e)}\uFF09\u21D2 ${unguardedWord(kinds)} \u8FD9\u51E0\u7C7B\u6B64\u523B\u65E0\u4EBA\u5B88`
|
|
7698
|
+
});
|
|
7699
|
+
return null;
|
|
7700
|
+
}
|
|
7701
|
+
}
|
|
7702
|
+
function readJsonl(file, kinds, scope, problems) {
|
|
7703
|
+
const text = readText(file, kinds, scope, problems);
|
|
7704
|
+
if (text === null) return [];
|
|
7705
|
+
const name = path9.basename(file);
|
|
7706
|
+
const rows = [];
|
|
7707
|
+
text.split("\n").forEach((raw, i) => {
|
|
7708
|
+
const line = raw.trim();
|
|
7709
|
+
if (line === "") return;
|
|
7710
|
+
let row;
|
|
7711
|
+
try {
|
|
7712
|
+
row = JSON.parse(line);
|
|
7713
|
+
} catch (e) {
|
|
7714
|
+
problems.push({
|
|
7715
|
+
scope: ENTRY_SCOPE,
|
|
7716
|
+
reason: "bad-line",
|
|
7717
|
+
kinds,
|
|
7718
|
+
message: `${name}:${i + 1} \u4E0D\u662F\u5408\u6CD5 JSON\uFF08${String(e)}\uFF09\u21D2 \u8FD9\u4E00\u884C\u7684\u53D6\u503C\u6CA1\u6709\u8FDB ${unguardedWord(kinds)}`
|
|
7719
|
+
});
|
|
7720
|
+
return;
|
|
7721
|
+
}
|
|
7722
|
+
if (typeof row !== "object" || row === null || Array.isArray(row)) {
|
|
7723
|
+
problems.push({
|
|
7724
|
+
scope: ENTRY_SCOPE,
|
|
7725
|
+
reason: "bad-line",
|
|
7726
|
+
kinds,
|
|
7727
|
+
message: `${name}:${i + 1} \u4E0D\u662F\u5BF9\u8C61\uFF08${row === null ? "null" : typeof row}\uFF09\u21D2 \u8FD9\u4E00\u884C\u7684\u53D6\u503C\u6CA1\u6709\u8FDB ${unguardedWord(kinds)}`
|
|
7728
|
+
});
|
|
7729
|
+
return;
|
|
7730
|
+
}
|
|
7731
|
+
rows.push(row);
|
|
7732
|
+
});
|
|
7733
|
+
return rows;
|
|
7734
|
+
}
|
|
7735
|
+
var UNREADABLE = /* @__PURE__ */ Symbol("unreadable");
|
|
7736
|
+
function readJson(file, kinds, scope, problems) {
|
|
7737
|
+
const text = readText(file, kinds, scope, problems);
|
|
7738
|
+
if (text === null) return UNREADABLE;
|
|
7739
|
+
try {
|
|
7740
|
+
return JSON.parse(text);
|
|
7741
|
+
} catch (e) {
|
|
7742
|
+
problems.push({
|
|
7743
|
+
scope,
|
|
7744
|
+
reason: REASON_UNREAD,
|
|
7745
|
+
kinds,
|
|
7746
|
+
message: `\u4EA7\u7269 ${path9.basename(file)} \u4E0D\u662F\u5408\u6CD5 JSON\uFF08${String(e)}\uFF09\u21D2 ${unguardedWord(kinds)} \u8FD9\u51E0\u7C7B\u6B64\u523B\u65E0\u4EBA\u5B88`
|
|
7747
|
+
});
|
|
7748
|
+
return UNREADABLE;
|
|
7749
|
+
}
|
|
7750
|
+
}
|
|
7751
|
+
function listUnder(payload, key, file, kinds, scope, problems) {
|
|
7752
|
+
const name = path9.basename(file);
|
|
7753
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
7754
|
+
problems.push({
|
|
7755
|
+
scope,
|
|
7756
|
+
reason: REASON_UNREAD,
|
|
7757
|
+
kinds,
|
|
7758
|
+
message: `\u4EA7\u7269 ${name} \u4E0D\u662F\u5BF9\u8C61\uFF08${payload === null ? "null" : Array.isArray(payload) ? "array" : typeof payload}\uFF09\u21D2 ${unguardedWord(kinds)} \u8FD9\u51E0\u7C7B\u6B64\u523B\u65E0\u4EBA\u5B88`
|
|
7759
|
+
});
|
|
7760
|
+
return [];
|
|
7761
|
+
}
|
|
7762
|
+
const body = payload;
|
|
7763
|
+
if (!(key in body)) {
|
|
7764
|
+
problems.push({
|
|
7765
|
+
scope,
|
|
7766
|
+
reason: REASON_UNREAD,
|
|
7767
|
+
kinds,
|
|
7768
|
+
message: `\u4EA7\u7269 ${name} \u91CC\u6CA1\u6709 ${key} \u8FD9\u4E2A\u952E\uFF08\u9876\u5C42\u952E\u662F ${JSON.stringify(Object.keys(body).sort())}\uFF09\u21D2 ${unguardedWord(kinds)} \u8FD9\u51E0\u7C7B\u6B64\u523B\u65E0\u4EBA\u5B88`
|
|
7769
|
+
});
|
|
7770
|
+
return [];
|
|
7771
|
+
}
|
|
7772
|
+
const value = body[key];
|
|
7773
|
+
if (!Array.isArray(value)) {
|
|
7774
|
+
problems.push({
|
|
7775
|
+
scope,
|
|
7776
|
+
reason: REASON_UNREAD,
|
|
7777
|
+
kinds,
|
|
7778
|
+
message: `\u4EA7\u7269 ${name} \u7684 ${key} \u4E0D\u662F\u6570\u7EC4\uFF08${value === null ? "null" : typeof value}\uFF09\u21D2 ${unguardedWord(kinds)} \u8FD9\u51E0\u7C7B\u6B64\u523B\u65E0\u4EBA\u5B88`
|
|
7779
|
+
});
|
|
7780
|
+
return [];
|
|
7781
|
+
}
|
|
7782
|
+
return value;
|
|
7783
|
+
}
|
|
7784
|
+
function deriveAclass(root, mappingArtifact = null) {
|
|
7785
|
+
const problems = [];
|
|
7786
|
+
const out = Object.fromEntries(ACLASS_KINDS.map((k) => [k, /* @__PURE__ */ new Set()]));
|
|
7787
|
+
for (const row of readJsonl(
|
|
7788
|
+
path9.join(root, ...FIELDS_INDEX),
|
|
7789
|
+
["fieldKey", "fieldLabel", "caliber"],
|
|
7790
|
+
CLASS_SCOPE,
|
|
7791
|
+
problems
|
|
7792
|
+
)) {
|
|
7793
|
+
const key = token(row.key);
|
|
7794
|
+
if (key !== null) out.fieldKey.add(key);
|
|
7795
|
+
const name = token(row.name);
|
|
7796
|
+
if (name !== null) out.fieldLabel.add(name);
|
|
7797
|
+
const synonyms = row.synonyms;
|
|
7798
|
+
for (const syn of Array.isArray(synonyms) ? synonyms : []) {
|
|
7799
|
+
const one = token(syn);
|
|
7800
|
+
if (one !== null) out.caliber.add(one);
|
|
7801
|
+
}
|
|
7802
|
+
}
|
|
7803
|
+
for (const row of readJsonl(
|
|
7804
|
+
path9.join(root, ...ENUM_MIRROR),
|
|
7805
|
+
["enumValue"],
|
|
7806
|
+
CLASS_SCOPE,
|
|
7807
|
+
problems
|
|
7808
|
+
)) {
|
|
7809
|
+
const enums = row.enums;
|
|
7810
|
+
for (const element of Array.isArray(enums) ? enums : []) {
|
|
7811
|
+
if (typeof element !== "object" || element === null || Array.isArray(element)) continue;
|
|
7812
|
+
for (const field of ENUM_ELEMENT_KEYS) {
|
|
7813
|
+
const one = token(element[field]);
|
|
7814
|
+
if (one !== null) out.enumValue.add(one);
|
|
7815
|
+
}
|
|
7816
|
+
}
|
|
7817
|
+
}
|
|
7818
|
+
const rosterPath = path9.join(root, ...MODEL_ROSTER);
|
|
7819
|
+
const roster = readJson(rosterPath, ["modelName"], CLASS_SCOPE, problems);
|
|
7820
|
+
if (roster !== UNREADABLE) {
|
|
7821
|
+
const rows = listUnder(roster, "models", rosterPath, ["modelName"], CLASS_SCOPE, problems);
|
|
7822
|
+
const entries = modelEntries(roster);
|
|
7823
|
+
const keys = modelKeys(roster);
|
|
7824
|
+
for (const key of keys) out.modelName.add(key);
|
|
7825
|
+
for (const entry of entries) {
|
|
7826
|
+
const name = token(entry.name);
|
|
7827
|
+
if (name !== null) out.modelName.add(name);
|
|
7828
|
+
}
|
|
7829
|
+
if (rows.length !== entries.length) {
|
|
7830
|
+
problems.push({
|
|
7831
|
+
scope: ENTRY_SCOPE,
|
|
7832
|
+
reason: "dropped-entries",
|
|
7833
|
+
kinds: ["modelName"],
|
|
7834
|
+
message: `models.json \u91CC ${rows.length} \u6761\u6A21\u578B\u6761\u76EE\u6709 ${rows.length - entries.length} \u6761\u4E0D\u662F\u5BF9\u8C61 \u21D2 \u5B83\u4EEC\u7684\u6A21\u578B\u540D\u6CA1\u6709\u8FDB modelName`
|
|
7835
|
+
});
|
|
7836
|
+
}
|
|
7837
|
+
if (entries.length !== keys.length) {
|
|
7838
|
+
problems.push({
|
|
7839
|
+
scope: ENTRY_SCOPE,
|
|
7840
|
+
reason: "dropped-keys",
|
|
7841
|
+
kinds: ["modelName"],
|
|
7842
|
+
message: `models.json \u91CC ${entries.length} \u6761\u6A21\u578B\u6761\u76EE\u6709 ${entries.length - keys.length} \u6761\u6CA1\u6709 modelKey \u21D2 \u5B83\u4EEC\u7684\u82F1\u6587\u6A21\u578B\u540D\u6CA1\u6709\u8FDB modelName`
|
|
7843
|
+
});
|
|
7844
|
+
}
|
|
7845
|
+
}
|
|
7846
|
+
const valueDomain = path9.join(root, VALUE_DOMAIN_DIR);
|
|
7847
|
+
if (existsSync4(valueDomain) && statSync2(valueDomain).isDirectory()) {
|
|
7848
|
+
const tables = readdirSync2(valueDomain).filter((f) => f.endsWith(".json") && !RESERVED_FILENAMES.includes(f)).sort();
|
|
7849
|
+
for (const name of tables) {
|
|
7850
|
+
const file = path9.join(valueDomain, name);
|
|
7851
|
+
const payload = readJson(file, ["codeValue"], ENTRY_SCOPE, problems);
|
|
7852
|
+
if (payload === UNREADABLE) continue;
|
|
7853
|
+
for (const row of listUnder(payload, "rows", file, ["codeValue"], ENTRY_SCOPE, problems)) {
|
|
7854
|
+
if (typeof row !== "object" || row === null || Array.isArray(row)) {
|
|
7855
|
+
problems.push({
|
|
7856
|
+
scope: ENTRY_SCOPE,
|
|
7857
|
+
reason: "bad-row",
|
|
7858
|
+
kinds: ["codeValue"],
|
|
7859
|
+
message: `${name} \u7684 rows \u91CC\u6709\u4E00\u4E2A\u5143\u7D20\u4E0D\u662F\u5BF9\u8C61\uFF08${row === null ? "null" : typeof row}\uFF09\u21D2 \u5B83\u7684\u53D6\u503C\u6CA1\u6709\u8FDB codeValue`
|
|
7860
|
+
});
|
|
7861
|
+
continue;
|
|
7862
|
+
}
|
|
7863
|
+
for (const value of scalarsWithin(row)) out.codeValue.add(value);
|
|
7864
|
+
}
|
|
7865
|
+
}
|
|
7866
|
+
} else {
|
|
7867
|
+
problems.push({
|
|
7868
|
+
scope: CLASS_SCOPE,
|
|
7869
|
+
reason: REASON_UNREAD,
|
|
7870
|
+
kinds: ["codeValue"],
|
|
7871
|
+
message: `\u7F3A\u4EA7\u7269\u76EE\u5F55 ${VALUE_DOMAIN_DIR}/ \u21D2 codeValue \u8FD9\u4E00\u7C7B\u6B64\u523B\u65E0\u4EBA\u5B88`
|
|
7872
|
+
});
|
|
7873
|
+
}
|
|
7874
|
+
let businessKeys = null;
|
|
7875
|
+
if (mappingArtifact === null) {
|
|
7876
|
+
problems.push({
|
|
7877
|
+
scope: CLASS_SCOPE,
|
|
7878
|
+
reason: REASON_UNREAD,
|
|
7879
|
+
kinds: ["naturalKey"],
|
|
7880
|
+
message: `\u6CA1\u6709\u6307\u5B9A\u6620\u5C04\u5DE5\u4EF6 \u21D2 naturalKey \u8FD9\u4E00\u7C7B\u6B64\u523B\u65E0\u4EBA\u5B88\uFF08\u628A ${MAPPING_ARTIFACT_HINT} \u7684\u8DEF\u5F84\u4F20\u8FDB\u6765\uFF09`
|
|
7881
|
+
});
|
|
7882
|
+
} else {
|
|
7883
|
+
try {
|
|
7884
|
+
businessKeys = readBusinessKeyFields(mappingArtifact);
|
|
7885
|
+
} catch (e) {
|
|
7886
|
+
const err = e;
|
|
7887
|
+
problems.push({
|
|
7888
|
+
scope: CLASS_SCOPE,
|
|
7889
|
+
reason: REASON_UNREAD,
|
|
7890
|
+
kinds: ["naturalKey"],
|
|
7891
|
+
message: `\u6620\u5C04\u5DE5\u4EF6\u8BFB\u4E0D\u52A8\uFF08[${err.reason ?? "unknown"}] ${err.message}\uFF09\u21D2 naturalKey \u8FD9\u4E00\u7C7B\u6B64\u523B\u65E0\u4EBA\u5B88`
|
|
7892
|
+
});
|
|
7893
|
+
}
|
|
7894
|
+
}
|
|
7895
|
+
for (const fields of businessKeys ?? []) {
|
|
7896
|
+
for (const field of fields) out.naturalKey.add(field);
|
|
7897
|
+
if (fields.length > 1) {
|
|
7898
|
+
out.naturalKey.add(fields.join("+"));
|
|
7899
|
+
}
|
|
7900
|
+
}
|
|
7901
|
+
const named = unguardedKinds(problems);
|
|
7902
|
+
for (const kind of ACLASS_KINDS) {
|
|
7903
|
+
if (out[kind].size === 0 && !named.has(kind)) {
|
|
7904
|
+
problems.push({
|
|
7905
|
+
scope: CLASS_SCOPE,
|
|
7906
|
+
reason: REASON_EMPTY,
|
|
7907
|
+
kinds: [kind],
|
|
7908
|
+
message: `${kind}\uFF1A\u4EA7\u7269\u8BFB\u5230\u4E86\uFF0C\u4F46\u4E00\u6761\u53D6\u503C\u90FD\u6CA1\u6D3E\u751F\u51FA\u6765 \u21D2 \u95E8 0 \u5BF9\u8FD9\u4E00\u7C7B\u6052\u7EFF\uFF08\u540C\u6279\u53F0\u8D26\u91CC\u82E5\u6709\u6761\u76EE\u7EA7\u95EE\u9898\uFF0C\u771F\u56E0\u5728\u90A3\u91CC\uFF09`
|
|
7909
|
+
});
|
|
7910
|
+
}
|
|
7911
|
+
}
|
|
7912
|
+
return { aclass: out, problems };
|
|
7913
|
+
}
|
|
7914
|
+
|
|
7915
|
+
// src/fde/gate0/facts.ts
|
|
7916
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
|
|
7917
|
+
import * as path10 from "path";
|
|
7918
|
+
var REGISTRY_SCOPE = "registry";
|
|
7919
|
+
var ENTRY_SCOPE2 = "entry";
|
|
7920
|
+
var REASON_NO_ENTRIES = "no-entries";
|
|
7921
|
+
var REASON_PROBE_UNVERIFIED = "probe-unverified";
|
|
7922
|
+
var REGISTRY_REASONS = [REASON_NO_ENTRIES, REASON_PROBE_UNVERIFIED];
|
|
7923
|
+
var REASON_PROBE_SHAPE = "probe-shape";
|
|
7924
|
+
var REASON_PROBE_ABSENT = "probe-absent";
|
|
7925
|
+
var REASON_DUPLICATE_TOKEN = "duplicate-token";
|
|
7926
|
+
var ENTRY_REASONS = [
|
|
7927
|
+
REASON_PROBE_SHAPE,
|
|
7928
|
+
REASON_PROBE_ABSENT,
|
|
7929
|
+
REASON_DUPLICATE_TOKEN
|
|
7930
|
+
];
|
|
7931
|
+
var FACTS_REASONS = [...REGISTRY_REASONS, ...ENTRY_REASONS];
|
|
7932
|
+
var PROBE_SHAPE = /^(?<file>[A-Za-z0-9_./-]+\.test\.ts)::(?<mark>[A-Za-z0-9_-]+)$/;
|
|
7933
|
+
var PROBE_MARK_PREFIX = "// P:";
|
|
7934
|
+
var PROTOCOL_FACTS = [
|
|
7935
|
+
{
|
|
7936
|
+
token: "/api/models/{model}/meta",
|
|
7937
|
+
probe: "packages/core/tests/fde-recon-crawl.test.ts::meta-path-wire-shape",
|
|
7938
|
+
note: "\u5B57\u6BB5\u5143\u6570\u636E\u8D70\u88F8\u8DEF\u5F84 GET\uFF0C\u9ED8\u8BA4\u4E0D\u7F00 ?type=\u3002\u63A2\u9488\u65AD\u7684\u662F**\u5B9E\u9645\u53D1\u51FA\u53BB\u7684 URL**\uFF0C\u4E0D\u662F\u62FC\u4E32\u51FD\u6570\u7684\u8FD4\u56DE\u503C\u3002"
|
|
7939
|
+
},
|
|
7940
|
+
{
|
|
7941
|
+
token: "/api/models/{model}/list/dsl",
|
|
7942
|
+
probe: "packages/core/tests/fde-recon-valuedomain.test.ts::list-dsl-count-probe",
|
|
7943
|
+
note: "\u5217\u8868\u67E5\u8BE2\u8D70 POST DSL\u3002\u63A2\u9488\u65AD\u7684\u662F\u884C\u4E3A\uFF1Asize=0 \u7684\u90A3\u4E00\u53D1\u5148\u51FA\u4E14\u8FD4\u56DE totalElements\uFF0C\u53D6\u9875\u5224\u636E\u4EE5\u6B64\u4E3A\u524D\u63D0\u3002"
|
|
7944
|
+
},
|
|
7945
|
+
{
|
|
7946
|
+
token: "/api/auth/login",
|
|
7947
|
+
probe: "packages/core/tests/fde-gate0-facts.test.ts::login-body-carries-tenant",
|
|
7948
|
+
note: "\u767B\u5F55\u7684 tenantId \u5728**\u8BF7\u6C42\u4F53**\u91CC\uFF0C\u4E0D\u5728 header\u3002\u63A2\u9488\u65AD\u7684\u662F\u5B9E\u9645\u53D1\u51FA\u53BB\u7684\u8BF7\u6C42\u4F53\u4E0E\u8BF7\u6C42\u5934\uFF0C\u5220\u6389 tenantId \u6216\u632A\u8FDB header \u90FD\u4F1A\u7EA2\u3002"
|
|
7949
|
+
}
|
|
7950
|
+
];
|
|
7951
|
+
function loadProtocolFacts(entries = PROTOCOL_FACTS, options = {}) {
|
|
7952
|
+
const problems = [];
|
|
7953
|
+
const probeRoot = options.probeRoot;
|
|
7954
|
+
if (probeRoot === void 0 && entries !== PROTOCOL_FACTS) {
|
|
7955
|
+
problems.push({
|
|
7956
|
+
scope: REGISTRY_SCOPE,
|
|
7957
|
+
reason: REASON_PROBE_UNVERIFIED,
|
|
7958
|
+
index: 0,
|
|
7959
|
+
token: null,
|
|
7960
|
+
message: "\u8FD9\u4EFD\u767B\u8BB0\u518C\u4E0D\u662F\u968F\u5305\u90A3\u4E00\u4EFD\uFF0C\u53C8\u6CA1\u6709\u7ED9\u63A2\u9488\u6839\u76EE\u5F55 \u21D2 \u4E00\u6761\u63A2\u9488\u90FD\u6CA1\u9A8C\u8FC7 \u2014\u2014\u300C\u63A2\u9488\u90FD\u5728\u300D\u4E0E\u300C\u6CA1\u67E5\u63A2\u9488\u300D\u5728\u8FD4\u56DE\u503C\u4E0A\u540C\u5F62\uFF0C\u6240\u4EE5\u8FD9\u91CC\u8BB0\u4E00\u7B14\uFF0C\u4E0D\u88C5\u4F5C\u9A8C\u8FC7"
|
|
7961
|
+
});
|
|
7962
|
+
}
|
|
7963
|
+
if (entries.length === 0) {
|
|
7964
|
+
problems.push({
|
|
7965
|
+
scope: REGISTRY_SCOPE,
|
|
7966
|
+
reason: REASON_NO_ENTRIES,
|
|
7967
|
+
index: 0,
|
|
7968
|
+
token: null,
|
|
7969
|
+
message: "\u767B\u8BB0\u518C\u4E00\u6761\u90FD\u6CA1\u6709 \u2014\u2014 **\u67E5\u8FC7\u4E14\u4E3A\u7A7A**\uFF0C\u4E0D\u662F\u300C\u6CA1\u67E5\u300D"
|
|
7970
|
+
});
|
|
7971
|
+
}
|
|
7972
|
+
const times = /* @__PURE__ */ new Map();
|
|
7973
|
+
for (const entry of entries) times.set(entry.token, (times.get(entry.token) ?? 0) + 1);
|
|
7974
|
+
const facts = [];
|
|
7975
|
+
entries.forEach((entry, position) => {
|
|
7976
|
+
const index = position + 1;
|
|
7977
|
+
if ((times.get(entry.token) ?? 0) > 1) {
|
|
7978
|
+
problems.push({
|
|
7979
|
+
scope: ENTRY_SCOPE2,
|
|
7980
|
+
reason: REASON_DUPLICATE_TOKEN,
|
|
7981
|
+
index,
|
|
7982
|
+
token: entry.token,
|
|
7983
|
+
message: `${entry.token} \u767B\u8BB0\u4E86\u4E0D\u6B62\u4E00\u6B21 \u21D2 \u4E24\u6761\u90FD\u4E0D\u8FDB P\uFF08\u7559\u4E0B\u4EFB\u4F55\u4E00\u8FB9\u90FD\u8981\u56DE\u7B54\u300C\u4EE5\u54EA\u4EFD\u4E3A\u51C6\u300D\uFF09`
|
|
7984
|
+
});
|
|
7985
|
+
return;
|
|
7986
|
+
}
|
|
7987
|
+
const shape = PROBE_SHAPE.exec(entry.probe);
|
|
7988
|
+
if (!shape?.groups) {
|
|
7989
|
+
problems.push({
|
|
7990
|
+
scope: ENTRY_SCOPE2,
|
|
7991
|
+
reason: REASON_PROBE_SHAPE,
|
|
7992
|
+
index,
|
|
7993
|
+
token: entry.token,
|
|
7994
|
+
message: `${entry.token} \u7684\u63A2\u9488 ${JSON.stringify(entry.probe)} \u4E0D\u662F \`<\u6D4B\u8BD5\u6587\u4EF6>.test.ts::<\u6807\u8BB0>\` \u5F62\u6001 \u2014\u2014 \u53EA\u7ED9\u6587\u4EF6\u8DEF\u5F84\u662F\u540C\u4E49\u53CD\u590D\uFF08\u300C\u90A3\u4E2A\u6587\u4EF6\u5B58\u5728\u300D\u4E0D\u7B97\u80CC\u4E66\uFF09`
|
|
7995
|
+
});
|
|
7996
|
+
return;
|
|
7997
|
+
}
|
|
7998
|
+
if (probeRoot !== void 0) {
|
|
7999
|
+
const file = path10.join(probeRoot, shape.groups.file);
|
|
8000
|
+
const mark = `${PROBE_MARK_PREFIX} ${shape.groups.mark}`;
|
|
8001
|
+
const found = existsSync5(file) && readFileSync6(file, "utf8").includes(mark);
|
|
8002
|
+
if (!found) {
|
|
8003
|
+
problems.push({
|
|
8004
|
+
scope: ENTRY_SCOPE2,
|
|
8005
|
+
reason: REASON_PROBE_ABSENT,
|
|
8006
|
+
index,
|
|
8007
|
+
token: entry.token,
|
|
8008
|
+
message: `${entry.token} \u7684\u63A2\u9488\u6307\u5411 ${entry.probe}\uFF0C\u4F46\u90A3\u91CC\u627E\u4E0D\u5230 \`${mark}\` \u8FD9\u6761\u6807\u8BB0 \u21D2 \u8FD9\u6761\u767B\u8BB0\u6CA1\u6709\u80CC\u4E66\uFF0C\u4E0D\u8FDB P`
|
|
8009
|
+
});
|
|
8010
|
+
return;
|
|
8011
|
+
}
|
|
8012
|
+
}
|
|
8013
|
+
facts.push(entry.token);
|
|
8014
|
+
});
|
|
8015
|
+
return { facts, problems };
|
|
8016
|
+
}
|
|
8017
|
+
|
|
8018
|
+
// src/fde/gate0/scan.ts
|
|
8019
|
+
import { readFileSync as readFileSync7, readdirSync as readdirSync3, readlinkSync, statSync as statSync3 } from "fs";
|
|
8020
|
+
import * as path11 from "path";
|
|
8021
|
+
var CHANNEL_ACLASS = "aclass";
|
|
8022
|
+
var CHANNEL_ARTIFACT = "artifact-path";
|
|
8023
|
+
var CHANNEL_UNREGISTERED = "protocol-unregistered";
|
|
8024
|
+
var CHANNEL_REGISTERED_IS_ACLASS = "registered-but-is-aclass";
|
|
8025
|
+
var CHANNEL_NO_PROBE = "no-probe";
|
|
8026
|
+
var CHANNELS = [
|
|
8027
|
+
CHANNEL_ACLASS,
|
|
8028
|
+
CHANNEL_ARTIFACT,
|
|
8029
|
+
CHANNEL_UNREGISTERED,
|
|
8030
|
+
CHANNEL_REGISTERED_IS_ACLASS,
|
|
8031
|
+
CHANNEL_NO_PROBE
|
|
8032
|
+
];
|
|
8033
|
+
var REASON_NO_SKILL_ROOT = "no-skill-root";
|
|
8034
|
+
var REASON_NOTHING_SCANNED = "nothing-scanned";
|
|
8035
|
+
var REASON_FILE_UNREADABLE = "file-unreadable";
|
|
8036
|
+
var REASON_ACLASS_EMPTY = "aclass-empty";
|
|
8037
|
+
var REASON_ACLASS_PARTIALLY_UNREAD = "aclass-partially-unread";
|
|
8038
|
+
var REASON_REGISTRY = "registry-not-established";
|
|
8039
|
+
var REASON_CHANNEL_FOUR_UNCHECKED = "channel-four-unchecked";
|
|
8040
|
+
var REASON_BASELINE_ROUND_NOT_COMPLETE = "baseline-round-not-complete";
|
|
8041
|
+
var REASON_SYMLINK_NOT_SCANNED = "symlink-not-scanned";
|
|
8042
|
+
var PREMISE_REASONS = [
|
|
8043
|
+
REASON_NO_SKILL_ROOT,
|
|
8044
|
+
REASON_NOTHING_SCANNED,
|
|
8045
|
+
REASON_FILE_UNREADABLE,
|
|
8046
|
+
REASON_ACLASS_EMPTY,
|
|
8047
|
+
REASON_ACLASS_PARTIALLY_UNREAD,
|
|
8048
|
+
REASON_REGISTRY,
|
|
8049
|
+
REASON_CHANNEL_FOUR_UNCHECKED,
|
|
8050
|
+
REASON_BASELINE_ROUND_NOT_COMPLETE,
|
|
8051
|
+
REASON_SYMLINK_NOT_SCANNED
|
|
8052
|
+
];
|
|
8053
|
+
var PREMISE_REGISTRY_REASONS = REGISTRY_REASONS.filter(
|
|
8054
|
+
(r) => r !== REASON_NO_ENTRIES
|
|
8055
|
+
);
|
|
8056
|
+
var WORD_RE = /[A-Za-z0-9_]+/g;
|
|
8057
|
+
var SIMPLE_TOKEN = /^[A-Za-z0-9_]+$/;
|
|
8058
|
+
var WORD_CHARS = new Set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_");
|
|
8059
|
+
function isWordChar(char) {
|
|
8060
|
+
return char !== void 0 && WORD_CHARS.has(char);
|
|
8061
|
+
}
|
|
8062
|
+
var ENDPOINT_PREFIXES = ["api", "ws"];
|
|
8063
|
+
var ENDPOINT_PATH = `/(?:${ENDPOINT_PREFIXES.join("|")})(?:/(?:\\{[A-Za-z0-9_]+\\}|[A-Za-z0-9_.\\-]+))+`;
|
|
8064
|
+
var BARE_ENDPOINT_RE = new RegExp(`(?<![A-Za-z0-9_.\\-/])${ENDPOINT_PATH}`, "g");
|
|
8065
|
+
var URL_ENDPOINT_RE = new RegExp(
|
|
8066
|
+
`[A-Za-z][A-Za-z0-9+.\\-]*://[^\\s"'\`<>]*?(${ENDPOINT_PATH})`,
|
|
8067
|
+
"g"
|
|
8068
|
+
);
|
|
8069
|
+
var HOSTLESS_HOST = `(?:(?:[A-Za-z0-9_\\-]+\\.)+[A-Za-z]{2,}(?::[0-9]+)?|[A-Za-z0-9_\\-]+:[0-9]+)`;
|
|
8070
|
+
var HOSTLESS_ENDPOINT_RE = new RegExp(
|
|
8071
|
+
`(?<![A-Za-z0-9_.\\-/:@])${HOSTLESS_HOST}(${ENDPOINT_PATH})`,
|
|
8072
|
+
"g"
|
|
8073
|
+
);
|
|
8074
|
+
var ERROR_CODE_RE = /(?<![A-Za-z0-9_])[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+(?![A-Za-z0-9_])/g;
|
|
8075
|
+
var PATHISH_RE = /(?<![A-Za-z0-9_./-])\.?[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+/g;
|
|
8076
|
+
var ARTIFACT_SUFFIXES = [".json", ".jsonl"];
|
|
8077
|
+
var PRODUCT_FILE_RE = new RegExp(
|
|
8078
|
+
`(?<![A-Za-z0-9_./\\-])[A-Za-z0-9_.\\-]+(?:${ARTIFACT_SUFFIXES.map((s) => s.replace(".", "\\.")).join("|")})(?![A-Za-z0-9_])`,
|
|
8079
|
+
"g"
|
|
8080
|
+
);
|
|
8081
|
+
var PLACEHOLDER_RE2 = /\{[A-Za-z0-9_]*\}/g;
|
|
8082
|
+
var ARTIFACT_ROOT = ".hcmnx";
|
|
8083
|
+
var RECON_DIRNAME = "recon";
|
|
8084
|
+
var RECON_SEGMENTS_BELOW_ROOT = 2;
|
|
8085
|
+
var PRODUCT_RELATIVE_SHAPES = [
|
|
8086
|
+
["index", ".md"],
|
|
8087
|
+
["index", ".jsonl"],
|
|
8088
|
+
["mapping", ".json"],
|
|
8089
|
+
["meta", ".json"],
|
|
8090
|
+
["valuedomain", ".json"],
|
|
8091
|
+
["valuedomain", ".jsonl"]
|
|
8092
|
+
];
|
|
8093
|
+
var REASON_ENVIRONMENT_FACT = "environment-fact";
|
|
8094
|
+
var REASON_PRODUCT_TREE_PATH = "product-tree-path";
|
|
8095
|
+
var REASON_PRODUCT_RELATIVE_PATH = "product-relative-path";
|
|
8096
|
+
var REASON_PRODUCT_FILE_NAME = "product-file-name";
|
|
8097
|
+
var REASON_ENDPOINT = "endpoint";
|
|
8098
|
+
var REASON_ERROR_CODE = "error-code";
|
|
8099
|
+
var REASON_REGISTERED_IS_ACLASS = "registered-is-aclass";
|
|
8100
|
+
var REASON_REJECTED_REGISTRATION = "rejected-registration";
|
|
8101
|
+
var RED_REASONS = {
|
|
8102
|
+
[CHANNEL_ACLASS]: [REASON_ENVIRONMENT_FACT],
|
|
8103
|
+
[CHANNEL_ARTIFACT]: [
|
|
8104
|
+
REASON_PRODUCT_TREE_PATH,
|
|
8105
|
+
REASON_PRODUCT_RELATIVE_PATH,
|
|
8106
|
+
REASON_PRODUCT_FILE_NAME
|
|
8107
|
+
],
|
|
8108
|
+
[CHANNEL_UNREGISTERED]: [REASON_ENDPOINT, REASON_ERROR_CODE],
|
|
8109
|
+
[CHANNEL_REGISTERED_IS_ACLASS]: [REASON_REGISTERED_IS_ACLASS],
|
|
8110
|
+
[CHANNEL_NO_PROBE]: [REASON_REJECTED_REGISTRATION]
|
|
8111
|
+
};
|
|
8112
|
+
var ARTIFACT_WORDING = {
|
|
8113
|
+
[REASON_PRODUCT_TREE_PATH]: (t) => `\u51FA\u73B0**\u4EA7\u7269\u6811\u91CC\u7684**\u8DEF\u5F84 ${JSON.stringify(t)}`,
|
|
8114
|
+
[REASON_PRODUCT_RELATIVE_PATH]: (t) => `\u51FA\u73B0**\u4EA7\u7269\u6839\u76F8\u5BF9\u8DEF\u5F84** ${JSON.stringify(t)}\uFF08\u4EA7\u7269\u5B50\u76EE\u5F55\u4E0B\u7684\u90A3\u4E00\u4EFD\uFF09`,
|
|
8115
|
+
[REASON_PRODUCT_FILE_NAME]: (t) => `\u51FA\u73B0\u4EA7\u7269\u6587\u4EF6\u540D\u5F62\u6001 ${JSON.stringify(t)}\uFF08\u4EE5 .json/.jsonl \u6536\u5C3E\uFF1B**\u8FD9\u4E00\u6863\u4F1A\u8FC7\u5224**\uFF0C\u5B83\u4E0E\u4EA7\u7269\u65E0\u5173\u65F6\u8BF7\u7167\u76F4\u8BF4\u660E\uFF09`
|
|
8116
|
+
};
|
|
8117
|
+
function artifactWordingFor(reason) {
|
|
8118
|
+
const wording = ARTIFACT_WORDING[reason];
|
|
8119
|
+
if (wording === void 0) {
|
|
8120
|
+
throw new Error(
|
|
8121
|
+
`\u4EA7\u7269\u8DEF\u5F84\u6210\u56E0 ${JSON.stringify(reason)} \u6CA1\u6709\u5BF9\u5E94\u7684\u63AA\u8F9E\uFF08\u8BA4\u5F97\u7684\u662F ${Object.keys(ARTIFACT_WORDING).join("/")}\uFF09`
|
|
8122
|
+
);
|
|
8123
|
+
}
|
|
8124
|
+
return wording;
|
|
8125
|
+
}
|
|
8126
|
+
var ACLASS_EMPTY_WHY = {
|
|
8127
|
+
[REASON_UNREAD]: "\uFF08\u4EA7\u7269/\u5DE5\u4EF6\u90A3\u4E00\u5934\u6839\u672C\u6CA1\u8BFB\u5230\uFF09",
|
|
8128
|
+
[REASON_EMPTY]: "\uFF08\u4EA7\u7269\u8BFB\u5230\u4E86\uFF0C\u4E00\u6761\u53D6\u503C\u90FD\u6CA1\u6D3E\u751F\u51FA\u6765\uFF09"
|
|
8129
|
+
};
|
|
8130
|
+
var GAP_NOISE_TOKEN = "pure-digit-or-single-char";
|
|
8131
|
+
var GAP_CJK_INSIDE_LONGER_RUN = "cjk-inside-longer-run";
|
|
8132
|
+
var GAP_REASONS = [GAP_NOISE_TOKEN, GAP_CJK_INSIDE_LONGER_RUN];
|
|
8133
|
+
function checkReds(reds) {
|
|
8134
|
+
for (const red of reds) {
|
|
8135
|
+
if (!CHANNELS.includes(red.channel)) {
|
|
8136
|
+
throw new Error(
|
|
8137
|
+
`\u5224\u7EA2\u7684 channel \u53EA\u6709 ${CHANNELS.join("/")} \u8FD9\u51E0\u4E2A\u53D6\u503C\uFF0C\u62FF\u5230\u7684\u662F ${JSON.stringify(red.channel)}\uFF08${red.message}\uFF09`
|
|
8138
|
+
);
|
|
8139
|
+
}
|
|
8140
|
+
if (!RED_REASONS[red.channel].includes(red.reason)) {
|
|
8141
|
+
throw new Error(
|
|
8142
|
+
`${JSON.stringify(red.reason)} \u8FD9\u4E2A reason \u4E0D\u5C5E\u4E8E ${red.channel} \u8FD9\u6761\u901A\u9053\uFF08\u8BA4\u5F97\u7684\u662F ${RED_REASONS[red.channel].join("/")}\uFF1B${red.message}\uFF09`
|
|
8143
|
+
);
|
|
8144
|
+
}
|
|
8145
|
+
}
|
|
8146
|
+
return [...reds];
|
|
8147
|
+
}
|
|
8148
|
+
function checkGaps(gaps) {
|
|
8149
|
+
for (const gap of gaps) {
|
|
8150
|
+
if (!GAP_REASONS.includes(gap.reason)) {
|
|
8151
|
+
throw new Error(
|
|
8152
|
+
`\u7F3A\u53E3\u53F0\u8D26\u7684 reason \u53EA\u6709 ${GAP_REASONS.join("/")} \u4E24\u4E2A\u53D6\u503C\uFF0C\u62FF\u5230\u7684\u662F ${JSON.stringify(gap.reason)}\uFF08${gap.why}\uFF09`
|
|
8153
|
+
);
|
|
8154
|
+
}
|
|
8155
|
+
}
|
|
8156
|
+
return [...gaps];
|
|
8157
|
+
}
|
|
8158
|
+
function checkPremise(problems) {
|
|
8159
|
+
for (const problem of problems) {
|
|
8160
|
+
if (!PREMISE_REASONS.includes(problem.reason)) {
|
|
8161
|
+
throw new Error(
|
|
8162
|
+
`\u524D\u63D0\u7EA7\u53F0\u8D26\u7684 reason \u4E0D\u5728\u53D6\u503C\u57DF\u91CC\uFF1A${JSON.stringify(problem.reason)}\uFF08${problem.message}\uFF09`
|
|
8163
|
+
);
|
|
8164
|
+
}
|
|
8165
|
+
}
|
|
8166
|
+
return [...problems];
|
|
8167
|
+
}
|
|
8168
|
+
function baselineRoundPremise(round) {
|
|
8169
|
+
const state = round && typeof round === "object" ? round.state : void 0;
|
|
8170
|
+
if (state === "complete") return [];
|
|
8171
|
+
return [
|
|
8172
|
+
{
|
|
8173
|
+
reason: REASON_BASELINE_ROUND_NOT_COMPLETE,
|
|
8174
|
+
message: `\u8FD9\u4EFD\u57FA\u7EBF\u7684\u8F6E\u6B21\u72B6\u6001\u662F ${JSON.stringify(state)}\uFF08\u4E0D\u662F complete\uFF09\u21D2 A \u7C7B\u4E8B\u5B9E\u662F\u4ECE**\u6DF7\u7740\u4E0D\u540C\u8F6E\u6B21 / \u5224\u4E0D\u4E86\u8F6E\u6B21**\u7684\u4EA7\u7269\u91CC\u73B0\u7B97\u7684\uFF0C\u300C\u5224\u7EA2 0 \u6761\u300D\u5BF9\u54EA\u4E00\u8F6E\u90FD\u4E0D\u7B97\u6570\u3002\u5148\u8DD1 \`hcm delivery recon round\` \u770B\u662F\u54EA\u51E0\u4EFD\uFF0C\u6216\u91CD\u8DD1\u4E00\u6B21 \`hcm delivery recon\`\u3002\u26A0\uFE0F **\`hcm delivery recon\` \u8981\u8FDE\u5BA2\u6237\u5185\u7F51\u7684 HCM** \u2014\u2014 \u62FF\u4E0D\u5230\u7F51\u7684\u673A\u5668\u4E0A\u8FD9\u6761\u51FA\u8DEF\u8D70\u4E0D\u901A\uFF0C\u6B64\u65F6**\u522B\u628A\u8FD9\u4EFD\u57FA\u7EBF\u5F53 complete \u7528**\uFF0C\u628A\u5B83\u5E26\u56DE\u8FDE\u5F97\u4E0A\u7684\u5730\u65B9\u91CD\u722C`
|
|
8175
|
+
}
|
|
8176
|
+
];
|
|
8177
|
+
}
|
|
8178
|
+
function inGap(token2) {
|
|
8179
|
+
return [...token2].length <= 1 || /^\p{Nd}+$/u.test(token2);
|
|
8180
|
+
}
|
|
8181
|
+
function allMatches(regex, line) {
|
|
8182
|
+
const out = [];
|
|
8183
|
+
const re = new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`);
|
|
8184
|
+
let m;
|
|
8185
|
+
while ((m = re.exec(line)) !== null) {
|
|
8186
|
+
if (m[0] === "") {
|
|
8187
|
+
re.lastIndex += 1;
|
|
8188
|
+
continue;
|
|
8189
|
+
}
|
|
8190
|
+
out.push(m);
|
|
8191
|
+
}
|
|
8192
|
+
return out;
|
|
8193
|
+
}
|
|
8194
|
+
function overlaps(span, spans) {
|
|
8195
|
+
return spans.some(([s, e]) => span[0] < e && s < span[1]);
|
|
8196
|
+
}
|
|
8197
|
+
function endpointsIn(line) {
|
|
8198
|
+
const out = [];
|
|
8199
|
+
for (const regex of [URL_ENDPOINT_RE, HOSTLESS_ENDPOINT_RE]) {
|
|
8200
|
+
for (const m of allMatches(regex, line)) {
|
|
8201
|
+
const span = [m.index, m.index + m[0].length];
|
|
8202
|
+
if (!overlaps(
|
|
8203
|
+
span,
|
|
8204
|
+
out.map(([, s]) => s)
|
|
8205
|
+
)) {
|
|
8206
|
+
out.push([m[1], span]);
|
|
8207
|
+
}
|
|
8208
|
+
}
|
|
8209
|
+
}
|
|
8210
|
+
for (const m of allMatches(BARE_ENDPOINT_RE, line)) {
|
|
8211
|
+
const span = [m.index, m.index + m[0].length];
|
|
8212
|
+
if (!overlaps(
|
|
8213
|
+
span,
|
|
8214
|
+
out.map(([, s]) => s)
|
|
8215
|
+
)) {
|
|
8216
|
+
out.push([m[0], span]);
|
|
8217
|
+
}
|
|
8218
|
+
}
|
|
8219
|
+
return out;
|
|
8220
|
+
}
|
|
8221
|
+
function normalize(token2) {
|
|
8222
|
+
return token2.replace(PLACEHOLDER_RE2, "{}");
|
|
8223
|
+
}
|
|
8224
|
+
function factsIndex(facts) {
|
|
8225
|
+
return facts.map((token2) => segmentsOf(normalize(token2)));
|
|
8226
|
+
}
|
|
8227
|
+
function covered(token2, index) {
|
|
8228
|
+
const segments = segmentsOf(normalize(token2));
|
|
8229
|
+
return index.some(
|
|
8230
|
+
(template) => template.length === segments.length && template.every((t, i) => t === "{}" || t === segments[i])
|
|
8231
|
+
);
|
|
8232
|
+
}
|
|
8233
|
+
var SEGMENT_SEPARATOR = "/";
|
|
8234
|
+
function segmentsOf(token2) {
|
|
8235
|
+
return token2.split(SEGMENT_SEPARATOR);
|
|
8236
|
+
}
|
|
8237
|
+
function artifactReason(token2) {
|
|
8238
|
+
const segments = segmentsOf(token2);
|
|
8239
|
+
const head = segments.slice(0, -1);
|
|
8240
|
+
const last = segments[segments.length - 1] ?? "";
|
|
8241
|
+
if (head.includes(ARTIFACT_ROOT)) return REASON_PRODUCT_TREE_PATH;
|
|
8242
|
+
for (let i = 0; i < head.length; i += 1) {
|
|
8243
|
+
if (head[i] === RECON_DIRNAME && segments.length - i - 1 > RECON_SEGMENTS_BELOW_ROOT) {
|
|
8244
|
+
return REASON_PRODUCT_TREE_PATH;
|
|
8245
|
+
}
|
|
8246
|
+
}
|
|
8247
|
+
if (segments.length === 2 && PRODUCT_RELATIVE_SHAPES.some(
|
|
8248
|
+
([subdir, suffix]) => segments[0] === subdir && last.endsWith(suffix)
|
|
8249
|
+
)) {
|
|
8250
|
+
return REASON_PRODUCT_RELATIVE_PATH;
|
|
8251
|
+
}
|
|
8252
|
+
if (ARTIFACT_SUFFIXES.some((suffix) => last.endsWith(suffix))) return REASON_PRODUCT_FILE_NAME;
|
|
8253
|
+
return null;
|
|
8254
|
+
}
|
|
8255
|
+
var CJK_RANGES = [
|
|
8256
|
+
[13312, 19903],
|
|
8257
|
+
[19968, 40959],
|
|
8258
|
+
[63744, 64255]
|
|
8259
|
+
];
|
|
8260
|
+
function isCjk(char) {
|
|
8261
|
+
const code = char?.codePointAt(0);
|
|
8262
|
+
return code !== void 0 && CJK_RANGES.some(([low, high]) => code >= low && code <= high);
|
|
8263
|
+
}
|
|
8264
|
+
function boundedFind(line, token2) {
|
|
8265
|
+
let start = line.indexOf(token2);
|
|
8266
|
+
while (start >= 0) {
|
|
8267
|
+
const end = start + token2.length;
|
|
8268
|
+
const leftOk = start === 0 || !(isWordChar(token2[0]) && isWordChar(line[start - 1]));
|
|
8269
|
+
const rightOk = end >= line.length || !(isWordChar(token2[token2.length - 1]) && isWordChar(line[end]));
|
|
8270
|
+
if (leftOk && rightOk) return start;
|
|
8271
|
+
start = line.indexOf(token2, start + 1);
|
|
8272
|
+
}
|
|
8273
|
+
return -1;
|
|
8274
|
+
}
|
|
8275
|
+
function cjkRunAt(line, start, token2) {
|
|
8276
|
+
const end = start + token2.length;
|
|
8277
|
+
if (isCjk(token2[0]) && start > 0 && isCjk(line[start - 1])) return true;
|
|
8278
|
+
return isCjk(token2[token2.length - 1]) && end < line.length && isCjk(line[end]);
|
|
8279
|
+
}
|
|
8280
|
+
var Matcher = class {
|
|
8281
|
+
owners;
|
|
8282
|
+
simple;
|
|
8283
|
+
byFirst;
|
|
8284
|
+
constructor(aclass) {
|
|
8285
|
+
this.owners = /* @__PURE__ */ new Map();
|
|
8286
|
+
for (const kind of ACLASS_KINDS) {
|
|
8287
|
+
for (const token2 of [...aclass[kind] ?? []].sort()) {
|
|
8288
|
+
const owned = this.owners.get(token2);
|
|
8289
|
+
if (owned) owned.push(kind);
|
|
8290
|
+
else this.owners.set(token2, [kind]);
|
|
8291
|
+
}
|
|
8292
|
+
}
|
|
8293
|
+
this.simple = new Set([...this.owners.keys()].filter((t) => SIMPLE_TOKEN.test(t)));
|
|
8294
|
+
this.byFirst = /* @__PURE__ */ new Map();
|
|
8295
|
+
for (const token2 of this.owners.keys()) {
|
|
8296
|
+
if (this.simple.has(token2)) continue;
|
|
8297
|
+
const first = token2[0];
|
|
8298
|
+
if (first === void 0) continue;
|
|
8299
|
+
const bucket = this.byFirst.get(first);
|
|
8300
|
+
if (bucket) bucket.push(token2);
|
|
8301
|
+
else this.byFirst.set(first, [token2]);
|
|
8302
|
+
}
|
|
8303
|
+
}
|
|
8304
|
+
/**
|
|
8305
|
+
* → 这一行命中的 `[(token, **有边界的**那一处的位置)]`,同一 token 一行只报一次。
|
|
8306
|
+
*
|
|
8307
|
+
* 🔴 **位置必须来自 `boundedFind`,不许再算一遍 `indexOf`**。起手那版用 `boundedFind`
|
|
8308
|
+
* 确认「这一行**存在**一处合法边界命中」,返回的却是第一处裸子串 —— 可能根本不是那一处。
|
|
8309
|
+
* 下游把这个位置交给 `cjkRunAt` 做分桶,于是**在同一行前面加一处诱饵写法就能把一条
|
|
8310
|
+
* 真红压掉**。辖区 = 首/末字符为 CJK、另一端为词字符的**混合形态** token;纯 ASCII 与
|
|
8311
|
+
* 纯 CJK 两个位置必然相等,所以此前每一项变异都照不到它。
|
|
8312
|
+
* 「存在一处合法命中」是「这一处合法」的**必要条件**,不是充分条件。
|
|
8313
|
+
*
|
|
8314
|
+
* 🔴 **`sort` 不是洁癖,它是这份输出的确定性本身**:按插入序遍历会让非纯词形 token 的
|
|
8315
|
+
* 先后随输入变化,集合、rc、判词全都一样,只有顺序变 —— 那正是「静默」的定义,
|
|
8316
|
+
* 而它坏的是**验证仪器**:任何「前后两版输出逐字相等」的复核都会假红。
|
|
8317
|
+
*/
|
|
8318
|
+
hits(line) {
|
|
8319
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8320
|
+
const result = [];
|
|
8321
|
+
for (const m of allMatches(WORD_RE, line)) {
|
|
8322
|
+
const token2 = m[0];
|
|
8323
|
+
if (token2 !== void 0 && this.simple.has(token2) && !seen.has(token2)) {
|
|
8324
|
+
seen.add(token2);
|
|
8325
|
+
result.push([token2, boundedFind(line, token2)]);
|
|
8326
|
+
}
|
|
8327
|
+
}
|
|
8328
|
+
for (const char of [.../* @__PURE__ */ new Set([...line])].sort()) {
|
|
8329
|
+
for (const token2 of this.byFirst.get(char) ?? []) {
|
|
8330
|
+
if (seen.has(token2)) continue;
|
|
8331
|
+
const start = boundedFind(line, token2);
|
|
8332
|
+
if (start >= 0) {
|
|
8333
|
+
seen.add(token2);
|
|
8334
|
+
result.push([token2, start]);
|
|
8335
|
+
}
|
|
8336
|
+
}
|
|
8337
|
+
}
|
|
8338
|
+
return result;
|
|
8339
|
+
}
|
|
8340
|
+
};
|
|
8341
|
+
function classReasons(problems) {
|
|
8342
|
+
const out = /* @__PURE__ */ new Map();
|
|
8343
|
+
for (const problem of problems ?? []) {
|
|
8344
|
+
if (problem.scope === CLASS_SCOPE) {
|
|
8345
|
+
for (const kind of problem.kinds) out.set(kind, problem.reason);
|
|
8346
|
+
}
|
|
8347
|
+
}
|
|
8348
|
+
return out;
|
|
8349
|
+
}
|
|
8350
|
+
var INVISIBLE_RE = /\p{Cf}/u;
|
|
8351
|
+
var FULLWIDTH_ALNUM_RANGES = [
|
|
8352
|
+
[65296, 65305, 65296 - 48],
|
|
8353
|
+
[65313, 65338, 65313 - 65],
|
|
8354
|
+
[65345, 65370, 65345 - 97]
|
|
8355
|
+
];
|
|
8356
|
+
var FOLDED_NOTE = "\uFF1B\u26A0\uFE0F \u8FD9\u4E00\u5904\u662F**\u5F52\u4E00\u4E4B\u540E**\u624D\u8BA4\u51FA\u6765\u7684\uFF1A\u539F\u6587\u91CC\u5939\u4E86\u4E0D\u53EF\u89C1\u5B57\u7B26\u6216\u5168\u89D2\u5B57\u7B26\uFF0C\u76F4\u63A5\u6309\u4E0A\u9762\u8FD9\u4E2A\u8BCD\u53BB grep \u4F1A\u641C\u4E0D\u5230";
|
|
8357
|
+
var foldedOriginal = (literal) => `\uFF0C\u539F\u6587\u91CC\u90A3\u4E00\u6BB5\u662F ${JSON.stringify(literal)}`;
|
|
8358
|
+
function foldWithMap(line) {
|
|
8359
|
+
const out = [];
|
|
8360
|
+
const origin = [];
|
|
8361
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
8362
|
+
const char = line[i];
|
|
8363
|
+
if (INVISIBLE_RE.test(char)) continue;
|
|
8364
|
+
const code = char.charCodeAt(0);
|
|
8365
|
+
let folded = char;
|
|
8366
|
+
for (const [low, high, offset] of FULLWIDTH_ALNUM_RANGES) {
|
|
8367
|
+
if (code >= low && code <= high) {
|
|
8368
|
+
folded = String.fromCharCode(code - offset);
|
|
8369
|
+
break;
|
|
8370
|
+
}
|
|
8371
|
+
}
|
|
8372
|
+
out.push(folded);
|
|
8373
|
+
origin.push(i);
|
|
8374
|
+
}
|
|
8375
|
+
return [out.join(""), origin];
|
|
8376
|
+
}
|
|
8377
|
+
function foldConfusables(line) {
|
|
8378
|
+
return foldWithMap(line)[0];
|
|
8379
|
+
}
|
|
8380
|
+
function foldedNoteFor(original, folded, token2) {
|
|
8381
|
+
if (original.includes(token2)) return "";
|
|
8382
|
+
let start = boundedFind(folded, token2);
|
|
8383
|
+
if (start < 0) start = folded.indexOf(token2);
|
|
8384
|
+
if (start < 0) {
|
|
8385
|
+
return FOLDED_NOTE;
|
|
8386
|
+
}
|
|
8387
|
+
const [, origin] = foldWithMap(original);
|
|
8388
|
+
const from = origin[start];
|
|
8389
|
+
const to = origin[start + token2.length - 1];
|
|
8390
|
+
if (from === void 0 || to === void 0) return FOLDED_NOTE;
|
|
8391
|
+
return FOLDED_NOTE + foldedOriginal(original.slice(from, to + 1));
|
|
8392
|
+
}
|
|
8393
|
+
function judgeLine(line, lineno, rel, matcher, index, reds, gaps) {
|
|
8394
|
+
const folded = foldConfusables(line);
|
|
8395
|
+
const lineReds = [];
|
|
8396
|
+
const lineGaps = [];
|
|
8397
|
+
judgePass(line, lineno, rel, matcher, index, lineReds, lineGaps);
|
|
8398
|
+
if (folded !== line) judgePass(folded, lineno, rel, matcher, index, lineReds, lineGaps);
|
|
8399
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8400
|
+
const unique2 = [];
|
|
8401
|
+
for (const red of lineReds) {
|
|
8402
|
+
const key = `${red.channel} ${red.token}`;
|
|
8403
|
+
if (seen.has(key)) continue;
|
|
8404
|
+
seen.add(key);
|
|
8405
|
+
unique2.push({ ...red, message: red.message + foldedNoteFor(line, folded, red.token) });
|
|
8406
|
+
}
|
|
8407
|
+
reds.push(...unique2);
|
|
8408
|
+
const redTokens = new Set(unique2.filter((r) => r.channel === CHANNEL_ACLASS).map((r) => r.token));
|
|
8409
|
+
const seenGaps = /* @__PURE__ */ new Set();
|
|
8410
|
+
for (const gap of lineGaps) {
|
|
8411
|
+
const key = `${gap.reason} ${gap.token}`;
|
|
8412
|
+
if (seenGaps.has(key) || folded !== line && redTokens.has(gap.token)) continue;
|
|
8413
|
+
seenGaps.add(key);
|
|
8414
|
+
gaps.push({ ...gap, why: gap.why + foldedNoteFor(line, folded, gap.token) });
|
|
8415
|
+
}
|
|
8416
|
+
}
|
|
8417
|
+
function judgePass(line, lineno, rel, matcher, index, reds, gaps) {
|
|
8418
|
+
for (const [token2, start] of matcher.hits(line)) {
|
|
8419
|
+
const kinds = matcher.owners.get(token2) ?? [];
|
|
8420
|
+
if (inGap(token2)) {
|
|
8421
|
+
gaps.push({
|
|
8422
|
+
reason: GAP_NOISE_TOKEN,
|
|
8423
|
+
token: token2,
|
|
8424
|
+
kinds,
|
|
8425
|
+
path: rel,
|
|
8426
|
+
line: lineno,
|
|
8427
|
+
why: `${rel}:${lineno} \u7EAF\u6570\u5B57 / \u5355\u5B57\u7B26 ${JSON.stringify(token2)} \u2014\u2014 \u5224\u7EA2\u8FD9\u4E00\u5C42**\u58F0\u660E\u7684\u7F3A\u53E3**\uFF0C\u67E5\u8FC7\u3001\u6309\u7F3A\u53E3\u653E\u884C`
|
|
8428
|
+
});
|
|
8429
|
+
continue;
|
|
8430
|
+
}
|
|
8431
|
+
if (cjkRunAt(line, start, token2)) {
|
|
8432
|
+
gaps.push({
|
|
8433
|
+
reason: GAP_CJK_INSIDE_LONGER_RUN,
|
|
8434
|
+
token: token2,
|
|
8435
|
+
kinds,
|
|
8436
|
+
path: rel,
|
|
8437
|
+
line: lineno,
|
|
8438
|
+
why: `${rel}:${lineno} ${JSON.stringify(token2)}\uFF08A \u7C7B ${kinds.join("/")}\uFF09\u843D\u5728\u4E00\u4E32\u66F4\u957F\u7684\u4E2D\u6587\u91CC \u2014\u2014 \u4E2D\u6587\u6CA1\u6709\u5206\u8BCD\u8FB9\u754C\uFF0C**\u8BF4\u4E0D\u51C6**\u5B83\u662F\u4E0D\u662F\u90A3\u4E2A\u53D6\u503C\uFF1B\u4E0D\u9876 rc\uFF0C\u8BF7\u81EA\u5DF1\u770B\u4E00\u773C`
|
|
8439
|
+
});
|
|
8440
|
+
continue;
|
|
8441
|
+
}
|
|
8442
|
+
reds.push({
|
|
8443
|
+
channel: CHANNEL_ACLASS,
|
|
8444
|
+
reason: REASON_ENVIRONMENT_FACT,
|
|
8445
|
+
token: token2,
|
|
8446
|
+
path: rel,
|
|
8447
|
+
line: lineno,
|
|
8448
|
+
kinds,
|
|
8449
|
+
message: `${rel}:${lineno} \u51FA\u73B0\u73AF\u5883\u4E8B\u5B9E ${JSON.stringify(token2)}\uFF08A \u7C7B ${kinds.join("/")}\uFF09\u2014\u2014 \u6362\u4E2A\u5BA2\u6237\u5C31\u5168\u9519\uFF0C**\u65E0\u8C41\u514D**`
|
|
8450
|
+
});
|
|
8451
|
+
}
|
|
8452
|
+
const spans = [];
|
|
8453
|
+
for (const [token2, span] of endpointsIn(line)) {
|
|
8454
|
+
spans.push(span);
|
|
8455
|
+
judgeProtocol(token2, REASON_ENDPOINT, "\u7AEF\u70B9", lineno, rel, matcher, index, reds);
|
|
8456
|
+
}
|
|
8457
|
+
for (const regex of [PATHISH_RE, PRODUCT_FILE_RE]) {
|
|
8458
|
+
for (const m of allMatches(regex, line)) {
|
|
8459
|
+
const span = [m.index, m.index + m[0].length];
|
|
8460
|
+
if (overlaps(span, spans)) continue;
|
|
8461
|
+
const token2 = m[0];
|
|
8462
|
+
const reason = artifactReason(token2);
|
|
8463
|
+
if (reason === null) continue;
|
|
8464
|
+
spans.push(span);
|
|
8465
|
+
reds.push({
|
|
8466
|
+
channel: CHANNEL_ARTIFACT,
|
|
8467
|
+
reason,
|
|
8468
|
+
token: token2,
|
|
8469
|
+
path: rel,
|
|
8470
|
+
line: lineno,
|
|
8471
|
+
kinds: [],
|
|
8472
|
+
message: `${rel}:${lineno} ${artifactWordingFor(reason)(token2)} \u2014\u2014 \u4EA7\u7269\u8DEF\u5F84**\u65E0\u8C41\u514D**\uFF0C\u767B\u8BB0\u518C\u4E0D\u53D7\u7406\u5B83\uFF1Bskill \u6B63\u6587\u8BE5\u8BF4\u7684\u662F\u8DD1\u54EA\u6761\u547D\u4EE4\uFF0C\u4E0D\u662F\u53BB\u8BFB\u54EA\u4E2A\u6587\u4EF6`
|
|
8473
|
+
});
|
|
8474
|
+
}
|
|
8475
|
+
}
|
|
8476
|
+
for (const m of allMatches(ERROR_CODE_RE, line)) {
|
|
8477
|
+
const span = [m.index, m.index + m[0].length];
|
|
8478
|
+
if (overlaps(span, spans)) continue;
|
|
8479
|
+
spans.push(span);
|
|
8480
|
+
judgeProtocol(m[0], REASON_ERROR_CODE, "\u9519\u8BEF\u7801", lineno, rel, matcher, index, reds);
|
|
8481
|
+
}
|
|
8482
|
+
}
|
|
8483
|
+
function judgeProtocol(token2, reason, shape, lineno, rel, matcher, index, reds) {
|
|
8484
|
+
if (matcher.owners.has(token2)) return;
|
|
8485
|
+
if (covered(token2, index)) return;
|
|
8486
|
+
reds.push({
|
|
8487
|
+
channel: CHANNEL_UNREGISTERED,
|
|
8488
|
+
reason,
|
|
8489
|
+
token: token2,
|
|
8490
|
+
path: rel,
|
|
8491
|
+
line: lineno,
|
|
8492
|
+
kinds: [],
|
|
8493
|
+
message: `${rel}:${lineno} \u51FA\u73B0${shape} ${JSON.stringify(token2)}\uFF0C\u800C\u5B83**\u6CA1\u6709\u767B\u8BB0**\u5728\u968F\u5305\u7684\u534F\u8BAE\u4E8B\u5B9E\u767B\u8BB0\u518C\u91CC \u21D2 \u8981\u4E48\u6539\u5199\u6210\u4E0D\u4F9D\u8D56\u5B83\u7684\u8BF4\u6CD5\uFF0C\u8981\u4E48\u7ED9 CLI \u63D0\u4E00\u6761\u767B\u8BB0\uFF08\u767B\u8BB0\u5FC5\u987B\u9644\u4E00\u4E2A\u65AD\u884C\u4E3A\u7684\u63A2\u9488\uFF09`
|
|
8494
|
+
});
|
|
8495
|
+
}
|
|
8496
|
+
function scan(skillRoot, aclass, facts, options = {}) {
|
|
8497
|
+
const reds = [];
|
|
8498
|
+
const premise = [];
|
|
8499
|
+
const gaps = [];
|
|
8500
|
+
const scanned = [];
|
|
8501
|
+
const matcher = new Matcher(aclass);
|
|
8502
|
+
const index = factsIndex(facts);
|
|
8503
|
+
const reasons = classReasons(options.aclassProblems);
|
|
8504
|
+
for (const kind of ACLASS_KINDS) {
|
|
8505
|
+
if ((aclass[kind]?.size ?? 0) > 0) continue;
|
|
8506
|
+
const reason = reasons.get(kind);
|
|
8507
|
+
let why = "";
|
|
8508
|
+
if (reason !== void 0) {
|
|
8509
|
+
if (!(reason in ACLASS_EMPTY_WHY)) {
|
|
8510
|
+
throw new Error(
|
|
8511
|
+
`\u7C7B\u7EA7\u6210\u56E0 ${JSON.stringify(reason)} \u6CA1\u6709\u5BF9\u5E94\u7684\u63AA\u8F9E\uFF08\u8BA4\u5F97\u7684\u662F ${CLASS_REASONS.join("/")}\uFF09`
|
|
8512
|
+
);
|
|
8513
|
+
}
|
|
8514
|
+
why = ACLASS_EMPTY_WHY[reason];
|
|
8515
|
+
}
|
|
8516
|
+
premise.push({
|
|
8517
|
+
reason: REASON_ACLASS_EMPTY,
|
|
8518
|
+
message: `A \u7C7B\u5B50\u96C6 ${kind} \u662F\u7A7A\u7684${why} \u21D2 \u95E8 0 \u5BF9\u8FD9\u4E00\u7C7B**\u6052\u7EFF**\uFF0C\u8FD9\u4E00\u8F6E\u7684\u7EFF\u4E0D\u7B97\u6570`
|
|
8519
|
+
});
|
|
8520
|
+
}
|
|
8521
|
+
for (const problem of options.aclassProblems ?? []) {
|
|
8522
|
+
if (problem.scope !== ENTRY_SCOPE) continue;
|
|
8523
|
+
premise.push({
|
|
8524
|
+
reason: REASON_ACLASS_PARTIALLY_UNREAD,
|
|
8525
|
+
message: `A \u7C7B\u5B50\u96C6 ${problem.kinds.join("/")} \u5C11\u4E86\u4E00\u90E8\u5206\uFF08${problem.reason}\uFF09\uFF1A${problem.message} \u21D2 \u5C11\u6389\u7684\u90A3\u90E8\u5206\u95E8 0 \u5224\u4E0D\u51FA\u6765\uFF0C\u8FD9\u4E00\u8F6E\u7684\u7EFF\u4E0D\u7B97\u6570`
|
|
8526
|
+
});
|
|
8527
|
+
}
|
|
8528
|
+
let factsProblems = options.factsProblems;
|
|
8529
|
+
if (factsProblems === void 0) {
|
|
8530
|
+
premise.push({
|
|
8531
|
+
reason: REASON_CHANNEL_FOUR_UNCHECKED,
|
|
8532
|
+
message: "\u6CA1\u6709\u628A loadProtocolFacts \u7684\u53F0\u8D26\u4F20\u8FDB\u6765 \u21D2 \u901A\u9053\u56DB\uFF08\u767B\u8BB0\u9879\u88AB\u62D2\uFF09\u4E00\u6761\u90FD\u6CA1\u67E5\u8FC7\u2014\u2014\u300C\u767B\u8BB0\u518C\u5E72\u51C0\u300D\u4E0E\u300C\u6CA1\u67E5\u767B\u8BB0\u518C\u300D\u5728\u8FD4\u56DE\u503C\u4E0A\u540C\u5F62"
|
|
8533
|
+
});
|
|
8534
|
+
factsProblems = [];
|
|
8535
|
+
}
|
|
8536
|
+
for (const problem of factsProblems) {
|
|
8537
|
+
if (problem.scope === REGISTRY_SCOPE && PREMISE_REGISTRY_REASONS.includes(problem.reason)) {
|
|
8538
|
+
premise.push({
|
|
8539
|
+
reason: REASON_REGISTRY,
|
|
8540
|
+
message: `\u767B\u8BB0\u518C\u8FD9\u4E00\u5C42\u6CA1\u7ACB\u4F4F\uFF08${problem.reason}\uFF09\uFF1A${problem.message} \u21D2 \u8C41\u514D\u901A\u9053 P \u4E0D\u53EF\u4FE1`
|
|
8541
|
+
});
|
|
8542
|
+
}
|
|
8543
|
+
}
|
|
8544
|
+
for (const problem of factsProblems) {
|
|
8545
|
+
if (problem.scope === REGISTRY_SCOPE) continue;
|
|
8546
|
+
reds.push({
|
|
8547
|
+
channel: CHANNEL_NO_PROBE,
|
|
8548
|
+
reason: REASON_REJECTED_REGISTRATION,
|
|
8549
|
+
token: problem.token ?? `(\u7B2C${problem.index}\u6761)`,
|
|
8550
|
+
path: null,
|
|
8551
|
+
line: 0,
|
|
8552
|
+
kinds: [],
|
|
8553
|
+
message: `\u767B\u8BB0\u518C\u7B2C ${problem.index} \u6761\u88AB\u62D2\uFF08${problem.reason}\uFF09\uFF1A${problem.message}`
|
|
8554
|
+
});
|
|
8555
|
+
}
|
|
8556
|
+
for (const token2 of [...facts].sort()) {
|
|
8557
|
+
const kinds = matcher.owners.get(token2);
|
|
8558
|
+
if (!kinds) continue;
|
|
8559
|
+
reds.push({
|
|
8560
|
+
channel: CHANNEL_REGISTERED_IS_ACLASS,
|
|
8561
|
+
reason: REASON_REGISTERED_IS_ACLASS,
|
|
8562
|
+
token: token2,
|
|
8563
|
+
path: null,
|
|
8564
|
+
line: 0,
|
|
8565
|
+
kinds,
|
|
8566
|
+
message: `\u767B\u8BB0\u518C\u767B\u8BB0\u4E86 ${JSON.stringify(token2)}\uFF0C\u800C\u5B83\u662F A \u7C7B\u73AF\u5883\u4E8B\u5B9E\uFF08${kinds.join("/")}\uFF09\u2014\u2014 recon \u67E5\u5F97\u5230\u7684\u4E1C\u897F\u4E0D\u662F\u7248\u672C\u7EA7\u534F\u8BAE\uFF0C\u767B\u8BB0\u5B83\u4E70\u4E0D\u5230\u4EFB\u4F55\u8C41\u514D`
|
|
8567
|
+
});
|
|
8568
|
+
}
|
|
8569
|
+
let files = [];
|
|
8570
|
+
if (!isDirectory(skillRoot)) {
|
|
8571
|
+
premise.push({
|
|
8572
|
+
reason: REASON_NO_SKILL_ROOT,
|
|
8573
|
+
message: `\u626B\u63CF\u9762 ${skillRoot} \u4E0D\u662F\u4E00\u4E2A\u76EE\u5F55 \u21D2 \u4E00\u4E2A\u5B57\u90FD\u6CA1\u626B\u8FC7\uFF08\u8FD9\u4E0D\u662F\u300C\u626B\u8FC7\u3001\u5E72\u51C0\u300D\uFF09`
|
|
8574
|
+
});
|
|
8575
|
+
} else {
|
|
8576
|
+
const walked = walk2(skillRoot);
|
|
8577
|
+
files = walked.files;
|
|
8578
|
+
for (const link of walked.unscannedLinks) {
|
|
8579
|
+
let target;
|
|
8580
|
+
try {
|
|
8581
|
+
target = readlinkSync(link);
|
|
8582
|
+
} catch {
|
|
8583
|
+
target = "(\u8BFB\u4E0D\u51FA\u94FE\u63A5\u76EE\u6807)";
|
|
8584
|
+
}
|
|
8585
|
+
premise.push({
|
|
8586
|
+
reason: REASON_SYMLINK_NOT_SCANNED,
|
|
8587
|
+
message: `${path11.relative(skillRoot, link)} \u662F\u4E00\u4E2A\u7B26\u53F7\u94FE\u63A5\uFF08\u6307\u5411 ${JSON.stringify(target)}\uFF09\u21D2 \u95E8 0 **\u6CA1\u6709\u987A\u7740\u5B83\u626B\u8FC7\u4E00\u4E2A\u5B57**\uFF0C\u8FD9\u4E00\u8F6E\u7684\u7EFF\u4E0D\u7B97\u6570\u3002\u51FA\u8DEF\uFF1A\u628A\u8FD9\u6761\u94FE\u4ECE skill \u91CC\u53BB\u6389 \u2014\u2014 \u94FE\u540E\u9762\u90A3\u4EFD\u5185\u5BB9\u672C\u8BE5\u88AB\u5224\u5C31 \`cp -RL\` \u62F7\u6210\u771F\u5185\u5BB9\uFF08\u26A0\uFE0F \u73AF\u8DEF\u4E0E\u65AD\u94FE\u4E0A\u5B83\u4F1A\u5F53\u573A\u62A5\u9519\uFF0C\u90A3\u4E24\u79CD\u76F4\u63A5\u5220\uFF09\uFF1B\u5185\u5BB9\u5DF2\u7ECF\u5728 skill \u91CC\uFF08\u6307\u5411\u540C\u6811\u7684\u522B\u540D\uFF09\u4E5F\u76F4\u63A5\u5220\u3002**\u4E0D\u8DDF\u968F**\u662F\u523B\u610F\u7684 \u2014\u2014 \u8DDF\u8FC7\u53BB\u4F1A\u8FDE\u4E0A\u73AF\u8DEF\u3001\u6216\u8D8A\u51FA skill \u76EE\u5F55\u626B\u5230\u65E0\u5173\u6587\u4EF6`
|
|
8588
|
+
});
|
|
8589
|
+
}
|
|
8590
|
+
if (files.length === 0) {
|
|
8591
|
+
premise.push({
|
|
8592
|
+
reason: REASON_NOTHING_SCANNED,
|
|
8593
|
+
message: `\u626B\u63CF\u9762 ${skillRoot} \u5728\u76D8\u4E0A\uFF0C\u4F46\u91CC\u9762\u4E00\u4E2A\u6587\u4EF6\u90FD\u6CA1\u6709 \u2014\u2014 \u67E5\u8FC7\u4E14\u4E3A\u7A7A`
|
|
8594
|
+
});
|
|
8595
|
+
}
|
|
8596
|
+
}
|
|
8597
|
+
for (const file of files) {
|
|
8598
|
+
let text;
|
|
8599
|
+
try {
|
|
8600
|
+
text = readFileSync7(file, "utf8");
|
|
8601
|
+
} catch (e) {
|
|
8602
|
+
premise.push({
|
|
8603
|
+
reason: REASON_FILE_UNREADABLE,
|
|
8604
|
+
message: `${file} \u8BFB\u4E0D\u52A8\uFF08${String(e)}\uFF09\u21D2 \u5224\u4E0D\u4E86\u5B83\u5E72\u4E0D\u5E72\u51C0\uFF0C\u95E8 0 \u4E0D\u80FD\u5BF9\u7740\u5B83\u8BF4\u7EFF`
|
|
8605
|
+
});
|
|
8606
|
+
continue;
|
|
8607
|
+
}
|
|
8608
|
+
const rel = path11.relative(skillRoot, file);
|
|
8609
|
+
scanned.push(rel);
|
|
8610
|
+
text.split("\n").forEach((line, i) => {
|
|
8611
|
+
judgeLine(line, i + 1, rel, matcher, index, reds, gaps);
|
|
8612
|
+
});
|
|
8613
|
+
}
|
|
8614
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8615
|
+
const unique2 = [];
|
|
8616
|
+
for (const red of checkReds(reds)) {
|
|
8617
|
+
const key = `${red.channel} ${red.token} ${red.path} ${red.line}`;
|
|
8618
|
+
if (seen.has(key)) continue;
|
|
8619
|
+
seen.add(key);
|
|
8620
|
+
unique2.push(red);
|
|
8621
|
+
}
|
|
8622
|
+
const channels = Object.fromEntries(
|
|
8623
|
+
CHANNELS.map((name) => [name, unique2.filter((r) => r.channel === name)])
|
|
8624
|
+
);
|
|
8625
|
+
return {
|
|
8626
|
+
red: unique2,
|
|
8627
|
+
channels,
|
|
8628
|
+
scanned,
|
|
8629
|
+
gaps: checkGaps(gaps),
|
|
8630
|
+
premise: checkPremise(premise)
|
|
8631
|
+
};
|
|
8632
|
+
}
|
|
8633
|
+
function isDirectory(target) {
|
|
8634
|
+
try {
|
|
8635
|
+
return statSync3(target).isDirectory();
|
|
8636
|
+
} catch {
|
|
8637
|
+
return false;
|
|
8638
|
+
}
|
|
8639
|
+
}
|
|
8640
|
+
function walk2(root) {
|
|
8641
|
+
const files = [];
|
|
8642
|
+
const unscannedLinks = [];
|
|
8643
|
+
const visit = (dir) => {
|
|
8644
|
+
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
8645
|
+
const full = path11.join(dir, entry.name);
|
|
8646
|
+
let followed = null;
|
|
8647
|
+
try {
|
|
8648
|
+
followed = statSync3(full);
|
|
8649
|
+
} catch {
|
|
8650
|
+
followed = null;
|
|
8651
|
+
}
|
|
8652
|
+
if (followed?.isFile()) {
|
|
8653
|
+
files.push(full);
|
|
8654
|
+
} else if (entry.isSymbolicLink()) {
|
|
8655
|
+
unscannedLinks.push(full);
|
|
8656
|
+
} else if (followed?.isDirectory()) {
|
|
8657
|
+
visit(full);
|
|
8658
|
+
}
|
|
8659
|
+
}
|
|
8660
|
+
};
|
|
8661
|
+
visit(root);
|
|
8662
|
+
return { files: files.sort(), unscannedLinks: unscannedLinks.sort() };
|
|
8663
|
+
}
|
|
8664
|
+
function exitCode(result) {
|
|
8665
|
+
if (result.premise.length > 0) return 2;
|
|
8666
|
+
if (result.red.length > 0) return 1;
|
|
8667
|
+
return 0;
|
|
8668
|
+
}
|
|
5742
8669
|
export {
|
|
8670
|
+
ACLASS_EMPTY_WHY,
|
|
8671
|
+
ACLASS_KINDS,
|
|
8672
|
+
ARTIFACT_ROOT,
|
|
8673
|
+
ARTIFACT_SUFFIXES,
|
|
8674
|
+
ARTIFACT_WORDING,
|
|
5743
8675
|
AUTH_INFO_PATH,
|
|
5744
8676
|
BLIND_SPOTS,
|
|
8677
|
+
CHANNELS,
|
|
8678
|
+
CHANNEL_ACLASS,
|
|
8679
|
+
CHANNEL_ARTIFACT,
|
|
8680
|
+
CHANNEL_NO_PROBE,
|
|
8681
|
+
CHANNEL_REGISTERED_IS_ACLASS,
|
|
8682
|
+
CHANNEL_UNREGISTERED,
|
|
8683
|
+
CLASS_REASONS,
|
|
8684
|
+
CLASS_SCOPE,
|
|
5745
8685
|
CliError,
|
|
5746
8686
|
CliErrorCode,
|
|
5747
8687
|
DEFAULT_AGENT_ID,
|
|
5748
8688
|
DEFAULT_SESSION_TTL_SECONDS,
|
|
5749
8689
|
DOCTOR_ANCHORS,
|
|
8690
|
+
ENDPOINT_PREFIXES,
|
|
8691
|
+
ENTRY_SCOPE,
|
|
8692
|
+
ENUM_ELEMENT_KEYS,
|
|
5750
8693
|
EXTENSION_SOURCE_HEADER,
|
|
5751
8694
|
ExitCode,
|
|
8695
|
+
FACTS_REASONS,
|
|
8696
|
+
FOLDED_NOTE,
|
|
8697
|
+
FULLWIDTH_ALNUM_RANGES,
|
|
8698
|
+
GAP_CJK_INSIDE_LONGER_RUN,
|
|
8699
|
+
GAP_NOISE_TOKEN,
|
|
8700
|
+
GAP_REASONS,
|
|
5752
8701
|
GATE_COMPONENTS,
|
|
5753
8702
|
HcmClient,
|
|
8703
|
+
KIT_CARRY_OVER_KEY,
|
|
8704
|
+
KIT_CRAWLED_AT_FILE,
|
|
8705
|
+
LEDGER_MISSING_WORD,
|
|
5754
8706
|
LayoutContainmentError,
|
|
5755
8707
|
MANIFEST_PATH,
|
|
8708
|
+
MAPPING_ARTIFACT_VERSION,
|
|
5756
8709
|
MAX_MANIFEST_PAGES,
|
|
5757
8710
|
MAX_SOURCE_PROBES,
|
|
5758
8711
|
META_PATH,
|
|
@@ -5763,21 +8716,51 @@ export {
|
|
|
5763
8716
|
MINI_REMOTE_ROOT,
|
|
5764
8717
|
MINI_STATE_DIR,
|
|
5765
8718
|
MODELS_PATH,
|
|
8719
|
+
MappingArtifactError,
|
|
8720
|
+
Matcher,
|
|
5766
8721
|
PASSWORD_CHANGE_REQUIRED,
|
|
8722
|
+
PREMISE_REASONS,
|
|
8723
|
+
PREMISE_REGISTRY_REASONS,
|
|
5767
8724
|
PROACTIVE_REFRESH_MARGIN_SECONDS,
|
|
8725
|
+
PROBE_MARK_PREFIX,
|
|
8726
|
+
PRODUCT_RELATIVE_SHAPES,
|
|
8727
|
+
PRODUCT_STATE_WORDS,
|
|
8728
|
+
PROTOCOL_FACTS,
|
|
8729
|
+
REASON_DUPLICATE_TOKEN,
|
|
8730
|
+
REASON_EMPTY,
|
|
8731
|
+
REASON_NO_ENTRIES,
|
|
8732
|
+
REASON_PROBE_ABSENT,
|
|
8733
|
+
REASON_PROBE_SHAPE,
|
|
8734
|
+
REASON_PROBE_UNVERIFIED,
|
|
8735
|
+
REASON_UNREAD,
|
|
8736
|
+
RECON_DIRNAME,
|
|
8737
|
+
RECON_SEGMENTS_BELOW_ROOT,
|
|
8738
|
+
RED_REASONS,
|
|
8739
|
+
REGISTRY_REASONS,
|
|
8740
|
+
REGISTRY_SCOPE,
|
|
8741
|
+
ROUND_EXIT_CODES,
|
|
8742
|
+
ROUND_STAMPED_PRODUCTS,
|
|
8743
|
+
ROUND_STATES,
|
|
8744
|
+
ROUND_SUBCOMMAND,
|
|
5768
8745
|
ReconLayout,
|
|
5769
8746
|
RefStore,
|
|
8747
|
+
SCOPES,
|
|
5770
8748
|
SDK_VERSION,
|
|
8749
|
+
SEGMENT_SEPARATOR,
|
|
8750
|
+
TargetModelNotFound,
|
|
5771
8751
|
TokenStore,
|
|
5772
8752
|
VERSION_PATH,
|
|
5773
8753
|
WsClient,
|
|
5774
8754
|
absoluteDownloadUrl,
|
|
5775
8755
|
adminResetPassword,
|
|
5776
8756
|
archiveDir,
|
|
8757
|
+
artifactReason,
|
|
5777
8758
|
assertSafeReferencePath,
|
|
5778
8759
|
assertSafeSkillFilePath,
|
|
5779
8760
|
assertSafeSkillId,
|
|
8761
|
+
baselineRoundPremise,
|
|
5780
8762
|
bootstrapTenant,
|
|
8763
|
+
boundedFind,
|
|
5781
8764
|
buildAnswer,
|
|
5782
8765
|
buildConfirm,
|
|
5783
8766
|
buildInterrupt,
|
|
@@ -5789,12 +8772,18 @@ export {
|
|
|
5789
8772
|
buildSteer,
|
|
5790
8773
|
camelizeKeys,
|
|
5791
8774
|
changePassword,
|
|
8775
|
+
checkGaps,
|
|
8776
|
+
checkPremise,
|
|
8777
|
+
checkReds,
|
|
8778
|
+
cjkRunAt,
|
|
5792
8779
|
classifyMessage,
|
|
5793
8780
|
clearMetaCache,
|
|
5794
8781
|
clearModelCache,
|
|
5795
8782
|
compareFingerprint,
|
|
5796
8783
|
computeFingerprint,
|
|
8784
|
+
computeReconSourceKey,
|
|
5797
8785
|
conversationStateFile,
|
|
8786
|
+
covered,
|
|
5798
8787
|
create,
|
|
5799
8788
|
createConversation,
|
|
5800
8789
|
createHttpClient,
|
|
@@ -5807,6 +8796,7 @@ export {
|
|
|
5807
8796
|
deleteIdentity,
|
|
5808
8797
|
deleteTenantMeta,
|
|
5809
8798
|
deleteWorkspaceFile,
|
|
8799
|
+
deriveAclass,
|
|
5810
8800
|
deriveNamelessToolLabel,
|
|
5811
8801
|
describePrincipal,
|
|
5812
8802
|
detectDelegationPause,
|
|
@@ -5814,12 +8804,15 @@ export {
|
|
|
5814
8804
|
detectPasswordChangeChallenge,
|
|
5815
8805
|
displayWidth,
|
|
5816
8806
|
downloadDocument,
|
|
8807
|
+
endpointsIn,
|
|
5817
8808
|
ensureSessionFresh,
|
|
5818
8809
|
envDir,
|
|
5819
8810
|
envFile,
|
|
5820
8811
|
envsDir,
|
|
8812
|
+
exitCode,
|
|
5821
8813
|
exitCodeFor,
|
|
5822
8814
|
extractNextSteps,
|
|
8815
|
+
factsIndex,
|
|
5823
8816
|
fetchConversationTimeline,
|
|
5824
8817
|
fetchConversationTimelineStrict,
|
|
5825
8818
|
fetchRecentConversations,
|
|
@@ -5831,6 +8824,8 @@ export {
|
|
|
5831
8824
|
filterByStage,
|
|
5832
8825
|
findLastAssistantSeq,
|
|
5833
8826
|
flattenSkills,
|
|
8827
|
+
foldConfusables,
|
|
8828
|
+
foldedNoteFor,
|
|
5834
8829
|
formatMiniVerifyReport,
|
|
5835
8830
|
formatObject,
|
|
5836
8831
|
formatRows,
|
|
@@ -5846,9 +8841,12 @@ export {
|
|
|
5846
8841
|
identitiesDir,
|
|
5847
8842
|
identityDir,
|
|
5848
8843
|
identityMetaFile,
|
|
8844
|
+
inGap,
|
|
5849
8845
|
inferEnvName,
|
|
5850
8846
|
inferMiniWritableTemplateOptionsFromModel,
|
|
5851
8847
|
initMiniAppProject,
|
|
8848
|
+
inspectRound,
|
|
8849
|
+
isCjk,
|
|
5852
8850
|
isDelegationToolName,
|
|
5853
8851
|
isServerSlidingSession,
|
|
5854
8852
|
listEnvs,
|
|
@@ -5861,6 +8859,7 @@ export {
|
|
|
5861
8859
|
loadGlobalConfig,
|
|
5862
8860
|
loadIdentity,
|
|
5863
8861
|
loadProfile,
|
|
8862
|
+
loadProtocolFacts,
|
|
5864
8863
|
loadWorkspaceFileContent,
|
|
5865
8864
|
loginClientCredentials,
|
|
5866
8865
|
loginPairing,
|
|
@@ -5871,6 +8870,8 @@ export {
|
|
|
5871
8870
|
modelEntries,
|
|
5872
8871
|
modelKeys,
|
|
5873
8872
|
needsRefresh,
|
|
8873
|
+
newRoundId,
|
|
8874
|
+
normalize,
|
|
5874
8875
|
normalizeReferences,
|
|
5875
8876
|
oneShot,
|
|
5876
8877
|
parseConfirmToolName,
|
|
@@ -5887,10 +8888,12 @@ export {
|
|
|
5887
8888
|
profileFile,
|
|
5888
8889
|
pullMiniAppProject,
|
|
5889
8890
|
pushMiniAppProject,
|
|
8891
|
+
readBusinessKeyFields,
|
|
5890
8892
|
readMiniContext,
|
|
5891
8893
|
readPullManifest,
|
|
5892
8894
|
refreshToken,
|
|
5893
8895
|
remove,
|
|
8896
|
+
renderRound,
|
|
5894
8897
|
replHistoryFile,
|
|
5895
8898
|
resetSettingItem,
|
|
5896
8899
|
resolveActiveEnv,
|
|
@@ -5900,9 +8903,11 @@ export {
|
|
|
5900
8903
|
resolveChironBase,
|
|
5901
8904
|
resolveRefs,
|
|
5902
8905
|
resolveSkillInstallOrder,
|
|
8906
|
+
roundAge,
|
|
5903
8907
|
runDoctor,
|
|
5904
8908
|
runImport,
|
|
5905
8909
|
runMiniSmokeChecks,
|
|
8910
|
+
runRecon,
|
|
5906
8911
|
safeSegment,
|
|
5907
8912
|
saveConversationState,
|
|
5908
8913
|
saveEnv,
|
|
@@ -5911,6 +8916,8 @@ export {
|
|
|
5911
8916
|
saveProfile,
|
|
5912
8917
|
saveTenantMeta,
|
|
5913
8918
|
saveWorkspaceFileContent,
|
|
8919
|
+
scan,
|
|
8920
|
+
segmentsOf,
|
|
5914
8921
|
sendMessageAndStream,
|
|
5915
8922
|
snakeToCamel,
|
|
5916
8923
|
tenantSegment,
|
|
@@ -5920,6 +8927,7 @@ export {
|
|
|
5920
8927
|
toTable,
|
|
5921
8928
|
toYaml,
|
|
5922
8929
|
truncateDisplay,
|
|
8930
|
+
unguardedKinds,
|
|
5923
8931
|
update,
|
|
5924
8932
|
uploadDocument,
|
|
5925
8933
|
validateMiniProject
|