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.
- package/README.md +4 -4
- package/dist/config.d.ts +22 -0
- package/dist/config.js +60 -0
- 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 +10 -0
- package/dist/index.js +204 -86
- package/dist/runtime.d.ts +3 -1
- package/dist/runtime.js +23 -1
- package/docs/SPECKIT.md +10 -0
- package/package.json +2 -2
- package/skills/handbook-dml/SKILL.md +174 -46
- package/src/assets/AGENTS.md +43 -7
- package/src/assets/apply.dml +2 -2
- package/src/assets/specs.dml +30 -26
- package/src/config.ts +91 -0
- package/src/diagram/extract.ts +225 -3
- package/src/diagram/grade.ts +4 -4
- package/src/index.ts +224 -97
- package/src/runtime.ts +30 -2
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,
|