kibi-cli 0.16.1 → 0.18.1
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/cli-json-command.d.ts.map +1 -1
- package/dist/cli-json-command.js +87 -11
- package/dist/cli-register-maintenance.d.ts.map +1 -1
- package/dist/cli-register-maintenance.js +8 -0
- package/dist/cli-register-reporting.d.ts.map +1 -1
- package/dist/cli-register-reporting.js +5 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -1
- package/dist/commands/aggregated-checks.d.ts.map +1 -1
- package/dist/commands/aggregated-checks.js +1 -0
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +1 -1
- package/dist/commands/coverage.d.ts +5 -0
- package/dist/commands/coverage.d.ts.map +1 -1
- package/dist/commands/coverage.js +5 -0
- package/dist/commands/discovery-table.d.ts.map +1 -1
- package/dist/commands/discovery-table.js +96 -5
- package/dist/commands/sync/cache.d.ts +2 -0
- package/dist/commands/sync/cache.d.ts.map +1 -1
- package/dist/commands/sync/cache.js +8 -0
- package/dist/commands/sync/manifest.d.ts +12 -1
- package/dist/commands/sync/manifest.d.ts.map +1 -1
- package/dist/commands/sync/manifest.js +22 -5
- package/dist/commands/sync/persistence.d.ts.map +1 -1
- package/dist/commands/sync/persistence.js +76 -2
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +72 -15
- package/dist/commands/usage-metrics.d.ts +66 -2
- package/dist/commands/usage-metrics.d.ts.map +1 -1
- package/dist/commands/usage-metrics.js +34 -24
- package/dist/commands/usage-remediation.d.ts +9 -0
- package/dist/commands/usage-remediation.d.ts.map +1 -0
- package/dist/commands/usage-remediation.js +52 -0
- package/dist/extractors/markdown.d.ts +17 -1
- package/dist/extractors/markdown.d.ts.map +1 -1
- package/dist/extractors/markdown.js +81 -2
- package/dist/logic/ir.d.ts +112 -0
- package/dist/logic/ir.d.ts.map +1 -0
- package/dist/logic/ir.js +729 -0
- package/dist/operations/modeling/logic-modeling.d.ts +22 -0
- package/dist/operations/modeling/logic-modeling.d.ts.map +1 -0
- package/dist/operations/modeling/logic-modeling.js +120 -0
- package/dist/operations/modeling/model-requirement.d.ts.map +1 -1
- package/dist/operations/modeling/model-requirement.js +83 -2
- package/dist/operations/modeling/predicate-applyplan.d.ts +1 -1
- package/dist/operations/modeling/predicate-applyplan.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-applyplan.js +29 -4
- package/dist/operations/modeling/predicate-inference.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-inference.js +3 -0
- package/dist/operations/modeling/predicate-loader.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-loader.js +7 -1
- package/dist/operations/modeling/predicate-ranker.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-ranker.js +40 -2
- package/dist/operations/modeling/predicate-types.d.ts +12 -1
- package/dist/operations/modeling/predicate-types.d.ts.map +1 -1
- package/dist/operations/modeling/requirement-types.d.ts +12 -0
- package/dist/operations/modeling/requirement-types.d.ts.map +1 -1
- package/dist/operations/modeling/suggest-predicates.d.ts.map +1 -1
- package/dist/operations/modeling/suggest-predicates.js +35 -11
- package/dist/operations/mutation/audit.js +1 -1
- package/dist/operations/mutation/contradictions.d.ts +1 -1
- package/dist/operations/mutation/contradictions.d.ts.map +1 -1
- package/dist/operations/mutation/contradictions.js +23 -18
- package/dist/operations/mutation/relationships.d.ts.map +1 -1
- package/dist/operations/mutation/relationships.js +8 -0
- package/dist/operations/mutation/serialization.d.ts.map +1 -1
- package/dist/operations/mutation/serialization.js +6 -0
- package/dist/operations/mutation/strict-fact.d.ts.map +1 -1
- package/dist/operations/mutation/strict-fact.js +68 -3
- package/dist/operations/mutation/upsert.d.ts +3 -1
- package/dist/operations/mutation/upsert.d.ts.map +1 -1
- package/dist/operations/mutation/upsert.js +51 -12
- package/dist/operations/mutation/validate-upsert.d.ts.map +1 -1
- package/dist/operations/mutation/validate-upsert.js +18 -2
- package/dist/operations/mutation/validation.d.ts.map +1 -1
- package/dist/operations/mutation/validation.js +8 -0
- package/dist/operations/semantic-advisor/analysis-receipt.d.ts +2 -2
- package/dist/operations/semantic-advisor/analysis-receipt.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/analysis-receipt.js +30 -16
- package/dist/operations/semantic-advisor/analyze-prose.d.ts +1 -1
- package/dist/operations/semantic-advisor/analyze-prose.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/analyze-prose.js +464 -27
- package/dist/operations/semantic-advisor/clauses.d.ts +1 -0
- package/dist/operations/semantic-advisor/clauses.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/clauses.js +7 -7
- package/dist/operations/semantic-advisor/ingestion-boundary.d.ts +18 -0
- package/dist/operations/semantic-advisor/ingestion-boundary.d.ts.map +1 -0
- package/dist/operations/semantic-advisor/ingestion-boundary.js +167 -0
- package/dist/operations/semantic-advisor/observation-plan.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/observation-plan.js +15 -3
- package/dist/operations/semantic-advisor/shared.d.ts +9 -0
- package/dist/operations/semantic-advisor/shared.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/shared.js +38 -4
- package/dist/operations/semantic-advisor/types.d.ts +66 -1
- package/dist/operations/semantic-advisor/types.d.ts.map +1 -1
- package/dist/prolog/codec.d.ts.map +1 -1
- package/dist/prolog/codec.js +19 -2
- package/dist/prolog.d.ts +6 -0
- package/dist/prolog.d.ts.map +1 -1
- package/dist/prolog.js +247 -63
- package/dist/public/diagnostic-usage.d.ts +17 -0
- package/dist/public/diagnostic-usage.d.ts.map +1 -0
- package/dist/public/diagnostic-usage.js +152 -0
- package/dist/public/distribution-parity.d.ts +90 -0
- package/dist/public/distribution-parity.d.ts.map +1 -0
- package/dist/public/distribution-parity.js +367 -0
- package/dist/public/impact/full-kb-quality.d.ts +2 -0
- package/dist/public/impact/full-kb-quality.d.ts.map +1 -1
- package/dist/public/impact/full-kb-quality.js +53 -1
- package/dist/public/impact-diagnostics.d.ts +2 -0
- package/dist/public/impact-diagnostics.d.ts.map +1 -1
- package/dist/public/impact-diagnostics.js +1 -0
- package/dist/public/operations/catalog.d.ts +235 -11
- package/dist/public/operations/catalog.d.ts.map +1 -1
- package/dist/public/operations/check-executor.d.ts.map +1 -1
- package/dist/public/operations/check-executor.js +4 -0
- package/dist/public/operations/discovery-executors.d.ts +6 -0
- package/dist/public/operations/discovery-executors.d.ts.map +1 -1
- package/dist/public/operations/discovery-executors.js +18 -2
- package/dist/public/operations/index.d.ts +5 -0
- package/dist/public/operations/index.d.ts.map +1 -1
- package/dist/public/operations/index.js +5 -0
- package/dist/public/operations/legacy-migration-plan.d.ts +113 -0
- package/dist/public/operations/legacy-migration-plan.d.ts.map +1 -0
- package/dist/public/operations/legacy-migration-plan.js +409 -0
- package/dist/public/operations/node-ports.d.ts.map +1 -1
- package/dist/public/operations/node-ports.js +90 -0
- package/dist/public/operations/repair-plan.d.ts +184 -0
- package/dist/public/operations/repair-plan.d.ts.map +1 -0
- package/dist/public/operations/repair-plan.js +397 -0
- package/dist/public/operations/runtime-types.d.ts +7 -0
- package/dist/public/operations/runtime-types.d.ts.map +1 -1
- package/dist/public/operations/specs/check.d.ts +3 -3
- package/dist/public/operations/specs/check.d.ts.map +1 -1
- package/dist/public/operations/specs/check.js +5 -2
- package/dist/public/operations/specs/discovery.d.ts +1 -1
- package/dist/public/operations/specs/discovery.js +1 -1
- package/dist/public/operations/specs/modeling.d.ts +35 -1
- package/dist/public/operations/specs/modeling.d.ts.map +1 -1
- package/dist/public/operations/specs/modeling.js +32 -1
- package/dist/public/operations/specs/mutation-schemas.d.ts +133 -3
- package/dist/public/operations/specs/mutation-schemas.d.ts.map +1 -1
- package/dist/public/operations/specs/mutation-schemas.js +67 -1
- package/dist/public/operations/specs/reporting.d.ts +42 -1
- package/dist/public/operations/specs/reporting.d.ts.map +1 -1
- package/dist/public/operations/specs/reporting.js +70 -6
- package/dist/public/operations/specs/semantic.d.ts +30 -2
- package/dist/public/operations/specs/semantic.d.ts.map +1 -1
- package/dist/public/operations/specs/semantic.js +35 -3
- package/dist/public/operations/specs/upsert.d.ts +133 -3
- package/dist/public/operations/specs/upsert.d.ts.map +1 -1
- package/dist/public/operations/workspace-snapshot.d.ts +10 -0
- package/dist/public/operations/workspace-snapshot.d.ts.map +1 -0
- package/dist/public/operations/workspace-snapshot.js +20 -0
- package/dist/public/schemas/entity.d.ts.map +1 -1
- package/dist/public/schemas/entity.js +332 -0
- package/dist/public/schemas/relationship.d.ts.map +1 -1
- package/dist/public/schemas/relationship.js +1 -0
- package/dist/public/skills/kibi-traceability/SKILL.md +7 -1
- package/dist/public/skills/kibi-usage/SKILL.md +44 -12
- package/dist/public/skills/kibi-usage/resources/fact-lanes.md +12 -2
- package/dist/public/skills/kibi-usage/resources/logic-ir.md +43 -0
- package/dist/public/skills/kibi-usage/resources/operation-access.md +19 -19
- package/dist/public/skills/kibi-usage/resources/ui-requirements.md +96 -0
- package/dist/public/skills/kibi-usage/resources/workflows.md +40 -8
- package/dist/public/symbol-granularity.d.ts +9 -0
- package/dist/public/symbol-granularity.d.ts.map +1 -1
- package/dist/public/symbol-granularity.js +16 -0
- package/dist/public/telemetry-acceptance.d.ts +89 -0
- package/dist/public/telemetry-acceptance.d.ts.map +1 -0
- package/dist/public/telemetry-acceptance.js +597 -0
- package/dist/public/telemetry-remediation.d.ts +33 -0
- package/dist/public/telemetry-remediation.d.ts.map +1 -0
- package/dist/public/telemetry-remediation.js +257 -0
- package/dist/public/verification-receipt.d.ts +75 -0
- package/dist/public/verification-receipt.d.ts.map +1 -0
- package/dist/public/verification-receipt.js +129 -0
- package/dist/schemas/entity.schema.json +401 -4
- package/dist/traceability/staged-diagnostics.d.ts +1 -1
- package/dist/traceability/staged-diagnostics.d.ts.map +1 -1
- package/dist/traceability/staged-diagnostics.js +17 -10
- package/dist/traceability/staged-impact-contract.js +1 -1
- package/dist/traceability/staged-symbols-manifest.d.ts.map +1 -1
- package/dist/traceability/staged-symbols-manifest.js +28 -10
- package/dist/traceability/temp-kb.d.ts.map +1 -1
- package/dist/traceability/temp-kb.js +49 -3
- package/dist/types/entities.d.ts +1 -0
- package/dist/types/entities.d.ts.map +1 -1
- package/dist/types/relationships.d.ts +1 -1
- package/dist/types/relationships.d.ts.map +1 -1
- package/dist/utils/rule-registry.d.ts +2 -0
- package/dist/utils/rule-registry.d.ts.map +1 -1
- package/dist/utils/rule-registry.js +18 -0
- package/dist/utils/schema-version.d.ts +1 -1
- package/dist/utils/schema-version.js +1 -1
- package/package.json +34 -2
- package/src/public/diagnostic-usage.ts +206 -0
- package/src/public/distribution-parity.ts +580 -0
- package/src/public/impact/full-kb-quality.ts +74 -1
- package/src/public/impact-diagnostics.ts +16 -0
- package/src/public/operations/check-executor.ts +4 -0
- package/src/public/operations/discovery-executors.ts +24 -2
- package/src/public/operations/index.ts +5 -0
- package/src/public/operations/legacy-migration-plan.ts +688 -0
- package/src/public/operations/node-ports.ts +109 -0
- package/src/public/operations/repair-plan.ts +562 -0
- package/src/public/operations/runtime-types.ts +8 -0
- package/src/public/operations/specs/check.ts +5 -2
- package/src/public/operations/specs/discovery.ts +1 -1
- package/src/public/operations/specs/modeling.ts +39 -1
- package/src/public/operations/specs/mutation-schemas.ts +77 -1
- package/src/public/operations/specs/reporting.ts +91 -6
- package/src/public/operations/specs/semantic.ts +42 -3
- package/src/public/operations/workspace-snapshot.ts +28 -0
- package/src/public/schemas/entity.ts +340 -1
- package/src/public/schemas/relationship.ts +1 -0
- package/src/public/skills/kibi-traceability/SKILL.md +7 -1
- package/src/public/skills/kibi-usage/SKILL.md +44 -12
- package/src/public/skills/kibi-usage/resources/fact-lanes.md +12 -2
- package/src/public/skills/kibi-usage/resources/logic-ir.md +43 -0
- package/src/public/skills/kibi-usage/resources/operation-access.md +19 -19
- package/src/public/skills/kibi-usage/resources/ui-requirements.md +96 -0
- package/src/public/skills/kibi-usage/resources/workflows.md +40 -8
- package/src/public/symbol-granularity.ts +25 -0
- package/src/public/telemetry-acceptance.ts +859 -0
- package/src/public/telemetry-remediation.ts +462 -0
- package/src/public/verification-receipt.ts +170 -0
- package/src/schemas/entity.schema.json +401 -4
- package/src/schemas/relationship.schema.json +1 -0
package/dist/logic/ir.js
ADDED
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { toPrologAtom } from "../prolog/codec.js";
|
|
3
|
+
/** The only serialized logic language accepted by Kibi. */
|
|
4
|
+
export const LOGIC_IR_VERSION = "kibi.logic.v1";
|
|
5
|
+
export const LOGIC_RULE_MAX_DEPTH = 16;
|
|
6
|
+
export const LOGIC_RULE_MAX_ATOMS = 64;
|
|
7
|
+
export const LOGIC_RULE_MAX_VARIABLES = 32;
|
|
8
|
+
const IDENTIFIER = /^[a-z][a-z0-9_:.\/-]*$/;
|
|
9
|
+
const VARIABLE = /^[A-Z][A-Za-z0-9_]*$/;
|
|
10
|
+
const TYPE_NAME = /^[a-z][a-z0-9_:.\/-]*$/;
|
|
11
|
+
const SCHEMA_ID = /^[A-Za-z][A-Za-z0-9_:.\/-]*$/;
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function asString(value) {
|
|
16
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
17
|
+
}
|
|
18
|
+
function invalidKeys(value, allowed, path, errors) {
|
|
19
|
+
const allowedKeys = new Set(allowed);
|
|
20
|
+
for (const key of Object.keys(value)) {
|
|
21
|
+
if (!allowedKeys.has(key)) {
|
|
22
|
+
errors.push(`${path}.${key} is not supported by ${LOGIC_IR_VERSION}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function stableValue(value) {
|
|
27
|
+
if (Array.isArray(value))
|
|
28
|
+
return value.map(stableValue);
|
|
29
|
+
if (!isRecord(value))
|
|
30
|
+
return value;
|
|
31
|
+
return Object.fromEntries(Object.entries(value)
|
|
32
|
+
.filter(([, nested]) => nested !== undefined)
|
|
33
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
34
|
+
.map(([key, nested]) => [key, stableValue(nested)]));
|
|
35
|
+
}
|
|
36
|
+
/** Canonical JSON is the persistence and semantic-identity boundary. */
|
|
37
|
+
export function canonicalLogicJson(ir) {
|
|
38
|
+
return JSON.stringify(stableValue(ir));
|
|
39
|
+
}
|
|
40
|
+
export function logicRuleHash(ir) {
|
|
41
|
+
return createHash("sha256").update(canonicalLogicJson(ir)).digest("hex");
|
|
42
|
+
}
|
|
43
|
+
export function logicSemanticKey(ir) {
|
|
44
|
+
return `SEM-${logicRuleHash(ir).slice(0, 24).toUpperCase()}`;
|
|
45
|
+
}
|
|
46
|
+
function termVariables(term) {
|
|
47
|
+
return term.kind === "var" ? new Set([term.name]) : new Set();
|
|
48
|
+
}
|
|
49
|
+
function mergeInto(target, source) {
|
|
50
|
+
for (const value of source)
|
|
51
|
+
target.add(value);
|
|
52
|
+
}
|
|
53
|
+
function validateTerm(value, path, declared, errors) {
|
|
54
|
+
if (!isRecord(value) || typeof value.kind !== "string") {
|
|
55
|
+
errors.push(`${path} must be a typed term object`);
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
switch (value.kind) {
|
|
59
|
+
case "var": {
|
|
60
|
+
invalidKeys(value, ["kind", "name", "type"], path, errors);
|
|
61
|
+
const name = asString(value.name);
|
|
62
|
+
const type = asString(value.type);
|
|
63
|
+
if (!name || !VARIABLE.test(name))
|
|
64
|
+
errors.push(`${path}.name must be an uppercase variable name`);
|
|
65
|
+
if (!type || !TYPE_NAME.test(type))
|
|
66
|
+
errors.push(`${path}.type must be a lowercase type name`);
|
|
67
|
+
if (name && !declared.has(name))
|
|
68
|
+
errors.push(`${path}.name ${name} is not declared in variables`);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
case "const":
|
|
72
|
+
invalidKeys(value, ["kind", "value", "type"], path, errors);
|
|
73
|
+
if (typeof value.value !== "string" || !value.value.trim())
|
|
74
|
+
errors.push(`${path}.value must be a non-empty string`);
|
|
75
|
+
if (value.type !== undefined &&
|
|
76
|
+
(typeof value.type !== "string" || !TYPE_NAME.test(value.type)))
|
|
77
|
+
errors.push(`${path}.type is invalid`);
|
|
78
|
+
return true;
|
|
79
|
+
case "number":
|
|
80
|
+
invalidKeys(value, ["kind", "value", "unit"], path, errors);
|
|
81
|
+
if (typeof value.value !== "number" || !Number.isFinite(value.value))
|
|
82
|
+
errors.push(`${path}.value must be finite`);
|
|
83
|
+
if (value.unit !== undefined &&
|
|
84
|
+
(typeof value.unit !== "string" || !TYPE_NAME.test(value.unit)))
|
|
85
|
+
errors.push(`${path}.unit is invalid`);
|
|
86
|
+
return true;
|
|
87
|
+
case "duration":
|
|
88
|
+
invalidKeys(value, ["kind", "value", "unit"], path, errors);
|
|
89
|
+
if (typeof value.value !== "number" ||
|
|
90
|
+
!Number.isFinite(value.value) ||
|
|
91
|
+
value.value < 0)
|
|
92
|
+
errors.push(`${path}.value must be a non-negative finite duration`);
|
|
93
|
+
if (!["ms", "s", "m", "h", "d", "w"].includes(String(value.unit)))
|
|
94
|
+
errors.push(`${path}.unit is invalid`);
|
|
95
|
+
return true;
|
|
96
|
+
case "timestamp":
|
|
97
|
+
invalidKeys(value, ["kind", "value"], path, errors);
|
|
98
|
+
if (typeof value.value !== "string" ||
|
|
99
|
+
Number.isNaN(Date.parse(value.value)))
|
|
100
|
+
errors.push(`${path}.value must be an ISO timestamp`);
|
|
101
|
+
return true;
|
|
102
|
+
case "interval":
|
|
103
|
+
invalidKeys(value, ["kind", "start", "end"], path, errors);
|
|
104
|
+
if (typeof value.start !== "string" ||
|
|
105
|
+
Number.isNaN(Date.parse(value.start)))
|
|
106
|
+
errors.push(`${path}.start must be an ISO timestamp`);
|
|
107
|
+
if (typeof value.end !== "string" || Number.isNaN(Date.parse(value.end)))
|
|
108
|
+
errors.push(`${path}.end must be an ISO timestamp`);
|
|
109
|
+
if (typeof value.start === "string" &&
|
|
110
|
+
typeof value.end === "string" &&
|
|
111
|
+
!Number.isNaN(Date.parse(value.start)) &&
|
|
112
|
+
!Number.isNaN(Date.parse(value.end)) &&
|
|
113
|
+
Date.parse(value.start) > Date.parse(value.end)) {
|
|
114
|
+
errors.push(`${path}.start must not be after ${path}.end`);
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
default:
|
|
118
|
+
errors.push(`${path}.kind ${String(value.kind)} is not supported`);
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function validateAtom(value, path, declared, errors) {
|
|
123
|
+
if (!isRecord(value) || value.kind !== "atom") {
|
|
124
|
+
errors.push(`${path} must be an atom`);
|
|
125
|
+
return { positiveVariables: new Set(), allVariables: new Set(), atoms: 0 };
|
|
126
|
+
}
|
|
127
|
+
invalidKeys(value, ["kind", "namespace", "name", "args", "polarity", "closedWorld"], path, errors);
|
|
128
|
+
const name = asString(value.name);
|
|
129
|
+
if (!name || !IDENTIFIER.test(name))
|
|
130
|
+
errors.push(`${path}.name must be a lowercase predicate identifier`);
|
|
131
|
+
if (value.namespace !== undefined &&
|
|
132
|
+
(typeof value.namespace !== "string" || !IDENTIFIER.test(value.namespace)))
|
|
133
|
+
errors.push(`${path}.namespace is invalid`);
|
|
134
|
+
if (!Array.isArray(value.args) || value.args.length > 8)
|
|
135
|
+
errors.push(`${path}.args must contain at most 8 terms`);
|
|
136
|
+
const allVariables = new Set();
|
|
137
|
+
const args = Array.isArray(value.args) ? value.args : [];
|
|
138
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
139
|
+
validateTerm(args[index], `${path}.args[${index}]`, declared, errors);
|
|
140
|
+
if (isRecord(args[index]) &&
|
|
141
|
+
args[index].kind === "var" &&
|
|
142
|
+
typeof args[index].name === "string")
|
|
143
|
+
allVariables.add(args[index].name);
|
|
144
|
+
}
|
|
145
|
+
if (value.polarity !== undefined &&
|
|
146
|
+
value.polarity !== "positive" &&
|
|
147
|
+
value.polarity !== "negative")
|
|
148
|
+
errors.push(`${path}.polarity must be positive or negative`);
|
|
149
|
+
if (value.closedWorld !== undefined && typeof value.closedWorld !== "boolean")
|
|
150
|
+
errors.push(`${path}.closedWorld must be boolean`);
|
|
151
|
+
const positiveVariables = value.polarity === "negative" ? new Set() : new Set(allVariables);
|
|
152
|
+
return { positiveVariables, allVariables, atoms: 1 };
|
|
153
|
+
}
|
|
154
|
+
function validateExpression(value, path, declared, errors, depth) {
|
|
155
|
+
if (depth > LOGIC_RULE_MAX_DEPTH) {
|
|
156
|
+
errors.push(`${path} exceeds maximum expression depth ${LOGIC_RULE_MAX_DEPTH}`);
|
|
157
|
+
return { positiveVariables: new Set(), allVariables: new Set(), atoms: 0 };
|
|
158
|
+
}
|
|
159
|
+
if (!isRecord(value) || typeof value.kind !== "string") {
|
|
160
|
+
errors.push(`${path} must be a typed expression object`);
|
|
161
|
+
return { positiveVariables: new Set(), allVariables: new Set(), atoms: 0 };
|
|
162
|
+
}
|
|
163
|
+
if (value.kind === "atom")
|
|
164
|
+
return validateAtom(value, path, declared, errors);
|
|
165
|
+
if (value.kind === "all" || value.kind === "any") {
|
|
166
|
+
invalidKeys(value, ["kind", "items"], path, errors);
|
|
167
|
+
if (!Array.isArray(value.items) ||
|
|
168
|
+
value.items.length === 0 ||
|
|
169
|
+
value.items.length > 32)
|
|
170
|
+
errors.push(`${path}.items must contain 1..32 expressions`);
|
|
171
|
+
const positiveVariables = new Set();
|
|
172
|
+
const allVariables = new Set();
|
|
173
|
+
let atoms = 0;
|
|
174
|
+
const children = [];
|
|
175
|
+
for (const [index, item] of (Array.isArray(value.items)
|
|
176
|
+
? value.items
|
|
177
|
+
: []).entries()) {
|
|
178
|
+
const facts = validateExpression(item, `${path}.items[${index}]`, declared, errors, depth + 1);
|
|
179
|
+
children.push(facts);
|
|
180
|
+
mergeInto(positiveVariables, facts.positiveVariables);
|
|
181
|
+
mergeInto(allVariables, facts.allVariables);
|
|
182
|
+
atoms += facts.atoms;
|
|
183
|
+
}
|
|
184
|
+
if (value.kind === "any") {
|
|
185
|
+
// A variable used outside a disjunction must be bound in every branch.
|
|
186
|
+
// This conservative rule keeps the finite interpreter range-restricted.
|
|
187
|
+
const commonPositive = children.reduce((common, child) => {
|
|
188
|
+
if (common === null)
|
|
189
|
+
return new Set(child.positiveVariables);
|
|
190
|
+
const intersection = new Set();
|
|
191
|
+
for (const variable of common) {
|
|
192
|
+
if (child.positiveVariables.has(variable))
|
|
193
|
+
intersection.add(variable);
|
|
194
|
+
}
|
|
195
|
+
return intersection;
|
|
196
|
+
}, null) ?? new Set();
|
|
197
|
+
const unsafeBranch = children.some((child) => [...child.allVariables].some((variable) => !child.positiveVariables.has(variable)));
|
|
198
|
+
return {
|
|
199
|
+
positiveVariables: unsafeBranch ? new Set() : commonPositive,
|
|
200
|
+
allVariables,
|
|
201
|
+
atoms,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return { positiveVariables, allVariables, atoms };
|
|
205
|
+
}
|
|
206
|
+
if (value.kind === "not") {
|
|
207
|
+
invalidKeys(value, ["kind", "item"], path, errors);
|
|
208
|
+
const facts = validateAtom(value.item, `${path}.item`, declared, errors);
|
|
209
|
+
if (isRecord(value.item) && value.item.closedWorld !== true)
|
|
210
|
+
errors.push(`${path}.item must explicitly set closedWorld=true for negation-as-failure`);
|
|
211
|
+
return {
|
|
212
|
+
positiveVariables: new Set(),
|
|
213
|
+
allVariables: facts.allVariables,
|
|
214
|
+
atoms: facts.atoms,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (value.kind === "compare") {
|
|
218
|
+
invalidKeys(value, ["kind", "operator", "left", "right"], path, errors);
|
|
219
|
+
if (!["eq", "neq", "lt", "lte", "gt", "gte"].includes(String(value.operator)))
|
|
220
|
+
errors.push(`${path}.operator is invalid`);
|
|
221
|
+
validateTerm(value.left, `${path}.left`, declared, errors);
|
|
222
|
+
validateTerm(value.right, `${path}.right`, declared, errors);
|
|
223
|
+
const variables = new Set();
|
|
224
|
+
if (isRecord(value.left) &&
|
|
225
|
+
value.left.kind === "var" &&
|
|
226
|
+
typeof value.left.name === "string")
|
|
227
|
+
variables.add(value.left.name);
|
|
228
|
+
if (isRecord(value.right) &&
|
|
229
|
+
value.right.kind === "var" &&
|
|
230
|
+
typeof value.right.name === "string")
|
|
231
|
+
variables.add(value.right.name);
|
|
232
|
+
validateComparableTerms(value.left, value.right, path, errors);
|
|
233
|
+
return { positiveVariables: new Set(), allVariables: variables, atoms: 0 };
|
|
234
|
+
}
|
|
235
|
+
if (value.kind === "count") {
|
|
236
|
+
invalidKeys(value, ["kind", "atom", "operator", "value"], path, errors);
|
|
237
|
+
const facts = validateAtom(value.atom, `${path}.atom`, declared, errors);
|
|
238
|
+
if (!["eq", "neq", "lt", "lte", "gt", "gte"].includes(String(value.operator)))
|
|
239
|
+
errors.push(`${path}.operator is invalid`);
|
|
240
|
+
if (typeof value.value !== "number" ||
|
|
241
|
+
!Number.isInteger(value.value) ||
|
|
242
|
+
value.value < 0)
|
|
243
|
+
errors.push(`${path}.value must be a non-negative integer`);
|
|
244
|
+
return {
|
|
245
|
+
positiveVariables: facts.positiveVariables,
|
|
246
|
+
allVariables: facts.allVariables,
|
|
247
|
+
atoms: facts.atoms,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
if (value.kind === "temporal") {
|
|
251
|
+
invalidKeys(value, ["kind", "relation", "left", "right"], path, errors);
|
|
252
|
+
if (!["before", "after", "during", "overlaps", "starts", "finishes"].includes(String(value.relation)))
|
|
253
|
+
errors.push(`${path}.relation is invalid`);
|
|
254
|
+
validateTerm(value.left, `${path}.left`, declared, errors);
|
|
255
|
+
validateTerm(value.right, `${path}.right`, declared, errors);
|
|
256
|
+
const variables = new Set();
|
|
257
|
+
for (const term of [value.left, value.right])
|
|
258
|
+
if (isRecord(term) &&
|
|
259
|
+
term.kind === "var" &&
|
|
260
|
+
typeof term.name === "string")
|
|
261
|
+
variables.add(term.name);
|
|
262
|
+
validateTemporalTerms(value.relation, value.left, value.right, path, errors);
|
|
263
|
+
return { positiveVariables: new Set(), allVariables: variables, atoms: 0 };
|
|
264
|
+
}
|
|
265
|
+
errors.push(`${path}.kind ${String(value.kind)} is not supported`);
|
|
266
|
+
return { positiveVariables: new Set(), allVariables: new Set(), atoms: 0 };
|
|
267
|
+
}
|
|
268
|
+
function termUnit(value) {
|
|
269
|
+
if (!isRecord(value))
|
|
270
|
+
return undefined;
|
|
271
|
+
if ((value.kind === "number" || value.kind === "duration") &&
|
|
272
|
+
typeof value.unit === "string") {
|
|
273
|
+
return value.unit;
|
|
274
|
+
}
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
function validateComparableTerms(left, right, path, errors) {
|
|
278
|
+
const leftUnit = termUnit(left);
|
|
279
|
+
const rightUnit = termUnit(right);
|
|
280
|
+
if (leftUnit && rightUnit && leftUnit !== rightUnit) {
|
|
281
|
+
errors.push(`${path} compares incompatible units ${leftUnit} and ${rightUnit}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function validateTemporalTerms(relation, left, right, path, errors) {
|
|
285
|
+
const leftKind = isRecord(left) ? left.kind : undefined;
|
|
286
|
+
const rightKind = isRecord(right) ? right.kind : undefined;
|
|
287
|
+
const isTimestampOrInterval = (kind) => kind === "timestamp" || kind === "interval";
|
|
288
|
+
if (relation === "before" || relation === "after") {
|
|
289
|
+
if (!isTimestampOrInterval(leftKind) || !isTimestampOrInterval(rightKind))
|
|
290
|
+
errors.push(`${path} ${String(relation)} requires timestamp or interval terms`);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (relation === "during") {
|
|
294
|
+
if (!isTimestampOrInterval(leftKind) || rightKind !== "interval")
|
|
295
|
+
errors.push(`${path} during requires a timestamp or interval inside an interval`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (!["overlaps", "starts", "finishes"].includes(String(relation)) ||
|
|
299
|
+
leftKind !== "interval" ||
|
|
300
|
+
rightKind !== "interval") {
|
|
301
|
+
errors.push(`${path} ${String(relation)} requires interval terms`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function normalizeTerm(term) {
|
|
305
|
+
if (term.kind === "var")
|
|
306
|
+
return {
|
|
307
|
+
...term,
|
|
308
|
+
name: term.name.trim(),
|
|
309
|
+
type: term.type.trim().toLowerCase(),
|
|
310
|
+
};
|
|
311
|
+
if (term.kind === "const")
|
|
312
|
+
return {
|
|
313
|
+
...term,
|
|
314
|
+
value: term.value.trim(),
|
|
315
|
+
...(term.type ? { type: term.type.trim().toLowerCase() } : {}),
|
|
316
|
+
};
|
|
317
|
+
if (term.kind === "number")
|
|
318
|
+
return {
|
|
319
|
+
...term,
|
|
320
|
+
value: term.value,
|
|
321
|
+
...(term.unit ? { unit: term.unit.trim().toLowerCase() } : {}),
|
|
322
|
+
};
|
|
323
|
+
if (term.kind === "duration")
|
|
324
|
+
return { ...term, value: term.value, unit: term.unit };
|
|
325
|
+
if (term.kind === "timestamp")
|
|
326
|
+
return { ...term, value: new Date(term.value).toISOString() };
|
|
327
|
+
return {
|
|
328
|
+
...term,
|
|
329
|
+
start: new Date(term.start).toISOString(),
|
|
330
|
+
end: new Date(term.end).toISOString(),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function normalizeExpression(expression) {
|
|
334
|
+
if (expression.kind === "atom")
|
|
335
|
+
return {
|
|
336
|
+
...expression,
|
|
337
|
+
name: expression.name.trim().toLowerCase(),
|
|
338
|
+
...(expression.namespace
|
|
339
|
+
? { namespace: expression.namespace.trim().toLowerCase() }
|
|
340
|
+
: {}),
|
|
341
|
+
args: expression.args.map(normalizeTerm),
|
|
342
|
+
};
|
|
343
|
+
if (expression.kind === "all" || expression.kind === "any") {
|
|
344
|
+
const items = expression.items
|
|
345
|
+
.map(normalizeExpression)
|
|
346
|
+
.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
|
347
|
+
return { kind: expression.kind, items };
|
|
348
|
+
}
|
|
349
|
+
if (expression.kind === "not")
|
|
350
|
+
return {
|
|
351
|
+
kind: "not",
|
|
352
|
+
item: normalizeExpression(expression.item),
|
|
353
|
+
};
|
|
354
|
+
if (expression.kind === "compare")
|
|
355
|
+
return {
|
|
356
|
+
...expression,
|
|
357
|
+
left: normalizeTerm(expression.left),
|
|
358
|
+
right: normalizeTerm(expression.right),
|
|
359
|
+
};
|
|
360
|
+
if (expression.kind === "count")
|
|
361
|
+
return {
|
|
362
|
+
...expression,
|
|
363
|
+
atom: normalizeExpression(expression.atom),
|
|
364
|
+
};
|
|
365
|
+
return {
|
|
366
|
+
...expression,
|
|
367
|
+
left: normalizeTerm(expression.left),
|
|
368
|
+
right: normalizeTerm(expression.right),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function normalizeAtom(atom) {
|
|
372
|
+
return normalizeExpression(atom);
|
|
373
|
+
}
|
|
374
|
+
function variableNamesInTerm(term, names) {
|
|
375
|
+
if (term.kind === "var" && !names.includes(term.name))
|
|
376
|
+
names.push(term.name);
|
|
377
|
+
}
|
|
378
|
+
function variableNamesInExpression(expression, names) {
|
|
379
|
+
if (expression.kind === "atom") {
|
|
380
|
+
for (const term of expression.args)
|
|
381
|
+
variableNamesInTerm(term, names);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (expression.kind === "all" || expression.kind === "any") {
|
|
385
|
+
for (const item of expression.items)
|
|
386
|
+
variableNamesInExpression(item, names);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (expression.kind === "not") {
|
|
390
|
+
variableNamesInExpression(expression.item, names);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (expression.kind === "count") {
|
|
394
|
+
variableNamesInExpression(expression.atom, names);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (expression.kind === "compare" || expression.kind === "temporal") {
|
|
398
|
+
variableNamesInTerm(expression.left, names);
|
|
399
|
+
variableNamesInTerm(expression.right, names);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function renameTerm(term, names) {
|
|
403
|
+
if (term.kind !== "var")
|
|
404
|
+
return term;
|
|
405
|
+
const renamed = names.get(term.name);
|
|
406
|
+
return renamed ? { ...term, name: renamed } : term;
|
|
407
|
+
}
|
|
408
|
+
function renameExpression(expression, names) {
|
|
409
|
+
if (expression.kind === "atom")
|
|
410
|
+
return {
|
|
411
|
+
...expression,
|
|
412
|
+
args: expression.args.map((term) => renameTerm(term, names)),
|
|
413
|
+
};
|
|
414
|
+
if (expression.kind === "all" || expression.kind === "any")
|
|
415
|
+
return {
|
|
416
|
+
...expression,
|
|
417
|
+
items: expression.items.map((item) => renameExpression(item, names)),
|
|
418
|
+
};
|
|
419
|
+
if (expression.kind === "not")
|
|
420
|
+
return {
|
|
421
|
+
...expression,
|
|
422
|
+
item: renameExpression(expression.item, names),
|
|
423
|
+
};
|
|
424
|
+
if (expression.kind === "count")
|
|
425
|
+
return {
|
|
426
|
+
...expression,
|
|
427
|
+
atom: renameExpression(expression.atom, names),
|
|
428
|
+
};
|
|
429
|
+
return {
|
|
430
|
+
...expression,
|
|
431
|
+
left: renameTerm(expression.left, names),
|
|
432
|
+
right: renameTerm(expression.right, names),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function alphaNormalizeVariables(ir) {
|
|
436
|
+
const names = [];
|
|
437
|
+
if (ir.head)
|
|
438
|
+
variableNamesInExpression(ir.head, names);
|
|
439
|
+
if (ir.body)
|
|
440
|
+
variableNamesInExpression(ir.body, names);
|
|
441
|
+
for (const exception of ir.exceptions ?? [])
|
|
442
|
+
variableNamesInExpression(exception, names);
|
|
443
|
+
for (const variable of [...(ir.variables ?? [])].sort((left, right) => left.name.localeCompare(right.name))) {
|
|
444
|
+
if (!names.includes(variable.name))
|
|
445
|
+
names.push(variable.name);
|
|
446
|
+
}
|
|
447
|
+
const renames = new Map(names.map((name, index) => [name, `V${index + 1}`]));
|
|
448
|
+
const variables = (ir.variables ?? [])
|
|
449
|
+
.map((variable) => ({
|
|
450
|
+
...variable,
|
|
451
|
+
name: renames.get(variable.name) ?? variable.name,
|
|
452
|
+
}))
|
|
453
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
454
|
+
return {
|
|
455
|
+
...ir,
|
|
456
|
+
...(ir.head
|
|
457
|
+
? { head: renameExpression(ir.head, renames) }
|
|
458
|
+
: {}),
|
|
459
|
+
...(ir.body ? { body: renameExpression(ir.body, renames) } : {}),
|
|
460
|
+
...(ir.exceptions
|
|
461
|
+
? {
|
|
462
|
+
exceptions: ir.exceptions.map((exception) => renameExpression(exception, renames)),
|
|
463
|
+
}
|
|
464
|
+
: {}),
|
|
465
|
+
...(variables.length > 0 ? { variables } : {}),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function normalizeIr(ir) {
|
|
469
|
+
return alphaNormalizeVariables({
|
|
470
|
+
version: LOGIC_IR_VERSION,
|
|
471
|
+
kind: ir.kind,
|
|
472
|
+
modality: ir.modality,
|
|
473
|
+
...(ir.head ? { head: normalizeAtom(ir.head) } : {}),
|
|
474
|
+
...(ir.body ? { body: normalizeExpression(ir.body) } : {}),
|
|
475
|
+
...(ir.variables?.length
|
|
476
|
+
? {
|
|
477
|
+
variables: [...ir.variables]
|
|
478
|
+
.map((variable) => ({
|
|
479
|
+
...variable,
|
|
480
|
+
name: variable.name.trim(),
|
|
481
|
+
type: variable.type.trim().toLowerCase(),
|
|
482
|
+
quantifier: variable.quantifier ?? "forall",
|
|
483
|
+
}))
|
|
484
|
+
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
485
|
+
}
|
|
486
|
+
: {}),
|
|
487
|
+
...(ir.exceptions?.length
|
|
488
|
+
? {
|
|
489
|
+
exceptions: ir.exceptions
|
|
490
|
+
.map(normalizeExpression)
|
|
491
|
+
.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))),
|
|
492
|
+
}
|
|
493
|
+
: {}),
|
|
494
|
+
...(ir.scope
|
|
495
|
+
? {
|
|
496
|
+
scope: {
|
|
497
|
+
...ir.scope,
|
|
498
|
+
...(ir.scope.authority
|
|
499
|
+
? { authority: ir.scope.authority.trim() }
|
|
500
|
+
: {}),
|
|
501
|
+
...(ir.scope.name ? { name: ir.scope.name.trim() } : {}),
|
|
502
|
+
...(ir.scope.tags
|
|
503
|
+
? { tags: [...ir.scope.tags].map((tag) => tag.trim()).sort() }
|
|
504
|
+
: {}),
|
|
505
|
+
},
|
|
506
|
+
}
|
|
507
|
+
: {}),
|
|
508
|
+
...(ir.validFrom
|
|
509
|
+
? { validFrom: new Date(ir.validFrom).toISOString() }
|
|
510
|
+
: {}),
|
|
511
|
+
...(ir.validTo ? { validTo: new Date(ir.validTo).toISOString() } : {}),
|
|
512
|
+
...(ir.ruleSchemaId ? { ruleSchemaId: ir.ruleSchemaId.trim() } : {}),
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
function prologTerm(term) {
|
|
516
|
+
switch (term.kind) {
|
|
517
|
+
case "var":
|
|
518
|
+
return term.name;
|
|
519
|
+
case "const":
|
|
520
|
+
return toPrologAtom(term.value);
|
|
521
|
+
case "number":
|
|
522
|
+
return String(term.value);
|
|
523
|
+
case "duration":
|
|
524
|
+
return `duration(${term.value},${term.unit})`;
|
|
525
|
+
case "timestamp":
|
|
526
|
+
return toPrologAtom(term.value);
|
|
527
|
+
case "interval":
|
|
528
|
+
return `interval(${toPrologAtom(new Date(term.start).toISOString())},${toPrologAtom(new Date(term.end).toISOString())})`;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function prologAtom(atom) {
|
|
532
|
+
const name = atom.namespace ? `${atom.namespace}:${atom.name}` : atom.name;
|
|
533
|
+
const rendered = `${toPrologAtom(name)}(${atom.args.map(prologTerm).join(",")})`;
|
|
534
|
+
return atom.polarity === "negative" ? `negative(${rendered})` : rendered;
|
|
535
|
+
}
|
|
536
|
+
function prologExpression(expression) {
|
|
537
|
+
if (expression.kind === "atom")
|
|
538
|
+
return prologAtom(expression);
|
|
539
|
+
if (expression.kind === "all")
|
|
540
|
+
return expression.items.map(prologExpression).join(", ");
|
|
541
|
+
if (expression.kind === "any")
|
|
542
|
+
return `(${expression.items.map(prologExpression).join(" ; ")})`;
|
|
543
|
+
if (expression.kind === "not")
|
|
544
|
+
return `not(${prologAtom(expression.item)})`;
|
|
545
|
+
if (expression.kind === "compare")
|
|
546
|
+
return `${prologTerm(expression.left)} ${expression.operator} ${prologTerm(expression.right)}`;
|
|
547
|
+
if (expression.kind === "count")
|
|
548
|
+
return `count(${prologAtom(expression.atom)}, ${expression.operator}, ${expression.value})`;
|
|
549
|
+
return `temporal(${expression.relation}, ${prologTerm(expression.left)}, ${prologTerm(expression.right)})`;
|
|
550
|
+
}
|
|
551
|
+
/** Rendered output is an audit preview only; it is never evaluated as source. */
|
|
552
|
+
export function renderLogicProlog(ir) {
|
|
553
|
+
const head = ir.head ? `${ir.modality}(${prologAtom(ir.head)})` : ir.modality;
|
|
554
|
+
if (!ir.body)
|
|
555
|
+
return `${head}.`;
|
|
556
|
+
return `${head} :- ${prologExpression(ir.body)}.`;
|
|
557
|
+
}
|
|
558
|
+
export function validateLogicIr(input) {
|
|
559
|
+
const errors = [];
|
|
560
|
+
const warnings = [];
|
|
561
|
+
if (!isRecord(input))
|
|
562
|
+
return { valid: false, errors: ["logic IR must be an object"], warnings };
|
|
563
|
+
invalidKeys(input, [
|
|
564
|
+
"version",
|
|
565
|
+
"kind",
|
|
566
|
+
"modality",
|
|
567
|
+
"head",
|
|
568
|
+
"body",
|
|
569
|
+
"variables",
|
|
570
|
+
"exceptions",
|
|
571
|
+
"scope",
|
|
572
|
+
"validFrom",
|
|
573
|
+
"validTo",
|
|
574
|
+
"ruleSchemaId",
|
|
575
|
+
], "logic", errors);
|
|
576
|
+
if (input.version !== LOGIC_IR_VERSION)
|
|
577
|
+
errors.push(`version must be ${LOGIC_IR_VERSION}`);
|
|
578
|
+
const kind = input.kind;
|
|
579
|
+
if (kind !== "atom" && kind !== "rule" && kind !== "constraint")
|
|
580
|
+
errors.push("kind must be atom, rule, or constraint");
|
|
581
|
+
const modality = input.modality;
|
|
582
|
+
if (!["assert", "deny", "oblige", "permit", "forbid"].includes(String(modality)))
|
|
583
|
+
errors.push("modality is invalid");
|
|
584
|
+
const variables = Array.isArray(input.variables) ? input.variables : [];
|
|
585
|
+
if (variables.length > LOGIC_RULE_MAX_VARIABLES)
|
|
586
|
+
errors.push(`variables exceed maximum ${LOGIC_RULE_MAX_VARIABLES}`);
|
|
587
|
+
const declared = new Set();
|
|
588
|
+
for (const [index, variable] of variables.entries()) {
|
|
589
|
+
if (!isRecord(variable)) {
|
|
590
|
+
errors.push(`variables[${index}] must be an object`);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
invalidKeys(variable, ["name", "type", "quantifier"], `variables[${index}]`, errors);
|
|
594
|
+
const name = asString(variable.name);
|
|
595
|
+
const type = asString(variable.type);
|
|
596
|
+
if (!name || !VARIABLE.test(name) || declared.has(name))
|
|
597
|
+
errors.push(`variables[${index}].name must be unique and uppercase`);
|
|
598
|
+
if (!type || !TYPE_NAME.test(type))
|
|
599
|
+
errors.push(`variables[${index}].type is invalid`);
|
|
600
|
+
if (variable.quantifier !== undefined &&
|
|
601
|
+
variable.quantifier !== "forall" &&
|
|
602
|
+
variable.quantifier !== "exists")
|
|
603
|
+
errors.push(`variables[${index}].quantifier is invalid`);
|
|
604
|
+
if (name)
|
|
605
|
+
declared.add(name);
|
|
606
|
+
}
|
|
607
|
+
let headFacts = {
|
|
608
|
+
positiveVariables: new Set(),
|
|
609
|
+
allVariables: new Set(),
|
|
610
|
+
atoms: 0,
|
|
611
|
+
};
|
|
612
|
+
let bodyFacts = {
|
|
613
|
+
positiveVariables: new Set(),
|
|
614
|
+
allVariables: new Set(),
|
|
615
|
+
atoms: 0,
|
|
616
|
+
};
|
|
617
|
+
if (input.head !== undefined)
|
|
618
|
+
headFacts = validateAtom(input.head, "head", declared, errors);
|
|
619
|
+
if (input.body !== undefined) {
|
|
620
|
+
bodyFacts = validateExpression(input.body, "body", declared, errors, 0);
|
|
621
|
+
}
|
|
622
|
+
const facts = {
|
|
623
|
+
positiveVariables: bodyFacts.positiveVariables,
|
|
624
|
+
allVariables: new Set([
|
|
625
|
+
...headFacts.allVariables,
|
|
626
|
+
...bodyFacts.allVariables,
|
|
627
|
+
]),
|
|
628
|
+
atoms: headFacts.atoms + bodyFacts.atoms,
|
|
629
|
+
};
|
|
630
|
+
if (facts.atoms > LOGIC_RULE_MAX_ATOMS)
|
|
631
|
+
errors.push(`rule contains more than ${LOGIC_RULE_MAX_ATOMS} atoms`);
|
|
632
|
+
if (kind === "atom" && (input.head === undefined || input.body !== undefined))
|
|
633
|
+
errors.push("atom kind requires head and forbids body");
|
|
634
|
+
if (kind === "rule" && (input.head === undefined || input.body === undefined))
|
|
635
|
+
errors.push("rule kind requires head and body");
|
|
636
|
+
if (kind === "constraint" &&
|
|
637
|
+
(input.body === undefined || input.head !== undefined))
|
|
638
|
+
errors.push("constraint kind requires body and forbids head");
|
|
639
|
+
if (isRecord(input.head)) {
|
|
640
|
+
const headVars = new Set();
|
|
641
|
+
for (const term of Array.isArray(input.head.args) ? input.head.args : [])
|
|
642
|
+
if (isRecord(term) &&
|
|
643
|
+
term.kind === "var" &&
|
|
644
|
+
typeof term.name === "string")
|
|
645
|
+
headVars.add(term.name);
|
|
646
|
+
for (const variable of headVars) {
|
|
647
|
+
if (!facts.positiveVariables.has(variable))
|
|
648
|
+
errors.push(`head variable ${variable} is not range-restricted by a positive body atom`);
|
|
649
|
+
const declaration = variables.find((candidate) => isRecord(candidate) && candidate.name === variable);
|
|
650
|
+
if (declaration?.quantifier === "exists")
|
|
651
|
+
errors.push(`head variable ${variable} cannot be existential`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
for (const variable of facts.allVariables)
|
|
655
|
+
if (!declared.has(variable))
|
|
656
|
+
errors.push(`variable ${variable} is not declared`);
|
|
657
|
+
for (const variable of bodyFacts.allVariables)
|
|
658
|
+
if (!bodyFacts.positiveVariables.has(variable))
|
|
659
|
+
errors.push(`body variable ${variable} is not range-restricted by a positive atom`);
|
|
660
|
+
if (input.exceptions !== undefined &&
|
|
661
|
+
(!Array.isArray(input.exceptions) || input.exceptions.length > 16))
|
|
662
|
+
errors.push("exceptions must contain at most 16 expressions");
|
|
663
|
+
if (input.exceptions !== undefined && Array.isArray(input.exceptions)) {
|
|
664
|
+
for (const [index, exception] of input.exceptions.entries()) {
|
|
665
|
+
const exceptionFacts = validateExpression(exception, `exceptions[${index}]`, declared, errors, 0);
|
|
666
|
+
for (const variable of exceptionFacts.allVariables)
|
|
667
|
+
if (!bodyFacts.positiveVariables.has(variable))
|
|
668
|
+
errors.push(`exception variable ${variable} is not range-restricted by a positive body atom`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (input.scope !== undefined) {
|
|
672
|
+
if (!isRecord(input.scope))
|
|
673
|
+
errors.push("scope must be an object");
|
|
674
|
+
else {
|
|
675
|
+
invalidKeys(input.scope, ["authority", "name", "tags"], "scope", errors);
|
|
676
|
+
for (const key of ["authority", "name"]) {
|
|
677
|
+
if (input.scope[key] !== undefined &&
|
|
678
|
+
typeof input.scope[key] !== "string")
|
|
679
|
+
errors.push(`scope.${key} must be a string`);
|
|
680
|
+
}
|
|
681
|
+
if (input.scope.tags !== undefined &&
|
|
682
|
+
(!Array.isArray(input.scope.tags) ||
|
|
683
|
+
input.scope.tags.some((tag) => typeof tag !== "string")))
|
|
684
|
+
errors.push("scope.tags must be an array of strings");
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (input.ruleSchemaId !== undefined &&
|
|
688
|
+
(typeof input.ruleSchemaId !== "string" ||
|
|
689
|
+
!SCHEMA_ID.test(input.ruleSchemaId)))
|
|
690
|
+
errors.push("ruleSchemaId is invalid");
|
|
691
|
+
if (input.validFrom !== undefined &&
|
|
692
|
+
(typeof input.validFrom !== "string" ||
|
|
693
|
+
Number.isNaN(Date.parse(input.validFrom))))
|
|
694
|
+
errors.push("validFrom must be an ISO timestamp");
|
|
695
|
+
if (input.validTo !== undefined &&
|
|
696
|
+
(typeof input.validTo !== "string" ||
|
|
697
|
+
Number.isNaN(Date.parse(input.validTo))))
|
|
698
|
+
errors.push("validTo must be an ISO timestamp");
|
|
699
|
+
if (input.validFrom &&
|
|
700
|
+
input.validTo &&
|
|
701
|
+
Date.parse(String(input.validFrom)) > Date.parse(String(input.validTo)))
|
|
702
|
+
errors.push("validFrom must not be after validTo");
|
|
703
|
+
if (errors.length > 0)
|
|
704
|
+
return { valid: false, errors, warnings };
|
|
705
|
+
const normalized = normalizeIr(input);
|
|
706
|
+
const canonicalJson = canonicalLogicJson(normalized);
|
|
707
|
+
const ruleHash = logicRuleHash(normalized);
|
|
708
|
+
const semanticKey = logicSemanticKey(normalized);
|
|
709
|
+
const renderedProlog = renderLogicProlog(normalized);
|
|
710
|
+
if (normalized.body &&
|
|
711
|
+
JSON.stringify(normalized.body).includes('"kind":"not"'))
|
|
712
|
+
warnings.push("negation is accepted only for explicitly closed-world predicates and requires stratification against stored schemas");
|
|
713
|
+
return {
|
|
714
|
+
valid: true,
|
|
715
|
+
errors,
|
|
716
|
+
warnings,
|
|
717
|
+
normalized,
|
|
718
|
+
canonicalJson,
|
|
719
|
+
ruleHash,
|
|
720
|
+
semanticKey,
|
|
721
|
+
renderedProlog,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
export function utf8Span(text, start, end) {
|
|
725
|
+
return {
|
|
726
|
+
start: Buffer.byteLength(text.slice(0, Math.max(0, start)), "utf8"),
|
|
727
|
+
end: Buffer.byteLength(text.slice(0, Math.max(start, end)), "utf8"),
|
|
728
|
+
};
|
|
729
|
+
}
|