deepclause-pi 0.4.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.
- package/README.md +4 -4
- package/dist/diagram/extract.d.ts +2 -0
- package/dist/diagram/extract.js +223 -3
- package/dist/diagram/grade.d.ts +1 -1
- package/dist/diagram/grade.js +4 -4
- package/dist/index.d.ts +9 -0
- package/dist/index.js +103 -81
- package/package.json +1 -1
- package/skills/handbook-dml/SKILL.md +174 -46
- package/src/assets/AGENTS.md +43 -7
- package/src/diagram/extract.ts +225 -3
- package/src/diagram/grade.ts +4 -4
- package/src/index.ts +120 -92
package/src/assets/AGENTS.md
CHANGED
|
@@ -111,15 +111,51 @@ A task can bind up to four outputs. Keep each task focused even though the runti
|
|
|
111
111
|
|
|
112
112
|
### `prompt/N`: an isolated model call
|
|
113
113
|
|
|
114
|
-
Use `prompt/N` for a subtask that should not inherit accumulated conversation memory:
|
|
114
|
+
Use `prompt/N` for a subtask that should not inherit accumulated conversation memory: adversarial review, rewriting, or formatting based only on explicitly supplied text. For a bounded classification, prefer the judgment predicates below.
|
|
115
115
|
|
|
116
116
|
```prolog
|
|
117
|
-
prompt("
|
|
118
|
-
string(
|
|
117
|
+
prompt("Rewrite this summary in plain language for a patient: {Text}. Store only the rewrite in Plain.",
|
|
118
|
+
string(Plain)).
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
Fresh context is not a security boundary. Untrusted text can still contain hostile instructions; delimit it, state how it may be used, and request narrow structured output.
|
|
122
122
|
|
|
123
|
+
### Semantic judgments: bounded, typed questions
|
|
124
|
+
|
|
125
|
+
Use the judgment predicates when the core question is a **bounded, typed question about explicit state**. They are not agentic: they do not read or write DML memory, they cannot call tools, and their answers are constrained to the options or levels you supply. That makes them cheaper and more reliable than a `task/N` for classification, rating, verification, and probability gates.
|
|
126
|
+
|
|
127
|
+
| Core question | Predicate |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| "Which label/route?" from a closed set | `choose(State, Question, Options, Choice)` |
|
|
130
|
+
| "How severe/frustrated/confident?" on an ordered scale | `rate(State, Question, Levels, Level)` |
|
|
131
|
+
| "Is X true?" (`yes`/`no`/`unknown`) | `verify(State, Question, Truth)`; `holds(State, Question)` for a semidet check |
|
|
132
|
+
| "How likely is X?" / "Is it above a threshold?" | `probability(State, Question, P)`; `holds(State, Question, Threshold)` |
|
|
133
|
+
|
|
134
|
+
Batch several questions into one request with `judge/2`:
|
|
135
|
+
|
|
136
|
+
```prolog
|
|
137
|
+
judge(Message, [
|
|
138
|
+
choose("Which team should handle this?", [billing, orders, account]) - Team,
|
|
139
|
+
verify("Does the message ask for a refund?") - Refund
|
|
140
|
+
]).
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Gate a calibrated probability and fall back when the backend only estimates:
|
|
144
|
+
|
|
145
|
+
```prolog
|
|
146
|
+
risk_band(Text, Band) :-
|
|
147
|
+
require_judgment(calibrated, probability(Text, "Risk of harm?", P)),
|
|
148
|
+
( P >= 0.8 -> Band = high ; Band = low ).
|
|
149
|
+
risk_band(Text, Band) :-
|
|
150
|
+
choose(Text, "Is the risk high or low?", [high, low], Band).
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`with_judgment(Backend, Goal)` selects a backend per scope; `require_judgment/2` fails before the judgment runs when the backend lacks a capability such as `calibrated`, so the second clause is the fallback. Answers are memoized per run by backend, model, state, and questions, so backtracking is cheap.
|
|
154
|
+
|
|
155
|
+
Do not name your own predicates `choose/4`, `rate/4`, `verify/3`, `probability/3`, `holds/2`, `holds/3`, `judge/2`, `with_judgment/2`, or `require_judgment/2`; they are runtime special predicates. `verify` is three-valued — never treat `\+ verify(...)` as "the model said no".
|
|
156
|
+
|
|
157
|
+
See `DML_REFERENCE.md` for the full syntax, options, and capability list.
|
|
158
|
+
|
|
123
159
|
### `llm/2`: low-level completion
|
|
124
160
|
|
|
125
161
|
Prefer `task/N` and `prompt/N`. Use `get_memory/1` plus `llm/2` only when the program intentionally needs a raw completion over an explicit message list and does not need the task loop's result tools or DML tools.
|
|
@@ -357,7 +393,7 @@ Represent stable facts and rules as Prolog clauses; expose narrow query/update t
|
|
|
357
393
|
|
|
358
394
|
### 6. Independent reviewers
|
|
359
395
|
|
|
360
|
-
Use `task/N` to draft and `prompt/N` to review from fresh context, then apply deterministic acceptance criteria. Best for code review, risk assessment, and editorial checks.
|
|
396
|
+
Use `task/N` to draft and `prompt/N` to review from fresh context, then apply deterministic acceptance criteria. Best for code review, risk assessment, and editorial checks. When the review is a bounded checklist ("does it state the referral threshold?"), use `verify/3` or `holds/2` instead of a free-form reviewer.
|
|
361
397
|
|
|
362
398
|
### 7. Pure deterministic utility
|
|
363
399
|
|
|
@@ -388,10 +424,10 @@ Pi can turn any DML file into a self-contained, offline Mermaid viewer. Ask for
|
|
|
388
424
|
|
|
389
425
|
Pi calls the `dc_diagram` model tool with the DML path and a grade:
|
|
390
426
|
|
|
391
|
-
- **presentation** — about 8-12 nodes, plain language, headline numbers (slides and overviews).
|
|
392
|
-
- **specification** — function names, task/tool roles, post-conditions (engineers).
|
|
427
|
+
- **presentation** — about 8-12 nodes, plain language, headline numbers and the headline decision (slides and overviews).
|
|
428
|
+
- **specification** — function names, task/tool roles, post-conditions, plus the core decision logic: each decision predicate with its conditions, thresholds and outcomes, and the rule fact tables (engineers).
|
|
393
429
|
|
|
394
|
-
The tool extracts a deterministic Mermaid seed, has pi rewrite it in the chosen grade, validates the result, writes the viewer under `.pi/deepclause/diagrams/`, and opens it. The DML file may live anywhere (workspace-relative or absolute); only the generated viewer stays under `.pi/deepclause/`.
|
|
430
|
+
The tool extracts a deterministic Mermaid seed, enriches the specification seed with a `LOGIC` section (decision predicates, guards, thresholds, judgments) and a `RULES` section (fact tables), has pi rewrite it in the chosen grade, validates the result, writes the viewer under `.pi/deepclause/diagrams/`, and opens it. The DML file may live anywhere (workspace-relative or absolute); only the generated viewer stays under `.pi/deepclause/`.
|
|
395
431
|
|
|
396
432
|
Do not hand-write Mermaid for the user, and do not copy diagram tooling into the workspace. Regenerating a grade replaces only that grade's sidecar (`<name>.presentation.mmd` / `<name>.specification.mmd`).
|
|
397
433
|
|
package/src/diagram/extract.ts
CHANGED
|
@@ -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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").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)
|
|
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
|
-
|
|
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;",
|
package/src/diagram/grade.ts
CHANGED
|
@@ -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
|
|
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
|
|
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,
|