deepclause-pi 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,8 @@
4
4
 
5
5
  export interface RenderOptions {
6
6
  hideOutput?: boolean;
7
+ /** Include core decision-logic blocks and rule facts. Defaults to true. */
8
+ includeLogic?: boolean;
7
9
  }
8
10
 
9
11
  interface Clause {
@@ -319,7 +321,7 @@ function extractClauses(src: string): ClauseMap {
319
321
  }
320
322
  if ("([{".includes(c)) depth++;
321
323
  else if (")]}".includes(c)) depth--;
322
- else if (c === "." && depth === 0 && /\s|$/.test(src.charAt(b + 1))) break;
324
+ else if (c === "." && depth === 0 && (b + 1 >= src.length || /\s/.test(src.charAt(b + 1)))) break;
323
325
  }
324
326
  if (!clauses.has(name)) clauses.set(name, []);
325
327
  clauses.get(name)!.push({ args, body: hasBody ? src.slice(k, b) : "" });
@@ -375,6 +377,156 @@ function agenticPredicates(clauses: ClauseMap): Set<string> {
375
377
  return agentic;
376
378
  }
377
379
 
380
+ // ---------- core logic extraction --------------------------------------------
381
+
382
+ const JUDGE_PREDICATES = new Set([
383
+ "judge", "choose", "rate", "verify", "probability", "holds", "with_judgment", "require_judgment",
384
+ ]);
385
+
386
+ const MECHANICAL_HELPERS = new Set([
387
+ "dget", "get_dict", "num", "to_num", "present", "lower", "lower_string", "upper",
388
+ "state_text", "norm_sign", "as_list", "truthy", "falsy", "format", "trunc", "basename",
389
+ ]);
390
+
391
+ const DECISION_RE = /(?:>=|=<|<|>|=:=|\bis\b|\bbetween\b|\bmod\b|\\\+)/;
392
+ const AGGREGATE_RE = /\b(findall|forall|setof|bagof|aggregate_all|maplist)\b/;
393
+
394
+ /** All `name(` tokens in a clause body, with strings and chars removed. */
395
+ function headNamesIn(text: string): string[] {
396
+ const bare = text
397
+ .replace(/"(?:[^"\\]|\\.)*"/g, '""')
398
+ .replace(/'(?:[^'\\]|\\.)*'/g, "''");
399
+ const names: string[] = [];
400
+ for (const m of bare.matchAll(/([a-z][a-zA-Z0-9_]*)\s*\(/g)) {
401
+ if (m[1]) names.push(m[1]);
402
+ }
403
+ return names;
404
+ }
405
+
406
+ function predicateArity(clauses: Clause[]): number {
407
+ return splitTop(clauses[0]?.args ?? "", ",").filter(Boolean).length;
408
+ }
409
+
410
+ function isFactClauses(clauses: Clause[]): boolean {
411
+ return clauses.every((clause) => !clause.body.trim());
412
+ }
413
+
414
+ /**
415
+ * A predicate carries core logic when it branches, applies thresholds or
416
+ * generators, or consults a semantic judgment. Mechanical plumbing helpers are
417
+ * excluded so the logic section stays about decisions, not string handling.
418
+ */
419
+ function isDecisionPredicate(name: string, clauses: Clause[]): boolean {
420
+ if (MECHANICAL_HELPERS.has(name)) return false;
421
+ if (clauses.length > 1) return true;
422
+ const body = clauses[0]?.body ?? "";
423
+ if (!body.trim()) return false;
424
+ if (DECISION_RE.test(body)) return true;
425
+ if (AGGREGATE_RE.test(body)) return true;
426
+ for (const h of headNamesIn(body)) if (JUDGE_PREDICATES.has(h)) return true;
427
+ return false;
428
+ }
429
+
430
+ const escLabel = (s: string): string =>
431
+ s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "'");
432
+
433
+ /** Join the summaries of a conjunction/disjunction into one readable line. */
434
+ function summarizeBody(text: string, max = 96): string {
435
+ const parts = splitGoals(text)
436
+ .map(({ goal }) => summarizeGoal(goal, max))
437
+ .filter(Boolean);
438
+ return [...new Set(parts)].slice(-2).join("; ");
439
+ }
440
+
441
+ /** One short, readable line for a single body goal. */
442
+ function summarizeGoal(goal: string, max = 96): string {
443
+ const g = stripOuterParens(goal).trim();
444
+ if (!g) return "";
445
+ const br = parseArrow(g);
446
+ if (br) {
447
+ const then = summarizeBody(br.then, Math.max(24, max - 12));
448
+ const otherwise = br.else.trim() ? summarizeBody(br.else, Math.max(24, max - 12)) : "";
449
+ return `if ${trunc(br.cond.replace(/\s+/g, " "), max)} -> ${then || "…"}${otherwise ? ` ; else: ${otherwise}` : ""}`;
450
+ }
451
+ return summarizeSingle(g, max);
452
+ }
453
+
454
+ function summarizeSingle(g: string, max: number): string {
455
+ const h = headName(g);
456
+ if (!h) return trunc(g.replace(/\s+/g, " "), max);
457
+ if (["format", "write", "print", "output", "answer"].includes(h)) {
458
+ const s = firstString(g);
459
+ if (!s) return `${h}(...)`;
460
+ const decision = /(?:decision|answer|verdict|conclusion):.*/i.exec(s);
461
+ return trunc(decision ? decision[0] : s, Math.max(max, 120));
462
+ }
463
+ if (JUDGE_PREDICATES.has(h)) {
464
+ const label = h === "require_judgment" ? "require calibrated" : h;
465
+ const s = firstString(g);
466
+ return s ? `${label}: ${trunc(s, Math.max(16, max - label.length - 2))}` : `${label}(...)`;
467
+ }
468
+ if (["findall", "forall", "setof", "bagof", "aggregate_all"].includes(h)) return `${h}(...)`;
469
+ const args = (): string[] => splitTop(g.slice(g.indexOf("(") + 1, g.lastIndexOf(")")), ",").map((p) => p.trim());
470
+ if (h === "get_dict") return args()[0] || "get_dict";
471
+ if (h === "dget" || h === "num" || h === "to_num") return args()[1] || h;
472
+ const inside = g.slice(g.indexOf("(") + 1, g.lastIndexOf(")")).replace(/\s+/g, " ").trim();
473
+ return inside ? `${h}(${trunc(inside, Math.max(12, max - h.length - 3))})` : h;
474
+ }
475
+
476
+ /** Render one clause as an escaped `<br/>`-joined list of its meaningful goals. */
477
+ function describeClause(clause: Clause): string {
478
+ if (!clause.body.trim()) return escLabel(trunc(clause.args.replace(/\s+/g, " "), 80));
479
+ const seen = new Set<string>();
480
+ const lines: string[] = [];
481
+ for (const { goal } of splitGoals(clause.body)) {
482
+ const line = summarizeGoal(goal);
483
+ if (!line) continue;
484
+ const escaped = escLabel(line);
485
+ if (seen.has(escaped)) continue;
486
+ seen.add(escaped);
487
+ lines.push(escaped);
488
+ }
489
+ return lines.join("<br/>");
490
+ }
491
+
492
+ function factSummary(name: string, clauses: Clause[]): string {
493
+ const sample = clauses
494
+ .slice(0, 6)
495
+ .map((clause) => escLabel(trunc(clause.args.replace(/\s+/g, " "), 32)))
496
+ .join(" · ");
497
+ return `${name}/${predicateArity(clauses)} — ${clauses.length} facts: ${sample}${clauses.length > 6 ? " …" : ""}`;
498
+ }
499
+
500
+ interface LogicExtraction {
501
+ logic: Map<string, Clause[]>;
502
+ facts: Map<string, Clause[]>;
503
+ }
504
+
505
+ /** Transitive closure of decision predicates and fact tables reachable from the flow. */
506
+ function collectLogic(clauses: ClauseMap, roots: Iterable<string>): LogicExtraction {
507
+ const logic = new Map<string, Clause[]>();
508
+ const facts = new Map<string, Clause[]>();
509
+ const seen = new Set<string>();
510
+ const visit = (name: string): void => {
511
+ if (seen.has(name)) return;
512
+ seen.add(name);
513
+ const bodies = clauses.get(name);
514
+ if (!bodies) return;
515
+ if (isFactClauses(bodies)) {
516
+ if (bodies.length >= 2) facts.set(name, bodies);
517
+ return;
518
+ }
519
+ if (isDecisionPredicate(name, bodies)) logic.set(name, bodies);
520
+ for (const clause of bodies) {
521
+ for (const inner of headNamesIn(clause.body)) {
522
+ if (inner !== name && clauses.has(inner) && !BUILTIN.has(inner)) visit(inner);
523
+ }
524
+ }
525
+ };
526
+ for (const root of roots) visit(root);
527
+ return { logic, facts };
528
+ }
529
+
378
530
  // ---------- rendering --------------------------------------------------------
