@ixo/editor 6.27.1 → 6.28.0
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/action-manifest.json +207 -0
- package/dist/{chunk-XLGG4VDV.js → chunk-SYEWBPNQ.js} +7699 -2234
- package/dist/chunk-SYEWBPNQ.js.map +1 -0
- package/dist/{chunk-WGCOGBWG.js → chunk-TT4TNNOV.js} +1930 -2
- package/dist/chunk-TT4TNNOV.js.map +1 -0
- package/dist/{chunk-ZGFQSAL7.js → chunk-Y6M7ODSU.js} +2 -2
- package/dist/core/index.d.ts +4 -4
- package/dist/core/index.js +2 -2
- package/dist/{graphql-client-pPdj3dsK.d.ts → graphql-client-zdKXQ8cz.d.ts} +1 -1
- package/dist/{index-CfkeGZGU.d.ts → index-BCR-Vx-S.d.ts} +855 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/mantine/index.d.ts +3 -3
- package/dist/mantine/index.js +2 -2
- package/dist/{store-CzML9Czj.d.ts → store-CuK7u-3u.d.ts} +1 -1
- package/package.json +2 -1
- package/dist/chunk-WGCOGBWG.js.map +0 -1
- package/dist/chunk-XLGG4VDV.js.map +0 -1
- /package/dist/{chunk-ZGFQSAL7.js.map → chunk-Y6M7ODSU.js.map} +0 -0
|
@@ -451,6 +451,10 @@ function didToMatrixUserId(did, homeserver) {
|
|
|
451
451
|
const localpart = did.replace(/:/g, "-");
|
|
452
452
|
return `@${localpart}:${homeserver}`;
|
|
453
453
|
}
|
|
454
|
+
function matrixUserIdToDid(userId) {
|
|
455
|
+
const localpart = userId.startsWith("@") ? userId.slice(1).split(":")[0] : userId;
|
|
456
|
+
return localpart.startsWith("did-") ? localpart.replace(/-/g, ":") : localpart;
|
|
457
|
+
}
|
|
454
458
|
async function findOrCreateDMRoom(matrixClient, targetUserId) {
|
|
455
459
|
try {
|
|
456
460
|
const directEvent = matrixClient.getAccountData("m.direct");
|
|
@@ -903,7 +907,12 @@ var SERVICE_GROUP_REQUIRED_HANDLERS = {
|
|
|
903
907
|
],
|
|
904
908
|
collectionUsers: ["collectionUsers.grant", "collectionUsers.revoke", "collectionUsers.list", "collectionUsers.classifyAddress", "collectionUsers.enumerateMembers"],
|
|
905
909
|
carbon: ["carbon.loadBatches", "carbon.harvest", "carbon.retire"],
|
|
906
|
-
kyc: ["kycLoadForm", "kycInitiate", "kycGetVerificationUrl", "kycGetStatus", "kycSaveCredential"]
|
|
910
|
+
kyc: ["kycLoadForm", "kycInitiate", "kycGetVerificationUrl", "kycGetStatus", "kycSaveCredential"],
|
|
911
|
+
evalRegister: ["requestPin", "mintBotDelegation", "depositDelegation", "registerEvalCollection"],
|
|
912
|
+
// Rubric publishing (qi/eval.rubric). `pinRubric` is best-effort (run() guards it), so it is not
|
|
913
|
+
// required for the group to be usable — `uploadRubric` (store the bytes) and `getRubricSchema`
|
|
914
|
+
// (the pre-publish shape check) are.
|
|
915
|
+
rubric: ["uploadRubric", "getRubricSchema"]
|
|
907
916
|
};
|
|
908
917
|
function getHandlerAtPath(handlers, path) {
|
|
909
918
|
return path.split(".").reduce((acc, key) => acc == null ? void 0 : acc[key], handlers);
|
|
@@ -1065,6 +1074,36 @@ function buildServicesFromHandlers(handlers) {
|
|
|
1065
1074
|
getVerificationUrl: async (params) => handlers.kycGetVerificationUrl(params),
|
|
1066
1075
|
getStatus: async (params) => handlers.kycGetStatus(params),
|
|
1067
1076
|
saveCredential: async (params) => handlers.kycSaveCredential(params)
|
|
1077
|
+
} : void 0,
|
|
1078
|
+
// Evaluation-Engine enrollment service (qi/eval.register — spec §6). All
|
|
1079
|
+
// signing + HTTP live host-side: `mintBotDelegation` (owns the oracle-DID
|
|
1080
|
+
// default + the claim-bot AND subscriptions-read capabilities + signing —
|
|
1081
|
+
// see EvalRegisterService), `depositDelegation` (UCAN Store
|
|
1082
|
+
// store/add), `registerEvalCollection` (engine). The core run() proof-guards
|
|
1083
|
+
// every result, so we only forward calls here.
|
|
1084
|
+
evalRegister: handlers?.requestPin && handlers?.mintBotDelegation && handlers?.depositDelegation && handlers?.registerEvalCollection ? {
|
|
1085
|
+
requestPin: async (config) => handlers.requestPin(config),
|
|
1086
|
+
mintBotDelegation: async (params) => handlers.mintBotDelegation(params),
|
|
1087
|
+
depositDelegation: async (params) => handlers.depositDelegation(params),
|
|
1088
|
+
registerCollection: async (params) => handlers.registerEvalCollection(params)
|
|
1089
|
+
} : void 0,
|
|
1090
|
+
// Rubric publishing service (qi/eval.rubric — spec §B.2/§C). Host-side: `uploadRubric` (Matrix
|
|
1091
|
+
// media store, returns the content hash used as the id), `getRubricSchema` (fetch the engine's
|
|
1092
|
+
// served JSON Schema for the pre-publish shape check), `pinRubric` (best-effort engine link).
|
|
1093
|
+
// The core run() forwards to these and proof-guards its result. Gated on `uploadRubric` so a
|
|
1094
|
+
// host that hasn't wired the group yet leaves the block cleanly disabled rather than crashing.
|
|
1095
|
+
rubric: handlers?.uploadRubric ? {
|
|
1096
|
+
uploadRubric: async (params) => handlers.uploadRubric(params),
|
|
1097
|
+
...handlers.getRubricSchema ? { getRubricSchema: async (url) => handlers.getRubricSchema(url) } : {},
|
|
1098
|
+
...handlers.pinRubric ? { pinRubric: async (params) => handlers.pinRubric(params) } : {},
|
|
1099
|
+
// §C.7 pre-publish dry run against the engine (schema + live form bind + settings); optional.
|
|
1100
|
+
...handlers.previewRubric ? { previewRubric: async (params) => handlers.previewRubric(params) } : {},
|
|
1101
|
+
// Best-effort live test of a §A.14 external source (the builder's Test button); optional, like pinRubric.
|
|
1102
|
+
...handlers.testExternalSource ? { testExternalSource: async (params) => handlers.testExternalSource(params) } : {},
|
|
1103
|
+
// Public did:web resolution from a source's endpoint (auto-fills its audience); optional.
|
|
1104
|
+
...handlers.resolveSourceDid ? { resolveSourceDid: async (params) => handlers.resolveSourceDid(params) } : {},
|
|
1105
|
+
// The entity's existing #rub resource, so a republish can delete+add in one tx; optional.
|
|
1106
|
+
...handlers.getRubricResource ? { getRubricResource: async (params) => handlers.getRubricResource(params) } : {}
|
|
1068
1107
|
} : void 0
|
|
1069
1108
|
};
|
|
1070
1109
|
}
|
|
@@ -8839,6 +8878,1839 @@ registerAction({
|
|
|
8839
8878
|
}
|
|
8840
8879
|
});
|
|
8841
8880
|
|
|
8881
|
+
// src/core/lib/actionRegistry/actions/evalRegister/description.ts
|
|
8882
|
+
var REPEAT_SUBMISSIONS_OPTIONS = [
|
|
8883
|
+
{ value: "normal", label: "Yes, that's normal", sentence: "Repeat submissions from the same person are expected and normal here." },
|
|
8884
|
+
{ value: "sometimes", label: "Sometimes", sentence: "Repeat submissions from the same person happen but are not the norm." },
|
|
8885
|
+
{ value: "once", label: "No, only once ever", sentence: "Each claim should be a distinct one-off; repeat submissions are unusual here." }
|
|
8886
|
+
];
|
|
8887
|
+
var DIFFERENT_WHEN_OPTIONS = [
|
|
8888
|
+
{ value: "date", label: "The date", phrase: "the date" },
|
|
8889
|
+
{ value: "amount", label: "The amount or weight", phrase: "the amount or weight" },
|
|
8890
|
+
{ value: "place", label: "The place", phrase: "the place" },
|
|
8891
|
+
{ value: "kind", label: "The type or kind", phrase: "the type or kind" },
|
|
8892
|
+
{ value: "photos", label: "The photos", phrase: "the photos" }
|
|
8893
|
+
];
|
|
8894
|
+
var WHAT_IS_COLLECTED_MAX = 300;
|
|
8895
|
+
var DIFFERENT_WHEN_OTHER_MAX = 100;
|
|
8896
|
+
function normalizeDifferentWhen(value) {
|
|
8897
|
+
if (!Array.isArray(value)) return [];
|
|
8898
|
+
return DIFFERENT_WHEN_OPTIONS.filter((option) => value.includes(option.value)).map((option) => option.value);
|
|
8899
|
+
}
|
|
8900
|
+
function normalizeRepeatSubmissions(value) {
|
|
8901
|
+
return REPEAT_SUBMISSIONS_OPTIONS.find((option) => option.value === value)?.value;
|
|
8902
|
+
}
|
|
8903
|
+
function composeCollectionDescription(answers) {
|
|
8904
|
+
const lines = [];
|
|
8905
|
+
const whatIsCollected = String(answers.whatIsCollected || "").trim().slice(0, WHAT_IS_COLLECTED_MAX);
|
|
8906
|
+
if (whatIsCollected) lines.push(`What is collected: ${whatIsCollected}`);
|
|
8907
|
+
const repeat = REPEAT_SUBMISSIONS_OPTIONS.find((option) => option.value === answers.repeatSubmissions);
|
|
8908
|
+
if (repeat) lines.push(`Repeat submissions: ${repeat.sentence}`);
|
|
8909
|
+
const ticked = normalizeDifferentWhen(answers.differentWhen);
|
|
8910
|
+
const differences = DIFFERENT_WHEN_OPTIONS.filter((option) => ticked.includes(option.value)).map((option) => option.phrase);
|
|
8911
|
+
const other = String(answers.differentWhenOther || "").trim().slice(0, DIFFERENT_WHEN_OTHER_MAX);
|
|
8912
|
+
if (other) differences.push(other);
|
|
8913
|
+
if (differences.length) lines.push(`Two claims are different when: any of these differ \u2014 ${differences.join(", ")}.`);
|
|
8914
|
+
return lines.length ? lines.join("\n") : void 0;
|
|
8915
|
+
}
|
|
8916
|
+
|
|
8917
|
+
// src/core/lib/actionRegistry/actions/evalRegister/evalRegister.ts
|
|
8918
|
+
async function runEvalRegister(inputs, ctx) {
|
|
8919
|
+
const service = ctx.services.evalRegister;
|
|
8920
|
+
if (!service) {
|
|
8921
|
+
throw new Error("evalRegister service not configured (ctx.services.evalRegister is undefined)");
|
|
8922
|
+
}
|
|
8923
|
+
const collectionId = String(inputs.collectionId || "").trim();
|
|
8924
|
+
const deedDid = String(inputs.deedDid || "").trim();
|
|
8925
|
+
const displayName = String(inputs.displayName || "").trim().slice(0, 120) || void 0;
|
|
8926
|
+
const notifyEmail = String(inputs.notifyEmail || "").trim();
|
|
8927
|
+
if (!notifyEmail) {
|
|
8928
|
+
throw new Error(
|
|
8929
|
+
"A billing email is needed: if a payment fails or your evaluations pause, that address is where we tell you. Add it in the block settings \u2014 it is filled from your account when we can read it."
|
|
8930
|
+
);
|
|
8931
|
+
}
|
|
8932
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(notifyEmail)) {
|
|
8933
|
+
throw new Error(`"${notifyEmail}" is not a valid email address \u2014 fix the billing email in the block settings.`);
|
|
8934
|
+
}
|
|
8935
|
+
const description = composeCollectionDescription(inputs);
|
|
8936
|
+
const ownerDid = String(inputs.ownerDid || ctx.actorDid || "").trim();
|
|
8937
|
+
const oracleDid = String(inputs.oracleDid || "").trim() || void 0;
|
|
8938
|
+
const ttlDays = typeof inputs.ttlDays === "number" && inputs.ttlDays > 0 ? inputs.ttlDays : void 0;
|
|
8939
|
+
const ucanStoreUrl = String(inputs.ucanStoreUrl || "").trim() || void 0;
|
|
8940
|
+
const evalEngineUrl = String(inputs.evalEngineUrl || "").trim() || void 0;
|
|
8941
|
+
const rubricId = String(inputs.rubricId || "").trim() || void 0;
|
|
8942
|
+
const allowAiChecks = inputs.allowAiChecks !== false;
|
|
8943
|
+
const allowImageChecks = allowAiChecks && inputs.allowImageChecks !== false;
|
|
8944
|
+
const allowChainEvaluation = inputs.allowChainEvaluation !== false;
|
|
8945
|
+
if (!collectionId) throw new Error("collectionId is required");
|
|
8946
|
+
if (!deedDid) throw new Error("deedDid (entity/deed DID) is required");
|
|
8947
|
+
if (!ownerDid) throw new Error("ownerDid is required (pass it explicitly, or run as the collection owner)");
|
|
8948
|
+
let pin = String(inputs.pin || "").trim();
|
|
8949
|
+
if (!pin) {
|
|
8950
|
+
pin = await service.requestPin({
|
|
8951
|
+
title: "Enable Evaluation Engine",
|
|
8952
|
+
description: "Enter your PIN to authorize the evaluation engine for this collection",
|
|
8953
|
+
submitText: "Authorize"
|
|
8954
|
+
});
|
|
8955
|
+
}
|
|
8956
|
+
if (!pin) throw new Error("PIN is required to authorize enrollment");
|
|
8957
|
+
const minted = await service.mintBotDelegation({ oracleDid, ttlDays, pin, evalEngineUrl });
|
|
8958
|
+
const delegationToken = String(minted?.token || "").trim();
|
|
8959
|
+
if (!delegationToken) {
|
|
8960
|
+
throw new Error("mintBotDelegation returned no token. The owner\u2192oracle delegation was not created.");
|
|
8961
|
+
}
|
|
8962
|
+
const deposited = await service.depositDelegation({
|
|
8963
|
+
token: delegationToken,
|
|
8964
|
+
note: `eval-engine enrollment: collection ${collectionId}`,
|
|
8965
|
+
ucanStoreUrl
|
|
8966
|
+
});
|
|
8967
|
+
const depositCid = String(deposited?.cid || "").trim();
|
|
8968
|
+
if (!depositCid) {
|
|
8969
|
+
throw new Error("depositDelegation returned no cid. The delegation was not stored in the UCAN Store.");
|
|
8970
|
+
}
|
|
8971
|
+
const registration = await service.registerCollection({
|
|
8972
|
+
collectionId,
|
|
8973
|
+
deedDid,
|
|
8974
|
+
ownerDid,
|
|
8975
|
+
rubricId,
|
|
8976
|
+
evalEngineUrl,
|
|
8977
|
+
displayName,
|
|
8978
|
+
notifyEmail,
|
|
8979
|
+
// Spread, not `description,`: the key must be genuinely ABSENT when nothing was answered
|
|
8980
|
+
// (see above), and a host that copies `params` field-by-field would otherwise forward an
|
|
8981
|
+
// explicit `undefined` as the erasing empty value.
|
|
8982
|
+
...description !== void 0 ? { description } : {},
|
|
8983
|
+
settings: { allowAiChecks, allowImageChecks, allowChainEvaluation }
|
|
8984
|
+
});
|
|
8985
|
+
const registrationId = String(registration?.id || "").trim();
|
|
8986
|
+
if (!registrationId) {
|
|
8987
|
+
throw new Error("registerCollection returned no id. The collection was not registered with the evaluation engine.");
|
|
8988
|
+
}
|
|
8989
|
+
let evaluateAuthzTxHash;
|
|
8990
|
+
if (allowChainEvaluation) {
|
|
8991
|
+
const granterAdminAddress = String(inputs.adminAddress || "").trim();
|
|
8992
|
+
if (!granterAdminAddress) {
|
|
8993
|
+
throw new Error("adminAddress (entity admin account) is required to grant the engine evaluator rights for on-chain decisions.");
|
|
8994
|
+
}
|
|
8995
|
+
const granteeDid = oracleDid || String(minted?.oracleDid || "").trim();
|
|
8996
|
+
const oracleAddress = String(inputs.oracleAddress || "").trim() || (granteeDid.startsWith("did:ixo:") ? granteeDid.slice("did:ixo:".length) : "");
|
|
8997
|
+
if (!oracleAddress) {
|
|
8998
|
+
throw new Error("The engine address is unknown: pass oracleAddress, or an oracleDid of the form did:ixo:<address> (hosts can echo it from mintBotDelegation).");
|
|
8999
|
+
}
|
|
9000
|
+
const grantService = ctx.services.collectionUsers;
|
|
9001
|
+
if (!grantService) throw new Error("collectionUsers service not configured (needed for the evaluate-authz grant)");
|
|
9002
|
+
const grant = await grantService.grant({
|
|
9003
|
+
granterAdminAddress,
|
|
9004
|
+
granteeAddress: oracleAddress,
|
|
9005
|
+
collectionId,
|
|
9006
|
+
role: "evaluate",
|
|
9007
|
+
// The engine acts on every claim in the collection — the chain rejects a 0
|
|
9008
|
+
// quota, so use the effectively-unlimited value the portal already grants
|
|
9009
|
+
// its subscription oracles.
|
|
9010
|
+
agentQuota: "9999999",
|
|
9011
|
+
// The cap on what the engine may pay out per claim (`maxCustomAmount`), in
|
|
9012
|
+
// BASE units and the owner's chosen denoms — the config UI collects them,
|
|
9013
|
+
// this never picks one. An EMPTY cap rejects any evaluation that carries
|
|
9014
|
+
// coins, and the engine echoes an ordinary claim's own amount when it
|
|
9015
|
+
// approves it (intent claims carry none — the escrow pays).
|
|
9016
|
+
maxAmount: Array.isArray(inputs.evaluateMaxAmount) && inputs.evaluateMaxAmount.length > 0 ? inputs.evaluateMaxAmount : void 0,
|
|
9017
|
+
deedDid
|
|
9018
|
+
});
|
|
9019
|
+
evaluateAuthzTxHash = String(grant?.transactionHash || "").trim() || void 0;
|
|
9020
|
+
if (!evaluateAuthzTxHash) {
|
|
9021
|
+
throw new Error("evaluate grant returned no transactionHash. The engine cannot submit decisions on chain \u2014 retry the registration.");
|
|
9022
|
+
}
|
|
9023
|
+
}
|
|
9024
|
+
return {
|
|
9025
|
+
output: {
|
|
9026
|
+
registrationId,
|
|
9027
|
+
collectionId,
|
|
9028
|
+
ownerDid,
|
|
9029
|
+
deedDid,
|
|
9030
|
+
depositCid,
|
|
9031
|
+
...oracleDid ? { oracleDid } : {},
|
|
9032
|
+
...evaluateAuthzTxHash ? { evaluateAuthzTxHash } : {}
|
|
9033
|
+
}
|
|
9034
|
+
};
|
|
9035
|
+
}
|
|
9036
|
+
|
|
9037
|
+
// src/core/lib/actionRegistry/actions/evalRubric/canonical.ts
|
|
9038
|
+
function canonicalJson(value) {
|
|
9039
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") {
|
|
9040
|
+
return JSON.stringify(value);
|
|
9041
|
+
}
|
|
9042
|
+
if (typeof value === "number") {
|
|
9043
|
+
if (!Number.isFinite(value)) throw new Error(`canonicalJson: non-finite number ${value}`);
|
|
9044
|
+
return JSON.stringify(value);
|
|
9045
|
+
}
|
|
9046
|
+
if (Array.isArray(value)) {
|
|
9047
|
+
return `[${value.map((item) => item === void 0 ? "null" : canonicalJson(item)).join(",")}]`;
|
|
9048
|
+
}
|
|
9049
|
+
if (typeof value === "object") {
|
|
9050
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`);
|
|
9051
|
+
return `{${entries.join(",")}}`;
|
|
9052
|
+
}
|
|
9053
|
+
throw new Error(`canonicalJson: unsupported value of type ${typeof value}`);
|
|
9054
|
+
}
|
|
9055
|
+
|
|
9056
|
+
// src/core/lib/actionRegistry/actions/evalRubric/fieldCatalog.ts
|
|
9057
|
+
var SEGMENT = /^[A-Za-z0-9_-]+$/;
|
|
9058
|
+
function scalarKind(type, inputType) {
|
|
9059
|
+
switch (type) {
|
|
9060
|
+
case "text":
|
|
9061
|
+
if (inputType === "number") return "number";
|
|
9062
|
+
if (inputType === "date" || inputType === "datetime-local") return "date";
|
|
9063
|
+
return "text";
|
|
9064
|
+
case "comment":
|
|
9065
|
+
return "text";
|
|
9066
|
+
case "rating":
|
|
9067
|
+
case "slider":
|
|
9068
|
+
return "number";
|
|
9069
|
+
case "dropdown":
|
|
9070
|
+
case "radiogroup":
|
|
9071
|
+
return "choice";
|
|
9072
|
+
case "checkbox":
|
|
9073
|
+
case "tagbox":
|
|
9074
|
+
case "ranking":
|
|
9075
|
+
return "multichoice";
|
|
9076
|
+
case "boolean":
|
|
9077
|
+
return "boolean";
|
|
9078
|
+
case "file":
|
|
9079
|
+
return "file";
|
|
9080
|
+
case "signaturepad":
|
|
9081
|
+
return "signature";
|
|
9082
|
+
case "geopoint":
|
|
9083
|
+
return "geo";
|
|
9084
|
+
default:
|
|
9085
|
+
return void 0;
|
|
9086
|
+
}
|
|
9087
|
+
}
|
|
9088
|
+
function normalizeChoices(raw) {
|
|
9089
|
+
if (!Array.isArray(raw)) return [];
|
|
9090
|
+
const out = [];
|
|
9091
|
+
for (const entry of raw) {
|
|
9092
|
+
let value = "";
|
|
9093
|
+
let text = "";
|
|
9094
|
+
if (typeof entry === "string" || typeof entry === "number") {
|
|
9095
|
+
value = String(entry);
|
|
9096
|
+
text = String(entry);
|
|
9097
|
+
} else if (entry && typeof entry === "object") {
|
|
9098
|
+
const obj = entry;
|
|
9099
|
+
if (obj.value === void 0 || obj.value === null) continue;
|
|
9100
|
+
value = String(obj.value);
|
|
9101
|
+
text = obj.text === void 0 || obj.text === null ? value : stripHtml2(String(obj.text));
|
|
9102
|
+
} else {
|
|
9103
|
+
continue;
|
|
9104
|
+
}
|
|
9105
|
+
out.push({ value, text });
|
|
9106
|
+
}
|
|
9107
|
+
return out;
|
|
9108
|
+
}
|
|
9109
|
+
function segmentChoices(raw) {
|
|
9110
|
+
return normalizeChoices(raw).filter((choice) => SEGMENT.test(choice.value));
|
|
9111
|
+
}
|
|
9112
|
+
function innerField(el, path, fallbackKind) {
|
|
9113
|
+
const type = typeof el.cellType === "string" && el.cellType ? el.cellType : typeof el.type === "string" ? el.type : "";
|
|
9114
|
+
const kind = scalarKind(type, el.inputType) ?? fallbackKind;
|
|
9115
|
+
if (!kind) return void 0;
|
|
9116
|
+
const choices = kind === "choice" || kind === "multichoice" ? normalizeChoices(el.choices) : void 0;
|
|
9117
|
+
return {
|
|
9118
|
+
path,
|
|
9119
|
+
kind,
|
|
9120
|
+
title: titleOf(el),
|
|
9121
|
+
isRequired: el.isRequired === true,
|
|
9122
|
+
...choices && choices.length > 0 ? { choices } : {}
|
|
9123
|
+
};
|
|
9124
|
+
}
|
|
9125
|
+
function extractRubricFieldCatalog(surveyTemplate, proof = "") {
|
|
9126
|
+
const root = surveyTemplate?.question ?? surveyTemplate;
|
|
9127
|
+
if (!root || typeof root !== "object") return { fields: [], proof };
|
|
9128
|
+
const fields = [];
|
|
9129
|
+
const seen2 = /* @__PURE__ */ new Set();
|
|
9130
|
+
const walk = (elements) => {
|
|
9131
|
+
if (!Array.isArray(elements)) return;
|
|
9132
|
+
for (const el of elements) {
|
|
9133
|
+
if (!el || typeof el !== "object") continue;
|
|
9134
|
+
if (el.type === "panel" && Array.isArray(el.elements)) {
|
|
9135
|
+
walk(el.elements);
|
|
9136
|
+
continue;
|
|
9137
|
+
}
|
|
9138
|
+
const name = typeof el.name === "string" ? el.name.trim() : "";
|
|
9139
|
+
const type = typeof el.type === "string" ? el.type : "";
|
|
9140
|
+
if (!name || !SEGMENT.test(name) || seen2.has(name) || type === "html" || type === "expression") continue;
|
|
9141
|
+
const field = extractQuestion(el, name, type);
|
|
9142
|
+
if (!field) continue;
|
|
9143
|
+
seen2.add(name);
|
|
9144
|
+
fields.push(field);
|
|
9145
|
+
}
|
|
9146
|
+
};
|
|
9147
|
+
if (Array.isArray(root.pages) && root.pages.length > 0) {
|
|
9148
|
+
for (const page of root.pages) walk(page.elements);
|
|
9149
|
+
}
|
|
9150
|
+
walk(root.elements);
|
|
9151
|
+
return { fields, proof };
|
|
9152
|
+
}
|
|
9153
|
+
function extractQuestion(el, name, type) {
|
|
9154
|
+
const base = { path: `$${name}`, title: titleOf(el), isRequired: el.isRequired === true };
|
|
9155
|
+
if (type === "imagepicker") {
|
|
9156
|
+
return { ...base, kind: el.multiSelect === true ? "multichoice" : "choice", choices: normalizeChoices(el.choices) };
|
|
9157
|
+
}
|
|
9158
|
+
const scalar = scalarKind(type, el.inputType);
|
|
9159
|
+
if (scalar) {
|
|
9160
|
+
const choices = scalar === "choice" || scalar === "multichoice" ? normalizeChoices(el.choices) : void 0;
|
|
9161
|
+
return { ...base, kind: scalar, ...choices && choices.length > 0 ? { choices } : {} };
|
|
9162
|
+
}
|
|
9163
|
+
switch (type) {
|
|
9164
|
+
case "matrix": {
|
|
9165
|
+
const rows = segmentChoices(el.rows);
|
|
9166
|
+
const choices = normalizeChoices(el.columns);
|
|
9167
|
+
const columns = rows.map(
|
|
9168
|
+
(row) => ({ path: `$${name}.${row.value}`, kind: "choice", title: `${base.title} \u2014 ${row.text}`, isRequired: base.isRequired, choices })
|
|
9169
|
+
);
|
|
9170
|
+
return { ...base, kind: "keyed_choice", rows, choices, columns };
|
|
9171
|
+
}
|
|
9172
|
+
case "matrixdropdown": {
|
|
9173
|
+
const rows = segmentChoices(el.rows);
|
|
9174
|
+
const columns = [];
|
|
9175
|
+
for (const rawCol of Array.isArray(el.columns) ? el.columns : []) {
|
|
9176
|
+
const col = rawCol;
|
|
9177
|
+
const colName = typeof col?.name === "string" ? col.name.trim() : "";
|
|
9178
|
+
if (!colName || !SEGMENT.test(colName)) continue;
|
|
9179
|
+
for (const row of rows) {
|
|
9180
|
+
const cell = innerField(col, `$${name}.${row.value}.${colName}`, "choice");
|
|
9181
|
+
if (cell) columns.push({ ...cell, title: `${base.title} \u2014 ${row.text} / ${titleOf(col) || colName}` });
|
|
9182
|
+
}
|
|
9183
|
+
}
|
|
9184
|
+
return { ...base, kind: "cells", rows, columns };
|
|
9185
|
+
}
|
|
9186
|
+
case "matrixdynamic": {
|
|
9187
|
+
const columns = [];
|
|
9188
|
+
for (const rawCol of Array.isArray(el.columns) ? el.columns : []) {
|
|
9189
|
+
const col = rawCol;
|
|
9190
|
+
const colName = typeof col?.name === "string" ? col.name.trim() : "";
|
|
9191
|
+
if (!colName || !SEGMENT.test(colName)) continue;
|
|
9192
|
+
const inner = innerField(col, `$${name}[*].${colName}`, "choice");
|
|
9193
|
+
if (inner) columns.push(inner);
|
|
9194
|
+
}
|
|
9195
|
+
return { ...base, kind: "rows", columns };
|
|
9196
|
+
}
|
|
9197
|
+
case "paneldynamic": {
|
|
9198
|
+
const columns = [];
|
|
9199
|
+
const walkTemplate = (elements) => {
|
|
9200
|
+
if (!Array.isArray(elements)) return;
|
|
9201
|
+
for (const child of elements) {
|
|
9202
|
+
if (!child || typeof child !== "object") continue;
|
|
9203
|
+
if (child.type === "panel" && Array.isArray(child.elements)) {
|
|
9204
|
+
walkTemplate(child.elements);
|
|
9205
|
+
continue;
|
|
9206
|
+
}
|
|
9207
|
+
const childName = typeof child.name === "string" ? child.name.trim() : "";
|
|
9208
|
+
const childType = typeof child.type === "string" ? child.type : "";
|
|
9209
|
+
if (!childName || !SEGMENT.test(childName) || childType === "html" || childType === "expression") continue;
|
|
9210
|
+
const inner = innerField(child, `$${name}[*].${childName}`);
|
|
9211
|
+
if (inner) columns.push(inner);
|
|
9212
|
+
}
|
|
9213
|
+
};
|
|
9214
|
+
walkTemplate(el.templateElements);
|
|
9215
|
+
return { ...base, kind: "rows", columns };
|
|
9216
|
+
}
|
|
9217
|
+
case "multipletext": {
|
|
9218
|
+
const columns = [];
|
|
9219
|
+
for (const rawItem of Array.isArray(el.items) ? el.items : []) {
|
|
9220
|
+
const item = rawItem;
|
|
9221
|
+
const itemName = typeof item?.name === "string" ? item.name.trim() : "";
|
|
9222
|
+
if (!itemName || !SEGMENT.test(itemName)) continue;
|
|
9223
|
+
columns.push({ path: `$${name}.${itemName}`, kind: "text", title: titleOf(item) || humanize2(itemName), isRequired: item.isRequired === true });
|
|
9224
|
+
}
|
|
9225
|
+
return { ...base, kind: "keyed_text", columns };
|
|
9226
|
+
}
|
|
9227
|
+
default:
|
|
9228
|
+
return void 0;
|
|
9229
|
+
}
|
|
9230
|
+
}
|
|
9231
|
+
function titleOf(el) {
|
|
9232
|
+
const title = typeof el.title === "string" ? stripHtml2(el.title.trim()) : "";
|
|
9233
|
+
return title || humanize2(typeof el.name === "string" ? el.name : "");
|
|
9234
|
+
}
|
|
9235
|
+
function humanize2(name) {
|
|
9236
|
+
return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
9237
|
+
}
|
|
9238
|
+
function stripHtml2(s) {
|
|
9239
|
+
return s.replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim();
|
|
9240
|
+
}
|
|
9241
|
+
|
|
9242
|
+
// src/core/lib/actionRegistry/actions/evalRubric/schemaGate.ts
|
|
9243
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
9244
|
+
|
|
9245
|
+
// src/core/lib/actionRegistry/actions/evalRubric/types.ts
|
|
9246
|
+
var RUBRIC_CTX_TOKENS = [
|
|
9247
|
+
"ctx.now",
|
|
9248
|
+
"ctx.collection.startDate",
|
|
9249
|
+
"ctx.collection.endDate",
|
|
9250
|
+
"ctx.claim.submitter",
|
|
9251
|
+
"ctx.claim.submitterAddress",
|
|
9252
|
+
"ctx.claim.submissionDate",
|
|
9253
|
+
"ctx.submitter.priorClaimCount",
|
|
9254
|
+
"ctx.submitter.priorApprovedCount",
|
|
9255
|
+
"ctx.collection.projectBoundary"
|
|
9256
|
+
];
|
|
9257
|
+
var RUBRIC_CONTEXT = {
|
|
9258
|
+
ixo: "https://w3id.org/ixo/ns/protocol/",
|
|
9259
|
+
"@id": "@type",
|
|
9260
|
+
type: "@type",
|
|
9261
|
+
"@protected": true
|
|
9262
|
+
};
|
|
9263
|
+
var RUBRIC_ENVELOPE_TYPE = "ixo:entity#rubric";
|
|
9264
|
+
function buildRubricEnvelope(body) {
|
|
9265
|
+
return { "@context": RUBRIC_CONTEXT, type: RUBRIC_ENVELOPE_TYPE, rubric: body };
|
|
9266
|
+
}
|
|
9267
|
+
var EMPTY_INPUTS = {
|
|
9268
|
+
deedDid: "",
|
|
9269
|
+
collectionId: "",
|
|
9270
|
+
rubric: null,
|
|
9271
|
+
claimSchemaSnapshot: null,
|
|
9272
|
+
evalEngineUrl: ""
|
|
9273
|
+
};
|
|
9274
|
+
function migrateRubric(rubric) {
|
|
9275
|
+
const review = rubric.review;
|
|
9276
|
+
if (!review) return rubric;
|
|
9277
|
+
let next = review;
|
|
9278
|
+
if (next.route?.matrixRoom === "collection") {
|
|
9279
|
+
const { route: _retired, ...withoutRoute } = next;
|
|
9280
|
+
next = withoutRoute;
|
|
9281
|
+
}
|
|
9282
|
+
const assignTo = next.assignTo;
|
|
9283
|
+
if (assignTo && "matrixUserId" in assignTo) {
|
|
9284
|
+
const { matrixUserId, assigneeDid, ...rest } = assignTo;
|
|
9285
|
+
const nextDid = typeof assigneeDid === "string" && assigneeDid ? assigneeDid : typeof matrixUserId === "string" && matrixUserId ? matrixUserIdToDid(matrixUserId) : void 0;
|
|
9286
|
+
next = { ...next, assignTo: { ...rest, ...nextDid ? { assigneeDid: nextDid } : {} } };
|
|
9287
|
+
}
|
|
9288
|
+
return next === review ? rubric : { ...rubric, review: next };
|
|
9289
|
+
}
|
|
9290
|
+
function parseEvalRubricInputs(raw) {
|
|
9291
|
+
try {
|
|
9292
|
+
const parsed = typeof raw === "string" ? JSON.parse(raw || "{}") : raw || {};
|
|
9293
|
+
const rubric = parsed.rubric && typeof parsed.rubric === "object" && !Array.isArray(parsed.rubric) ? migrateRubric(parsed.rubric) : null;
|
|
9294
|
+
const snapshot = parsed.claimSchemaSnapshot && typeof parsed.claimSchemaSnapshot === "object" && Array.isArray(parsed.claimSchemaSnapshot.fields) ? { fields: parsed.claimSchemaSnapshot.fields, proof: typeof parsed.claimSchemaSnapshot.proof === "string" ? parsed.claimSchemaSnapshot.proof : "" } : null;
|
|
9295
|
+
return {
|
|
9296
|
+
deedDid: typeof parsed.deedDid === "string" ? parsed.deedDid : "",
|
|
9297
|
+
collectionId: typeof parsed.collectionId === "string" ? parsed.collectionId : "",
|
|
9298
|
+
rubric,
|
|
9299
|
+
claimSchemaSnapshot: snapshot,
|
|
9300
|
+
evalEngineUrl: typeof parsed.evalEngineUrl === "string" ? parsed.evalEngineUrl : ""
|
|
9301
|
+
};
|
|
9302
|
+
} catch {
|
|
9303
|
+
return { ...EMPTY_INPUTS };
|
|
9304
|
+
}
|
|
9305
|
+
}
|
|
9306
|
+
function serializeEvalRubricInputs(inputs) {
|
|
9307
|
+
return JSON.stringify(inputs);
|
|
9308
|
+
}
|
|
9309
|
+
|
|
9310
|
+
// src/core/lib/actionRegistry/actions/evalRubric/schemaGate.ts
|
|
9311
|
+
var compiledValidator = null;
|
|
9312
|
+
function resetRubricSchemaCache() {
|
|
9313
|
+
compiledValidator = null;
|
|
9314
|
+
}
|
|
9315
|
+
async function getValidator(fetchSchema, evalEngineUrl) {
|
|
9316
|
+
if (compiledValidator) return compiledValidator;
|
|
9317
|
+
const schema = await fetchSchema(evalEngineUrl);
|
|
9318
|
+
if (!schema || typeof schema !== "object") throw new Error("the rules service returned no schema");
|
|
9319
|
+
const validate = new Ajv2020({ allErrors: true, strict: false }).compile(schema);
|
|
9320
|
+
compiledValidator = validate;
|
|
9321
|
+
return validate;
|
|
9322
|
+
}
|
|
9323
|
+
async function validateRubricShape(body, fetchSchema, evalEngineUrl) {
|
|
9324
|
+
if (typeof fetchSchema !== "function") return { ok: false, reason: "The rules service is not available to check these rules." };
|
|
9325
|
+
let validate;
|
|
9326
|
+
try {
|
|
9327
|
+
validate = await getValidator(fetchSchema, evalEngineUrl);
|
|
9328
|
+
} catch (error) {
|
|
9329
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
9330
|
+
}
|
|
9331
|
+
if (validate(buildRubricEnvelope(body))) return { ok: true, problems: [] };
|
|
9332
|
+
let errors = validate.errors ?? [];
|
|
9333
|
+
if (errors.length > 0 && errors.every((error) => error.keyword === "additionalProperties")) {
|
|
9334
|
+
resetRubricSchemaCache();
|
|
9335
|
+
try {
|
|
9336
|
+
validate = await getValidator(fetchSchema, evalEngineUrl);
|
|
9337
|
+
} catch (error) {
|
|
9338
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
9339
|
+
}
|
|
9340
|
+
if (validate(buildRubricEnvelope(body))) return { ok: true, problems: [] };
|
|
9341
|
+
errors = validate.errors ?? [];
|
|
9342
|
+
}
|
|
9343
|
+
return { ok: true, problems: translateAjvErrors(errors) };
|
|
9344
|
+
}
|
|
9345
|
+
var SCHEMA_PROBLEM_MESSAGE = "This rule has a setting the rules service will not accept.";
|
|
9346
|
+
var SCHEMA_UNKNOWN_KEY_PREFIX = "The rules service doesn\u2019t recognise";
|
|
9347
|
+
function unknownKeyMessage(key) {
|
|
9348
|
+
return `${SCHEMA_UNKNOWN_KEY_PREFIX} '${key}' \u2014 the rules service may be out of date; refresh and try again.`;
|
|
9349
|
+
}
|
|
9350
|
+
function translateAjvErrors(errors) {
|
|
9351
|
+
const seen2 = /* @__PURE__ */ new Set();
|
|
9352
|
+
const diagnostics = [];
|
|
9353
|
+
for (const error of errors) {
|
|
9354
|
+
const path = sectionPath(bodyPath(error.instancePath));
|
|
9355
|
+
const unknownKey = error.keyword === "additionalProperties" ? String(error.params?.additionalProperty ?? "") : "";
|
|
9356
|
+
const dedupeKey = `${path}@@${unknownKey}`;
|
|
9357
|
+
if (seen2.has(dedupeKey)) continue;
|
|
9358
|
+
seen2.add(dedupeKey);
|
|
9359
|
+
diagnostics.push({ code: "RUB_SCHEMA", severity: "error", message: unknownKey ? unknownKeyMessage(unknownKey) : SCHEMA_PROBLEM_MESSAGE, path });
|
|
9360
|
+
}
|
|
9361
|
+
return diagnostics;
|
|
9362
|
+
}
|
|
9363
|
+
function bodyPath(instancePath) {
|
|
9364
|
+
if (instancePath === "/rubric") return "";
|
|
9365
|
+
return instancePath.startsWith("/rubric/") ? instancePath.slice("/rubric".length) : instancePath;
|
|
9366
|
+
}
|
|
9367
|
+
var RULE_ROOT = /^(\/gates\/\d+|\/scoring\/criteria\/\d+|\/review\/tasks\/\d+|\/derived\/\d+)/;
|
|
9368
|
+
function sectionPath(path) {
|
|
9369
|
+
const rule = RULE_ROOT.exec(path);
|
|
9370
|
+
if (rule) return rule[1];
|
|
9371
|
+
if (path.startsWith("/claimSchema")) {
|
|
9372
|
+
if (path.startsWith("/claimSchema/proof")) return "/claimSchema/proof";
|
|
9373
|
+
if (path.startsWith("/claimSchema/protocol")) return "/claimSchema/protocol";
|
|
9374
|
+
if (path.startsWith("/claimSchema/resource")) return "/claimSchema/resource";
|
|
9375
|
+
return "/claimSchema";
|
|
9376
|
+
}
|
|
9377
|
+
if (path.startsWith("/review/assignTo")) return "/review/assignTo";
|
|
9378
|
+
if (path.startsWith("/review")) return "/review";
|
|
9379
|
+
if (path.startsWith("/scoring")) return "/scoring";
|
|
9380
|
+
if (path.startsWith("/settlement")) return "/settlement";
|
|
9381
|
+
if (path.startsWith("/derived")) return "/derived";
|
|
9382
|
+
if (path.startsWith("/unique")) return "/unique";
|
|
9383
|
+
if (path.startsWith("/frequency")) return "/frequency";
|
|
9384
|
+
return "";
|
|
9385
|
+
}
|
|
9386
|
+
|
|
9387
|
+
// src/core/lib/actionRegistry/actions/evalRubric/expression.ts
|
|
9388
|
+
var EXPR_FUNCTIONS = {
|
|
9389
|
+
min: { minArgs: 2, maxArgs: Infinity },
|
|
9390
|
+
max: { minArgs: 2, maxArgs: Infinity },
|
|
9391
|
+
clamp: { minArgs: 3, maxArgs: 3 },
|
|
9392
|
+
abs: { minArgs: 1, maxArgs: 1 },
|
|
9393
|
+
floor: { minArgs: 1, maxArgs: 1 },
|
|
9394
|
+
ceil: { minArgs: 1, maxArgs: 1 },
|
|
9395
|
+
round: { minArgs: 1, maxArgs: 1 },
|
|
9396
|
+
sum: { minArgs: 1, maxArgs: 1 },
|
|
9397
|
+
avg: { minArgs: 1, maxArgs: 1 },
|
|
9398
|
+
count: { minArgs: 1, maxArgs: 1 },
|
|
9399
|
+
countWhere: { minArgs: 3, maxArgs: 3 },
|
|
9400
|
+
daysBetween: { minArgs: 2, maxArgs: 2 },
|
|
9401
|
+
hoursBetween: { minArgs: 2, maxArgs: 2 },
|
|
9402
|
+
yearsBetween: { minArgs: 2, maxArgs: 2 },
|
|
9403
|
+
distanceMeters: { minArgs: 2, maxArgs: 2 },
|
|
9404
|
+
length: { minArgs: 1, maxArgs: 1 }
|
|
9405
|
+
};
|
|
9406
|
+
var COUNT_WHERE_OPS = ["==", "!=", ">", ">=", "<", "<="];
|
|
9407
|
+
var COUNT_WHERE_ORDERING_OPS = [">", ">=", "<", "<="];
|
|
9408
|
+
var ExpressionSyntaxError = class extends Error {
|
|
9409
|
+
constructor(message, position) {
|
|
9410
|
+
super(`${message} (at offset ${position})`);
|
|
9411
|
+
this.position = position;
|
|
9412
|
+
this.name = "ExpressionSyntaxError";
|
|
9413
|
+
}
|
|
9414
|
+
};
|
|
9415
|
+
var IDENT = /[A-Za-z_][A-Za-z0-9_]*/y;
|
|
9416
|
+
var NUMBER = /(?:\d+\.?\d*|\.\d+)/y;
|
|
9417
|
+
var FIELD_REF = /\$[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+|\[\*\])*/y;
|
|
9418
|
+
var SIGIL_REF = /~[A-Za-z_][A-Za-z0-9_]*/y;
|
|
9419
|
+
var CTX_REF = /ctx(?:\.[A-Za-z][A-Za-z0-9]*)+/y;
|
|
9420
|
+
function parseExpression(src) {
|
|
9421
|
+
let pos = 0;
|
|
9422
|
+
const skipWs = () => {
|
|
9423
|
+
while (pos < src.length && /\s/.test(src[pos])) pos++;
|
|
9424
|
+
};
|
|
9425
|
+
const match = (re) => {
|
|
9426
|
+
re.lastIndex = pos;
|
|
9427
|
+
const m = re.exec(src);
|
|
9428
|
+
if (!m) return void 0;
|
|
9429
|
+
pos = re.lastIndex;
|
|
9430
|
+
return m[0];
|
|
9431
|
+
};
|
|
9432
|
+
const fail = (message) => {
|
|
9433
|
+
throw new ExpressionSyntaxError(message, pos);
|
|
9434
|
+
};
|
|
9435
|
+
const parseExpr = () => {
|
|
9436
|
+
let left = parseTerm();
|
|
9437
|
+
for (; ; ) {
|
|
9438
|
+
skipWs();
|
|
9439
|
+
const ch = src[pos];
|
|
9440
|
+
if (ch !== "+" && ch !== "-") return left;
|
|
9441
|
+
pos++;
|
|
9442
|
+
left = { kind: "bin", op: ch, left, right: parseTerm() };
|
|
9443
|
+
}
|
|
9444
|
+
};
|
|
9445
|
+
const parseTerm = () => {
|
|
9446
|
+
let left = parseFactor();
|
|
9447
|
+
for (; ; ) {
|
|
9448
|
+
skipWs();
|
|
9449
|
+
const ch = src[pos];
|
|
9450
|
+
if (ch !== "*" && ch !== "/") return left;
|
|
9451
|
+
pos++;
|
|
9452
|
+
left = { kind: "bin", op: ch, left, right: parseFactor() };
|
|
9453
|
+
}
|
|
9454
|
+
};
|
|
9455
|
+
const parseFactor = () => {
|
|
9456
|
+
skipWs();
|
|
9457
|
+
const ch = src[pos];
|
|
9458
|
+
if (ch === void 0) return fail("unexpected end of expression");
|
|
9459
|
+
if (ch === "-") {
|
|
9460
|
+
pos++;
|
|
9461
|
+
return { kind: "neg", operand: parseFactor() };
|
|
9462
|
+
}
|
|
9463
|
+
if (ch === "(") {
|
|
9464
|
+
pos++;
|
|
9465
|
+
const inner = parseExpr();
|
|
9466
|
+
skipWs();
|
|
9467
|
+
if (src[pos] !== ")") return fail("expected ')'");
|
|
9468
|
+
pos++;
|
|
9469
|
+
return inner;
|
|
9470
|
+
}
|
|
9471
|
+
if (ch === "'" || ch === '"') {
|
|
9472
|
+
const quote = ch;
|
|
9473
|
+
const start = ++pos;
|
|
9474
|
+
while (pos < src.length && src[pos] !== quote) pos++;
|
|
9475
|
+
if (pos >= src.length) return fail("unterminated string literal");
|
|
9476
|
+
return { kind: "str", value: src.slice(start, pos++) };
|
|
9477
|
+
}
|
|
9478
|
+
if (ch === "$") {
|
|
9479
|
+
const ref = match(FIELD_REF);
|
|
9480
|
+
if (!ref) return fail("malformed $field reference");
|
|
9481
|
+
return { kind: "ref", ref };
|
|
9482
|
+
}
|
|
9483
|
+
if (ch === "~") {
|
|
9484
|
+
const ref = match(SIGIL_REF);
|
|
9485
|
+
if (!ref) return fail("malformed ~-reference");
|
|
9486
|
+
return { kind: "ref", ref };
|
|
9487
|
+
}
|
|
9488
|
+
const numeric = match(NUMBER);
|
|
9489
|
+
if (numeric) return { kind: "num", value: Number(numeric) };
|
|
9490
|
+
const ctxRef = match(CTX_REF);
|
|
9491
|
+
if (ctxRef) return { kind: "ref", ref: ctxRef };
|
|
9492
|
+
const ident = match(IDENT);
|
|
9493
|
+
if (ident) {
|
|
9494
|
+
if (ident === "true" || ident === "false") return { kind: "bool", value: ident === "true" };
|
|
9495
|
+
skipWs();
|
|
9496
|
+
if (src[pos] !== "(") return fail(`'${ident}' is not a value \u2014 bare identifiers must be function calls`);
|
|
9497
|
+
pos++;
|
|
9498
|
+
const args = [];
|
|
9499
|
+
skipWs();
|
|
9500
|
+
if (src[pos] === ")") {
|
|
9501
|
+
pos++;
|
|
9502
|
+
} else {
|
|
9503
|
+
for (; ; ) {
|
|
9504
|
+
args.push(parseExpr());
|
|
9505
|
+
skipWs();
|
|
9506
|
+
if (src[pos] === ",") {
|
|
9507
|
+
pos++;
|
|
9508
|
+
continue;
|
|
9509
|
+
}
|
|
9510
|
+
if (src[pos] === ")") {
|
|
9511
|
+
pos++;
|
|
9512
|
+
break;
|
|
9513
|
+
}
|
|
9514
|
+
return fail("expected ',' or ')'");
|
|
9515
|
+
}
|
|
9516
|
+
}
|
|
9517
|
+
return { kind: "call", name: ident, args };
|
|
9518
|
+
}
|
|
9519
|
+
return fail(`unexpected character '${ch}'`);
|
|
9520
|
+
};
|
|
9521
|
+
const root = parseExpr();
|
|
9522
|
+
skipWs();
|
|
9523
|
+
if (pos < src.length) fail(`unexpected trailing input '${src.slice(pos)}'`);
|
|
9524
|
+
return root;
|
|
9525
|
+
}
|
|
9526
|
+
|
|
9527
|
+
// src/core/lib/actionRegistry/actions/evalRubric/validate.ts
|
|
9528
|
+
var RUBRIC_OPERATOR_KINDS = {
|
|
9529
|
+
present: "all",
|
|
9530
|
+
empty: "all",
|
|
9531
|
+
"==": ["text", "number", "date", "choice", "boolean"],
|
|
9532
|
+
"!=": ["text", "number", "date", "choice", "boolean"],
|
|
9533
|
+
">": ["number", "date"],
|
|
9534
|
+
">=": ["number", "date"],
|
|
9535
|
+
"<": ["number", "date"],
|
|
9536
|
+
"<=": ["number", "date"],
|
|
9537
|
+
between: ["number", "date"],
|
|
9538
|
+
approx: ["number"],
|
|
9539
|
+
in: ["text", "number", "choice"],
|
|
9540
|
+
notIn: ["text", "number", "choice"],
|
|
9541
|
+
matches: ["text"],
|
|
9542
|
+
startsWith: ["text"],
|
|
9543
|
+
endsWith: ["text"],
|
|
9544
|
+
lengthBetween: ["text"],
|
|
9545
|
+
contains: ["multichoice"],
|
|
9546
|
+
containsAll: ["multichoice"],
|
|
9547
|
+
containsAny: ["multichoice"],
|
|
9548
|
+
subsetOf: ["multichoice"],
|
|
9549
|
+
countBetween: ["multichoice", "rows"],
|
|
9550
|
+
isTrue: ["boolean"],
|
|
9551
|
+
isFalse: ["boolean"],
|
|
9552
|
+
before: ["date"],
|
|
9553
|
+
after: ["date"],
|
|
9554
|
+
withinWindow: ["date"],
|
|
9555
|
+
withinDays: ["date"],
|
|
9556
|
+
notInFuture: ["date"],
|
|
9557
|
+
mediaTypeIn: ["file", "signature"],
|
|
9558
|
+
maxSizeMB: ["file", "signature"],
|
|
9559
|
+
withinBoundary: ["geo"],
|
|
9560
|
+
withinRadius: ["geo"]
|
|
9561
|
+
};
|
|
9562
|
+
function operatorsForKind(kind) {
|
|
9563
|
+
return Object.keys(RUBRIC_OPERATOR_KINDS).filter((op) => {
|
|
9564
|
+
const kinds = RUBRIC_OPERATOR_KINDS[op];
|
|
9565
|
+
return kinds === "all" || kinds.includes(kind);
|
|
9566
|
+
});
|
|
9567
|
+
}
|
|
9568
|
+
var GATE_FAILURE_CLASSES = ["refuted", "invalid_evidence", "out_of_authority", "insufficient_evidence", "requires_human_review"];
|
|
9569
|
+
var CTX_TOKEN_KIND = {
|
|
9570
|
+
"ctx.now": "date",
|
|
9571
|
+
"ctx.collection.startDate": "date",
|
|
9572
|
+
"ctx.collection.endDate": "date",
|
|
9573
|
+
"ctx.claim.submitter": "text",
|
|
9574
|
+
"ctx.claim.submitterAddress": "text",
|
|
9575
|
+
"ctx.claim.submissionDate": "date",
|
|
9576
|
+
"ctx.submitter.priorClaimCount": "number",
|
|
9577
|
+
"ctx.submitter.priorApprovedCount": "number",
|
|
9578
|
+
"ctx.collection.projectBoundary": "geo"
|
|
9579
|
+
};
|
|
9580
|
+
var FIELD_REF2 = /^\$[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+|\[\*\])*$/;
|
|
9581
|
+
var ROW_REF = /^\.[A-Za-z0-9_-]+$/;
|
|
9582
|
+
var DERIVED_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
9583
|
+
var EXT_REF = /^ext\.([A-Za-z0-9_-]+)\.(valid|score|reason)$/;
|
|
9584
|
+
var AI_REF = /^ai\.([A-Za-z0-9_-]+)\.(valid|reason)$/;
|
|
9585
|
+
var SOURCE_NAME = /^[A-Za-z0-9_-]+$/;
|
|
9586
|
+
function validateRubric(body, catalog, authoringCatalog) {
|
|
9587
|
+
const diags = [];
|
|
9588
|
+
const error = (code, message, path) => diags.push({ code, severity: "error", message, path });
|
|
9589
|
+
const warn = (code, message, path) => diags.push({ code, severity: "warning", message, path });
|
|
9590
|
+
const fieldIndex = indexCatalog(catalog);
|
|
9591
|
+
const snapshotIndex = authoringCatalog ? indexCatalog(authoringCatalog) : void 0;
|
|
9592
|
+
const derivedNames = new Set((body.derived ?? []).map((d) => d.name));
|
|
9593
|
+
const sourcesByName = new Map((body.sources ?? []).map((s) => [s.name, s]));
|
|
9594
|
+
const aiChecksByName = new Map((body.aiChecks ?? []).map((c) => [c.name, c]));
|
|
9595
|
+
const checkFieldRef = (ref, path) => {
|
|
9596
|
+
if (!FIELD_REF2.test(ref)) {
|
|
9597
|
+
error("RUB_FIELD_PATH", `malformed field path '${ref}'`, path);
|
|
9598
|
+
return void 0;
|
|
9599
|
+
}
|
|
9600
|
+
const field = fieldIndex.get(ref);
|
|
9601
|
+
if (field) {
|
|
9602
|
+
const snapshotKind = snapshotIndex?.get(ref)?.kind;
|
|
9603
|
+
if (snapshotKind && snapshotKind !== field.kind) {
|
|
9604
|
+
error("RUB_SCHEMA_DRIFT", `'${ref}' changed from ${snapshotKind} to ${field.kind} since the rules were authored`, path);
|
|
9605
|
+
return void 0;
|
|
9606
|
+
}
|
|
9607
|
+
return field.kind;
|
|
9608
|
+
}
|
|
9609
|
+
if (snapshotIndex?.has(ref)) {
|
|
9610
|
+
error("RUB_SCHEMA_DRIFT", `'${ref}' no longer exists on the claim form (deleted or renamed since the rules were authored)`, path);
|
|
9611
|
+
return void 0;
|
|
9612
|
+
}
|
|
9613
|
+
const rootName = /^\$[A-Za-z0-9_-]+/.exec(ref)?.[0] ?? ref;
|
|
9614
|
+
if (ref !== rootName && fieldIndex.has(rootName)) {
|
|
9615
|
+
error("RUB_FIELD_PATH", `'${ref}' does not address a row/column of ${rootName}`, path);
|
|
9616
|
+
} else {
|
|
9617
|
+
error("RUB_FIELD_NOT_IN_SCHEMA", `'${ref}' does not resolve to a question in the bound form`, path);
|
|
9618
|
+
}
|
|
9619
|
+
return void 0;
|
|
9620
|
+
};
|
|
9621
|
+
const checkExtRef = (ref, path) => {
|
|
9622
|
+
const match = EXT_REF.exec(ref);
|
|
9623
|
+
if (!match) {
|
|
9624
|
+
error("RUB_EXT_FIELD_INVALID", `'${ref}' is not a valid external-check reference \u2014 use ext.<name>.valid, .score or .reason`, path);
|
|
9625
|
+
return void 0;
|
|
9626
|
+
}
|
|
9627
|
+
const [, name, fieldName] = match;
|
|
9628
|
+
const source = sourcesByName.get(name);
|
|
9629
|
+
if (!source) {
|
|
9630
|
+
error("RUB_EXT_SOURCE_UNKNOWN", `no external check named '${name}' is defined`, path);
|
|
9631
|
+
return void 0;
|
|
9632
|
+
}
|
|
9633
|
+
if (fieldName === "valid" && source.expect !== "boolean") {
|
|
9634
|
+
error("RUB_EXT_FIELD_INVALID", `external check '${name}' returns a score, not a yes/no \u2014 read ext.${name}.score`, path);
|
|
9635
|
+
return void 0;
|
|
9636
|
+
}
|
|
9637
|
+
if (fieldName === "score" && source.expect !== "score") {
|
|
9638
|
+
error("RUB_EXT_FIELD_INVALID", `external check '${name}' returns a yes/no, not a score \u2014 read ext.${name}.valid`, path);
|
|
9639
|
+
return void 0;
|
|
9640
|
+
}
|
|
9641
|
+
return fieldName === "score" ? "number" : fieldName === "valid" ? "boolean" : "text";
|
|
9642
|
+
};
|
|
9643
|
+
const checkAiRef = (ref, path) => {
|
|
9644
|
+
const match = AI_REF.exec(ref);
|
|
9645
|
+
if (!match) {
|
|
9646
|
+
error("RUB_AI_FIELD_INVALID", `'${ref}' is not a valid AI-check reference \u2014 use ai.<name>.valid or ai.<name>.reason`, path);
|
|
9647
|
+
return void 0;
|
|
9648
|
+
}
|
|
9649
|
+
const [, name, fieldName] = match;
|
|
9650
|
+
if (!aiChecksByName.has(name)) {
|
|
9651
|
+
error("RUB_AI_CHECK_UNKNOWN", `no AI check named '${name}' is defined`, path);
|
|
9652
|
+
return void 0;
|
|
9653
|
+
}
|
|
9654
|
+
return fieldName === "valid" ? "boolean" : "text";
|
|
9655
|
+
};
|
|
9656
|
+
const checkConditionField = (ref, path, forEachTarget) => {
|
|
9657
|
+
if (typeof ref !== "string" || ref.length === 0) {
|
|
9658
|
+
error("RUB_FIELD_PATH", "condition field must be a reference string", path);
|
|
9659
|
+
return void 0;
|
|
9660
|
+
}
|
|
9661
|
+
if (ref.startsWith("~")) {
|
|
9662
|
+
if (!derivedNames.has(ref.slice(1))) {
|
|
9663
|
+
error("RUB_UNKNOWN_REF", `derived value '${ref}' is not defined`, path);
|
|
9664
|
+
return void 0;
|
|
9665
|
+
}
|
|
9666
|
+
return "number";
|
|
9667
|
+
}
|
|
9668
|
+
if (ref.startsWith("ext.")) return checkExtRef(ref, path);
|
|
9669
|
+
if (ref.startsWith("ai.")) return checkAiRef(ref, path);
|
|
9670
|
+
if (ref.startsWith(".")) {
|
|
9671
|
+
if (!forEachTarget) {
|
|
9672
|
+
error("RUB_FIELD_PATH", `row-relative ref '${ref}' is only legal inside a forEach`, path);
|
|
9673
|
+
return void 0;
|
|
9674
|
+
}
|
|
9675
|
+
if (!ROW_REF.test(ref)) {
|
|
9676
|
+
error("RUB_FIELD_PATH", `malformed row-relative ref '${ref}'`, path);
|
|
9677
|
+
return void 0;
|
|
9678
|
+
}
|
|
9679
|
+
return checkFieldRef(`${forEachTarget}[*]${ref}`, path);
|
|
9680
|
+
}
|
|
9681
|
+
if (ref.startsWith("$")) return checkFieldRef(ref, path);
|
|
9682
|
+
error("RUB_FIELD_PATH", `'${ref}' is not a field reference ($answer, ~derived, or a row-relative .column)`, path);
|
|
9683
|
+
return void 0;
|
|
9684
|
+
};
|
|
9685
|
+
const isRefString = (v) => typeof v === "string" && (v.startsWith("$") || v.startsWith("~") || v.startsWith("ctx.") || v.startsWith("ext.") || v.startsWith("ai."));
|
|
9686
|
+
const unwrapLiteral = (v) => {
|
|
9687
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v) && "@value" in v) {
|
|
9688
|
+
return { literal: v["@value"] };
|
|
9689
|
+
}
|
|
9690
|
+
return isRefString(v) ? void 0 : { literal: v };
|
|
9691
|
+
};
|
|
9692
|
+
const checkValueRef = (ref, path) => {
|
|
9693
|
+
if (ref.startsWith("$")) {
|
|
9694
|
+
if (FIELD_REF2.test(ref) && (fieldIndex.has(ref) || snapshotIndex?.has(ref))) return checkFieldRef(ref, path);
|
|
9695
|
+
error("RUB_UNKNOWN_REF", `'${ref}' does not resolve \u2014 wrap a literal that starts with a sigil in {"@value": \u2026}`, path);
|
|
9696
|
+
return void 0;
|
|
9697
|
+
}
|
|
9698
|
+
if (ref.startsWith("~")) {
|
|
9699
|
+
if (!derivedNames.has(ref.slice(1))) {
|
|
9700
|
+
error("RUB_UNKNOWN_REF", `derived value '${ref}' is not defined`, path);
|
|
9701
|
+
return void 0;
|
|
9702
|
+
}
|
|
9703
|
+
return "number";
|
|
9704
|
+
}
|
|
9705
|
+
if (ref.startsWith("ext.")) return checkExtRef(ref, path);
|
|
9706
|
+
if (ref.startsWith("ai.")) return checkAiRef(ref, path);
|
|
9707
|
+
if (RUBRIC_CTX_TOKENS.includes(ref)) return CTX_TOKEN_KIND[ref];
|
|
9708
|
+
error("RUB_UNKNOWN_REF", `unknown context token '${ref}'`, path);
|
|
9709
|
+
return void 0;
|
|
9710
|
+
};
|
|
9711
|
+
const isDateString = (v) => typeof v === "string" && !Number.isNaN(Date.parse(v));
|
|
9712
|
+
const literalFitsKind = (literal, kind) => {
|
|
9713
|
+
switch (kind) {
|
|
9714
|
+
case "number":
|
|
9715
|
+
return typeof literal === "number";
|
|
9716
|
+
case "boolean":
|
|
9717
|
+
return typeof literal === "boolean";
|
|
9718
|
+
case "date":
|
|
9719
|
+
return isDateString(literal);
|
|
9720
|
+
default:
|
|
9721
|
+
return typeof literal === "string";
|
|
9722
|
+
}
|
|
9723
|
+
};
|
|
9724
|
+
const checkComparable = (value, kind, path) => {
|
|
9725
|
+
if (isRefString(value)) {
|
|
9726
|
+
const refKind = checkValueRef(value, path);
|
|
9727
|
+
if (refKind && refKind !== kind) error("RUB_VALUE_TYPE", `'${value}' is a ${refKind}, expected a ${kind}`, path);
|
|
9728
|
+
return;
|
|
9729
|
+
}
|
|
9730
|
+
const unwrapped = unwrapLiteral(value);
|
|
9731
|
+
if (!unwrapped || !literalFitsKind(unwrapped.literal, kind)) {
|
|
9732
|
+
error("RUB_VALUE_TYPE", `expected a ${kind} value`, path);
|
|
9733
|
+
}
|
|
9734
|
+
};
|
|
9735
|
+
const checkPair = (value, path, check) => {
|
|
9736
|
+
if (!Array.isArray(value) || value.length !== 2) {
|
|
9737
|
+
error("RUB_VALUE_TYPE", "expected a [min, max] pair", path);
|
|
9738
|
+
return;
|
|
9739
|
+
}
|
|
9740
|
+
check(value[0], `${path}/0`);
|
|
9741
|
+
check(value[1], `${path}/1`);
|
|
9742
|
+
};
|
|
9743
|
+
const checkInteger = (value, path) => {
|
|
9744
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) error("RUB_VALUE_TYPE", "expected a non-negative integer", path);
|
|
9745
|
+
};
|
|
9746
|
+
const checkSimpleCondition = (cond, path, forEachTarget) => {
|
|
9747
|
+
const kind = checkConditionField(cond.field, `${path}/field`, forEachTarget);
|
|
9748
|
+
const legalKinds = RUBRIC_OPERATOR_KINDS[cond.op];
|
|
9749
|
+
if (!legalKinds) {
|
|
9750
|
+
error("RUB_OPERATOR_TYPE_MISMATCH", `unknown operator '${String(cond.op)}'`, `${path}/op`);
|
|
9751
|
+
return;
|
|
9752
|
+
}
|
|
9753
|
+
if (kind === void 0) return;
|
|
9754
|
+
if (legalKinds !== "all" && !legalKinds.includes(kind)) {
|
|
9755
|
+
error("RUB_OPERATOR_TYPE_MISMATCH", `'${cond.op}' is not legal for a ${kind} field`, `${path}/op`);
|
|
9756
|
+
return;
|
|
9757
|
+
}
|
|
9758
|
+
const value = cond.value;
|
|
9759
|
+
const valuePath = `${path}/value`;
|
|
9760
|
+
switch (cond.op) {
|
|
9761
|
+
case "present":
|
|
9762
|
+
case "empty":
|
|
9763
|
+
case "isTrue":
|
|
9764
|
+
case "isFalse":
|
|
9765
|
+
case "withinWindow":
|
|
9766
|
+
case "notInFuture":
|
|
9767
|
+
if (value !== void 0) error("RUB_VALUE_TYPE", `'${cond.op}' takes no value`, valuePath);
|
|
9768
|
+
break;
|
|
9769
|
+
case "==":
|
|
9770
|
+
case "!=": {
|
|
9771
|
+
if (isRefString(value)) {
|
|
9772
|
+
const refKind = checkValueRef(value, valuePath);
|
|
9773
|
+
if (refKind && refKind !== kind) error("RUB_VALUE_TYPE", `'${value}' is a ${refKind}, expected a ${kind}`, valuePath);
|
|
9774
|
+
break;
|
|
9775
|
+
}
|
|
9776
|
+
const unwrapped = unwrapLiteral(value);
|
|
9777
|
+
if (!unwrapped || !literalFitsKind(unwrapped.literal, kind)) error("RUB_VALUE_TYPE", `expected a ${kind} value`, valuePath);
|
|
9778
|
+
break;
|
|
9779
|
+
}
|
|
9780
|
+
case ">":
|
|
9781
|
+
case ">=":
|
|
9782
|
+
case "<":
|
|
9783
|
+
case "<=":
|
|
9784
|
+
checkComparable(value, kind === "date" ? "date" : "number", valuePath);
|
|
9785
|
+
break;
|
|
9786
|
+
case "between":
|
|
9787
|
+
checkPair(value, valuePath, (v, p) => checkComparable(v, kind === "date" ? "date" : "number", p));
|
|
9788
|
+
break;
|
|
9789
|
+
case "approx":
|
|
9790
|
+
checkComparable(value, "number", valuePath);
|
|
9791
|
+
if (typeof cond.tolerance !== "number" || cond.tolerance < 0) error("RUB_VALUE_TYPE", "'approx' needs a non-negative numeric tolerance", `${path}/tolerance`);
|
|
9792
|
+
break;
|
|
9793
|
+
case "in":
|
|
9794
|
+
case "notIn":
|
|
9795
|
+
case "containsAll":
|
|
9796
|
+
case "containsAny":
|
|
9797
|
+
case "subsetOf": {
|
|
9798
|
+
if (!Array.isArray(value)) {
|
|
9799
|
+
error("RUB_VALUE_TYPE", "expected a list of values", valuePath);
|
|
9800
|
+
break;
|
|
9801
|
+
}
|
|
9802
|
+
const elementKind = kind === "number" ? "number" : "text";
|
|
9803
|
+
for (const [i, element] of value.entries()) {
|
|
9804
|
+
if (isRefString(element)) checkValueRef(element, `${valuePath}/${i}`);
|
|
9805
|
+
else if (!literalFitsKind(element, elementKind)) error("RUB_VALUE_TYPE", `list entry must be a ${elementKind === "number" ? "number" : "string"}`, `${valuePath}/${i}`);
|
|
9806
|
+
}
|
|
9807
|
+
break;
|
|
9808
|
+
}
|
|
9809
|
+
case "contains": {
|
|
9810
|
+
if (isRefString(value)) checkValueRef(value, valuePath);
|
|
9811
|
+
else {
|
|
9812
|
+
const unwrapped = unwrapLiteral(value);
|
|
9813
|
+
if (!unwrapped || typeof unwrapped.literal !== "string" && typeof unwrapped.literal !== "number" && typeof unwrapped.literal !== "boolean") {
|
|
9814
|
+
error("RUB_VALUE_TYPE", "'contains' expects a single literal value", valuePath);
|
|
9815
|
+
}
|
|
9816
|
+
}
|
|
9817
|
+
break;
|
|
9818
|
+
}
|
|
9819
|
+
case "matches": {
|
|
9820
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
9821
|
+
error("RUB_VALUE_TYPE", "'matches' expects a regex pattern string", valuePath);
|
|
9822
|
+
break;
|
|
9823
|
+
}
|
|
9824
|
+
try {
|
|
9825
|
+
new RegExp(value);
|
|
9826
|
+
} catch {
|
|
9827
|
+
error("RUB_VALUE_TYPE", `'${value}' is not a valid pattern`, valuePath);
|
|
9828
|
+
break;
|
|
9829
|
+
}
|
|
9830
|
+
const unsafeReason = unsafeRegexReason(value);
|
|
9831
|
+
if (unsafeReason) error("RUB_REGEX_UNSAFE", `pattern outside the safe subset: ${unsafeReason}`, valuePath);
|
|
9832
|
+
break;
|
|
9833
|
+
}
|
|
9834
|
+
case "startsWith":
|
|
9835
|
+
case "endsWith": {
|
|
9836
|
+
const unwrapped = unwrapLiteral(value);
|
|
9837
|
+
if (!unwrapped || typeof unwrapped.literal !== "string") error("RUB_VALUE_TYPE", `'${cond.op}' expects a string`, valuePath);
|
|
9838
|
+
break;
|
|
9839
|
+
}
|
|
9840
|
+
case "lengthBetween":
|
|
9841
|
+
case "countBetween":
|
|
9842
|
+
checkPair(value, valuePath, checkInteger);
|
|
9843
|
+
break;
|
|
9844
|
+
case "before":
|
|
9845
|
+
case "after":
|
|
9846
|
+
checkComparable(value, "date", valuePath);
|
|
9847
|
+
break;
|
|
9848
|
+
case "withinDays":
|
|
9849
|
+
checkInteger(value, valuePath);
|
|
9850
|
+
break;
|
|
9851
|
+
case "mediaTypeIn":
|
|
9852
|
+
if (!Array.isArray(value) || value.length === 0 || value.some((v) => typeof v !== "string")) {
|
|
9853
|
+
error("RUB_VALUE_TYPE", "'mediaTypeIn' expects a list of MIME globs", valuePath);
|
|
9854
|
+
}
|
|
9855
|
+
break;
|
|
9856
|
+
case "maxSizeMB":
|
|
9857
|
+
if (typeof value !== "number" || value <= 0) error("RUB_VALUE_TYPE", "'maxSizeMB' expects a positive number", valuePath);
|
|
9858
|
+
break;
|
|
9859
|
+
case "withinBoundary":
|
|
9860
|
+
if (value !== "ctx.collection.projectBoundary") {
|
|
9861
|
+
error("RUB_VALUE_TYPE", "'withinBoundary' expects ctx.collection.projectBoundary", valuePath);
|
|
9862
|
+
}
|
|
9863
|
+
break;
|
|
9864
|
+
case "withinRadius": {
|
|
9865
|
+
const spec = value;
|
|
9866
|
+
if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
|
|
9867
|
+
error("RUB_VALUE_TYPE", "'withinRadius' expects { center, meters }", valuePath);
|
|
9868
|
+
break;
|
|
9869
|
+
}
|
|
9870
|
+
if (typeof spec.meters !== "number" || spec.meters <= 0) error("RUB_VALUE_TYPE", "meters must be a positive number", `${valuePath}/meters`);
|
|
9871
|
+
if (typeof spec.center === "string") {
|
|
9872
|
+
const centerKind = checkValueRef(spec.center, `${valuePath}/center`);
|
|
9873
|
+
if (centerKind && centerKind !== "geo") error("RUB_VALUE_TYPE", "center must be a geo answer or {lat, lng}", `${valuePath}/center`);
|
|
9874
|
+
} else {
|
|
9875
|
+
const center = spec.center;
|
|
9876
|
+
if (!center || typeof center.lat !== "number" || typeof center.lng !== "number") {
|
|
9877
|
+
error("RUB_VALUE_TYPE", "center must be a geo answer or {lat, lng}", `${valuePath}/center`);
|
|
9878
|
+
}
|
|
9879
|
+
}
|
|
9880
|
+
break;
|
|
9881
|
+
}
|
|
9882
|
+
}
|
|
9883
|
+
};
|
|
9884
|
+
const checkCondition = (cond, path, forEachTarget) => {
|
|
9885
|
+
if (!cond || typeof cond !== "object") return;
|
|
9886
|
+
if ("forEach" in cond && cond.forEach !== void 0) {
|
|
9887
|
+
const targetKind = typeof cond.forEach === "string" && cond.forEach.startsWith("$") ? checkFieldRef(cond.forEach, `${path}/forEach`) : void 0;
|
|
9888
|
+
if (typeof cond.forEach !== "string" || !cond.forEach.startsWith("$")) {
|
|
9889
|
+
error("RUB_FIELD_PATH", "forEach must reference a repeating-rows question", `${path}/forEach`);
|
|
9890
|
+
} else if (targetKind && targetKind !== "rows") {
|
|
9891
|
+
error("RUB_OPERATOR_TYPE_MISMATCH", `forEach needs a repeating-rows question, '${cond.forEach}' is a ${targetKind}`, `${path}/forEach`);
|
|
9892
|
+
}
|
|
9893
|
+
if (cond.every) checkCondition(cond.every, `${path}/every`, cond.forEach);
|
|
9894
|
+
if (cond.some) checkCondition(cond.some, `${path}/some`, cond.forEach);
|
|
9895
|
+
return;
|
|
9896
|
+
}
|
|
9897
|
+
if ("all" in cond || "any" in cond) {
|
|
9898
|
+
const key = "all" in cond ? "all" : "any";
|
|
9899
|
+
const members = "all" in cond ? cond.all : cond.any;
|
|
9900
|
+
if (!Array.isArray(members) || members.length === 0) {
|
|
9901
|
+
error("RUB_SCHEMA", `'${key}' needs at least one check inside it`, `${path}/${key}`);
|
|
9902
|
+
return;
|
|
9903
|
+
}
|
|
9904
|
+
members.forEach((member, i) => checkCondition(member, `${path}/${key}/${i}`, forEachTarget));
|
|
9905
|
+
return;
|
|
9906
|
+
}
|
|
9907
|
+
if ("not" in cond) {
|
|
9908
|
+
checkCondition(cond.not, `${path}/not`, forEachTarget);
|
|
9909
|
+
return;
|
|
9910
|
+
}
|
|
9911
|
+
if ("atLeast" in cond || "of" in cond) {
|
|
9912
|
+
const { atLeast: n, of: members } = cond;
|
|
9913
|
+
if (!Array.isArray(members) || members.length === 0) {
|
|
9914
|
+
error("RUB_SCHEMA", `'atLeast' needs a non-empty 'of' list of checks`, `${path}/of`);
|
|
9915
|
+
return;
|
|
9916
|
+
}
|
|
9917
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 1) {
|
|
9918
|
+
error("RUB_VALUE_TYPE", `'atLeast' must be a whole number, 1 or more \u2014 got ${String(n)}`, `${path}/atLeast`);
|
|
9919
|
+
} else if (n > members.length) {
|
|
9920
|
+
error("RUB_QUORUM_INVALID", `atLeast ${n} of ${members.length} \u2014 atLeast cannot exceed the number of checks in 'of'`, `${path}/atLeast`);
|
|
9921
|
+
}
|
|
9922
|
+
members.forEach((member, i) => checkCondition(member, `${path}/of/${i}`, forEachTarget));
|
|
9923
|
+
return;
|
|
9924
|
+
}
|
|
9925
|
+
if ("field" in cond) checkSimpleCondition(cond, path, forEachTarget);
|
|
9926
|
+
};
|
|
9927
|
+
const checkExpression = (expr, path, earlierDerived) => {
|
|
9928
|
+
let root;
|
|
9929
|
+
try {
|
|
9930
|
+
root = parseExpression(expr);
|
|
9931
|
+
} catch (e) {
|
|
9932
|
+
error("RUB_EXPR_SYNTAX", e instanceof ExpressionSyntaxError ? e.message : String(e), path);
|
|
9933
|
+
return;
|
|
9934
|
+
}
|
|
9935
|
+
const checkNode = (node, allowLiteral) => {
|
|
9936
|
+
switch (node.kind) {
|
|
9937
|
+
case "str":
|
|
9938
|
+
case "bool":
|
|
9939
|
+
if (!allowLiteral) error("RUB_EXPR_SYNTAX", "string and true/false literals are only legal as countWhere's operator or value", path);
|
|
9940
|
+
break;
|
|
9941
|
+
case "ref": {
|
|
9942
|
+
const ref = node.ref;
|
|
9943
|
+
if (ref.startsWith("$")) checkFieldRef(ref, path);
|
|
9944
|
+
else if (ref.startsWith("~")) {
|
|
9945
|
+
const name = ref.slice(1);
|
|
9946
|
+
if (earlierDerived.has(name)) break;
|
|
9947
|
+
if (derivedNames.has(name)) error("RUB_EXPR_SYNTAX", `'${ref}' must be defined earlier in the derived list (no cycles)`, path);
|
|
9948
|
+
else error("RUB_UNKNOWN_REF", `derived value '${ref}' is not defined`, path);
|
|
9949
|
+
} else if (!RUBRIC_CTX_TOKENS.includes(ref)) {
|
|
9950
|
+
error("RUB_UNKNOWN_REF", `unknown context token '${ref}'`, path);
|
|
9951
|
+
}
|
|
9952
|
+
break;
|
|
9953
|
+
}
|
|
9954
|
+
case "call": {
|
|
9955
|
+
const signature = EXPR_FUNCTIONS[node.name];
|
|
9956
|
+
if (!signature) {
|
|
9957
|
+
error("RUB_EXPR_UNKNOWN_FUNC", `'${node.name}' is not in the function library`, path);
|
|
9958
|
+
break;
|
|
9959
|
+
}
|
|
9960
|
+
if (node.args.length < signature.minArgs || node.args.length > signature.maxArgs) {
|
|
9961
|
+
error("RUB_EXPR_SYNTAX", `'${node.name}' takes ${signature.minArgs === signature.maxArgs ? signature.minArgs : `${signature.minArgs}+`} argument(s)`, path);
|
|
9962
|
+
}
|
|
9963
|
+
if (node.name === "sum" || node.name === "avg" || node.name === "count" || node.name === "countWhere") {
|
|
9964
|
+
const selector = node.args[0];
|
|
9965
|
+
if (!selector || selector.kind !== "ref" || !selector.ref.includes("[*]")) {
|
|
9966
|
+
error("RUB_EXPR_SYNTAX", `'${node.name}' needs a $rows[*].column selector`, path);
|
|
9967
|
+
}
|
|
9968
|
+
}
|
|
9969
|
+
if (node.name === "countWhere") {
|
|
9970
|
+
const op = node.args[1];
|
|
9971
|
+
const value = node.args[2];
|
|
9972
|
+
if (!op || op.kind !== "str" || !COUNT_WHERE_OPS.includes(op.value)) {
|
|
9973
|
+
error("RUB_EXPR_SYNTAX", `countWhere's operator must be one of ${COUNT_WHERE_OPS.join(" ")}`, path);
|
|
9974
|
+
} else if (value) {
|
|
9975
|
+
const ordering = COUNT_WHERE_ORDERING_OPS.includes(op.value);
|
|
9976
|
+
if (ordering && value.kind !== "num" && value.kind !== "ref") {
|
|
9977
|
+
error("RUB_EXPR_SYNTAX", `countWhere's '${op.value}' compares numbers \u2014 only == and != take a true/false or text value`, path);
|
|
9978
|
+
}
|
|
9979
|
+
}
|
|
9980
|
+
}
|
|
9981
|
+
node.args.forEach((arg, i) => checkNode(arg, node.name === "countWhere" && (i === 1 || i === 2)));
|
|
9982
|
+
break;
|
|
9983
|
+
}
|
|
9984
|
+
case "bin":
|
|
9985
|
+
checkNode(node.left, false);
|
|
9986
|
+
checkNode(node.right, false);
|
|
9987
|
+
break;
|
|
9988
|
+
case "neg":
|
|
9989
|
+
checkNode(node.operand, false);
|
|
9990
|
+
break;
|
|
9991
|
+
default:
|
|
9992
|
+
break;
|
|
9993
|
+
}
|
|
9994
|
+
};
|
|
9995
|
+
checkNode(root, false);
|
|
9996
|
+
};
|
|
9997
|
+
const seenCodes = /* @__PURE__ */ new Map();
|
|
9998
|
+
const checkDuplicate = (code, path) => {
|
|
9999
|
+
if (typeof code !== "string" || !code) return;
|
|
10000
|
+
const first = seenCodes.get(code);
|
|
10001
|
+
if (first) error("RUB_DUPLICATE_CODE", `'${code}' already used at ${first}`, path);
|
|
10002
|
+
else seenCodes.set(code, path);
|
|
10003
|
+
};
|
|
10004
|
+
const earlier = /* @__PURE__ */ new Set();
|
|
10005
|
+
(body.derived ?? []).forEach((derived, i) => {
|
|
10006
|
+
if (typeof derived.name !== "string" || !DERIVED_NAME.test(derived.name)) {
|
|
10007
|
+
error("RUB_EXPR_SYNTAX", `'${String(derived.name)}' is not a valid derived name`, `/derived/${i}/name`);
|
|
10008
|
+
}
|
|
10009
|
+
checkDuplicate(derived.name, `/derived/${i}/name`);
|
|
10010
|
+
if (typeof derived.expr === "string") checkExpression(derived.expr, `/derived/${i}/expr`, earlier);
|
|
10011
|
+
earlier.add(derived.name);
|
|
10012
|
+
});
|
|
10013
|
+
(body.gates ?? []).forEach((gate, i) => {
|
|
10014
|
+
checkDuplicate(gate.code, `/gates/${i}/code`);
|
|
10015
|
+
const klass = gate.class;
|
|
10016
|
+
if (typeof klass !== "string" || !GATE_FAILURE_CLASSES.includes(klass)) {
|
|
10017
|
+
error("RUB_SCHEMA", `reject rule '${gate.code}' is missing the outcome it declares (it must be one of ${GATE_FAILURE_CLASSES.join(", ")})`, `/gates/${i}/class`);
|
|
10018
|
+
}
|
|
10019
|
+
if (gate.appliesWhen) checkCondition(gate.appliesWhen, `/gates/${i}/appliesWhen`);
|
|
10020
|
+
checkCondition(gate.when, `/gates/${i}/when`);
|
|
10021
|
+
});
|
|
10022
|
+
const scoring = body.scoring;
|
|
10023
|
+
const criteria = scoring?.criteria ?? [];
|
|
10024
|
+
const reviewTaskIds = new Set((body.review?.tasks ?? []).map((t) => t.id));
|
|
10025
|
+
if (scoring) {
|
|
10026
|
+
const { approveAt, partialFloor, reviewFloor } = scoring;
|
|
10027
|
+
if (typeof partialFloor === "number" && approveAt <= partialFloor) {
|
|
10028
|
+
error("RUB_THRESHOLD_ORDER", `approveAt (${approveAt}) must be greater than partialFloor (${partialFloor})`, "/scoring");
|
|
10029
|
+
}
|
|
10030
|
+
if (typeof reviewFloor === "number" && approveAt <= reviewFloor) {
|
|
10031
|
+
error("RUB_THRESHOLD_ORDER", `approveAt (${approveAt}) must be greater than reviewFloor (${reviewFloor})`, "/scoring");
|
|
10032
|
+
}
|
|
10033
|
+
if (typeof partialFloor === "number" && typeof reviewFloor === "number" && partialFloor < reviewFloor) {
|
|
10034
|
+
error("RUB_THRESHOLD_ORDER", `partialFloor (${partialFloor}) must be at least reviewFloor (${reviewFloor})`, "/scoring");
|
|
10035
|
+
}
|
|
10036
|
+
const quorum = scoring.quorum;
|
|
10037
|
+
if (quorum && typeof quorum.need === "number" && typeof quorum.of === "number" && quorum.need > quorum.of) {
|
|
10038
|
+
error("RUB_QUORUM_INVALID", `quorum needs ${quorum.need} of ${quorum.of} \u2014 need cannot exceed of`, "/scoring/quorum");
|
|
10039
|
+
}
|
|
10040
|
+
if (scoring.method !== "weighted_average" && criteria.length > 0) {
|
|
10041
|
+
warn("RUB_WEIGHT_IGNORED", `'${scoring.method}' combines rules by min/count and never reads weights \u2014 the stars set on your scored rules have no effect`, "/scoring/method");
|
|
10042
|
+
}
|
|
10043
|
+
criteria.forEach((criterion, i) => {
|
|
10044
|
+
const path = `/scoring/criteria/${i}`;
|
|
10045
|
+
checkDuplicate(criterion.code, `${path}/code`);
|
|
10046
|
+
if (typeof criterion.weight !== "number" || !Number.isInteger(criterion.weight) || criterion.weight < 1 || criterion.weight > 5) {
|
|
10047
|
+
error("RUB_WEIGHT_INVALID", `weight must be an integer 1..5, got ${String(criterion.weight)}`, `${path}/weight`);
|
|
10048
|
+
}
|
|
10049
|
+
if (criterion.appliesWhen) checkCondition(criterion.appliesWhen, `${path}/appliesWhen`);
|
|
10050
|
+
checkScoreNode(criterion, `${path}/score`);
|
|
10051
|
+
});
|
|
10052
|
+
}
|
|
10053
|
+
function checkScoreNode(criterion, path) {
|
|
10054
|
+
const score = criterion.score;
|
|
10055
|
+
if (!score || typeof score !== "object") return;
|
|
10056
|
+
switch (score["@type"]) {
|
|
10057
|
+
case "BooleanScore":
|
|
10058
|
+
checkCondition(score.when, `${path}/when`);
|
|
10059
|
+
break;
|
|
10060
|
+
case "BandsScore":
|
|
10061
|
+
checkNumericValueRef(score.value, `${path}/value`, "BandsScore");
|
|
10062
|
+
(score.bands ?? []).forEach((band, i) => {
|
|
10063
|
+
if ("op" in band) checkNumericAnchor(band.value, `${path}/bands/${i}/value`);
|
|
10064
|
+
});
|
|
10065
|
+
checkBandsReachable(score.bands, `${path}/bands`);
|
|
10066
|
+
break;
|
|
10067
|
+
case "LinearScore":
|
|
10068
|
+
checkNumericValueRef(score.value, `${path}/value`, "LinearScore");
|
|
10069
|
+
checkNumericAnchor(score.zeroAt, `${path}/zeroAt`);
|
|
10070
|
+
checkNumericAnchor(score.fullAt, `${path}/fullAt`);
|
|
10071
|
+
break;
|
|
10072
|
+
case "LevelsScore": {
|
|
10073
|
+
let elseAt = -1;
|
|
10074
|
+
(score.levels ?? []).forEach((level, i) => {
|
|
10075
|
+
if (elseAt >= 0) warn("RUB_UNREACHABLE_BAND", `level shadowed by the catch-all level at index ${elseAt}`, `${path}/levels/${i}`);
|
|
10076
|
+
if ("else" in level && level.else) elseAt = i;
|
|
10077
|
+
else if ("when" in level) checkCondition(level.when, `${path}/levels/${i}/when`);
|
|
10078
|
+
});
|
|
10079
|
+
break;
|
|
10080
|
+
}
|
|
10081
|
+
case "MapScore": {
|
|
10082
|
+
if (typeof score.value === "string" && score.value.startsWith("$")) checkFieldRef(score.value, `${path}/value`);
|
|
10083
|
+
else if (typeof score.value === "string" && score.value.startsWith("~")) checkValueRef(score.value, `${path}/value`);
|
|
10084
|
+
else error("RUB_FIELD_PATH", "MapScore.value must be a $answer or ~derived ref", `${path}/value`);
|
|
10085
|
+
break;
|
|
10086
|
+
}
|
|
10087
|
+
case "DeductionsScore":
|
|
10088
|
+
(score.deduct ?? []).forEach((deduction, i) => {
|
|
10089
|
+
checkDuplicate(deduction.code, `${path}/deduct/${i}/code`);
|
|
10090
|
+
checkCondition(deduction.when, `${path}/deduct/${i}/when`);
|
|
10091
|
+
});
|
|
10092
|
+
break;
|
|
10093
|
+
case "ManualScore":
|
|
10094
|
+
if (typeof score.field === "string" && score.field.startsWith("$")) checkFieldRef(score.field, `${path}/field`);
|
|
10095
|
+
else error("RUB_FIELD_PATH", "ManualScore.field must be a $answer ref", `${path}/field`);
|
|
10096
|
+
if (!reviewTaskIds.has(score.reviewTask)) {
|
|
10097
|
+
error("RUB_REVIEW_TASK_UNMAPPED", `ManualScore names review task '${String(score.reviewTask)}' but no such task exists`, `${path}/reviewTask`);
|
|
10098
|
+
}
|
|
10099
|
+
break;
|
|
10100
|
+
default:
|
|
10101
|
+
break;
|
|
10102
|
+
}
|
|
10103
|
+
}
|
|
10104
|
+
function checkNumericAnchor(value, path) {
|
|
10105
|
+
if (typeof value === "number") return;
|
|
10106
|
+
if (typeof value !== "string" || !value.startsWith("~")) {
|
|
10107
|
+
error("RUB_VALUE_TYPE", `'${String(value)}' is not a number \u2014 an anchor takes a number or a ~calculated value`, path);
|
|
10108
|
+
return;
|
|
10109
|
+
}
|
|
10110
|
+
const kind = checkValueRef(value, path);
|
|
10111
|
+
if (kind && kind !== "number") error("RUB_VALUE_TYPE", `'${value}' is a ${kind}, expected a number`, path);
|
|
10112
|
+
}
|
|
10113
|
+
function checkNumericValueRef(value, path, node) {
|
|
10114
|
+
if (typeof value !== "string" || !(value.startsWith("$") || value.startsWith("~") || value.startsWith("ext."))) {
|
|
10115
|
+
error("RUB_FIELD_PATH", `${node}.value must be a $answer, ~derived, or ext.<name>.score ref`, path);
|
|
10116
|
+
return;
|
|
10117
|
+
}
|
|
10118
|
+
const kind = value.startsWith("$") ? checkFieldRef(value, path) : checkValueRef(value, path);
|
|
10119
|
+
if (kind && kind !== "number") error("RUB_OPERATOR_TYPE_MISMATCH", `${node} needs a number, '${value}' is a ${kind}`, path);
|
|
10120
|
+
}
|
|
10121
|
+
function checkBandsReachable(bands, path) {
|
|
10122
|
+
if (!Array.isArray(bands)) return;
|
|
10123
|
+
let elseAt = -1;
|
|
10124
|
+
bands.forEach((band, i) => {
|
|
10125
|
+
if (elseAt >= 0) {
|
|
10126
|
+
warn("RUB_UNREACHABLE_BAND", `band shadowed by the catch-all band at index ${elseAt}`, `${path}/${i}`);
|
|
10127
|
+
return;
|
|
10128
|
+
}
|
|
10129
|
+
if ("else" in band && band.else) {
|
|
10130
|
+
elseAt = i;
|
|
10131
|
+
return;
|
|
10132
|
+
}
|
|
10133
|
+
for (let j = 0; j < i; j++) {
|
|
10134
|
+
const earlierBand = bands[j];
|
|
10135
|
+
if ("else" in earlierBand) continue;
|
|
10136
|
+
if ("op" in band && bandCovers(earlierBand, band)) {
|
|
10137
|
+
warn("RUB_UNREACHABLE_BAND", `band shadowed by the band at index ${j}`, `${path}/${i}`);
|
|
10138
|
+
break;
|
|
10139
|
+
}
|
|
10140
|
+
}
|
|
10141
|
+
});
|
|
10142
|
+
}
|
|
10143
|
+
(body.review?.tasks ?? []).forEach((task, i) => {
|
|
10144
|
+
const path = `/review/tasks/${i}`;
|
|
10145
|
+
checkDuplicate(task.id, `${path}/id`);
|
|
10146
|
+
(task.show ?? []).forEach((ref, j) => {
|
|
10147
|
+
if (typeof ref === "string" && ref.startsWith("$")) checkFieldRef(ref, `${path}/show/${j}`);
|
|
10148
|
+
else error("RUB_FIELD_PATH", "show entries must be $answer refs", `${path}/show/${j}`);
|
|
10149
|
+
});
|
|
10150
|
+
const map = task.map ?? {};
|
|
10151
|
+
for (const [answer, verdict] of Object.entries(map)) {
|
|
10152
|
+
if (verdict !== "approve" && verdict !== "partial" && verdict !== "reject") {
|
|
10153
|
+
error("RUB_VALUE_TYPE", `answer '${answer}' maps to '${String(verdict)}' \u2014 must be approve | partial | reject`, `${path}/map/${answer}`);
|
|
10154
|
+
}
|
|
10155
|
+
}
|
|
10156
|
+
const required = task.answer?.type === "boolean" ? ["true", "false"] : task.answer?.options ?? [];
|
|
10157
|
+
for (const answer of required) {
|
|
10158
|
+
if (!(answer in map)) error("RUB_REVIEW_ANSWER_UNMAPPED", `answer '${answer}' has no verdict mapping`, `${path}/map`);
|
|
10159
|
+
}
|
|
10160
|
+
});
|
|
10161
|
+
const seenSourceNames = /* @__PURE__ */ new Set();
|
|
10162
|
+
(body.sources ?? []).forEach((source, i) => {
|
|
10163
|
+
const path = `/sources/${i}`;
|
|
10164
|
+
const name = source.name;
|
|
10165
|
+
if (typeof name !== "string" || !SOURCE_NAME.test(name)) {
|
|
10166
|
+
error("RUB_SCHEMA", `external check name '${String(name)}' may use only letters, numbers, dashes and underscores`, `${path}/name`);
|
|
10167
|
+
} else if (seenSourceNames.has(name)) {
|
|
10168
|
+
error("RUB_SCHEMA", `two external checks are named '${name}' \u2014 a name must be unique so ext.${name}.* points at one`, `${path}/name`);
|
|
10169
|
+
} else {
|
|
10170
|
+
seenSourceNames.add(name);
|
|
10171
|
+
}
|
|
10172
|
+
if (typeof source.endpoint !== "string" || !/^https?:\/\//.test(source.endpoint)) {
|
|
10173
|
+
error("RUB_SCHEMA", `external check '${String(name)}' needs a web address starting with http:// or https://`, `${path}/endpoint`);
|
|
10174
|
+
}
|
|
10175
|
+
if (typeof source.audience !== "string" || !/^did:web:/.test(source.audience)) {
|
|
10176
|
+
error("RUB_SCHEMA", `external check '${String(name)}' needs an audience starting with did:web:`, `${path}/audience`);
|
|
10177
|
+
}
|
|
10178
|
+
const send = source.send;
|
|
10179
|
+
if (!send || typeof send !== "object" || Array.isArray(send) || Object.keys(send).length === 0) {
|
|
10180
|
+
error("RUB_SCHEMA", `external check '${String(name)}' must send at least one value`, `${path}/send`);
|
|
10181
|
+
} else {
|
|
10182
|
+
for (const [key, value] of Object.entries(send)) {
|
|
10183
|
+
if (!SOURCE_NAME.test(key)) {
|
|
10184
|
+
error("RUB_SCHEMA", `external check '${String(name)}' has a send key '${key}' that may use only letters, numbers, dashes and underscores`, `${path}/send/${key}`);
|
|
10185
|
+
}
|
|
10186
|
+
if (isRefString(value)) checkValueRef(value, `${path}/send/${key}`);
|
|
10187
|
+
}
|
|
10188
|
+
}
|
|
10189
|
+
});
|
|
10190
|
+
const seenAiNames = /* @__PURE__ */ new Set();
|
|
10191
|
+
(body.aiChecks ?? []).forEach((check, i) => {
|
|
10192
|
+
const path = `/aiChecks/${i}`;
|
|
10193
|
+
const name = check.name;
|
|
10194
|
+
if (typeof name !== "string" || !SOURCE_NAME.test(name)) {
|
|
10195
|
+
error("RUB_SCHEMA", `AI check name '${String(name)}' may use only letters, numbers, dashes and underscores`, `${path}/name`);
|
|
10196
|
+
} else if (seenAiNames.has(name)) {
|
|
10197
|
+
error("RUB_SCHEMA", `two AI checks are named '${name}' \u2014 a name must be unique so ai.${name}.* points at one`, `${path}/name`);
|
|
10198
|
+
} else {
|
|
10199
|
+
seenAiNames.add(name);
|
|
10200
|
+
}
|
|
10201
|
+
const instructions = check.instructions;
|
|
10202
|
+
if (!Array.isArray(instructions) || instructions.filter((rule) => typeof rule === "string" && rule.trim().length > 0).length === 0) {
|
|
10203
|
+
error("RUB_SCHEMA", `AI check '${String(name)}' has nothing to check \u2014 write at least one rule for the AI`, `${path}/instructions`);
|
|
10204
|
+
}
|
|
10205
|
+
const send = check.send;
|
|
10206
|
+
if (!send || typeof send !== "object" || Array.isArray(send) || Object.keys(send).length === 0) {
|
|
10207
|
+
error("RUB_SCHEMA", `AI check '${String(name)}' must send at least one value`, `${path}/send`);
|
|
10208
|
+
} else {
|
|
10209
|
+
for (const [key, value] of Object.entries(send)) {
|
|
10210
|
+
if (!SOURCE_NAME.test(key)) {
|
|
10211
|
+
error("RUB_SCHEMA", `AI check '${String(name)}' has a send key '${key}' that may use only letters, numbers, dashes and underscores`, `${path}/send/${key}`);
|
|
10212
|
+
}
|
|
10213
|
+
if (isRefString(value)) checkValueRef(value, `${path}/send/${key}`);
|
|
10214
|
+
}
|
|
10215
|
+
}
|
|
10216
|
+
});
|
|
10217
|
+
(body.unique?.by ?? []).forEach((entry, i) => {
|
|
10218
|
+
const ref = typeof entry === "string" ? entry : entry?.ref;
|
|
10219
|
+
const path = `/unique/by/${i}`;
|
|
10220
|
+
if (typeof ref !== "string" || ref.length === 0) {
|
|
10221
|
+
error("RUB_FIELD_PATH", "a unique-key entry must be a field reference", path);
|
|
10222
|
+
} else if (ref.startsWith("~")) {
|
|
10223
|
+
error("RUB_UNIQUE_KEY_DERIVED", `calculated value '${ref}' cannot be part of the unique key \u2014 pick answer fields`, path);
|
|
10224
|
+
} else if (ref.startsWith("$")) {
|
|
10225
|
+
checkFieldRef(ref, path);
|
|
10226
|
+
} else {
|
|
10227
|
+
error("RUB_FIELD_PATH", `'${ref}' is not an answer-field reference ($answer)`, path);
|
|
10228
|
+
}
|
|
10229
|
+
});
|
|
10230
|
+
const prorateBy = body.settlement?.onPartial?.prorateBy;
|
|
10231
|
+
if (prorateBy !== void 0 && prorateBy !== "total" && prorateBy !== "none") {
|
|
10232
|
+
if (typeof prorateBy === "string" && (prorateBy.startsWith("$") || prorateBy.startsWith("~"))) {
|
|
10233
|
+
checkValueRef(prorateBy, "/settlement/onPartial/prorateBy");
|
|
10234
|
+
} else {
|
|
10235
|
+
error("RUB_VALUE_TYPE", "prorateBy must be 'total', 'none', or a $answer/~derived fraction", "/settlement/onPartial/prorateBy");
|
|
10236
|
+
}
|
|
10237
|
+
}
|
|
10238
|
+
if ((body.gates ?? []).length === 0 && criteria.length === 0) {
|
|
10239
|
+
error("RUB_NO_RULES", "no rules at all \u2014 this rubric would approve (and charge for) every claim; add at least one rule", "");
|
|
10240
|
+
}
|
|
10241
|
+
return diags;
|
|
10242
|
+
}
|
|
10243
|
+
function indexCatalog(catalog) {
|
|
10244
|
+
const index = /* @__PURE__ */ new Map();
|
|
10245
|
+
const add = (field) => {
|
|
10246
|
+
index.set(field.path, field);
|
|
10247
|
+
field.columns?.forEach(add);
|
|
10248
|
+
};
|
|
10249
|
+
for (const field of catalog.fields ?? []) add(field);
|
|
10250
|
+
return index;
|
|
10251
|
+
}
|
|
10252
|
+
function bandCovers(earlier, later) {
|
|
10253
|
+
if (!("op" in earlier)) return false;
|
|
10254
|
+
const [a, b] = [earlier.value, later.value];
|
|
10255
|
+
if (typeof a !== "number" || typeof b !== "number") return false;
|
|
10256
|
+
switch (earlier.op) {
|
|
10257
|
+
case ">=":
|
|
10258
|
+
return later.op === ">=" || later.op === ">" ? a <= b : later.op === "==" ? b >= a : false;
|
|
10259
|
+
case ">":
|
|
10260
|
+
return later.op === ">" ? a <= b : later.op === ">=" ? a < b : later.op === "==" ? b > a : false;
|
|
10261
|
+
case "<=":
|
|
10262
|
+
return later.op === "<=" || later.op === "<" ? a >= b : later.op === "==" ? b <= a : false;
|
|
10263
|
+
case "<":
|
|
10264
|
+
return later.op === "<" ? a >= b : later.op === "<=" ? a > b : later.op === "==" ? b < a : false;
|
|
10265
|
+
case "==":
|
|
10266
|
+
return later.op === "==" && a === b;
|
|
10267
|
+
default:
|
|
10268
|
+
return false;
|
|
10269
|
+
}
|
|
10270
|
+
}
|
|
10271
|
+
function firstOverlap(a, b) {
|
|
10272
|
+
if (a.wild || b.wild) return true;
|
|
10273
|
+
return a.char === b.char;
|
|
10274
|
+
}
|
|
10275
|
+
function unsafeRegexReason(src) {
|
|
10276
|
+
let pos = 0;
|
|
10277
|
+
const parseAlternation = () => {
|
|
10278
|
+
let unbounded = false;
|
|
10279
|
+
let ambiguous = false;
|
|
10280
|
+
const firsts = [];
|
|
10281
|
+
let first = { wild: true };
|
|
10282
|
+
let atStart = true;
|
|
10283
|
+
for (; ; ) {
|
|
10284
|
+
const seq = parseSequence();
|
|
10285
|
+
if (atStart) {
|
|
10286
|
+
first = seq.first;
|
|
10287
|
+
atStart = false;
|
|
10288
|
+
}
|
|
10289
|
+
unbounded = seq.unbounded || unbounded;
|
|
10290
|
+
ambiguous = seq.ambiguous || ambiguous;
|
|
10291
|
+
firsts.push(seq.first);
|
|
10292
|
+
if (src[pos] === "|") {
|
|
10293
|
+
pos++;
|
|
10294
|
+
continue;
|
|
10295
|
+
}
|
|
10296
|
+
break;
|
|
10297
|
+
}
|
|
10298
|
+
if (firsts.length > 1) {
|
|
10299
|
+
for (let i = 0; i < firsts.length && !ambiguous; i++) {
|
|
10300
|
+
for (let j = i + 1; j < firsts.length; j++) {
|
|
10301
|
+
if (firstOverlap(firsts[i], firsts[j])) {
|
|
10302
|
+
ambiguous = true;
|
|
10303
|
+
break;
|
|
10304
|
+
}
|
|
10305
|
+
}
|
|
10306
|
+
}
|
|
10307
|
+
}
|
|
10308
|
+
return { unbounded, ambiguous, first };
|
|
10309
|
+
};
|
|
10310
|
+
const parseSequence = () => {
|
|
10311
|
+
let unbounded = false;
|
|
10312
|
+
let ambiguous = false;
|
|
10313
|
+
let first = { wild: true };
|
|
10314
|
+
let atStart = true;
|
|
10315
|
+
while (pos < src.length && src[pos] !== "|" && src[pos] !== ")") {
|
|
10316
|
+
const atom = parseAtom();
|
|
10317
|
+
const quantifier = parseQuantifier();
|
|
10318
|
+
if (atStart) {
|
|
10319
|
+
first = quantifier?.nullable ? { wild: true } : atom.first;
|
|
10320
|
+
atStart = false;
|
|
10321
|
+
}
|
|
10322
|
+
if (quantifier?.unbounded) {
|
|
10323
|
+
if (atom.unbounded) throw new Error("unbounded repetition nested inside unbounded repetition");
|
|
10324
|
+
if (atom.ambiguous) throw new Error("a repeated group with overlapping choices can backtrack catastrophically");
|
|
10325
|
+
unbounded = true;
|
|
10326
|
+
}
|
|
10327
|
+
unbounded = unbounded || atom.unbounded;
|
|
10328
|
+
ambiguous = ambiguous || atom.ambiguous;
|
|
10329
|
+
}
|
|
10330
|
+
return { unbounded, ambiguous, first };
|
|
10331
|
+
};
|
|
10332
|
+
const parseAtom = () => {
|
|
10333
|
+
const ch = src[pos];
|
|
10334
|
+
if (ch === "(") {
|
|
10335
|
+
pos++;
|
|
10336
|
+
if (src[pos] === "?") {
|
|
10337
|
+
if (src[pos + 1] === ":") pos += 2;
|
|
10338
|
+
else throw new Error("lookaround and special groups are not allowed");
|
|
10339
|
+
}
|
|
10340
|
+
const inner = parseAlternation();
|
|
10341
|
+
if (src[pos] !== ")") throw new Error("unbalanced group");
|
|
10342
|
+
pos++;
|
|
10343
|
+
return { unbounded: inner.unbounded, ambiguous: inner.ambiguous, first: { wild: true } };
|
|
10344
|
+
}
|
|
10345
|
+
if (ch === "[") {
|
|
10346
|
+
pos++;
|
|
10347
|
+
if (src[pos] === "^") pos++;
|
|
10348
|
+
if (src[pos] === "]") pos++;
|
|
10349
|
+
while (pos < src.length && src[pos] !== "]") {
|
|
10350
|
+
if (src[pos] === "\\") pos++;
|
|
10351
|
+
pos++;
|
|
10352
|
+
}
|
|
10353
|
+
if (src[pos] !== "]") throw new Error("unterminated character class");
|
|
10354
|
+
pos++;
|
|
10355
|
+
return { unbounded: false, ambiguous: false, first: { wild: true } };
|
|
10356
|
+
}
|
|
10357
|
+
if (ch === "\\") {
|
|
10358
|
+
const next = src[pos + 1];
|
|
10359
|
+
if (next === void 0) throw new Error("dangling escape");
|
|
10360
|
+
if (/[1-9]/.test(next)) throw new Error("backreferences are not allowed");
|
|
10361
|
+
if (next === "k") throw new Error("named backreferences are not allowed");
|
|
10362
|
+
pos += 2;
|
|
10363
|
+
return { unbounded: false, ambiguous: false, first: { wild: true } };
|
|
10364
|
+
}
|
|
10365
|
+
if (ch === "*" || ch === "+" || ch === "?") throw new Error("quantifier without a target");
|
|
10366
|
+
pos++;
|
|
10367
|
+
const first = ch === "." || ch === "^" || ch === "$" ? { wild: true } : { wild: false, char: ch };
|
|
10368
|
+
return { unbounded: false, ambiguous: false, first };
|
|
10369
|
+
};
|
|
10370
|
+
const parseQuantifier = () => {
|
|
10371
|
+
const ch = src[pos];
|
|
10372
|
+
if (ch === "*") {
|
|
10373
|
+
pos++;
|
|
10374
|
+
if (src[pos] === "?") pos++;
|
|
10375
|
+
return { unbounded: true, nullable: true };
|
|
10376
|
+
}
|
|
10377
|
+
if (ch === "+") {
|
|
10378
|
+
pos++;
|
|
10379
|
+
if (src[pos] === "?") pos++;
|
|
10380
|
+
return { unbounded: true, nullable: false };
|
|
10381
|
+
}
|
|
10382
|
+
if (ch === "?") {
|
|
10383
|
+
pos++;
|
|
10384
|
+
if (src[pos] === "?") pos++;
|
|
10385
|
+
return { unbounded: false, nullable: true };
|
|
10386
|
+
}
|
|
10387
|
+
if (ch === "{") {
|
|
10388
|
+
const match = /^\{(\d+)(?:(,)(\d+)?)?\}/.exec(src.slice(pos));
|
|
10389
|
+
if (!match) return null;
|
|
10390
|
+
pos += match[0].length;
|
|
10391
|
+
if (src[pos] === "?") pos++;
|
|
10392
|
+
return { unbounded: match[2] === "," && match[3] === void 0, nullable: Number(match[1]) === 0 };
|
|
10393
|
+
}
|
|
10394
|
+
return null;
|
|
10395
|
+
};
|
|
10396
|
+
try {
|
|
10397
|
+
parseAlternation();
|
|
10398
|
+
if (pos < src.length) throw new Error("unbalanced group");
|
|
10399
|
+
return null;
|
|
10400
|
+
} catch (e) {
|
|
10401
|
+
return e instanceof Error ? e.message : String(e);
|
|
10402
|
+
}
|
|
10403
|
+
}
|
|
10404
|
+
|
|
10405
|
+
// src/core/lib/actionRegistry/actions/evalRubric/evalRubric.ts
|
|
10406
|
+
async function preflightEvalRubric(inputs, ctx, settings) {
|
|
10407
|
+
const service = ctx.services.rubric;
|
|
10408
|
+
if (!service) {
|
|
10409
|
+
throw new Error("rubric service not configured (ctx.services.rubric is undefined)");
|
|
10410
|
+
}
|
|
10411
|
+
const handlers = ctx.handlers;
|
|
10412
|
+
if (!handlers) {
|
|
10413
|
+
throw new Error("handlers not available (ctx.handlers is undefined)");
|
|
10414
|
+
}
|
|
10415
|
+
const deedDid = String(inputs.deedDid || "").trim();
|
|
10416
|
+
const collectionId = String(inputs.collectionId || "").trim();
|
|
10417
|
+
const evalEngineUrl = String(inputs.evalEngineUrl || "").trim() || void 0;
|
|
10418
|
+
if (!deedDid) throw new Error("deedDid (entity/deed DID) is required");
|
|
10419
|
+
if (!collectionId) throw new Error("collectionId is required");
|
|
10420
|
+
const rubric = dropRemovedKeys(parseObjectInput(inputs.rubric, "rubric"));
|
|
10421
|
+
const snapshot = parseObjectInput(inputs.claimSchemaSnapshot, "claimSchemaSnapshot");
|
|
10422
|
+
if (!Array.isArray(snapshot.fields)) {
|
|
10423
|
+
throw new Error("claimSchemaSnapshot must carry the authored field catalog (fields[])");
|
|
10424
|
+
}
|
|
10425
|
+
throwOnErrors(validateRubric(rubric, snapshot), "The rules are not valid");
|
|
10426
|
+
if (typeof handlers.getDeedSurveyTemplate !== "function") {
|
|
10427
|
+
throw new Error("getDeedSurveyTemplate handler not implemented \u2014 the rules cannot be re-checked against the live claim form");
|
|
10428
|
+
}
|
|
10429
|
+
const template = await hostCall("getDeedSurveyTemplate", () => handlers.getDeedSurveyTemplate(deedDid, collectionId));
|
|
10430
|
+
const surveyTemplate = template?.surveyTemplate;
|
|
10431
|
+
if (!surveyTemplate) {
|
|
10432
|
+
throw new Error(`No claim form found for collection ${collectionId}. Rules can only be published against an existing form.`);
|
|
10433
|
+
}
|
|
10434
|
+
const liveCatalog = extractRubricFieldCatalog(surveyTemplate, String(template?.proof || "") || snapshot.proof);
|
|
10435
|
+
throwOnErrors(validateRubric(rubric, liveCatalog, snapshot), "The claim form changed since these rules were written");
|
|
10436
|
+
console.info("[eval.rubric] step validate \u2014 rules valid against the snapshot and the live form");
|
|
10437
|
+
const preview = service.previewRubric ? await service.previewRubric({ collectionId, rubric, evalEngineUrl, interactive: true, ...settings ? { settings } : {} }).catch((error) => {
|
|
10438
|
+
throw new Error(`Couldn't reach the rules service to check your rules \u2014 try again. (${errorMessageOf(error)})`);
|
|
10439
|
+
}) : null;
|
|
10440
|
+
if (preview) {
|
|
10441
|
+
throwOnErrors(preview.problems, "These rules cannot be published");
|
|
10442
|
+
console.info("[eval.rubric] step engine preview \u2014 passed");
|
|
10443
|
+
} else {
|
|
10444
|
+
const shape = await validateRubricShape(rubric, service.getRubricSchema, evalEngineUrl);
|
|
10445
|
+
if (!shape.ok) {
|
|
10446
|
+
throw new Error(`Couldn't reach the rules service to check your rules \u2014 try again. (${shape.reason})`);
|
|
10447
|
+
}
|
|
10448
|
+
throwOnErrors(shape.problems, "These rules have a setting the rules service will not accept");
|
|
10449
|
+
console.info("[eval.rubric] step schema gate \u2014 passed (host has no previewRubric)");
|
|
10450
|
+
}
|
|
10451
|
+
return { rubric, collectionId, evalEngineUrl };
|
|
10452
|
+
}
|
|
10453
|
+
async function publishEvalRubric(preflight, ctx) {
|
|
10454
|
+
const service = ctx.services.rubric;
|
|
10455
|
+
if (!service) {
|
|
10456
|
+
throw new Error("rubric service not configured (ctx.services.rubric is undefined)");
|
|
10457
|
+
}
|
|
10458
|
+
const handlers = ctx.handlers;
|
|
10459
|
+
if (!handlers) {
|
|
10460
|
+
throw new Error("handlers not available (ctx.handlers is undefined)");
|
|
10461
|
+
}
|
|
10462
|
+
const { rubric, collectionId, evalEngineUrl } = preflight;
|
|
10463
|
+
const bytes = canonicalJson(buildRubricEnvelope(rubric));
|
|
10464
|
+
console.info("[eval.rubric] step canonicalize \u2014 envelope bytes built", { byteLength: bytes.length });
|
|
10465
|
+
const uploaded = await hostCall("uploadRubric", () => service.uploadRubric({ json: bytes, fileName: "rubric" }));
|
|
10466
|
+
console.info("[eval.rubric] step upload \u2014 stored", { url: uploaded?.url, sha256: uploaded?.sha256 });
|
|
10467
|
+
const resourceUrl = String(uploaded?.url || "").trim();
|
|
10468
|
+
if (!resourceUrl) {
|
|
10469
|
+
throw new Error("uploadRubric returned no url. The rubric document was not stored.");
|
|
10470
|
+
}
|
|
10471
|
+
const rubricId = String(uploaded?.sha256 || "").trim();
|
|
10472
|
+
if (!rubricId) {
|
|
10473
|
+
throw new Error("uploadRubric returned no sha256. Without the content hash the rubric has no id and cannot be anchored.");
|
|
10474
|
+
}
|
|
10475
|
+
if (typeof handlers.createAddLinkedResourceMessage !== "function") {
|
|
10476
|
+
throw new Error("createAddLinkedResourceMessage handler not implemented");
|
|
10477
|
+
}
|
|
10478
|
+
if (typeof handlers.executeTransaction !== "function") {
|
|
10479
|
+
throw new Error("executeTransaction handler not implemented");
|
|
10480
|
+
}
|
|
10481
|
+
const protocolDid = String(rubric.claimSchema?.protocol || "").trim();
|
|
10482
|
+
if (!protocolDid) {
|
|
10483
|
+
throw new Error("rubric.claimSchema.protocol is required \u2014 it names the protocol entity that carries the claim form and the rules");
|
|
10484
|
+
}
|
|
10485
|
+
const messages = [];
|
|
10486
|
+
if (service.getRubricResource) {
|
|
10487
|
+
let existing = null;
|
|
10488
|
+
try {
|
|
10489
|
+
existing = await service.getRubricResource({ protocolDid });
|
|
10490
|
+
} catch (error) {
|
|
10491
|
+
console.error("[eval.rubric] getRubricResource rejected", error);
|
|
10492
|
+
throw new Error(`Couldn't check whether this protocol already has published rules \u2014 try again. (${errorMessageOf(error)})`);
|
|
10493
|
+
}
|
|
10494
|
+
console.info("[eval.rubric] step existing #rub \u2014", existing?.id ?? "none");
|
|
10495
|
+
if (existing?.id) {
|
|
10496
|
+
if (typeof handlers.createDeleteLinkedResourceMessage !== "function") {
|
|
10497
|
+
throw new Error("This protocol already has rules, and the createDeleteLinkedResourceMessage handler is not implemented \u2014 they cannot be replaced.");
|
|
10498
|
+
}
|
|
10499
|
+
messages.push(await hostCall("createDeleteLinkedResourceMessage", () => handlers.createDeleteLinkedResourceMessage({ entityDid: protocolDid, resourceId: existing.id })));
|
|
10500
|
+
}
|
|
10501
|
+
} else {
|
|
10502
|
+
console.info("[eval.rubric] step existing #rub \u2014 host has no getRubricResource; composing add-only");
|
|
10503
|
+
}
|
|
10504
|
+
const resourceId = "{id}#rub";
|
|
10505
|
+
messages.push(
|
|
10506
|
+
await hostCall(
|
|
10507
|
+
"createAddLinkedResourceMessage",
|
|
10508
|
+
() => handlers.createAddLinkedResourceMessage({
|
|
10509
|
+
entityDid: protocolDid,
|
|
10510
|
+
linkedResource: {
|
|
10511
|
+
id: resourceId,
|
|
10512
|
+
type: "rubric",
|
|
10513
|
+
description: rubric.title || "Claim Approval Rules",
|
|
10514
|
+
mediaType: "application/ld+json",
|
|
10515
|
+
serviceEndpoint: resourceUrl,
|
|
10516
|
+
// The id IS the content commitment (§A.10) — one value, both jobs.
|
|
10517
|
+
proof: rubricId,
|
|
10518
|
+
encrypted: "false",
|
|
10519
|
+
right: ""
|
|
10520
|
+
}
|
|
10521
|
+
})
|
|
10522
|
+
)
|
|
10523
|
+
);
|
|
10524
|
+
console.info(
|
|
10525
|
+
"[eval.rubric] step compose \u2014 messages",
|
|
10526
|
+
messages.map((message) => message?.typeUrl ?? typeof message)
|
|
10527
|
+
);
|
|
10528
|
+
const anchored = await hostCall("executeTransaction", () => handlers.executeTransaction({ messages, memo: "Set claim approval rules" }));
|
|
10529
|
+
console.info("[eval.rubric] step anchor \u2014 executeTransaction returned", anchored);
|
|
10530
|
+
const resourceTxHash = String(anchored?.transactionHash || "").trim();
|
|
10531
|
+
if (!resourceTxHash) {
|
|
10532
|
+
throw new Error("The anchor transaction completed but returned no transaction hash. The rules were not published.");
|
|
10533
|
+
}
|
|
10534
|
+
if (service.pinRubric) {
|
|
10535
|
+
try {
|
|
10536
|
+
await service.pinRubric({ collectionId, rubricId, evalEngineUrl });
|
|
10537
|
+
console.info("[eval.rubric] step pin \u2014 engine notified of the new rubricId");
|
|
10538
|
+
} catch (error) {
|
|
10539
|
+
console.warn(`[eval.rubric] step pin \u2014 failed for collection ${collectionId}; the engine will re-resolve from chain. ${errorMessageOf(error)}`);
|
|
10540
|
+
}
|
|
10541
|
+
} else {
|
|
10542
|
+
console.info("[eval.rubric] step pin \u2014 skipped (host has no pinRubric)");
|
|
10543
|
+
}
|
|
10544
|
+
return {
|
|
10545
|
+
output: {
|
|
10546
|
+
rubricId,
|
|
10547
|
+
resourceId,
|
|
10548
|
+
protocolDid,
|
|
10549
|
+
collectionId,
|
|
10550
|
+
resourceUrl,
|
|
10551
|
+
resourceTxHash
|
|
10552
|
+
}
|
|
10553
|
+
};
|
|
10554
|
+
}
|
|
10555
|
+
function errorMessageOf(error) {
|
|
10556
|
+
if (error instanceof Error) return error.message;
|
|
10557
|
+
if (typeof error === "string" && error) return error;
|
|
10558
|
+
if (error && typeof error === "object" && "message" in error && error.message) {
|
|
10559
|
+
return String(error.message);
|
|
10560
|
+
}
|
|
10561
|
+
try {
|
|
10562
|
+
const serialized = JSON.stringify(error);
|
|
10563
|
+
if (serialized && serialized !== "{}" && serialized !== "null" && serialized !== "undefined") return serialized;
|
|
10564
|
+
} catch {
|
|
10565
|
+
}
|
|
10566
|
+
return "Execution failed";
|
|
10567
|
+
}
|
|
10568
|
+
async function hostCall(step, call) {
|
|
10569
|
+
try {
|
|
10570
|
+
return await call();
|
|
10571
|
+
} catch (error) {
|
|
10572
|
+
console.error(`[eval.rubric] ${step} rejected`, error);
|
|
10573
|
+
throw new Error(`${step} failed: ${errorMessageOf(error)}`);
|
|
10574
|
+
}
|
|
10575
|
+
}
|
|
10576
|
+
function dropRemovedKeys(body) {
|
|
10577
|
+
const settlement = body.settlement;
|
|
10578
|
+
if (!settlement || !("maturityRung" in settlement)) return body;
|
|
10579
|
+
const { maturityRung: _removed, ...rest } = settlement;
|
|
10580
|
+
console.info("[eval.rubric] dropped removed key settlement.maturityRung from a pre-existing draft");
|
|
10581
|
+
return { ...body, settlement: rest };
|
|
10582
|
+
}
|
|
10583
|
+
function parseObjectInput(raw, name) {
|
|
10584
|
+
const value = typeof raw === "string" ? safeParse(raw, name) : raw;
|
|
10585
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
10586
|
+
throw new Error(`${name} is required and must be an object`);
|
|
10587
|
+
}
|
|
10588
|
+
return value;
|
|
10589
|
+
}
|
|
10590
|
+
function safeParse(raw, name) {
|
|
10591
|
+
try {
|
|
10592
|
+
return JSON.parse(raw);
|
|
10593
|
+
} catch {
|
|
10594
|
+
throw new Error(`${name} must be valid JSON`);
|
|
10595
|
+
}
|
|
10596
|
+
}
|
|
10597
|
+
function throwOnErrors(diagnostics, summary) {
|
|
10598
|
+
const errors = diagnostics.filter((d) => d.severity === "error");
|
|
10599
|
+
if (errors.length === 0) return;
|
|
10600
|
+
const detail = errors.map((d) => `${d.code}${d.path ? ` at ${d.path}` : ""}: ${d.message}`).join("; ");
|
|
10601
|
+
throw new Error(`${summary} \u2014 ${errors.length} problem(s) must be fixed before publishing. ${detail}`);
|
|
10602
|
+
}
|
|
10603
|
+
|
|
10604
|
+
// src/core/lib/actionRegistry/actions/evalRegister/index.ts
|
|
10605
|
+
var EVAL_REGISTER_OUTPUT_SCHEMA = [
|
|
10606
|
+
{ path: "registrationId", displayName: "Registration ID", type: "string", description: "Evaluation engine registration id (proof of enrollment)" },
|
|
10607
|
+
{ path: "collectionId", displayName: "Collection ID", type: "string", description: "Enrolled claim collection identifier" },
|
|
10608
|
+
{ path: "ownerDid", displayName: "Owner DID", type: "string", description: "Collection owner DID (delegation issuer)" },
|
|
10609
|
+
{ path: "deedDid", displayName: "Deed DID", type: "string", description: "Entity (deed) DID the collection belongs to" },
|
|
10610
|
+
{ path: "depositCid", displayName: "Deposit CID", type: "string", description: "CID of the delegation deposited in the UCAN Store" },
|
|
10611
|
+
{ path: "oracleDid", displayName: "Oracle DID", type: "string", description: "Evaluation engine DID (delegation audience), when overridden" },
|
|
10612
|
+
{
|
|
10613
|
+
path: "evaluateAuthzTxHash",
|
|
10614
|
+
displayName: "Evaluate Authz Tx",
|
|
10615
|
+
type: "string",
|
|
10616
|
+
description: "On-chain evaluate-authz grant tx hash \u2014 present when decisions are submitted on chain"
|
|
10617
|
+
}
|
|
10618
|
+
];
|
|
10619
|
+
|
|
10620
|
+
// src/core/lib/actionRegistry/actions/evalRubric/index.ts
|
|
10621
|
+
var EVAL_RUBRIC_OUTPUT_SCHEMA = [
|
|
10622
|
+
{ path: "rubricId", displayName: "Rubric ID", type: "string", description: "sha256 of the published rubric bytes \u2014 the id, and the on-chain proof (\xA7A.10)" },
|
|
10623
|
+
{ path: "resourceId", displayName: "Linked Resource ID", type: "string", description: "The #rub LinkedResource id on the protocol entity" },
|
|
10624
|
+
{ path: "protocolDid", displayName: "Protocol DID", type: "string", description: "Protocol entity the rubric is anchored on (carries #vct and #rub)" },
|
|
10625
|
+
{ path: "collectionId", displayName: "Collection ID", type: "string", description: "Claim collection these rules govern" },
|
|
10626
|
+
{ path: "resourceUrl", displayName: "Rubric URL", type: "string", description: "Public Matrix media URL of the rubric document" },
|
|
10627
|
+
{ path: "resourceTxHash", displayName: "Anchor Tx", type: "string", description: "Transaction hash of the on-chain anchor (proof of publish)" }
|
|
10628
|
+
];
|
|
10629
|
+
|
|
10630
|
+
// src/core/lib/actionRegistry/actions/evalEngine/index.ts
|
|
10631
|
+
var EVAL_ENGINE_ACTION_TYPE = "qi/eval.engine";
|
|
10632
|
+
var EVAL_ENGINE_OUTPUT_SCHEMA = [
|
|
10633
|
+
...EVAL_REGISTER_OUTPUT_SCHEMA,
|
|
10634
|
+
...EVAL_RUBRIC_OUTPUT_SCHEMA.filter((f) => f.path !== "collectionId")
|
|
10635
|
+
];
|
|
10636
|
+
|
|
10637
|
+
// src/core/lib/actionRegistry/actions/evalEngine/evalEngine.ts
|
|
10638
|
+
registerAction({
|
|
10639
|
+
type: EVAL_ENGINE_ACTION_TYPE,
|
|
10640
|
+
can: "eval/engine",
|
|
10641
|
+
sideEffect: true,
|
|
10642
|
+
dynamicResolutionMode: "replace",
|
|
10643
|
+
proof: { fields: ["registrationId", "rubricId", "resourceTxHash"] },
|
|
10644
|
+
done: doneWhenCompleted,
|
|
10645
|
+
executionOwner: "human",
|
|
10646
|
+
cardinality: "once",
|
|
10647
|
+
defaultRequiresConfirmation: true,
|
|
10648
|
+
requiredCapability: "flow/block/execute",
|
|
10649
|
+
outputSchema: EVAL_ENGINE_OUTPUT_SCHEMA,
|
|
10650
|
+
getDynamicOutputSchema: () => EVAL_ENGINE_OUTPUT_SCHEMA,
|
|
10651
|
+
// Same listener story as qi/eval.register: a Claim Collection `created` event can bind the
|
|
10652
|
+
// collection; the rules are then authored in flow mode against that collection's form.
|
|
10653
|
+
eligibleForEventTrigger: true,
|
|
10654
|
+
inputSchema: {
|
|
10655
|
+
type: "object",
|
|
10656
|
+
required: ["collectionId", "deedDid", "notifyEmail", "rubric", "claimSchemaSnapshot"],
|
|
10657
|
+
properties: {
|
|
10658
|
+
// ---- shared context ----
|
|
10659
|
+
collectionId: { type: "string", description: "Target claim collection to enroll and govern." },
|
|
10660
|
+
deedDid: { type: "string", description: "Entity (deed) DID the collection belongs to." },
|
|
10661
|
+
// ---- enrolment (qi/eval.register) ----
|
|
10662
|
+
displayName: { type: "string", description: "Optional collection name, shown on the engine's billing emails next to the collection id." },
|
|
10663
|
+
notifyEmail: { type: "string", description: "Required billing email \u2014 failed payments and paused evaluations are announced there." },
|
|
10664
|
+
// The three plain answers about what this collection collects. They are composed into the
|
|
10665
|
+
// engine's one opaque `description` string inside `runEvalRegister` — an agent authoring
|
|
10666
|
+
// this block answers the questions, it never writes the sentence.
|
|
10667
|
+
whatIsCollected: { type: "string", description: "One sentence on what people do to make a claim here (max 300)." },
|
|
10668
|
+
repeatSubmissions: {
|
|
10669
|
+
type: "string",
|
|
10670
|
+
enum: ["normal", "sometimes", "once"],
|
|
10671
|
+
description: "Can the same person claim again tomorrow? normal = expected, sometimes = happens, once = one-off only."
|
|
10672
|
+
},
|
|
10673
|
+
differentWhen: {
|
|
10674
|
+
type: "array",
|
|
10675
|
+
description: "What differs between two legitimate claims from the same person: date, amount, place, kind, photos."
|
|
10676
|
+
},
|
|
10677
|
+
differentWhenOther: { type: "string", description: "Anything else that differs, in the owner\u2019s own words (max 100)." },
|
|
10678
|
+
ownerDid: { type: "string", description: "Collection owner DID. Defaults to the connected actor." },
|
|
10679
|
+
oracleDid: { type: "string", description: "Optional override of the engine DID (delegation audience). Blank = host default." },
|
|
10680
|
+
ttlDays: { type: "number", description: "Optional delegation lifetime override in days. Blank = host default." },
|
|
10681
|
+
ucanStoreUrl: { type: "string", description: "UCAN Store base URL. Optional; host resolves from network if omitted." },
|
|
10682
|
+
evalEngineUrl: { type: "string", description: "Evaluation engine base URL. Optional; host resolves from network if omitted." },
|
|
10683
|
+
adminAddress: { type: "string", description: "Granter entity admin account address \u2014 required when decisions are submitted on chain." },
|
|
10684
|
+
oracleAddress: { type: "string", description: "Engine grantee address override for the on-chain grants. Blank = derived from the oracle DID." },
|
|
10685
|
+
pin: { type: "string", description: "Verification PIN; requested at runtime if omitted." },
|
|
10686
|
+
allowAiChecks: { type: "boolean", default: true, description: "Engine setting: allow paid AI checks for this collection." },
|
|
10687
|
+
allowImageChecks: { type: "boolean", default: true, description: "Engine setting: allow fake-photo detection (needs AI checks on)." },
|
|
10688
|
+
allowChainEvaluation: { type: "boolean", default: true, description: "Engine setting: submit the decision on chain (releases payment); makes adminAddress required." },
|
|
10689
|
+
evaluateMaxAmount: { type: "array", description: "Per-claim payout cap on the evaluate grant, base-unit coins in the owner's denoms." },
|
|
10690
|
+
// ---- rules (qi/eval.rubric) ----
|
|
10691
|
+
rubric: {
|
|
10692
|
+
type: "object",
|
|
10693
|
+
description: "The authored rubric body (\xA7A.2), the duplicate-claim `unique` block included (dup-detection spec \xA76); envelope + @id computed at publish."
|
|
10694
|
+
},
|
|
10695
|
+
claimSchemaSnapshot: { type: "object", description: "The field catalog the rules were authored against (\xA7B.4)." }
|
|
10696
|
+
}
|
|
10697
|
+
},
|
|
10698
|
+
run: async (inputs, ctx) => {
|
|
10699
|
+
if (inputs.allowChainEvaluation !== false && !String(inputs.adminAddress || "").trim()) {
|
|
10700
|
+
throw new Error("adminAddress (entity admin account) is required to grant the engine evaluator rights for on-chain decisions.");
|
|
10701
|
+
}
|
|
10702
|
+
const intendedSettings = {
|
|
10703
|
+
allowAiChecks: inputs.allowAiChecks !== false,
|
|
10704
|
+
allowImageChecks: inputs.allowAiChecks !== false && inputs.allowImageChecks !== false,
|
|
10705
|
+
allowChainEvaluation: inputs.allowChainEvaluation !== false
|
|
10706
|
+
};
|
|
10707
|
+
const preflight = await preflightEvalRubric(inputs, ctx, intendedSettings);
|
|
10708
|
+
const registered = await runEvalRegister(inputs, ctx);
|
|
10709
|
+
const published = await publishEvalRubric(preflight, ctx);
|
|
10710
|
+
return { output: { ...registered.output, ...published.output } };
|
|
10711
|
+
}
|
|
10712
|
+
});
|
|
10713
|
+
|
|
8842
10714
|
// src/core/lib/actionRegistry/actions/kycVerify.ts
|
|
8843
10715
|
registerAction({
|
|
8844
10716
|
type: "qi/kyc.verify",
|
|
@@ -11321,6 +13193,46 @@ registerDiffResolver("qi/entity.transfer", {
|
|
|
11321
13193
|
}
|
|
11322
13194
|
});
|
|
11323
13195
|
|
|
13196
|
+
// src/core/lib/actionRegistry/actions/evalEngine/evalEngine.diff.ts
|
|
13197
|
+
registerDiffResolver(EVAL_ENGINE_ACTION_TYPE, {
|
|
13198
|
+
resolver: async (inputs) => {
|
|
13199
|
+
const collectionId = String(inputs?.collectionId || "").trim();
|
|
13200
|
+
if (!collectionId) return [];
|
|
13201
|
+
const displayName = String(inputs?.displayName || "").trim();
|
|
13202
|
+
const rows = [
|
|
13203
|
+
{ key: "collection", label: "Claim collection", before: null, after: displayName ? `${displayName} (${collectionId})` : collectionId, changeType: "add" },
|
|
13204
|
+
{ key: "access", label: "Evaluation Engine", before: null, after: "Reads & evaluates this collection\u2019s claims on your behalf", changeType: "add" },
|
|
13205
|
+
{
|
|
13206
|
+
key: "decisions",
|
|
13207
|
+
label: "Decisions",
|
|
13208
|
+
before: null,
|
|
13209
|
+
after: inputs?.allowChainEvaluation !== false ? "Submitted on chain, which releases payment \u2014 you grant the engine evaluator rights" : "Recorded only \u2014 nothing is submitted on chain",
|
|
13210
|
+
changeType: "add"
|
|
13211
|
+
}
|
|
13212
|
+
];
|
|
13213
|
+
const { rubric } = parseEvalRubricInputs(inputs);
|
|
13214
|
+
if (rubric) {
|
|
13215
|
+
const rejectRules = rubric.gates?.length ?? 0;
|
|
13216
|
+
const scoredQuestions = rubric.scoring?.criteria?.length ?? 0;
|
|
13217
|
+
const humanChecks = rubric.review?.tasks?.length ?? 0;
|
|
13218
|
+
rows.push({
|
|
13219
|
+
key: "rules",
|
|
13220
|
+
label: "Rules",
|
|
13221
|
+
before: null,
|
|
13222
|
+
after: [plural(rejectRules, "reject rule"), plural(scoredQuestions, "scored question"), plural(humanChecks, "human check")].join(" \xB7 "),
|
|
13223
|
+
changeType: "add"
|
|
13224
|
+
});
|
|
13225
|
+
if (typeof rubric.scoring?.approveAt === "number") {
|
|
13226
|
+
rows.push({ key: "approveAt", label: "Approved at a score of", before: null, after: `${Math.trunc(rubric.scoring.approveAt / 100)} / 100`, changeType: "add" });
|
|
13227
|
+
}
|
|
13228
|
+
}
|
|
13229
|
+
return rows;
|
|
13230
|
+
}
|
|
13231
|
+
});
|
|
13232
|
+
function plural(count, noun) {
|
|
13233
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
13234
|
+
}
|
|
13235
|
+
|
|
11324
13236
|
// src/core/lib/flowEngine/dmNotificationState.ts
|
|
11325
13237
|
var DM_NOTIFICATIONS_MAP_KEY = "dmNotificationState";
|
|
11326
13238
|
var DM_NOTIFICATIONS_KEY = "__dm_notifications";
|
|
@@ -18279,6 +20191,7 @@ export {
|
|
|
18279
20191
|
createUcanService,
|
|
18280
20192
|
getHomeserver,
|
|
18281
20193
|
didToMatrixUserId,
|
|
20194
|
+
matrixUserIdToDid,
|
|
18282
20195
|
findOrCreateDMRoom,
|
|
18283
20196
|
sendDirectMessage,
|
|
18284
20197
|
canToType,
|
|
@@ -18344,6 +20257,21 @@ export {
|
|
|
18344
20257
|
buildGovernanceGroupLinkedEntities,
|
|
18345
20258
|
tempDomainCreatorSurvey,
|
|
18346
20259
|
resolveEntityTypeFromSchema,
|
|
20260
|
+
REPEAT_SUBMISSIONS_OPTIONS,
|
|
20261
|
+
DIFFERENT_WHEN_OPTIONS,
|
|
20262
|
+
WHAT_IS_COLLECTED_MAX,
|
|
20263
|
+
DIFFERENT_WHEN_OTHER_MAX,
|
|
20264
|
+
normalizeDifferentWhen,
|
|
20265
|
+
normalizeRepeatSubmissions,
|
|
20266
|
+
extractRubricFieldCatalog,
|
|
20267
|
+
buildRubricEnvelope,
|
|
20268
|
+
parseEvalRubricInputs,
|
|
20269
|
+
serializeEvalRubricInputs,
|
|
20270
|
+
validateRubricShape,
|
|
20271
|
+
SCHEMA_UNKNOWN_KEY_PREFIX,
|
|
20272
|
+
operatorsForKind,
|
|
20273
|
+
validateRubric,
|
|
20274
|
+
EVAL_ENGINE_ACTION_TYPE,
|
|
18347
20275
|
parseDelegatedToolInputs,
|
|
18348
20276
|
serializeDelegatedToolInputs,
|
|
18349
20277
|
fieldValues,
|
|
@@ -18575,4 +20503,4 @@ export {
|
|
|
18575
20503
|
executeQueuedFlowAgentCoreCommands,
|
|
18576
20504
|
FlowAgentService
|
|
18577
20505
|
};
|
|
18578
|
-
//# sourceMappingURL=chunk-
|
|
20506
|
+
//# sourceMappingURL=chunk-TT4TNNOV.js.map
|