kibi-mcp 0.16.0 → 0.17.2

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.
@@ -0,0 +1,1367 @@
1
+ import { createHash } from "node:crypto";
2
+ export const SEMANTIC_ADVISOR_VERSION = "semantic-advisor-v1";
3
+ const SIGNAL_PATTERNS = [
4
+ {
5
+ kind: "numeric_cardinality",
6
+ candidateLane: "strict_property",
7
+ confidence: 0.92,
8
+ pattern: /\b(?:(?:at\s+most|at\s+least|exactly|no\s+more\s+than|up\s+to|cap(?:ped)?\s+at)\s+)?(?:\d+|zero|one|two|three|four|five|six|seven|eight|nine|ten)\b/i,
9
+ },
10
+ {
11
+ kind: "numeric_threshold",
12
+ candidateLane: "strict_property",
13
+ confidence: 0.86,
14
+ pattern: /\b(?:maximum|minimum|under|within|below|above|expires?|retained\s+for)\s+(?:\d+|zero|one|two|three|four|five|six|seven|eight|nine|ten)\b/i,
15
+ },
16
+ {
17
+ kind: "conditional",
18
+ candidateLane: "predicate",
19
+ confidence: 0.82,
20
+ pattern: /\b(?:if|when|unless|except|only\s+if|provided\s+that)\b/i,
21
+ },
22
+ {
23
+ kind: "permission",
24
+ candidateLane: "predicate",
25
+ confidence: 0.8,
26
+ pattern: /\b(?:only|may|can|allowed|denied|forbidden|must\s+not|cannot|can't)\b/i,
27
+ },
28
+ {
29
+ kind: "state_or_default",
30
+ candidateLane: "predicate",
31
+ confidence: 0.74,
32
+ pattern: /\b(?:state|mode|defaults?\s+to|ready|disabled|enabled|terminal)\b/i,
33
+ },
34
+ {
35
+ kind: "normative_modal",
36
+ candidateLane: "observation_review",
37
+ confidence: 0.65,
38
+ pattern: /\b(?:must|shall|should|may|must\s+not|cannot|can't)\b/i,
39
+ },
40
+ ];
41
+ function isRecord(value) {
42
+ return typeof value === "object" && value !== null && !Array.isArray(value);
43
+ }
44
+ function canonicalize(value) {
45
+ if (Array.isArray(value)) {
46
+ return `[${value.map((item) => canonicalize(item)).join(",")}]`;
47
+ }
48
+ if (isRecord(value)) {
49
+ return `{${Object.keys(value)
50
+ .sort()
51
+ .map((key) => `${JSON.stringify(key)}:${canonicalize(value[key])}`)
52
+ .join(",")}}`;
53
+ }
54
+ return JSON.stringify(value);
55
+ }
56
+ function payloadHash(payload) {
57
+ const stable = Object.fromEntries(Object.entries(payload).filter(([key]) => !key.startsWith("_")));
58
+ return createHash("sha256").update(canonicalize(stable)).digest("hex");
59
+ }
60
+ function shortHash(value) {
61
+ return createHash("sha256").update(value).digest("hex").slice(0, 12);
62
+ }
63
+ function stringValue(value) {
64
+ return typeof value === "string" ? value.trim() : "";
65
+ }
66
+ function extractProse(payload) {
67
+ const properties = isRecord(payload.properties) ? payload.properties : {};
68
+ return [properties.title, properties.text_ref, properties.description]
69
+ .map(stringValue)
70
+ .filter((value) => value.length > 0)
71
+ .join("\n");
72
+ }
73
+ function extractStatement(payload) {
74
+ const properties = isRecord(payload.properties) ? payload.properties : {};
75
+ const textRef = stringValue(properties.text_ref);
76
+ if (textRef)
77
+ return textRef;
78
+ return stringValue(properties.title);
79
+ }
80
+ function extractSource(payload) {
81
+ const properties = isRecord(payload.properties) ? payload.properties : {};
82
+ return stringValue(properties.source) || "mcp://kibi/semantic-advisor";
83
+ }
84
+ function relationshipTypes(payload) {
85
+ const relationships = Array.isArray(payload.relationships)
86
+ ? payload.relationships
87
+ : [];
88
+ const types = new Set();
89
+ for (const relationship of relationships) {
90
+ if (!isRecord(relationship))
91
+ continue;
92
+ const type = stringValue(relationship.type);
93
+ if (type)
94
+ types.add(type);
95
+ }
96
+ return types;
97
+ }
98
+ function isRequirementPayload(payload) {
99
+ return stringValue(payload.type) === "req";
100
+ }
101
+ function hasModeledRelationships(payload) {
102
+ const types = relationshipTypes(payload);
103
+ return ((types.has("constrains") && types.has("requires_property")) ||
104
+ types.has("requires_predicate"));
105
+ }
106
+ function detectSignals(prose) {
107
+ const signals = [];
108
+ const seen = new Set();
109
+ for (const signalPattern of SIGNAL_PATTERNS) {
110
+ const match = prose.match(signalPattern.pattern);
111
+ if (!match?.[0] || seen.has(signalPattern.kind))
112
+ continue;
113
+ seen.add(signalPattern.kind);
114
+ signals.push({
115
+ kind: signalPattern.kind,
116
+ evidence: match[0],
117
+ candidate_lane: signalPattern.candidateLane,
118
+ confidence: signalPattern.confidence,
119
+ });
120
+ }
121
+ return signals;
122
+ }
123
+ function chooseLane(signals) {
124
+ if (signals.some((signal) => signal.kind === "numeric_cardinality" ||
125
+ signal.kind === "numeric_threshold")) {
126
+ return "strict_property";
127
+ }
128
+ if (signals.some((signal) => signal.kind === "conditional" ||
129
+ signal.kind === "permission" ||
130
+ signal.kind === "state_or_default")) {
131
+ return "predicate";
132
+ }
133
+ if (signals.some((signal) => signal.kind === "normative_modal")) {
134
+ return "observation_review";
135
+ }
136
+ return "none";
137
+ }
138
+ function suggestedTools(lane) {
139
+ if (lane === "strict_property")
140
+ return ["kb_model_requirement"];
141
+ if (lane === "predicate")
142
+ return ["kb_suggest_predicates"];
143
+ if (lane === "observation_review") {
144
+ return ["kb_model_requirement", "kb_suggest_predicates"];
145
+ }
146
+ return [];
147
+ }
148
+ function ambiguityWitnesses(signals) {
149
+ return signals
150
+ .filter((signal) => signal.kind === "numeric_cardinality")
151
+ .map((signal) => ({
152
+ signal_kind: signal.kind,
153
+ evidence: signal.evidence,
154
+ interpretations: [
155
+ "exactly",
156
+ "at_most",
157
+ "at_least",
158
+ "named_membership",
159
+ "illustrative_example",
160
+ ],
161
+ message: "Numeric cardinality prose can mean an exact count, an upper/lower bound, named membership, or an example; model it explicitly before relying on contradiction checks.",
162
+ }));
163
+ }
164
+ const NUMBER_WORDS = new Map([
165
+ ["zero", 0],
166
+ ["one", 1],
167
+ ["two", 2],
168
+ ["three", 3],
169
+ ["four", 4],
170
+ ["five", 5],
171
+ ["six", 6],
172
+ ["seven", 7],
173
+ ["eight", 8],
174
+ ["nine", 9],
175
+ ["ten", 10],
176
+ ]);
177
+ function parseNumberToken(value) {
178
+ const normalized = value.toLowerCase();
179
+ if (/^\d+$/.test(normalized))
180
+ return Number(normalized);
181
+ return NUMBER_WORDS.get(normalized) ?? null;
182
+ }
183
+ function normalizeKey(value) {
184
+ return value
185
+ .trim()
186
+ .toLowerCase()
187
+ .replace(/['’]/g, "")
188
+ .replace(/[^a-z0-9]+/g, "_")
189
+ .replace(/^_+|_+$/g, "");
190
+ }
191
+ function normalizePredicateToken(value) {
192
+ return value
193
+ .trim()
194
+ .replace(/\b(?:a|an|the)\b\s*/gi, "")
195
+ .replace(/['’]/g, "")
196
+ .replace(/\s+/g, "_")
197
+ .replace(/[^A-Za-z0-9_-]+/g, "_")
198
+ .replace(/^_+|_+$/g, "");
199
+ }
200
+ function singularize(value) {
201
+ if (["status", "results"].includes(value))
202
+ return value;
203
+ return value.endsWith("s") && value.length > 3 ? value.slice(0, -1) : value;
204
+ }
205
+ function normalizeSubjectKey(value) {
206
+ return normalizeKey(value).split("_").map(singularize).join(".");
207
+ }
208
+ function titleFor(payload, fallback) {
209
+ const properties = isRecord(payload.properties) ? payload.properties : {};
210
+ return stringValue(properties.title) || fallback;
211
+ }
212
+ function relationshipPlan(requirementId, factId, type) {
213
+ return { type, from: requirementId, to: factId };
214
+ }
215
+ function buildStrictApplyPlan(payload, claim) {
216
+ const reqId = stringValue(payload.id) || `REQ-SEMANTIC-${shortHash(claim.subject_key)}`;
217
+ const source = extractSource(payload);
218
+ const subjectId = `FACT-SUBJECT-${shortHash(claim.subject_key)}`;
219
+ const propertyId = `FACT-PROP-${shortHash(`${claim.subject_key}.${claim.property_key}.${claim.operator}.${canonicalize(claim)}`)}`;
220
+ return [
221
+ {
222
+ type: "fact",
223
+ id: subjectId,
224
+ properties: {
225
+ title: `${claim.subject_key} subject`,
226
+ status: "active",
227
+ source,
228
+ fact_kind: "subject",
229
+ subject_key: claim.subject_key,
230
+ canonical_key: claim.subject_key,
231
+ tags: ["lane:strict", "semantic-advisor-suggestion"],
232
+ },
233
+ relationships: [],
234
+ },
235
+ {
236
+ type: "fact",
237
+ id: propertyId,
238
+ properties: {
239
+ title: `${claim.subject_key} ${claim.property_key}`,
240
+ status: "active",
241
+ source,
242
+ fact_kind: "property_value",
243
+ subject_key: claim.subject_key,
244
+ property_key: claim.property_key,
245
+ operator: claim.operator,
246
+ value_type: claim.value_type,
247
+ ...(claim.value_string !== undefined
248
+ ? { value_string: claim.value_string }
249
+ : {}),
250
+ ...(claim.value_int !== undefined
251
+ ? { value_int: claim.value_int }
252
+ : {}),
253
+ ...(claim.value_number !== undefined
254
+ ? { value_number: claim.value_number }
255
+ : {}),
256
+ ...(claim.value_bool !== undefined
257
+ ? { value_bool: claim.value_bool }
258
+ : {}),
259
+ ...(claim.unit ? { unit: claim.unit } : {}),
260
+ canonical_key: `${claim.subject_key}.${claim.property_key}.${claim.operator}.${claim.value_string ?? claim.value_int ?? claim.value_number ?? claim.value_bool}`,
261
+ tags: ["lane:strict", "semantic-advisor-suggestion"],
262
+ },
263
+ relationships: [],
264
+ },
265
+ {
266
+ type: "req",
267
+ id: reqId,
268
+ properties: {
269
+ title: titleFor(payload, "Semantic advisor requirement suggestion"),
270
+ status: "open",
271
+ source,
272
+ text_ref: extractStatement(payload),
273
+ tags: ["semantic-advisor-suggestion"],
274
+ },
275
+ relationships: [
276
+ relationshipPlan(reqId, subjectId, "constrains"),
277
+ relationshipPlan(reqId, propertyId, "requires_property"),
278
+ ],
279
+ },
280
+ ];
281
+ }
282
+ function strictSuggestion(payload, evidence, claim, rationale, confidence = 0.9) {
283
+ return {
284
+ kind: "strict_property",
285
+ confidence,
286
+ evidence,
287
+ rationale,
288
+ suggested_next_tool: "kb_model_requirement",
289
+ claim,
290
+ rejected_alternatives: ["predicate", "observation_review"],
291
+ applyPlan: buildStrictApplyPlan(payload, claim),
292
+ };
293
+ }
294
+ function buildPredicateApplyPlan(payload, predicate) {
295
+ const source = extractSource(payload);
296
+ return [
297
+ {
298
+ type: "fact",
299
+ id: `FACT-PRED-${shortHash(predicate.canonical_key)}`,
300
+ properties: {
301
+ title: `${predicate.predicate_name} suggestion`,
302
+ status: "active",
303
+ source,
304
+ fact_kind: "predicate",
305
+ predicate_name: predicate.predicate_name,
306
+ predicate_args: predicate.predicate_args,
307
+ canonical_key: predicate.canonical_key,
308
+ polarity: predicate.polarity,
309
+ tags: ["lane:ontology", "semantic-advisor-suggestion"],
310
+ },
311
+ relationships: [],
312
+ },
313
+ ];
314
+ }
315
+ function predicateSuggestion(payload, evidence, predicate, rationale) {
316
+ const applyPlan = buildPredicateApplyPlan(payload, predicate);
317
+ const reqId = stringValue(payload.id);
318
+ const factId = stringValue(applyPlan[0]?.id);
319
+ return {
320
+ kind: "predicate",
321
+ confidence: 0.84,
322
+ evidence,
323
+ rationale,
324
+ suggested_next_tool: "kb_suggest_predicates",
325
+ predicate,
326
+ rejected_alternatives: ["strict_property"],
327
+ applyPlan,
328
+ relationshipPlan: reqId && factId
329
+ ? {
330
+ applyAfter: factId,
331
+ requiresExistingReq: reqId,
332
+ relationship: relationshipPlan(reqId, factId, "requires_predicate"),
333
+ }
334
+ : null,
335
+ };
336
+ }
337
+ function observationApplyPlan(payload, title, tags) {
338
+ const source = extractSource(payload);
339
+ return [
340
+ {
341
+ type: "fact",
342
+ id: `FACT-OBS-${shortHash(`${stringValue(payload.id)}.${title}.${extractStatement(payload)}`)}`,
343
+ properties: {
344
+ title,
345
+ status: "active",
346
+ source,
347
+ fact_kind: "observation",
348
+ text_ref: extractStatement(payload),
349
+ tags,
350
+ },
351
+ relationships: [],
352
+ },
353
+ ];
354
+ }
355
+ function detectStrictPropertySuggestion(payload, statement) {
356
+ const expiry = statement.match(/^(?<subject>.+?)\s+expir(?:e|es)\s+after\s+(?<value>\d+)\s+(?<unit>days?|months?|years?|hours?|minutes?)\.?$/i);
357
+ if (expiry?.groups?.subject && expiry.groups.value && expiry.groups.unit) {
358
+ const unit = expiry.groups.unit.toLowerCase().replace(/s?$/, "s");
359
+ return strictSuggestion(payload, `expire after ${expiry.groups.value} ${expiry.groups.unit}`, {
360
+ subject_key: normalizeSubjectKey(expiry.groups.subject),
361
+ property_key: `expiry_${unit}`,
362
+ operator: "eq",
363
+ value_type: "int",
364
+ value_int: Number(expiry.groups.value),
365
+ unit,
366
+ }, "Expiry duration is a scalar strict property and should be explicit.", 0.88);
367
+ }
368
+ const comparative = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+(?<operator>less\s+than|greater\s+than|below|above|minimum|maximum|at\s+least|at\s+most)\s+(?<value>\d+)\.?$/i);
369
+ if (comparative?.groups?.subject &&
370
+ comparative.groups.operator &&
371
+ comparative.groups.value) {
372
+ const operatorText = comparative.groups.operator.toLowerCase();
373
+ const operator = /less|below/.test(operatorText)
374
+ ? "lt"
375
+ : /greater|above/.test(operatorText)
376
+ ? "gt"
377
+ : /minimum|least/.test(operatorText)
378
+ ? "gte"
379
+ : "lte";
380
+ return strictSuggestion(payload, `${comparative.groups.operator} ${comparative.groups.value}`, {
381
+ subject_key: normalizeSubjectKey(comparative.groups.subject),
382
+ property_key: "value",
383
+ operator,
384
+ value_type: "int",
385
+ value_int: Number(comparative.groups.value),
386
+ }, "Comparative numeric prose is a strict property constraint.", 0.86);
387
+ }
388
+ const threshold = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+return\s+within\s+(?<value>\d+)\s+(?<unit>ms|milliseconds?|s|sec|secs|seconds?)\.?$/i);
389
+ if (threshold?.groups?.subject &&
390
+ threshold.groups.value &&
391
+ threshold.groups.unit) {
392
+ const unit = threshold.groups.unit.toLowerCase().startsWith("m")
393
+ ? "ms"
394
+ : "seconds";
395
+ return strictSuggestion(payload, `within ${threshold.groups.value} ${threshold.groups.unit}`, {
396
+ subject_key: normalizeSubjectKey(threshold.groups.subject),
397
+ property_key: unit === "ms" ? "latency_ms" : "latency_seconds",
398
+ operator: "lte",
399
+ value_type: "int",
400
+ value_int: Number(threshold.groups.value),
401
+ unit,
402
+ }, "Response-time thresholds are bounded numeric properties and should be modeled explicitly.", 0.88);
403
+ }
404
+ const booleanState = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+(?<state>enabled|disabled)\.?$/i);
405
+ if (booleanState?.groups?.subject && booleanState.groups.state) {
406
+ const enabled = booleanState.groups.state.toLowerCase() === "enabled";
407
+ return strictSuggestion(payload, `be ${booleanState.groups.state}`, {
408
+ subject_key: normalizeSubjectKey(booleanState.groups.subject),
409
+ property_key: "enabled",
410
+ operator: "eq",
411
+ value_type: "bool",
412
+ value_bool: enabled,
413
+ }, "Enabled/disabled requirements are boolean properties and can participate in strict checks.", 0.88);
414
+ }
415
+ const retention = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+retained\s+for\s+(?<value>\d+)\s+(?<unit>days?|months?|years?)\.?$/i);
416
+ if (retention?.groups) {
417
+ const subject = retention.groups.subject;
418
+ const value = retention.groups.value;
419
+ const unit = retention.groups.unit;
420
+ if (subject && value && unit) {
421
+ const normalizedUnit = unit.toLowerCase().replace(/s$/, "s");
422
+ return strictSuggestion(payload, `retained for ${value} ${unit}`, {
423
+ subject_key: normalizeSubjectKey(subject),
424
+ property_key: `retention_${normalizedUnit}`,
425
+ operator: "eq",
426
+ value_type: "int",
427
+ value_int: Number(value),
428
+ unit: normalizedUnit,
429
+ }, "Retention duration is a scalar strict property and can participate in contradiction checks.", 0.92);
430
+ }
431
+ }
432
+ const enumSet = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+one\s+of\s+(?<values>.+?)\.?$/i);
433
+ if (enumSet?.groups?.subject && enumSet.groups.values) {
434
+ const values = enumSet.groups.values
435
+ .split(/,|\bor\b/i)
436
+ .map((value) => value.trim())
437
+ .filter((value) => value.length > 0);
438
+ if (values.length > 1) {
439
+ return strictSuggestion(payload, `one of ${enumSet.groups.values}`, {
440
+ subject_key: normalizeSubjectKey(enumSet.groups.subject),
441
+ property_key: "allowed_values",
442
+ operator: "eq",
443
+ value_type: "string",
444
+ value_string: values.join("|"),
445
+ }, "Allowed enum sets are explicit property values and should not remain prose-only.", 0.86);
446
+ }
447
+ }
448
+ const cardinality = statement.match(/\b(?<operator>at\s+most|at\s+least|exactly|no\s+more\s+than|up\s+to)\s+(?<value>\d+|zero|one|two|three|four|five|six|seven|eight|nine|ten)\s+(?<resource>[a-z][a-z\s_-]*?)\.?$/i);
449
+ if (cardinality?.groups) {
450
+ const value = parseNumberToken(cardinality.groups.value ?? "");
451
+ const resource = cardinality.groups.resource;
452
+ const operatorText = cardinality.groups.operator;
453
+ if (value !== null && resource && operatorText) {
454
+ const operator = /at\s+least/i.test(operatorText)
455
+ ? "gte"
456
+ : /exactly/i.test(operatorText)
457
+ ? "eq"
458
+ : "lte";
459
+ const normalizedResource = normalizeKey(resource);
460
+ return strictSuggestion(payload, `${operatorText} ${cardinality.groups.value}`, {
461
+ subject_key: singularize(normalizedResource.split("_").slice(-1)[0] ?? normalizedResource) === "session"
462
+ ? "user.session"
463
+ : normalizedResource.replace(/_/g, "."),
464
+ property_key: normalizedResource.includes("active_session")
465
+ ? "active_count"
466
+ : "count",
467
+ operator,
468
+ value_type: "int",
469
+ value_int: value,
470
+ }, "Bounded cardinality is a strict numeric property and should be modeled explicitly.", 0.9);
471
+ }
472
+ }
473
+ const capped = statement.match(/^(?<subject>.+?)\s+cap(?:s|ped)?\s+at\s+(?:(?<property>[a-z][a-z\s_-]*?)\s+)?(?<value>\d+|zero|one|two|three|four|five|six|seven|eight|nine|ten)\.?$/i);
474
+ if (capped?.groups?.subject && capped.groups.value) {
475
+ const value = parseNumberToken(capped.groups.value);
476
+ if (value !== null) {
477
+ const propertyKey = capped.groups.property
478
+ ? `${normalizeKey(capped.groups.property)}_cap`
479
+ : "count";
480
+ return strictSuggestion(payload, `cap at ${capped.groups.value}`, {
481
+ subject_key: normalizeSubjectKey(capped.groups.subject),
482
+ property_key: propertyKey,
483
+ operator: "lte",
484
+ value_type: "int",
485
+ value_int: value,
486
+ }, "Cap-at prose is an upper-bound strict property and should be modeled explicitly.", 0.9);
487
+ }
488
+ }
489
+ return null;
490
+ }
491
+ function detectPredicateSuggestion(payload, statement) {
492
+ const disabledUntil = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)?\s*(?:stay|remain)?\s*disabled\s+until\s+(?<condition>.+?)\.?$/i);
493
+ if (disabledUntil?.groups?.subject && disabledUntil.groups.condition) {
494
+ const subject = normalizeKey(disabledUntil.groups.subject);
495
+ const condition = normalizePredicateToken(disabledUntil.groups.condition);
496
+ const predicate = {
497
+ predicate_name: "guard",
498
+ predicate_args: [subject, condition, "disabled"],
499
+ canonical_key: `guard(${subject},${condition},disabled)`,
500
+ polarity: "assert",
501
+ };
502
+ return predicateSuggestion(payload, disabledUntil[0], predicate, "Disabled-until prose defines a condition that guards behavior availability and should be queryable as a predicate.");
503
+ }
504
+ const temporal = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+(?<before>[a-z][a-z\s_-]*?)\s+before\s+(?<after>.+?)\.?$/i);
505
+ if (temporal?.groups?.subject &&
506
+ temporal.groups.before &&
507
+ temporal.groups.after) {
508
+ const subject = normalizeKey(temporal.groups.subject).replace(/_/g, ".");
509
+ const beforeEvent = normalizePredicateToken(temporal.groups.before);
510
+ const afterEvent = normalizePredicateToken(temporal.groups.after);
511
+ const predicate = {
512
+ predicate_name: "temporal_order",
513
+ predicate_args: [subject, beforeEvent, afterEvent],
514
+ canonical_key: `temporal_order(${subject},${beforeEvent},${afterEvent})`,
515
+ polarity: "assert",
516
+ };
517
+ return predicateSuggestion(payload, temporal[0], predicate, "Before/after ordering is relational temporal logic and should be modeled as a predicate.");
518
+ }
519
+ const conditional = statement.match(/^if\s+(?:(?:a|an|the)\s+)?(?<conditionSubject>[a-z][a-z_-]*)\s+(?<condition>.+?),\s*(?:it|they|the\s+[a-z][a-z\s_-]*?)\s+(?<behavior>.+?)\.?$/i);
520
+ if (conditional?.groups?.conditionSubject &&
521
+ conditional.groups.condition &&
522
+ conditional.groups.behavior) {
523
+ const subject = singularize(normalizeKey(conditional.groups.conditionSubject));
524
+ const condition = normalizePredicateToken(conditional.groups.condition);
525
+ const behavior = normalizePredicateToken(conditional.groups.behavior);
526
+ const predicate = {
527
+ predicate_name: "conditional_behavior",
528
+ predicate_args: [subject, condition, behavior],
529
+ canonical_key: `conditional_behavior(${subject},${condition},${behavior})`,
530
+ polarity: "assert",
531
+ };
532
+ return predicateSuggestion(payload, conditional[0], predicate, "If/then requirement prose is conditional behavior and should be queryable as a predicate.");
533
+ }
534
+ const whenMust = statement.match(/^when\s+(?<condition>.+?),\s*(?:the\s+)?(?<subject>.+?)\s+(?:must|shall|should)\s+(?<behavior>.+?)\.?$/i);
535
+ if (whenMust?.groups?.condition && whenMust.groups.subject) {
536
+ const subject = normalizeKey(whenMust.groups.subject);
537
+ const condition = normalizePredicateToken(whenMust.groups.condition);
538
+ const behavior = normalizePredicateToken(whenMust.groups.behavior ?? "behavior");
539
+ const predicate = {
540
+ predicate_name: "conditional_behavior",
541
+ predicate_args: [subject, condition, behavior],
542
+ canonical_key: `conditional_behavior(${subject},${condition},${behavior})`,
543
+ polarity: "assert",
544
+ };
545
+ return predicateSuggestion(payload, whenMust[0], predicate, "When/must prose is conditional behavior and should be queryable as a predicate.");
546
+ }
547
+ const exception = statement.match(/^(?:the\s+)?(?<subject>[a-z][a-z\s_-]*?)\s+(?:must|shall|should)\s+(?<behavior>.+?)\s+unless\s+(?:the\s+)?(?<exception>.+?)\.?$/i);
548
+ if (exception?.groups?.subject &&
549
+ exception.groups.behavior &&
550
+ exception.groups.exception) {
551
+ const subject = normalizeSubjectKey(exception.groups.subject);
552
+ const behavior = normalizePredicateToken(exception.groups.behavior);
553
+ const exceptionCondition = normalizePredicateToken(exception.groups.exception);
554
+ const predicate = {
555
+ predicate_name: "exception_rule",
556
+ predicate_args: [subject, behavior, exceptionCondition],
557
+ canonical_key: `exception_rule(${subject},${behavior},${exceptionCondition})`,
558
+ polarity: "assert",
559
+ };
560
+ return predicateSuggestion(payload, exception[0], predicate, "Unless/except prose defines an explicit exception to required behavior and should be queryable as a predicate.");
561
+ }
562
+ const mutualExclusion = statement.match(/^(?<left>.+?)\s+and\s+(?<right>.+?)\s+(?:must|shall|should)\s+be\s+mutually\s+exclusive\.?$/i);
563
+ if (mutualExclusion?.groups?.left && mutualExclusion.groups.right) {
564
+ const left = normalizeKey(mutualExclusion.groups.left);
565
+ const right = normalizeKey(mutualExclusion.groups.right);
566
+ const predicate = {
567
+ predicate_name: "mutual_exclusion",
568
+ predicate_args: [left, right],
569
+ canonical_key: `mutual_exclusion(${left},${right})`,
570
+ polarity: "assert",
571
+ };
572
+ return predicateSuggestion(payload, mutualExclusion[0], predicate, "Mutual-exclusion prose is a relational constraint and should be queryable as a predicate.");
573
+ }
574
+ const dependency = statement.match(/^(?<subject>.+?)\s+requires\s+(?<prerequisite>.+?)\s+before\s+(?<dependent>.+?)\.?$/i);
575
+ if (dependency?.groups?.subject &&
576
+ dependency.groups.prerequisite &&
577
+ dependency.groups.dependent) {
578
+ const subject = normalizeKey(dependency.groups.subject);
579
+ const prerequisite = normalizePredicateToken(dependency.groups.prerequisite);
580
+ const dependent = normalizePredicateToken(dependency.groups.dependent);
581
+ const predicate = {
582
+ predicate_name: "dependency_rule",
583
+ predicate_args: [subject, prerequisite, dependent],
584
+ canonical_key: `dependency_rule(${subject},${prerequisite},${dependent})`,
585
+ polarity: "assert",
586
+ };
587
+ return predicateSuggestion(payload, dependency[0], predicate, "Requires-before prose defines a prerequisite relationship and should be queryable as a predicate.");
588
+ }
589
+ const ownership = statement.match(/^(?<resource>.+?)\s+(?:is|are)\s+owned\s+by\s+(?:the\s+)?(?<owner>.+?)\.?$/i);
590
+ if (ownership?.groups?.resource && ownership.groups.owner) {
591
+ const resource = normalizeKey(ownership.groups.resource);
592
+ const owner = normalizeKey(ownership.groups.owner);
593
+ const predicate = {
594
+ predicate_name: "ownership_rule",
595
+ predicate_args: [resource, owner],
596
+ canonical_key: `ownership_rule(${resource},${owner})`,
597
+ polarity: "assert",
598
+ };
599
+ return predicateSuggestion(payload, ownership[0], predicate, "Ownership prose assigns responsibility for a resource or behavior and should be queryable as a predicate.");
600
+ }
601
+ const retryPolicy = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+retry\s+up\s+to\s+(?<count>\d+)\s+(?<unit>times|attempts?)\.?$/i);
602
+ if (retryPolicy?.groups?.subject &&
603
+ retryPolicy.groups.count &&
604
+ retryPolicy.groups.unit) {
605
+ const subject = normalizeKey(retryPolicy.groups.subject);
606
+ const count = retryPolicy.groups.count;
607
+ const unit = normalizeKey(retryPolicy.groups.unit);
608
+ const predicate = {
609
+ predicate_name: "retry_policy",
610
+ predicate_args: [subject, count, unit],
611
+ canonical_key: `retry_policy(${subject},${count},${unit})`,
612
+ polarity: "assert",
613
+ };
614
+ return predicateSuggestion(payload, retryPolicy[0], predicate, "Retry prose defines bounded recovery behavior and should be queryable as a predicate.");
615
+ }
616
+ const escalationRule = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+escalate\s+to\s+(?<target>.+?)\s+after\s+(?<delay>\d+)\s+(?<unit>[a-z]+)\.?$/i);
617
+ if (escalationRule?.groups?.subject &&
618
+ escalationRule.groups.target &&
619
+ escalationRule.groups.delay &&
620
+ escalationRule.groups.unit) {
621
+ const subject = normalizeKey(escalationRule.groups.subject);
622
+ const target = normalizeKey(escalationRule.groups.target);
623
+ const delay = escalationRule.groups.delay;
624
+ const unit = normalizeKey(escalationRule.groups.unit);
625
+ const predicate = {
626
+ predicate_name: "escalation_rule",
627
+ predicate_args: [subject, target, delay, unit],
628
+ canonical_key: `escalation_rule(${subject},${target},${delay},${unit})`,
629
+ polarity: "assert",
630
+ };
631
+ return predicateSuggestion(payload, escalationRule[0], predicate, "Escalation prose defines delayed handoff behavior and should be queryable as a predicate.");
632
+ }
633
+ const availabilitySla = statement.match(/^(?<subject>.+?)\s+availability\s+(?:must|shall|should)\s+be\s+at\s+least\s+(?<threshold>\d+(?:\.\d+)?)\s+(?<unit>percent|%)\s+(?<window>[a-z]+)\.?$/i);
634
+ if (availabilitySla?.groups?.subject &&
635
+ availabilitySla.groups.threshold &&
636
+ availabilitySla.groups.unit &&
637
+ availabilitySla.groups.window) {
638
+ const subject = normalizeKey(availabilitySla.groups.subject);
639
+ const threshold = availabilitySla.groups.threshold;
640
+ const unit = availabilitySla.groups.unit === "%"
641
+ ? "percent"
642
+ : normalizeKey(availabilitySla.groups.unit);
643
+ const window = normalizeKey(availabilitySla.groups.window);
644
+ const predicate = {
645
+ predicate_name: "availability_sla",
646
+ predicate_args: [subject, threshold, unit, window],
647
+ canonical_key: `availability_sla(${subject},${threshold},${unit},${window})`,
648
+ polarity: "assert",
649
+ };
650
+ return predicateSuggestion(payload, availabilitySla[0], predicate, "Availability SLA prose defines a service target over a window and should be queryable as a predicate.");
651
+ }
652
+ const notificationRoute = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+notify\s+(?<recipient>.+?)\s+by\s+(?<channel>[a-z]+)\.?$/i);
653
+ if (notificationRoute?.groups?.subject &&
654
+ notificationRoute.groups.recipient &&
655
+ notificationRoute.groups.channel) {
656
+ const subject = normalizeKey(notificationRoute.groups.subject);
657
+ const recipient = normalizeKey(notificationRoute.groups.recipient);
658
+ const channel = normalizeKey(notificationRoute.groups.channel);
659
+ const predicate = {
660
+ predicate_name: "notification_route",
661
+ predicate_args: [subject, recipient, channel],
662
+ canonical_key: `notification_route(${subject},${recipient},${channel})`,
663
+ polarity: "assert",
664
+ };
665
+ return predicateSuggestion(payload, notificationRoute[0], predicate, "Notification routing prose defines a recipient and channel and should be queryable as a predicate.");
666
+ }
667
+ const idempotencyRule = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+idempotent\s+by\s+(?<key>.+?)\.?$/i);
668
+ if (idempotencyRule?.groups?.subject && idempotencyRule.groups.key) {
669
+ const subject = normalizeKey(idempotencyRule.groups.subject);
670
+ const key = normalizeKey(idempotencyRule.groups.key);
671
+ const predicate = {
672
+ predicate_name: "idempotency_rule",
673
+ predicate_args: [subject, key],
674
+ canonical_key: `idempotency_rule(${subject},${key})`,
675
+ polarity: "assert",
676
+ };
677
+ return predicateSuggestion(payload, idempotencyRule[0], predicate, "Idempotency prose defines deduplication behavior and should be queryable as a predicate.");
678
+ }
679
+ const deduplicated = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+deduplicated\s+to\s+prevent\s+redundant\s+requests\s+during\s+(?<key>.+?)\.?$/i);
680
+ if (deduplicated?.groups?.subject && deduplicated.groups.key) {
681
+ const subject = normalizeKey(deduplicated.groups.subject);
682
+ const key = normalizeKey(deduplicated.groups.key);
683
+ const predicate = {
684
+ predicate_name: "idempotency_rule",
685
+ predicate_args: [subject, key],
686
+ canonical_key: `idempotency_rule(${subject},${key})`,
687
+ polarity: "assert",
688
+ };
689
+ return predicateSuggestion(payload, deduplicated[0], predicate, "Deduplication prose defines idempotent handling of repeated or concurrent operations and should be queryable as a predicate.");
690
+ }
691
+ const dataResidencyRule = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+(?:stored|processed|kept)\s+in\s+(?:the\s+)?(?<region>.+?\b(?:region|jurisdiction|country|zone|area))\.?$/i);
692
+ if (dataResidencyRule?.groups?.subject && dataResidencyRule.groups.region) {
693
+ const subject = normalizeKey(dataResidencyRule.groups.subject);
694
+ const region = normalizeKey(dataResidencyRule.groups.region);
695
+ const predicate = {
696
+ predicate_name: "data_residency_rule",
697
+ predicate_args: [subject, region],
698
+ canonical_key: `data_residency_rule(${subject},${region})`,
699
+ polarity: "assert",
700
+ };
701
+ return predicateSuggestion(payload, dataResidencyRule[0], predicate, "Data residency prose defines regional storage or processing constraints and should be queryable as a predicate.");
702
+ }
703
+ const auditEventRule = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+(?:recorded|logged|audited)\s+in\s+(?:the\s+)?(?<log>audit\s+(?:log|trail))\.?$/i);
704
+ if (auditEventRule?.groups?.subject && auditEventRule.groups.log) {
705
+ const subject = normalizeKey(auditEventRule.groups.subject);
706
+ const log = normalizeKey(auditEventRule.groups.log);
707
+ const predicate = {
708
+ predicate_name: "audit_event_rule",
709
+ predicate_args: [subject, log],
710
+ canonical_key: `audit_event_rule(${subject},${log})`,
711
+ polarity: "assert",
712
+ };
713
+ return predicateSuggestion(payload, auditEventRule[0], predicate, "Audit logging prose defines durable audit evidence and should be queryable as a predicate.");
714
+ }
715
+ const consentRule = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+require\s+(?<consent>.+?consent)\s+before\s+(?<purpose>.+?)\.?$/i);
716
+ if (consentRule?.groups?.subject &&
717
+ consentRule.groups.consent &&
718
+ consentRule.groups.purpose) {
719
+ const subject = normalizeKey(consentRule.groups.subject);
720
+ const consent = normalizeKey(consentRule.groups.consent);
721
+ const purpose = normalizePredicateToken(consentRule.groups.purpose);
722
+ const predicate = {
723
+ predicate_name: "consent_rule",
724
+ predicate_args: [subject, consent, purpose],
725
+ canonical_key: `consent_rule(${subject},${consent},${purpose})`,
726
+ polarity: "assert",
727
+ };
728
+ return predicateSuggestion(payload, consentRule[0], predicate, "Consent prose defines a privacy prerequisite and should be queryable as a predicate.");
729
+ }
730
+ const lifecycleRule = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+(?<action>archived|deleted|expired)\s+after\s+(?<duration>\d+)\s+(?<unit>[a-z]+)\.?$/i);
731
+ if (lifecycleRule?.groups?.subject &&
732
+ lifecycleRule.groups.action &&
733
+ lifecycleRule.groups.duration &&
734
+ lifecycleRule.groups.unit) {
735
+ const subject = normalizeKey(lifecycleRule.groups.subject);
736
+ const action = normalizeKey(lifecycleRule.groups.action);
737
+ const duration = lifecycleRule.groups.duration;
738
+ const unit = normalizeKey(lifecycleRule.groups.unit);
739
+ const predicate = {
740
+ predicate_name: "lifecycle_rule",
741
+ predicate_args: [subject, action, duration, unit],
742
+ canonical_key: `lifecycle_rule(${subject},${action},${duration},${unit})`,
743
+ polarity: "assert",
744
+ };
745
+ return predicateSuggestion(payload, lifecycleRule[0], predicate, "Lifecycle prose defines archive/delete/expiry behavior over time and should be queryable as a predicate.");
746
+ }
747
+ const conflictResolutionRule = statement.match(/^when\s+(?<subject>.+?)\s+conflicts?,\s+(?:the\s+)?(?<strategy>.+?)\.?$/i);
748
+ if (conflictResolutionRule?.groups?.subject &&
749
+ conflictResolutionRule.groups.strategy) {
750
+ const subject = normalizeKey(conflictResolutionRule.groups.subject);
751
+ const strategy = normalizePredicateToken(conflictResolutionRule.groups.strategy);
752
+ const predicate = {
753
+ predicate_name: "conflict_resolution_rule",
754
+ predicate_args: [subject, strategy],
755
+ canonical_key: `conflict_resolution_rule(${subject},${strategy})`,
756
+ polarity: "assert",
757
+ };
758
+ return predicateSuggestion(payload, conflictResolutionRule[0], predicate, "Conflict-resolution prose defines synchronization merge behavior and should be queryable as a predicate.");
759
+ }
760
+ const fallbackRule = statement.match(/^if\s+(?<condition>.+?),\s+(?<subject>.+?)\s+(?:must|shall|should)\s+fall\s+back\s+to\s+(?<target>.+?)\.?$/i);
761
+ if (fallbackRule?.groups?.condition &&
762
+ fallbackRule.groups.subject &&
763
+ fallbackRule.groups.target) {
764
+ const condition = normalizePredicateToken(fallbackRule.groups.condition);
765
+ const subject = normalizeKey(fallbackRule.groups.subject);
766
+ const target = normalizePredicateToken(fallbackRule.groups.target);
767
+ const predicate = {
768
+ predicate_name: "fallback_rule",
769
+ predicate_args: [condition, subject, target],
770
+ canonical_key: `fallback_rule(${condition},${subject},${target})`,
771
+ polarity: "assert",
772
+ };
773
+ return predicateSuggestion(payload, fallbackRule[0], predicate, "Fallback prose defines degraded behavior under a condition and should be queryable as a predicate.");
774
+ }
775
+ const batchOperationRule = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+process\s+(?<resource>.+?)\s+in\s+batches\s+of\s+(?<size>\d+)\.?$/i);
776
+ if (batchOperationRule?.groups?.subject &&
777
+ batchOperationRule.groups.resource &&
778
+ batchOperationRule.groups.size) {
779
+ const subject = normalizeKey(batchOperationRule.groups.subject);
780
+ const resource = normalizeKey(batchOperationRule.groups.resource);
781
+ const size = batchOperationRule.groups.size;
782
+ const predicate = {
783
+ predicate_name: "batch_operation_rule",
784
+ predicate_args: [subject, resource, size],
785
+ canonical_key: `batch_operation_rule(${subject},${resource},${size})`,
786
+ polarity: "assert",
787
+ };
788
+ return predicateSuggestion(payload, batchOperationRule[0], predicate, "Batching prose defines bounded bulk-processing behavior and should be queryable as a predicate.");
789
+ }
790
+ const consistencyRule = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+reference\s+(?<target>an?\s+existing\s+.+?)\.?$/i);
791
+ if (consistencyRule?.groups?.subject && consistencyRule.groups.target) {
792
+ const subject = normalizeKey(consistencyRule.groups.subject);
793
+ const target = normalizePredicateToken(consistencyRule.groups.target);
794
+ const predicate = {
795
+ predicate_name: "consistency_rule",
796
+ predicate_args: [subject, target],
797
+ canonical_key: `consistency_rule(${subject},${target})`,
798
+ polarity: "assert",
799
+ };
800
+ return predicateSuggestion(payload, consistencyRule[0], predicate, "Consistency prose defines reference integrity and should be queryable as a predicate.");
801
+ }
802
+ const buildConstraint = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+(?<property>deterministic)\s+at\s+(?<scope>build\s+time)\.?$/i);
803
+ if (buildConstraint?.groups?.subject) {
804
+ const subject = normalizeKey(buildConstraint.groups.subject);
805
+ const property = normalizePredicateToken(buildConstraint.groups.property ?? "property");
806
+ const scope = normalizePredicateToken(buildConstraint.groups.scope ?? "scope");
807
+ const predicate = {
808
+ predicate_name: "build_constraint",
809
+ predicate_args: [subject, property, scope],
810
+ canonical_key: `build_constraint(${subject},${property},${scope})`,
811
+ polarity: "assert",
812
+ };
813
+ return predicateSuggestion(payload, buildConstraint[0], predicate, "Build-time prose defines deterministic generation or deployment constraints and should be queryable as a predicate.");
814
+ }
815
+ const environmentSafety = statement.match(/^(?<action>.+?)\s+(?:must|shall|should)\s+be\s+(?<decision>forbidden|read-only|allowed)\s+in\s+(?<environment>production|staging|development)\.?$/i);
816
+ if (environmentSafety?.groups?.action &&
817
+ environmentSafety.groups.environment) {
818
+ const action = normalizeKey(environmentSafety.groups.action);
819
+ const decision = normalizePredicateToken(environmentSafety.groups.decision ?? "decision");
820
+ const environment = normalizeKey(environmentSafety.groups.environment);
821
+ const predicate = {
822
+ predicate_name: "environment_safety_rule",
823
+ predicate_args: [action, decision, environment],
824
+ canonical_key: `environment_safety_rule(${action},${decision},${environment})`,
825
+ polarity: "assert",
826
+ };
827
+ return predicateSuggestion(payload, environmentSafety[0], predicate, "Environment safety prose defines action permissions by deployment environment and should be queryable as a predicate.");
828
+ }
829
+ const schemaInvariant = statement.match(/^(?<field>.+?)\s+(?:must|shall|should)\s+be\s+(?<kind>immutable)\s+after\s+(?<scope>.+?)\.?$/i);
830
+ if (schemaInvariant?.groups?.field && schemaInvariant.groups.scope) {
831
+ const field = normalizeKey(schemaInvariant.groups.field);
832
+ const kind = normalizePredicateToken(schemaInvariant.groups.kind ?? "invariant");
833
+ const scope = normalizePredicateToken(`after ${schemaInvariant.groups.scope}`);
834
+ const predicate = {
835
+ predicate_name: "schema_invariant_rule",
836
+ predicate_args: [field, kind, scope],
837
+ canonical_key: `schema_invariant_rule(${field},${kind},${scope})`,
838
+ polarity: "assert",
839
+ };
840
+ return predicateSuggestion(payload, schemaInvariant[0], predicate, "Schema invariant prose defines a field-level invariant and should be queryable as a predicate.");
841
+ }
842
+ const codingStandard = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+(?<action>use|avoid)\s+(?<target>.+?)\.?$/i);
843
+ if (codingStandard?.groups?.subject &&
844
+ codingStandard.groups.target &&
845
+ /\b(?:api|apis|code|component|computed|framework|hook|pattern|signal|schema|type)\b/i.test(statement)) {
846
+ const subject = normalizeKey(codingStandard.groups.subject);
847
+ const action = normalizePredicateToken(codingStandard.groups.action ?? "action");
848
+ const target = normalizePredicateToken(codingStandard.groups.target);
849
+ const predicate = {
850
+ predicate_name: "coding_standard_rule",
851
+ predicate_args: [subject, action, target],
852
+ canonical_key: `coding_standard_rule(${subject},${action},${target})`,
853
+ polarity: "assert",
854
+ };
855
+ return predicateSuggestion(payload, codingStandard[0], predicate, "Coding-standard prose defines developer-facing API or pattern requirements and should be queryable as a predicate.");
856
+ }
857
+ const migrationBoundary = statement.match(/^(?<subject>.+?)\s+may\s+only\s+be\s+(?<action>read)\s+as\s+(?<scope>migration\s+input)(?:\s+by\s+.+?)?\.?$/i);
858
+ if (migrationBoundary?.groups?.subject && migrationBoundary.groups.scope) {
859
+ const subject = normalizeKey(migrationBoundary.groups.subject);
860
+ const action = normalizePredicateToken(migrationBoundary.groups.action ?? "action");
861
+ const scope = normalizePredicateToken(migrationBoundary.groups.scope);
862
+ const predicate = {
863
+ predicate_name: "migration_boundary_rule",
864
+ predicate_args: [subject, action, scope],
865
+ canonical_key: `migration_boundary_rule(${subject},${action},${scope})`,
866
+ polarity: "assert",
867
+ };
868
+ return predicateSuggestion(payload, migrationBoundary[0], predicate, "Migration-boundary prose defines legacy input usage limits and should be queryable as a predicate.");
869
+ }
870
+ const absenceRequirement = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+(?<state>absent|removed)\.?$/i);
871
+ if (absenceRequirement?.groups?.subject && absenceRequirement.groups.state) {
872
+ const subject = normalizePredicateToken(absenceRequirement.groups.subject);
873
+ const state = normalizePredicateToken(absenceRequirement.groups.state);
874
+ const predicate = {
875
+ predicate_name: "absence_requirement",
876
+ predicate_args: [subject, state],
877
+ canonical_key: `absence_requirement(${subject},${state})`,
878
+ polarity: "assert",
879
+ };
880
+ return predicateSuggestion(payload, absenceRequirement[0], predicate, "Absence prose defines negative existence requirements and should be queryable as a predicate.");
881
+ }
882
+ const declarativeAbsence = statement.match(/^no\s+(?<subject>.+?)\.?$/i);
883
+ if (declarativeAbsence?.groups?.subject) {
884
+ const subject = normalizePredicateToken(declarativeAbsence.groups.subject);
885
+ const predicate = {
886
+ predicate_name: "absence_requirement",
887
+ predicate_args: [subject, "absent"],
888
+ canonical_key: `absence_requirement(${subject},absent)`,
889
+ polarity: "assert",
890
+ };
891
+ return predicateSuggestion(payload, declarativeAbsence[0], predicate, "Declarative no-X prose defines a negative existence requirement and should be queryable as a predicate.");
892
+ }
893
+ const offlineBehavior = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+(?<behavior>non-blocking|resilient)\s+during\s+(?<condition>offline\s+conditions)\.?$/i);
894
+ if (offlineBehavior?.groups?.subject && offlineBehavior.groups.condition) {
895
+ const subject = normalizeKey(offlineBehavior.groups.subject);
896
+ const behavior = normalizePredicateToken(offlineBehavior.groups.behavior ?? "behavior");
897
+ const condition = normalizePredicateToken(offlineBehavior.groups.condition);
898
+ const predicate = {
899
+ predicate_name: "offline_behavior_rule",
900
+ predicate_args: [subject, behavior, condition],
901
+ canonical_key: `offline_behavior_rule(${subject},${behavior},${condition})`,
902
+ polarity: "assert",
903
+ };
904
+ return predicateSuggestion(payload, offlineBehavior[0], predicate, "Offline behavior prose defines resilient/non-blocking behavior under offline conditions and should be queryable as a predicate.");
905
+ }
906
+ const releaseGate = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+pass\s+(?<gate>.+?)\s+before\s+(?<target>.+?)\.?$/i);
907
+ if (releaseGate?.groups?.subject &&
908
+ releaseGate.groups.target &&
909
+ /\b(?:app store|build|deployment|distribution|release|testflight)\b/i.test(statement)) {
910
+ const subject = normalizeKey(releaseGate.groups.subject);
911
+ const gate = normalizePredicateToken(releaseGate.groups.gate ?? "gate");
912
+ const target = normalizeKey(releaseGate.groups.target);
913
+ const predicate = {
914
+ predicate_name: "release_gate_rule",
915
+ predicate_args: [subject, gate, target],
916
+ canonical_key: `release_gate_rule(${subject},${gate},${target})`,
917
+ polarity: "assert",
918
+ };
919
+ return predicateSuggestion(payload, releaseGate[0], predicate, "Release-gate prose defines required gates before distribution or deployment and should be queryable as a predicate.");
920
+ }
921
+ const platformConsistency = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+synchronize\s+across\s+(?<platforms>.+?)\.?$/i);
922
+ if (platformConsistency?.groups?.subject &&
923
+ platformConsistency.groups.platforms) {
924
+ const subject = normalizeKey(platformConsistency.groups.subject);
925
+ const platforms = platformConsistency.groups.platforms
926
+ .split(/,|\band\b/i)
927
+ .map((part) => normalizeKey(part.trim()))
928
+ .filter((part) => part.length > 0)
929
+ .join(",");
930
+ const predicate = {
931
+ predicate_name: "platform_consistency_rule",
932
+ predicate_args: [subject, platforms],
933
+ canonical_key: `platform_consistency_rule(${subject},${platforms})`,
934
+ polarity: "assert",
935
+ };
936
+ return predicateSuggestion(payload, platformConsistency[0], predicate, "Platform consistency prose defines synchronization across platforms and should be queryable as a predicate.");
937
+ }
938
+ const preservationRule = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+preserve\s+(?<preserved>.+?)\s+when\s+(?:the\s+)?(?<condition>.+?)\.?$/i);
939
+ if (preservationRule?.groups?.subject && preservationRule.groups.condition) {
940
+ const subject = normalizeKey(preservationRule.groups.subject);
941
+ const preserved = normalizeKey(preservationRule.groups.preserved ?? "preserved");
942
+ const condition = normalizePredicateToken(preservationRule.groups.condition);
943
+ const predicate = {
944
+ predicate_name: "preservation_rule",
945
+ predicate_args: [subject, preserved, condition],
946
+ canonical_key: `preservation_rule(${subject},${preserved},${condition})`,
947
+ polarity: "assert",
948
+ };
949
+ return predicateSuggestion(payload, preservationRule[0], predicate, "Preservation prose defines data preserved across deletion/removal boundaries and should be queryable as a predicate.");
950
+ }
951
+ const abstractionBoundary = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+persisted\s+as\s+(?<contract>.+?)\.?$/i);
952
+ if (abstractionBoundary?.groups?.subject &&
953
+ abstractionBoundary.groups.contract &&
954
+ /\b(?:neutral|contract|renderer|runtime|vendor)\b/i.test(statement)) {
955
+ const subject = normalizeKey(abstractionBoundary.groups.subject);
956
+ const contract = normalizePredicateToken(abstractionBoundary.groups.contract);
957
+ const predicate = {
958
+ predicate_name: "abstraction_boundary_rule",
959
+ predicate_args: [subject, "persisted_as", contract],
960
+ canonical_key: `abstraction_boundary_rule(${subject},persisted_as,${contract})`,
961
+ polarity: "assert",
962
+ };
963
+ return predicateSuggestion(payload, abstractionBoundary[0], predicate, "Abstraction-boundary prose defines renderer/vendor-neutral persistence or contract constraints and should be queryable as a predicate.");
964
+ }
965
+ const securityConfiguration = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+have\s+explicit\s+(?<setting>[A-Za-z0-9_.-]+)\s+(?<value>[A-Za-z0-9_.-]+)\.?$/i);
966
+ if (securityConfiguration?.groups?.subject &&
967
+ securityConfiguration.groups.setting &&
968
+ securityConfiguration.groups.value &&
969
+ /\b(?:database|deployment|function|rpc|search_path|security|trigger)\b/i.test(statement)) {
970
+ const subject = normalizeKey(securityConfiguration.groups.subject);
971
+ const setting = normalizeKey(securityConfiguration.groups.setting);
972
+ const value = normalizeKey(securityConfiguration.groups.value);
973
+ const predicate = {
974
+ predicate_name: "security_configuration_rule",
975
+ predicate_args: [subject, setting, value],
976
+ canonical_key: `security_configuration_rule(${subject},${setting},${value})`,
977
+ polarity: "assert",
978
+ };
979
+ return predicateSuggestion(payload, securityConfiguration[0], predicate, "Security-configuration prose defines explicit infrastructure or database settings and should be queryable as a predicate.");
980
+ }
981
+ const orderedStrategy = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+use\s+(?<kind>.+?)\s+in\s+priority\s+order\s+(?<values>.+?)\.?$/i);
982
+ if (orderedStrategy?.groups?.subject && orderedStrategy.groups.values) {
983
+ const subject = normalizeKey(orderedStrategy.groups.subject);
984
+ const kind = normalizeKey(orderedStrategy.groups.kind ?? "strategy");
985
+ const values = orderedStrategy.groups.values
986
+ .split(/,|>/)
987
+ .map((value) => normalizePredicateToken(value))
988
+ .filter((value) => value.length > 0)
989
+ .join(",");
990
+ const predicate = {
991
+ predicate_name: "ordered_strategy_rule",
992
+ predicate_args: [subject, kind, values],
993
+ canonical_key: `ordered_strategy_rule(${subject},${kind},${values})`,
994
+ polarity: "assert",
995
+ };
996
+ return predicateSuggestion(payload, orderedStrategy[0], predicate, "Ordered-strategy prose defines a required priority order and should be queryable as a predicate.");
997
+ }
998
+ const refreshPolicy = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+automatically\s+refresh\s+(?<target>.+?)\s+without\s+requiring\s+manual\s+page\s+reload\.?$/i);
999
+ if (refreshPolicy?.groups?.subject && refreshPolicy.groups.target) {
1000
+ const subject = normalizeKey(refreshPolicy.groups.subject);
1001
+ const target = normalizeKey(refreshPolicy.groups.target);
1002
+ const predicate = {
1003
+ predicate_name: "refresh_policy_rule",
1004
+ predicate_args: [subject, target, "automatic"],
1005
+ canonical_key: `refresh_policy_rule(${subject},${target},automatic)`,
1006
+ polarity: "assert",
1007
+ };
1008
+ return predicateSuggestion(payload, refreshPolicy[0], predicate, "Refresh-policy prose defines automatic refresh behavior and should be queryable as a predicate.");
1009
+ }
1010
+ const scopedAuthorization = statement.match(/^(?<actor>.+?)\s+(?:must|shall|should)\s+be\s+denied\s+(?<action>.+?)\.?$/i);
1011
+ if (scopedAuthorization?.groups?.actor &&
1012
+ scopedAuthorization.groups.action &&
1013
+ /\b(?:assigned|unassigned|owner|member|scoped)\b/i.test(statement)) {
1014
+ const actor = normalizeKey(scopedAuthorization.groups.actor);
1015
+ const action = normalizeKey(scopedAuthorization.groups.action);
1016
+ const predicate = {
1017
+ predicate_name: "scoped_authorization_rule",
1018
+ predicate_args: [actor, action, "deny"],
1019
+ canonical_key: `scoped_authorization_rule(${actor},${action},deny)`,
1020
+ polarity: "assert",
1021
+ };
1022
+ return predicateSuggestion(payload, scopedAuthorization[0], predicate, "Scoped-authorization prose defines assignment/ownership-qualified authorization and should be queryable as a predicate.");
1023
+ }
1024
+ const documentationStandard = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+documented\s+in\s+(?<artifact>.+?)\.?$/i);
1025
+ if (documentationStandard?.groups?.subject &&
1026
+ documentationStandard.groups.artifact) {
1027
+ const subject = normalizeKey(documentationStandard.groups.subject);
1028
+ const artifact = normalizeKey(documentationStandard.groups.artifact);
1029
+ const predicate = {
1030
+ predicate_name: "documentation_standard_rule",
1031
+ predicate_args: [subject, "documented_in", artifact],
1032
+ canonical_key: `documentation_standard_rule(${subject},documented_in,${artifact})`,
1033
+ polarity: "assert",
1034
+ };
1035
+ return predicateSuggestion(payload, documentationStandard[0], predicate, "Documentation-standard prose defines required documentation artifacts and should be queryable as a predicate.");
1036
+ }
1037
+ const warmupPolicy = statement.match(/^(?:the\s+)?(?<subject>.+?)\s+(?:must|shall|should)\s+warm\s+up\s+on\s+(?<trigger>.+?)\.?$/i);
1038
+ if (warmupPolicy?.groups?.subject && warmupPolicy.groups.trigger) {
1039
+ const subject = normalizeKey(warmupPolicy.groups.subject);
1040
+ const trigger = normalizeKey(warmupPolicy.groups.trigger);
1041
+ const predicate = {
1042
+ predicate_name: "warmup_policy_rule",
1043
+ predicate_args: [subject, trigger],
1044
+ canonical_key: `warmup_policy_rule(${subject},${trigger})`,
1045
+ polarity: "assert",
1046
+ };
1047
+ return predicateSuggestion(payload, warmupPolicy[0], predicate, "Warmup-policy prose defines a required warmup trigger and should be queryable as a predicate.");
1048
+ }
1049
+ const visualLayout = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+remain\s+visually\s+aligned\s+with\s+(?<target>.+?)\.?$/i);
1050
+ if (visualLayout?.groups?.subject && visualLayout.groups.target) {
1051
+ const subject = normalizeKey(visualLayout.groups.subject);
1052
+ const target = normalizeKey(visualLayout.groups.target);
1053
+ const predicate = {
1054
+ predicate_name: "visual_layout_rule",
1055
+ predicate_args: [subject, "aligned_with", target],
1056
+ canonical_key: `visual_layout_rule(${subject},aligned_with,${target})`,
1057
+ polarity: "assert",
1058
+ };
1059
+ return predicateSuggestion(payload, visualLayout[0], predicate, "Visual-layout prose defines UI alignment requirements and should be queryable as a predicate.");
1060
+ }
1061
+ const enforcementLocation = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+enforced\s+at\s+(?<location>.+?)\.?$/i);
1062
+ if (enforcementLocation?.groups?.subject &&
1063
+ enforcementLocation.groups.location) {
1064
+ const subject = normalizeKey(enforcementLocation.groups.subject);
1065
+ const location = normalizeKey(enforcementLocation.groups.location);
1066
+ const predicate = {
1067
+ predicate_name: "enforcement_location_rule",
1068
+ predicate_args: [subject, location],
1069
+ canonical_key: `enforcement_location_rule(${subject},${location})`,
1070
+ polarity: "assert",
1071
+ };
1072
+ return predicateSuggestion(payload, enforcementLocation[0], predicate, "Enforcement-location prose defines the layer where a constraint is enforced and should be queryable as a predicate.");
1073
+ }
1074
+ const reconciliation = statement.match(/^on\s+(?<trigger>.+?),\s*(?<subject>.+?)\s+(?:must|shall|should)\s+reconcile\s+(?<target>.+?)\s+and\s+(?<action>clear\s+stale\s+.+?)\.?$/i);
1075
+ if (reconciliation?.groups?.subject && reconciliation.groups.target) {
1076
+ const subject = normalizeKey(reconciliation.groups.subject);
1077
+ const trigger = normalizeKey(reconciliation.groups.trigger ?? "trigger");
1078
+ const target = normalizeKey(reconciliation.groups.target);
1079
+ const action = normalizeKey(reconciliation.groups.action ?? "action");
1080
+ const predicate = {
1081
+ predicate_name: "reconciliation_rule",
1082
+ predicate_args: [subject, trigger, target, action],
1083
+ canonical_key: `reconciliation_rule(${subject},${trigger},${target},${action})`,
1084
+ polarity: "assert",
1085
+ };
1086
+ return predicateSuggestion(payload, reconciliation[0], predicate, "Reconciliation prose defines trigger-based cleanup of stale records and should be queryable as a predicate.");
1087
+ }
1088
+ const throttlePolicy = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+throttled\s+for\s+(?<condition>.+?)\.?$/i);
1089
+ if (throttlePolicy?.groups?.subject && throttlePolicy.groups.condition) {
1090
+ const subject = normalizeKey(throttlePolicy.groups.subject);
1091
+ const condition = normalizePredicateToken(throttlePolicy.groups.condition);
1092
+ const predicate = {
1093
+ predicate_name: "throttle_policy_rule",
1094
+ predicate_args: [subject, condition],
1095
+ canonical_key: `throttle_policy_rule(${subject},${condition})`,
1096
+ polarity: "assert",
1097
+ };
1098
+ return predicateSuggestion(payload, throttlePolicy[0], predicate, "Throttle-policy prose defines throttling behavior under high-frequency conditions and should be queryable as a predicate.");
1099
+ }
1100
+ const initializesAfter = statement.match(/^(?:the\s+)?(?<subject>.+?)\s+initializes\s+after\s+(?:the\s+)?(?<ready>.+?)\s+is\s+ready\.?$/i);
1101
+ if (initializesAfter?.groups?.subject && initializesAfter.groups.ready) {
1102
+ const subject = normalizeKey(initializesAfter.groups.subject);
1103
+ const ready = `${normalizeKey(initializesAfter.groups.ready)}_ready`;
1104
+ const predicate = {
1105
+ predicate_name: "temporal_order",
1106
+ predicate_args: [subject, ready, "initializes"],
1107
+ canonical_key: `temporal_order(${subject},${ready},initializes)`,
1108
+ polarity: "assert",
1109
+ };
1110
+ return predicateSuggestion(payload, initializesAfter[0], predicate, "Initializes-after prose defines temporal readiness ordering and should be queryable as a predicate.");
1111
+ }
1112
+ const transition = statement.match(/^when\s+(?<trigger>.+?),\s*(?:the\s+)?(?<subject>[a-z][a-z\s_-]*?)\s+transitions?\s+from\s+(?<from>[a-z][a-z0-9_-]*)\s+to\s+(?<to>[a-z][a-z0-9_-]*)\.?$/i);
1113
+ if (transition?.groups?.subject &&
1114
+ transition.groups.from &&
1115
+ transition.groups.to &&
1116
+ transition.groups.trigger) {
1117
+ const subject = normalizeSubjectKey(transition.groups.subject);
1118
+ const from = normalizePredicateToken(transition.groups.from);
1119
+ const to = normalizePredicateToken(transition.groups.to);
1120
+ const trigger = normalizePredicateToken(transition.groups.trigger);
1121
+ const predicate = {
1122
+ predicate_name: "state_transition",
1123
+ predicate_args: [subject, from, to, trigger],
1124
+ canonical_key: `state_transition(${subject},${from},${to},${trigger})`,
1125
+ polarity: "assert",
1126
+ };
1127
+ return predicateSuggestion(payload, transition[0], predicate, "State transitions have source state, target state, and trigger; model them as predicate facts.");
1128
+ }
1129
+ const prohibition = statement.match(/^(?<actor>[a-z][a-z\s_-]*?)\s+(?:must\s+not|cannot|can't|is\s+forbidden\s+to)\s+(?<action>[a-z][a-z_-]*)\s+(?<resource>.+?)\.?$/i);
1130
+ if (prohibition?.groups?.actor &&
1131
+ prohibition.groups.action &&
1132
+ prohibition.groups.resource) {
1133
+ const actor = singularize(normalizeKey(prohibition.groups.actor));
1134
+ const action = normalizePredicateToken(prohibition.groups.action);
1135
+ const resource = normalizePredicateToken(prohibition.groups.resource);
1136
+ const predicate = {
1137
+ predicate_name: "permission_rule",
1138
+ predicate_args: [actor, action, resource, "deny"],
1139
+ canonical_key: `permission_rule(${actor},${action},${resource},deny)`,
1140
+ polarity: "deny",
1141
+ };
1142
+ return predicateSuggestion(payload, prohibition[0], predicate, "Prohibitions are negative permission rules and should preserve deny polarity.");
1143
+ }
1144
+ const uniqueness = statement.match(/^(?:there\s+)?(?:must|shall|should)\s+be\s+at\s+most\s+one\s+(?<subject>[a-z][a-z\s_-]*?)\s+per\s+(?<scope>.+?)\.?$/i);
1145
+ if (uniqueness?.groups?.subject && uniqueness.groups.scope) {
1146
+ const subject = normalizeKey(uniqueness.groups.subject);
1147
+ const scope = uniqueness.groups.scope
1148
+ .split(/\s+per\s+/i)
1149
+ .map((part) => part.trim())
1150
+ .filter((part) => part.length > 0)
1151
+ .join(",");
1152
+ const normalizedScope = scope
1153
+ .split(",")
1154
+ .map((part) => normalizePredicateToken(part))
1155
+ .join(",");
1156
+ const predicate = {
1157
+ predicate_name: "uniqueness_constraint",
1158
+ predicate_args: [subject, normalizedScope],
1159
+ canonical_key: `uniqueness_constraint(${subject},${normalizedScope})`,
1160
+ polarity: "assert",
1161
+ };
1162
+ return predicateSuggestion(payload, uniqueness[0], predicate, "Per-scope uniqueness is relational and should be modeled as a predicate rather than a generic count.");
1163
+ }
1164
+ const defaultValue = statement.match(/^(?:the\s+)?(?<subject>[a-z][a-z\s_-]*?)\s+defaults?\s+to\s+(?<value>[a-z][a-z0-9\s_-]*?)(?:\s+(?<property>mode|state|status))?\.?$/i);
1165
+ if (defaultValue?.groups?.subject && defaultValue.groups.value) {
1166
+ const subject = normalizeSubjectKey(defaultValue.groups.subject);
1167
+ const property = normalizeKey(defaultValue.groups.property ?? "value");
1168
+ const value = normalizeKey(defaultValue.groups.value);
1169
+ const predicate = {
1170
+ predicate_name: "default_value",
1171
+ predicate_args: [subject, property, value],
1172
+ canonical_key: `default_value(${subject},${property},${value})`,
1173
+ polarity: "assert",
1174
+ };
1175
+ return predicateSuggestion(payload, defaultValue[0], predicate, "Defaults are relational product behavior and should be explicit ontology predicates.");
1176
+ }
1177
+ const stateMembership = statement.match(/^(?<subject>.+?)\s+(?:terminal\s+)?states\s+are\s+(?<states>.+?)\.?$/i);
1178
+ if (stateMembership?.groups?.subject && stateMembership.groups.states) {
1179
+ const subject = normalizeSubjectKey(stateMembership.groups.subject);
1180
+ const states = stateMembership.groups.states
1181
+ .split(/,|\band\b|\bor\b/i)
1182
+ .map((state) => state.trim())
1183
+ .filter((state) => state.length > 0)
1184
+ .map(normalizePredicateToken)
1185
+ .join(",");
1186
+ if (states) {
1187
+ const predicate = {
1188
+ predicate_name: "state_membership",
1189
+ predicate_args: [subject, states],
1190
+ canonical_key: `state_membership(${subject},${states})`,
1191
+ polarity: "assert",
1192
+ };
1193
+ return predicateSuggestion(payload, stateMembership[0], predicate, "State sets are relational workflow constraints and should be queryable as predicate facts.");
1194
+ }
1195
+ }
1196
+ const rateLimit = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+rate\s+limited\s+to\s+(?<count>\d+)\s+(?<action>[a-z][a-z\s_-]*?)\s+per\s+(?<window>[a-z]+)\.?$/i);
1197
+ if (rateLimit?.groups?.subject &&
1198
+ rateLimit.groups.count &&
1199
+ rateLimit.groups.action &&
1200
+ rateLimit.groups.window) {
1201
+ const subject = `${normalizeKey(rateLimit.groups.subject).replace(/_requests?$/, "")}.request`;
1202
+ const action = normalizePredicateToken(rateLimit.groups.action);
1203
+ const window = normalizePredicateToken(rateLimit.groups.window);
1204
+ const count = rateLimit.groups.count;
1205
+ const predicate = {
1206
+ predicate_name: "rate_limit",
1207
+ predicate_args: [subject, action, window, count],
1208
+ canonical_key: `rate_limit(${subject},${action},${window},${count})`,
1209
+ polarity: "assert",
1210
+ };
1211
+ return predicateSuggestion(payload, rateLimit[0], predicate, "Rate limits are bounded action-window constraints and now map to the production rate_limit predicate.");
1212
+ }
1213
+ const permission = statement.match(/^only\s+(?<actor>[a-z][a-z\s_-]*?)\s+can\s+(?<action>[a-z][a-z_-]*)\s+(?<resource>.+?)(?:\s+when\s+.+)?\.?$/i);
1214
+ if (permission?.groups?.actor &&
1215
+ permission.groups.action &&
1216
+ permission.groups.resource) {
1217
+ const actor = singularize(normalizeKey(permission.groups.actor));
1218
+ const action = normalizeKey(permission.groups.action);
1219
+ const resource = normalizeKey(permission.groups.resource);
1220
+ const predicate = {
1221
+ predicate_name: "permission_rule",
1222
+ predicate_args: [actor, action, resource, "assert"],
1223
+ canonical_key: `permission_rule(${actor},${action},${resource},assert)`,
1224
+ polarity: "assert",
1225
+ };
1226
+ return predicateSuggestion(payload, permission[0], predicate, "Actor/action/resource permission prose is better represented as an ontology predicate than a scalar property.");
1227
+ }
1228
+ return null;
1229
+ }
1230
+ function detectAmbiguitySuggestion(payload, statement) {
1231
+ const ambiguous = statement.match(/\b(?<value>\d+|zero|one|two|three|four|five|six|seven|eight|nine|ten)\s+(?<resource>active\s+sessions?|sessions?)\b/i);
1232
+ if (!ambiguous?.groups?.value ||
1233
+ /at\s+most|at\s+least|exactly|no\s+more\s+than|up\s+to/i.test(statement)) {
1234
+ return null;
1235
+ }
1236
+ return {
1237
+ kind: "ambiguity_observation",
1238
+ confidence: 0.78,
1239
+ evidence: `${ambiguous.groups.value} ${ambiguous.groups.resource}`,
1240
+ rationale: "Cardinality without an explicit operator is ambiguous and should be clarified before strict modeling.",
1241
+ ambiguity: ["exactly", "at_most", "at_least", "illustrative_example"],
1242
+ suggested_next_tool: "kb_model_requirement",
1243
+ applyPlan: observationApplyPlan(payload, "Ambiguous cardinality requirement", ["semantic-advisor-suggestion", "review:ambiguity"]),
1244
+ };
1245
+ }
1246
+ function detectOntologyGapSuggestion(payload, statement) {
1247
+ const rateLimit = statement.match(/\brate\s+limited\s+to\s+(?<count>\d+)\s+(?<action>[a-z][a-z\s_-]*?)\s+per\s+(?<window>[a-z]+)\b/i);
1248
+ if (!rateLimit?.groups)
1249
+ return null;
1250
+ return {
1251
+ kind: "ontology_gap",
1252
+ confidence: 0.82,
1253
+ evidence: rateLimit[0],
1254
+ rationale: "Rate limiting is logical and machine-checkable, but the current built-in predicate set needs a dedicated schema before grounding it safely.",
1255
+ suggested_next_tool: "kb_suggest_predicates",
1256
+ recommendedPredicateSchema: {
1257
+ predicate_name: "rate_limit",
1258
+ argument_names: ["subject", "action", "window", "count"],
1259
+ argument_types: ["entity", "action", "duration", "number"],
1260
+ },
1261
+ applyPlan: observationApplyPlan(payload, "Ontology gap: rate_limit", [
1262
+ "semantic-advisor-suggestion",
1263
+ "review:ontology-gap",
1264
+ "needs_schema_extension",
1265
+ ]),
1266
+ };
1267
+ }
1268
+ function modelingSuggestions(payload, modeled) {
1269
+ if (!isRequirementPayload(payload) || modeled)
1270
+ return [];
1271
+ const statement = extractStatement(payload);
1272
+ if (!statement)
1273
+ return [];
1274
+ const wholeStatement = statement.trim().replace(/[.]+$/g, "");
1275
+ const splitStatements = statement
1276
+ .split(/\s+and\s+(?=[a-z][a-z\s_-]*(?:expire|must|shall|should|default|transition|states?\s+are))/i)
1277
+ .map((part) => part.trim().replace(/[.]+$/g, ""))
1278
+ .filter((part) => part.length > 0);
1279
+ const suggestionStatements = Array.from(new Set(/\bmutually\s+exclusive\b/i.test(statement)
1280
+ ? [wholeStatement, ...splitStatements]
1281
+ : splitStatements));
1282
+ const detectors = [
1283
+ detectPredicateSuggestion,
1284
+ detectOntologyGapSuggestion,
1285
+ detectAmbiguitySuggestion,
1286
+ detectStrictPropertySuggestion,
1287
+ ];
1288
+ const suggestions = [];
1289
+ const seen = new Set();
1290
+ for (const candidateStatement of suggestionStatements) {
1291
+ for (const detector of detectors) {
1292
+ const suggestion = detector(payload, candidateStatement);
1293
+ if (!suggestion)
1294
+ continue;
1295
+ const key = `${suggestion.kind}:${suggestion.evidence}:${suggestion.suggested_next_tool}`;
1296
+ if (!seen.has(key)) {
1297
+ seen.add(key);
1298
+ suggestions.push(suggestion);
1299
+ }
1300
+ break;
1301
+ }
1302
+ }
1303
+ return suggestions;
1304
+ }
1305
+ function summaryFor(readiness, lane) {
1306
+ if (readiness === "modeled") {
1307
+ return "Requirement already links to strict or predicate facts; semantic advisor has no repair warning.";
1308
+ }
1309
+ if (readiness === "not_applicable") {
1310
+ return "No strong machine-checkable requirement signals were detected.";
1311
+ }
1312
+ if (lane === "strict_property") {
1313
+ return "Requirement prose appears to contain scalar, threshold, or cardinality logic that should be modeled with strict facts.";
1314
+ }
1315
+ if (lane === "predicate") {
1316
+ return "Requirement prose appears to contain relational or behavioral logic that should be modeled with ontology predicates.";
1317
+ }
1318
+ return "Requirement prose appears normative but needs review before it can participate in logic checks.";
1319
+ }
1320
+ function laneForSuggestions(suggestions) {
1321
+ if (suggestions.some((suggestion) => suggestion.kind === "strict_property")) {
1322
+ return "strict_property";
1323
+ }
1324
+ if (suggestions.some((suggestion) => suggestion.kind === "predicate")) {
1325
+ return "predicate";
1326
+ }
1327
+ return null;
1328
+ }
1329
+ function warningsFor(receipt) {
1330
+ if (receipt.logic_readiness !== "needs_modeling")
1331
+ return [];
1332
+ const tools = receipt.suggested_next_tools.join(" or ");
1333
+ return [
1334
+ `Semantic advisor: ${receipt.summary} Next action: call ${tools} before treating this requirement as Prolog-checkable.`,
1335
+ ];
1336
+ }
1337
+ export function analyzeSemanticAdvisorInput(input) {
1338
+ const payload = input.payload;
1339
+ const signals = isRequirementPayload(payload)
1340
+ ? detectSignals(extractProse(payload))
1341
+ : [];
1342
+ const modeled = isRequirementPayload(payload) && hasModeledRelationships(payload);
1343
+ const suggestions = modelingSuggestions(payload, modeled);
1344
+ const candidateLane = modeled
1345
+ ? "none"
1346
+ : (laneForSuggestions(suggestions) ?? chooseLane(signals));
1347
+ const readiness = modeled
1348
+ ? "modeled"
1349
+ : candidateLane === "none"
1350
+ ? "not_applicable"
1351
+ : "needs_modeling";
1352
+ const receipt = {
1353
+ version: SEMANTIC_ADVISOR_VERSION,
1354
+ payload_hash: payloadHash(payload),
1355
+ logic_readiness: readiness,
1356
+ candidate_lane: candidateLane,
1357
+ signals,
1358
+ ambiguity_witnesses: modeled ? [] : ambiguityWitnesses(signals),
1359
+ suggestions,
1360
+ suggested_next_tools: modeled ? [] : suggestedTools(candidateLane),
1361
+ summary: summaryFor(readiness, candidateLane),
1362
+ };
1363
+ return {
1364
+ receipt,
1365
+ warnings: warningsFor(receipt),
1366
+ };
1367
+ }