379
531
 
380
532
  function parseArrow(goal: string): { cond: string; then: string; else: string } | null {
@@ -460,6 +612,9 @@ interface RenderState {
460
612
  stack: string[];
461
613
  sawAnswer: boolean;
462
614
  phaseOpen: boolean;
615
+ includeLogic: boolean;
616
+ logicNodes: Map<string, string[]>;
617
+ factRefs: Set<string>;
463
618
  }
464
619
 
465
620
  export function renderDml(file: string, src: string, opts: RenderOptions = {}): string {
@@ -474,6 +629,9 @@ export function renderDml(file: string, src: string, opts: RenderOptions = {}):
474
629
  stack: [],
475
630
  sawAnswer: false,
476
631
  phaseOpen: false,
632
+ includeLogic: opts.includeLogic !== false,
633
+ logicNodes: new Map(),
634
+ factRefs: new Set(),
477
635
  };
478
636
  const nid = () => `n${++state.n}`;
479
637
  const node = (shape: [string, string], label: string, klass: string): string => {
@@ -484,6 +642,22 @@ export function renderDml(file: string, src: string, opts: RenderOptions = {}):
484
642
  const edge = (a: string, b: string, label?: string) =>
485
643
  state.lines.push(` ${a} ${label ? `-->|${label}|` : "-->"} ${b}`);
486
644
 
645
+ /** Record user predicates referenced by a condition so their logic still gets a block. */
646
+ const registerLogicRefs = (text: string, nodeId: string): void => {
647
+ for (const name of headNamesIn(text)) {
648
+ const bodies = clauses.get(name);
649
+ if (!bodies || BUILTIN.has(name) || name === "agent_main") continue;
650
+ if (isFactClauses(bodies)) {
651
+ if (bodies.length >= 2) state.factRefs.add(name);
652
+ continue;
653
+ }
654
+ if (agentic.has(name)) continue;
655
+ const referenced = state.logicNodes.get(name) ?? [];
656
+ referenced.push(nodeId);
657
+ state.logicNodes.set(name, referenced);
658
+ }
659
+ };
660
+
487
661
  function renderGoals(goals: Goal[], ctx = "", topLevel = false): Segment {
488
662
  let entry: string | null = null;
489
663
  let exit: string | null = null;
@@ -519,6 +693,7 @@ export function renderDml(file: string, src: string, opts: RenderOptions = {}):
519
693
  const br = parseArrow(goal);
520
694
  if (br) {
521
695
  const d = node(["{", "}"], `if ${trunc(br.cond, 46)}`, "det");
696
+ if (state.includeLogic) registerLogicRefs(br.cond, d);
522
697
  const t = renderGoals(splitGoals(br.then), ctx);
523
698
  const e = br.else.trim() ? renderGoals(splitGoals(br.else), ctx) : null;
524
699
  const merge = nid();
@@ -557,14 +732,24 @@ export function renderDml(file: string, src: string, opts: RenderOptions = {}):
557
732
  if (h && clauses.has(h)) {
558
733
  const bodies = clauses.get(h)!;
559
734
  const isFact = bodies.every((c) => !c.body.trim());
560
- if (isFact) return null;
735
+ if (isFact) {
736
+ if (bodies.length >= 2) state.factRefs.add(h);
737
+ return null;
738
+ }
561
739
  if (agentic.has(h) && !state.stack.includes(h)) {
562
740
  state.stack.push(h);
563
741
  const seg = renderGoals(splitGoals(bodies[0]?.body ?? ""), h);
564
742
  state.stack.pop();
565
743
  return seg.entry ? seg : null;
566
744
  }
567
- return { entry: node(["[[", "]]"], `${h}()`, "det"), exit: null };
745
+ const label = state.includeLogic ? `${h}/${predicateArity(bodies)}` : `${h}()`;
746
+ const id = node(["[[", "]]"], label, "det");
747
+ if (state.includeLogic) {
748
+ const referenced = state.logicNodes.get(h) ?? [];
749
+ referenced.push(id);
750
+ state.logicNodes.set(h, referenced);
751
+ }
752
+ return { entry: id, exit: null };
568
753
  }
569
754
  return null;
570
755
  }
@@ -588,6 +773,43 @@ export function renderDml(file: string, src: string, opts: RenderOptions = {}):
588
773
  tools.forEach((t, i) => state.lines.push(` tool${i}[["${t.name}(${trunc(t.args, 26)})"]]:::tool`));
589
774
  state.lines.push(" end");
590
775
  }
776
+
777
+ if (state.includeLogic) {
778
+ const { logic, facts } = collectLogic(clauses, [...state.logicNodes.keys(), ...state.factRefs]);
779
+ if (logic.size || facts.size) {
780
+ const edges: string[] = [];
781
+ if (logic.size) {
782
+ state.lines.push(' subgraph LOGIC["core decision logic"]');
783
+ state.lines.push(" direction TB");
784
+ let index = 0;
785
+ for (const [name, bodies] of logic) {
786
+ const sg = `lg${++index}`;
787
+ state.lines.push(` subgraph ${sg}["${name}/${predicateArity(bodies)}"]`);
788
+ state.lines.push(" direction TB");
789
+ bodies.forEach((clause, ci) => {
790
+ const description = describeClause(clause) || "(clause)";
791
+ state.lines.push(` ${sg}c${ci}["${ci + 1}) ${description}"]:::det`);
792
+ });
793
+ state.lines.push(" end");
794
+ for (const nodeId of state.logicNodes.get(name) ?? []) {
795
+ edges.push(` ${nodeId} -.->|"core logic"| ${sg}`);
796
+ }
797
+ }
798
+ state.lines.push(" end");
799
+ }
800
+ if (facts.size) {
801
+ state.lines.push(' subgraph RULES["rule facts"]');
802
+ state.lines.push(" direction LR");
803
+ let index = 0;
804
+ for (const [name, bodies] of facts) {
805
+ state.lines.push(` rf${++index}["${factSummary(name, bodies)}"]:::det`);
806
+ }
807
+ state.lines.push(" end");
808
+ }
809
+ state.lines.push(...edges);
810
+ }
811
+ }
812
+
591
813
  state.lines.push(
592
814
  " classDef start fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;",
593
815
  " classDef terminal fill:#e3f2fd,stroke:#1565c0;",
@@ -4,13 +4,13 @@ export type DiagramGrade = "presentation" | "specification";
4
4
  export type RequestedGrade = DiagramGrade | "both";
5
5
 
6
6
  export const GRADE_SYSTEM_PROMPT =
7
- "You are a meticulous Mermaid v11 diagram editor. You output only valid Mermaid source, never wrapped in code fences, and you keep all facts accurate.";
7
+ "You are a meticulous Mermaid v11 diagram editor. You output only valid Mermaid source, never wrapped in code fences, and you keep all facts and decision logic accurate. Never invent or drop a rule, threshold, or outcome.";
8
8
 
9
9
  export const GRADE_PROMPTS: Record<DiagramGrade, string> = {
10
10
  presentation:
11
- "Produce a PRESENTATION-GRADE version: about 8-12 nodes, plain non-technical language, NO function names or framework jargon, highlight the headline numbers and the main steps, and add 1-2 short callouts. Keep it accurate but simple enough for a general audience.",
11
+ "Produce a PRESENTATION-GRADE version: about 8-12 nodes, plain non-technical language, NO function names or framework jargon, highlight the headline numbers, the main steps and the headline decision, and add 1-2 short callouts. Keep it accurate but simple enough for a general audience; you may compress the detailed logic but must not change the final decision or omit a rule that flips it.",
12
12
  specification:
13
- "Produce a SPECIFICATION-GRADE (detailed engineering) version: keep the technical detail (function names, task/tool roles, post-conditions, seed data), but improve labels, grouping and readability so an engineer can follow it precisely.",
13
+ "Produce a SPECIFICATION-GRADE (detailed engineering) version: keep the technical detail (function names, task/tool roles, post-conditions, seed data) AND the domain decision logic. Preserve the decision predicates from the seed's `LOGIC` section — each with its exact conditions, thresholds and outcomes — and every `RULES` fact table. You may group or collapse pure arithmetic, date and formatting helpers, but never drop or alter a domain rule, threshold or outcome. Expand each retained logic block from the DML source. Improve labels, grouping and readability so an engineer can trace every domain decision precisely.",
14
14
  };
15
15
 
16
16
  /**
@@ -38,7 +38,7 @@ function buildPrompt(grade: DiagramGrade, view: MermaidView, source: string, see
38
38
  return [
39
39
  GRADE_PROMPTS[grade],
40
40
  "",
41
- `Rules: output ONLY Mermaid v11 source (no code fences); keep it a '${syntax}' diagram; keep the facts accurate; do not use reserved words such as 'end' as node ids or class names.`,
41
+ `Rules: output ONLY Mermaid v11 source (no code fences); keep it a '${syntax}' diagram; keep the facts and decision logic accurate; preserve the seed's domain LOGIC blocks (conditions, thresholds, outcomes) and RULES facts instead of collapsing them into generic nodes, while grouping pure arithmetic/date/format helpers; do not use reserved words such as 'end' as node ids or class names.`,
42
42
  "",
43
43
  "DML source:",
44
44
  source,