pi-gnosis 0.1.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/pi.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "pi-gnosis",
3
+ "package": "pi-gnosis",
4
+ "version": "0.2.0",
5
+ "kind": "pi-package",
6
+ "description": "Circuitry-first Pi package that turns research and tutoring plans into executable Circuitry graph programs.",
7
+ "dependsOn": {
8
+ "pi-circuitry": "github:darkhorseprojects/pi-circuitry#v0.2.6",
9
+ "circuitry": "transitive through pi-circuitry",
10
+ "pi-web-access": "npm:pi-web-access@^0.10.7",
11
+ "pi-exa-search": "optional github:najibninaba/pi-exa-search#main"
12
+ },
13
+ "runtimeDefaults": {
14
+ "provider": "pi",
15
+ "model": "inherit"
16
+ },
17
+ "entrypoints": {
18
+ "skill": "skills/pi-gnosis/SKILL.md",
19
+ "manimSkill": "skills/manim-video/SKILL.md",
20
+ "config": "config/gnosis.config.json",
21
+ "graphs": {
22
+ "research": "graphs/research.circuitry.yaml",
23
+ "tutoringSession": "graphs/tutoring-session.circuitry.yaml",
24
+ "noteExport": "graphs/note-export.circuitry.yaml",
25
+ "manimLecture": "graphs/manim-lecture.circuitry.yaml",
26
+ "cleanup": "graphs/cleanup.circuitry.yaml",
27
+ "smoke": "graphs/minimal-smoke.circuitry.yaml"
28
+ },
29
+ "cli": "bin/pi-gnosis.js"
30
+ },
31
+ "capabilities": [
32
+ "circuitry-graph-programs",
33
+ "source-grounded-research",
34
+ "web-search-via-pi-web-access",
35
+ "optional-exa-source-discovery",
36
+ "open-ended-diagnostic-probing",
37
+ "knowledge-tracing-dag-state",
38
+ "obsidian-compatible-note-export",
39
+ "manim-lecture-generation",
40
+ "safe-cleanup-planning"
41
+ ]
42
+ }
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env python3
2
+ """Structural checker for Pi-GNOSIS Circuitry v0.2 graph programs.
3
+
4
+ This is not a replacement for the official pi-circuitry/circuitry validator. It is a
5
+ local smoke verifier used in offline packaging to catch schema drift, legacy graph
6
+ shapes, missing model inheritance, unsafe wiring, and the prior architecture mistake
7
+ of treating Circuitry itself as a single generic orchestration node.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ import yaml
16
+
17
+ LEGACY_TOP_LEVEL = {"agents", "inputs", "edges"}
18
+ EXECUTABLE_TYPES = {"agent", "tool"}
19
+ BANNED_IDENTITIES = {"Generic Simulation Node", "Simulation Orchestrator"}
20
+ WEB_TOOLS = {"web_search", "fetch_content", "exa_search"}
21
+
22
+
23
+ def fail(path: Path, message: str) -> str:
24
+ return f"{path}: {message}"
25
+
26
+
27
+ def as_list(value: Any) -> list[Any]:
28
+ return value if isinstance(value, list) else []
29
+
30
+
31
+ def validate_graph(path: Path) -> list[str]:
32
+ issues: list[str] = []
33
+ try:
34
+ data = yaml.safe_load(path.read_text(encoding="utf-8"))
35
+ except Exception as exc: # pragma: no cover - defensive
36
+ return [fail(path, f"cannot parse yaml: {exc}")]
37
+
38
+ if not isinstance(data, dict):
39
+ return [fail(path, "graph must be a mapping")]
40
+
41
+ if str(data.get("circuitry")) != "0.2":
42
+ issues.append(fail(path, "must declare circuitry: \"0.2\""))
43
+
44
+ runtime = data.get("runtime") or {}
45
+ if not isinstance(runtime, dict):
46
+ issues.append(fail(path, "runtime must be a mapping"))
47
+ else:
48
+ if runtime.get("provider") != "pi":
49
+ issues.append(fail(path, "runtime.provider must be pi"))
50
+ if runtime.get("model") != "inherit":
51
+ issues.append(fail(path, "runtime.model must be inherit"))
52
+
53
+ for key in LEGACY_TOP_LEVEL:
54
+ if key in data:
55
+ issues.append(fail(path, f"must not mix v0.2 resources with legacy top-level {key}"))
56
+
57
+ resources = data.get("resources")
58
+ if not isinstance(resources, dict) or not resources:
59
+ issues.append(fail(path, "resources must be a non-empty map"))
60
+ return issues
61
+
62
+ ids = set(resources)
63
+ agent_count = 0
64
+ web_tool_seen = False
65
+
66
+ for rid, entry in resources.items():
67
+ if not isinstance(entry, dict):
68
+ issues.append(fail(path, f"resource {rid} must be a map"))
69
+ continue
70
+ rtype = entry.get("type")
71
+ if rtype not in {"text", "agent", "tool"}:
72
+ issues.append(fail(path, f"resource {rid} has invalid type {rtype!r}"))
73
+ inputs = as_list(entry.get("inputs"))
74
+ if rid in inputs:
75
+ issues.append(fail(path, f"resource {rid} has a self-loop"))
76
+ for target in inputs:
77
+ if target not in ids:
78
+ issues.append(fail(path, f"resource {rid} references unknown input {target!r}"))
79
+ if rtype in EXECUTABLE_TYPES and not inputs:
80
+ issues.append(fail(path, f"executable resource {rid} must have inputs"))
81
+ if rtype == "agent":
82
+ agent_count += 1
83
+ identity = entry.get("identity")
84
+ if not identity:
85
+ issues.append(fail(path, f"agent {rid} must have identity"))
86
+ if identity in BANNED_IDENTITIES:
87
+ issues.append(fail(path, f"agent {rid} uses banned identity {identity}"))
88
+ if entry.get("model") != "inherit":
89
+ issues.append(fail(path, f"agent {rid} must use model: inherit"))
90
+ if not entry.get("instructions"):
91
+ issues.append(fail(path, f"agent {rid} must have instructions"))
92
+ if not isinstance(entry.get("expect"), dict):
93
+ issues.append(fail(path, f"agent {rid} must declare expect schema"))
94
+ if WEB_TOOLS.intersection(set(as_list(entry.get("tools")))):
95
+ web_tool_seen = True
96
+
97
+ # Cycle check through DFS.
98
+ graph = {rid: as_list(entry.get("inputs")) if isinstance(entry, dict) else [] for rid, entry in resources.items()}
99
+ visiting: set[str] = set()
100
+ visited: set[str] = set()
101
+
102
+ def dfs(node: str, stack: list[str]) -> None:
103
+ if node in visiting:
104
+ issues.append(fail(path, "cycle detected: " + " -> ".join(stack + [node])))
105
+ return
106
+ if node in visited:
107
+ return
108
+ visiting.add(node)
109
+ for dep in graph.get(node, []):
110
+ if dep in graph:
111
+ dfs(dep, stack + [node])
112
+ visiting.remove(node)
113
+ visited.add(node)
114
+
115
+ for rid in resources:
116
+ dfs(rid, [])
117
+
118
+ if "research" in path.name and not web_tool_seen:
119
+ issues.append(fail(path, "research graph must include at least one agent with pi-web-access/pi-exa-search tools"))
120
+
121
+ if agent_count == 0:
122
+ issues.append(fail(path, "graph must include at least one agent"))
123
+
124
+ return issues
125
+
126
+
127
+ def main(argv: list[str]) -> int:
128
+ if len(argv) < 2:
129
+ print("usage: check-circuitry-v02.py <graph...>", file=sys.stderr)
130
+ return 2
131
+ paths: list[Path] = []
132
+ for arg in argv[1:]:
133
+ matched = list(Path().glob(arg)) if any(ch in arg for ch in "*?[") else [Path(arg)]
134
+ paths.extend(matched)
135
+ issues: list[str] = []
136
+ for path in paths:
137
+ issues.extend(validate_graph(path))
138
+ if issues:
139
+ print("Circuitry graph validation failed:", file=sys.stderr)
140
+ for issue in issues:
141
+ print("- " + issue, file=sys.stderr)
142
+ return 1
143
+ print(f"Validated {len(paths)} Circuitry v0.2 graph(s).")
144
+ return 0
145
+
146
+
147
+ if __name__ == "__main__":
148
+ raise SystemExit(main(sys.argv))
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: manim-video
3
+ description: Local Manim CE lecture/video generation skill for Pi-GNOSIS. Use when a Circuitry graph or Pi user requests animated explanations, mathematical or technical visualizations, algorithm walkthroughs, architecture diagrams, paper explainers, or a 3Blue1Brown-style lecture output. Inspired by the public Nous/Hermes Manim Video skill docs; not an official dependency.
4
+ model: inherit
5
+ ---
6
+
7
+ # Manim Video Production
8
+
9
+ Credit: this local skill is inspired by Nous Research / Hermes Agent's public Manim Video documentation. It is rewritten for Pi-GNOSIS and bundled locally because there is no official Pi dependency to require.
10
+
11
+ ## Use
12
+
13
+ Use Manim Community Edition to create educational videos and animated lectures. Plan before coding. The output should teach a concept visually, not merely animate text.
14
+
15
+ ## Required project structure
16
+
17
+ ```text
18
+ <project>/
19
+ plan.md
20
+ scene_spec.json
21
+ script.py
22
+ render.sh
23
+ README.md
24
+ media/ # generated by Manim, disposable unless final clips are needed
25
+ ```
26
+
27
+ ## Pipeline
28
+
29
+ 1. Plan the narrative: misconception, prerequisite, visual metaphor, aha moment, scene list.
30
+ 2. Write `scene_spec.json` with scene names, objective, visual elements, narration beats, and estimated duration.
31
+ 3. Write one `script.py` with one Manim Scene class per scene.
32
+ 4. Run preflight before render: Python syntax, `manim --version`, `ffmpeg -version`, LaTeX availability if MathTex is used.
33
+ 5. Render draft with low quality first.
34
+ 6. Review stills/clips against the learning objective.
35
+ 7. Keep final video and source script; mark intermediate media for cleanup.
36
+
37
+ ## Quality rules
38
+
39
+ - Every scene must have one visual objective.
40
+ - Do not dump paragraphs onto the canvas.
41
+ - Prefer spatial structure, transformations, highlighting, and progressive reveal.
42
+ - Use consistent typography and timing.
43
+ - Pause after important reveals.
44
+ - Avoid decorative animation that does not teach.
45
+ - For research topics, include uncertainty and source ids in the narration plan when needed.
46
+
47
+ ## Safety and side effects
48
+
49
+ Write only inside the requested Manim output root. Do not delete source scripts, final videos, note exports, or DAG state. Media cache cleanup belongs to the cleanup graph.
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: pi-gnosis
3
+ description: Source-grounded, non-linear research tutoring for Pi. Use when the user wants to learn, research, build understanding, make notes, get a lecture/video, resume a topic, audit prior knowledge, generate open-ended probes, or maintain an Obsidian learning vault. This skill routes work into Circuitry graph programs through pi-circuitry instead of making the main Pi agent do everything directly.
4
+ model: inherit
5
+ ---
6
+
7
+ # Pi-GNOSIS
8
+
9
+ You are the coordinator, not the whole pipeline. Use this skill to decide when to launch Pi-GNOSIS Circuitry graph programs and how to interpret their outputs.
10
+
11
+ ## Core rule
12
+
13
+ Circuitry is the agentic programming layer. The graphs are the multi-agent simulations/conversations/workflows. Do not create a generic "simulation node". Run the appropriate graph program.
14
+
15
+ ## Graph selection
16
+
17
+ - Need fresh research or source grounding: run `graphs/research.circuitry.yaml`.
18
+ - Need one tutoring turn, diagnosis, feedback, or next step: run `graphs/tutoring-session.circuitry.yaml`.
19
+ - Need Obsidian notes or vault update: run `graphs/note-export.circuitry.yaml`.
20
+ - Need video/lecture/visual explanation: run `graphs/manim-lecture.circuitry.yaml` and let its Manim agent load `manim-video`.
21
+ - Need to clean temp artifacts after a run: run `graphs/cleanup.circuitry.yaml`.
22
+ - Need a quick dependency smoke check: run `graphs/minimal-smoke.circuitry.yaml`.
23
+
24
+ ## Conversation behavior
25
+
26
+ The learner can quit, resume, jump topics, or change modality at any point. Do not force a fixed curriculum. Maintain enough state for continuation, then let Pi talk normally.
27
+
28
+ Ask at most one useful preference question before starting, unless the user already specified enough. Good question: "Do you want text notes, Obsidian notes, a Manim video/lecture, or just a short explanation first?"
29
+
30
+ Move on when the learner demonstrates understanding through open-ended evidence, not because a script says the section is done.
31
+
32
+ ## Probes
33
+
34
+ Never use multiple-choice diagnostic questions. Use recall, explain, transfer, contrast, debug, teach-back, worked example, predict-observe-explain, or source-check probes.
35
+
36
+ ## State separation
37
+
38
+ DAG state is canonical runtime memory. Obsidian is the learner's study surface.
39
+
40
+ - Save source ledgers, claim ledgers, KT DAG, learner state, probe results, review schedules, and manifests under `.pi-gnosis/state/<run-id>/`.
41
+ - Save learner-readable notes under the configured Obsidian-compatible notes path.
42
+ - Treat Obsidian notes as evidence of learner understanding, not as source truth unless imported and validated.
43
+
44
+ ## Web research
45
+
46
+ For research graph nodes, prefer the package tools from `pi-web-access`: `web_search` and `fetch_content`. If `pi-exa-search` is installed, use `exa_search` first for source discovery and follow with `fetch_content` for selected URLs.
47
+
48
+ ## Writes and cleanup
49
+
50
+ Before any non-read-only work, require the graph agent to state the target root and output manifest. After non-read-only runs, run or offer the cleanup graph. Cleanup is plan-first and may delete only safe temporary files unless the user explicitly authorizes more.
51
+
52
+ ## Manim
53
+
54
+ For video output, run the Manim graph. The relevant agent must include `skills: [pi-gnosis, manim-video]`, so it can read the bundled Manim skill. Generate projects under the configured Manim root. Rendering requires local Manim CE, LaTeX, and ffmpeg; if unavailable, return a runnable project and clear preflight report.
@@ -0,0 +1,43 @@
1
+ import { relative, normalize } from 'node:path';
2
+
3
+ const PROTECTED_PATTERNS = [
4
+ /^\.pi-gnosis\/state\//,
5
+ /^notes\//,
6
+ /source_ledger\.json$/,
7
+ /claim_ledger\.json$/,
8
+ /kt_dag\.json$/,
9
+ /learner_state\.json$/,
10
+ /final\.(mp4|mov|webm)$/,
11
+ /script\.py$/,
12
+ /package\.json$/,
13
+ /pi\.json$/,
14
+ /^config\//,
15
+ /^skills\//,
16
+ /^graphs\//,
17
+ ];
18
+
19
+ function cleanPath(p) {
20
+ return normalize(String(p || '')).replace(/\\/g, '/').replace(/^\.\//, '');
21
+ }
22
+
23
+ export function classifyArtifact(path, options = {}) {
24
+ const p = cleanPath(path);
25
+ const tempRoot = cleanPath(options.tempRoot || '.pi-gnosis/tmp');
26
+ const manimRoot = cleanPath(options.manimRoot || 'manim');
27
+ if (!p || p.includes('..')) return { path: p, category: 'unknown', protected: true, reason: 'empty or parent traversal' };
28
+ if (PROTECTED_PATTERNS.some((re) => re.test(p))) return { path: p, category: 'protected', protected: true, reason: 'protected artifact type' };
29
+ if (p === tempRoot || p.startsWith(`${tempRoot}/`)) return { path: p, category: 'temporary', protected: false, reason: 'inside configured temporary root' };
30
+ if (p.startsWith(`${manimRoot}/`) && p.includes('/media/')) return { path: p, category: 'generated-media', protected: false, reason: 'Manim media cache' };
31
+ return { path: p, category: 'unknown', protected: true, reason: 'not in a safe cleanup root' };
32
+ }
33
+
34
+ export function planCleanup(paths, options = {}) {
35
+ if (!Array.isArray(paths)) throw new TypeError('paths must be an array');
36
+ const apply = Boolean(options.applyCleanup);
37
+ const allowMedia = Boolean(options.allowGeneratedMediaCleanup);
38
+ return paths.map((path) => {
39
+ const c = classifyArtifact(path, options);
40
+ const allowedNow = !c.protected && apply && (c.category === 'temporary' || (c.category === 'generated-media' && allowMedia));
41
+ return { ...c, allowed_now: allowedNow };
42
+ });
43
+ }
package/src/config.js ADDED
@@ -0,0 +1,40 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const PACKAGE_ROOT = resolve(__dirname, '..');
7
+ const CONFIG_PATH = resolve(PACKAGE_ROOT, 'config/gnosis.config.json');
8
+
9
+ export function loadConfig(path = CONFIG_PATH) {
10
+ const raw = readFileSync(path, 'utf8');
11
+ const config = JSON.parse(raw);
12
+ if (config?.runtime?.provider !== 'pi') {
13
+ throw new Error('Pi-GNOSIS config must use runtime.provider = pi');
14
+ }
15
+ if (config?.runtime?.model !== 'inherit') {
16
+ throw new Error('Pi-GNOSIS config must default runtime.model = inherit');
17
+ }
18
+ return config;
19
+ }
20
+
21
+ export function packageRoot() {
22
+ return PACKAGE_ROOT;
23
+ }
24
+
25
+ export function graphPath(name) {
26
+ const names = {
27
+ research: 'graphs/research.circuitry.yaml',
28
+ tutoring: 'graphs/tutoring-session.circuitry.yaml',
29
+ 'tutoring-session': 'graphs/tutoring-session.circuitry.yaml',
30
+ notes: 'graphs/note-export.circuitry.yaml',
31
+ 'note-export': 'graphs/note-export.circuitry.yaml',
32
+ manim: 'graphs/manim-lecture.circuitry.yaml',
33
+ 'manim-lecture': 'graphs/manim-lecture.circuitry.yaml',
34
+ cleanup: 'graphs/cleanup.circuitry.yaml',
35
+ smoke: 'graphs/minimal-smoke.circuitry.yaml',
36
+ };
37
+ const rel = names[name];
38
+ if (!rel) throw new Error(`Unknown graph name: ${name}`);
39
+ return resolve(PACKAGE_ROOT, rel);
40
+ }
@@ -0,0 +1,10 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { graphPath } from './config.js';
3
+
4
+ export function getGraphProgram(name) {
5
+ return readFileSync(graphPath(name), 'utf8');
6
+ }
7
+
8
+ export function listGraphPrograms() {
9
+ return ['research', 'tutoring-session', 'note-export', 'manim-lecture', 'cleanup', 'smoke'];
10
+ }
@@ -0,0 +1,40 @@
1
+ import { readFileSync } from 'node:fs';
2
+
3
+ const LEGACY_TOP_LEVEL = ['agents:', 'edges:'];
4
+ const BANNED_IDENTITIES = ['Generic Simulation Node', 'Simulation Orchestrator'];
5
+
6
+ export function validateCircuitryText(text, filename = '<graph>') {
7
+ const issues = [];
8
+ if (!text.includes('circuitry: "0.2"') && !text.includes("circuitry: '0.2'") && !text.includes('circuitry: 0.2')) {
9
+ issues.push(`${filename}: missing circuitry: "0.2"`);
10
+ }
11
+ if (!/runtime:\s*\n\s*provider:\s*pi\s*\n\s*model:\s*inherit/m.test(text)) {
12
+ issues.push(`${filename}: runtime must be provider pi and model inherit`);
13
+ }
14
+ if (!/resources:\s*\n/.test(text)) {
15
+ issues.push(`${filename}: missing resources map`);
16
+ }
17
+ for (const key of LEGACY_TOP_LEVEL) {
18
+ const re = new RegExp(`^${key}`, 'm');
19
+ if (re.test(text)) issues.push(`${filename}: must not use legacy top-level ${key}`);
20
+ }
21
+ for (const phrase of BANNED_IDENTITIES) {
22
+ if (text.includes(phrase)) issues.push(`${filename}: contains banned fake simulation node identity`);
23
+ }
24
+ const agentCount = (text.match(/type:\s*agent/g) || []).length;
25
+ const inheritCount = (text.match(/model:\s*inherit/g) || []).length;
26
+ if (agentCount > 0 && inheritCount < agentCount + 1) {
27
+ issues.push(`${filename}: every agent plus runtime must use model: inherit`);
28
+ }
29
+ return { ok: issues.length === 0, issues };
30
+ }
31
+
32
+ export function validateCircuitryFile(path) {
33
+ return validateCircuitryText(readFileSync(path, 'utf8'), path);
34
+ }
35
+
36
+ export function assertValidCircuitryText(text, filename = '<graph>') {
37
+ const result = validateCircuitryText(text, filename);
38
+ if (!result.ok) throw new Error(result.issues.join('\n'));
39
+ return true;
40
+ }
package/src/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { loadConfig, packageRoot, graphPath } from './config.js';
2
+ export { getGraphProgram, listGraphPrograms } from './graph-template.js';
3
+ export { validateCircuitryFile, validateCircuitryText, assertValidCircuitryText } from './graph-validator.js';
4
+ export { buildReviewSchedule, intervalDaysForConcept } from './review-scheduler.js';
5
+ export { storagePlan } from './storage-contract.js';
6
+ export { assertAllowedProbe, isForbiddenProbe, recommendedProbeTypes } from './probe-policy.js';
7
+ export { classifyArtifact, planCleanup } from './cleanup-policy.js';
8
+ export { buildManimProject } from './manim-project.js';
@@ -0,0 +1,44 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ function slugify(s) {
5
+ return String(s || 'lecture').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'lecture';
6
+ }
7
+
8
+ export function buildManimProject({ topic = 'knowledge tracing DAGs', outputRoot = 'manim', write = false } = {}) {
9
+ const slug = slugify(topic);
10
+ const projectRoot = resolve(outputRoot, slug);
11
+ const sceneSpec = {
12
+ topic,
13
+ scenes: [
14
+ {
15
+ className: 'OpeningMap',
16
+ learningGoal: 'Show that Pi-GNOSIS separates conversation, Circuitry programs, DAG state, and notes.',
17
+ visualMetaphor: 'Four labeled boxes connected by arrows',
18
+ durationSeconds: 25,
19
+ },
20
+ {
21
+ className: 'KTDagScene',
22
+ learningGoal: 'Show why learner state is a graph rather than a checklist.',
23
+ visualMetaphor: 'Concept nodes with prerequisite and misconception edges',
24
+ durationSeconds: 35,
25
+ },
26
+ ],
27
+ };
28
+ const plan = `# ${topic}\n\nNarrative arc: start with one Pi conversation, reveal delegated Circuitry graph programs, then show how evidence updates a KT DAG while Obsidian stays human-readable.\n\nAha moment: the graph is not just notes; it is executable state for tutoring decisions.\n`;
29
+ const script = `from manim import *\n\n\nclass OpeningMap(Scene):\n def construct(self):\n title = Text(\"Pi-GNOSIS\", font_size=42)\n subtitle = Text(\"conversation -> Circuitry -> state -> notes\", font_size=28).next_to(title, DOWN)\n self.play(Write(title))\n self.play(FadeIn(subtitle))\n self.wait(1)\n\n\nclass KTDagScene(Scene):\n def construct(self):\n nodes = VGroup(*[Circle(radius=0.35).shift(RIGHT * (i - 1) * 2) for i in range(3)])\n labels = VGroup(Text(\"A\", font_size=24).move_to(nodes[0]), Text(\"B\", font_size=24).move_to(nodes[1]), Text(\"C\", font_size=24).move_to(nodes[2]))\n arrows = VGroup(Arrow(nodes[0].get_right(), nodes[1].get_left(), buff=0.1), Arrow(nodes[1].get_right(), nodes[2].get_left(), buff=0.1))\n caption = Text(\"open-ended evidence updates the DAG\", font_size=28).next_to(nodes, DOWN)\n self.play(Create(nodes), Write(labels))\n self.play(Create(arrows))\n self.play(FadeIn(caption))\n self.wait(1)\n`;
30
+ const render = '#!/usr/bin/env bash\nset -euo pipefail\nmanim -ql script.py OpeningMap KTDagScene\n';
31
+ const readme = `# ${topic}\n\nGenerated by Pi-GNOSIS. Run \`./render.sh\` after installing Manim CE, ffmpeg, and LaTeX if needed.\n`;
32
+ const files = {
33
+ 'plan.md': plan,
34
+ 'scene_spec.json': JSON.stringify(sceneSpec, null, 2) + '\n',
35
+ 'script.py': script,
36
+ 'render.sh': render,
37
+ 'README.md': readme,
38
+ };
39
+ if (write) {
40
+ mkdirSync(projectRoot, { recursive: true });
41
+ for (const [name, content] of Object.entries(files)) writeFileSync(resolve(projectRoot, name), content, 'utf8');
42
+ }
43
+ return { projectRoot, files };
44
+ }
@@ -0,0 +1,22 @@
1
+ const FORBIDDEN_RECOGNITION_PATTERNS = [
2
+ /multiple\s*choice/i,
3
+ /select\s+one/i,
4
+ /choose\s+the\s+correct/i,
5
+ /which\s+of\s+the\s+following/i,
6
+ /\bA\)\s+.*\bB\)/s,
7
+ ];
8
+
9
+ export function isForbiddenProbe(prompt) {
10
+ return FORBIDDEN_RECOGNITION_PATTERNS.some((re) => re.test(String(prompt || '')));
11
+ }
12
+
13
+ export function assertAllowedProbe(prompt) {
14
+ if (isForbiddenProbe(prompt)) {
15
+ throw new Error('Recognition-only diagnostic probe is forbidden by Pi-GNOSIS policy');
16
+ }
17
+ return true;
18
+ }
19
+
20
+ export function recommendedProbeTypes() {
21
+ return ['recall', 'explain', 'transfer', 'contrast', 'debug', 'teach_back', 'worked_example', 'predict_observe_explain', 'source_check'];
22
+ }
@@ -0,0 +1,54 @@
1
+ const BASE_INTERVAL_BY_DECAY_RISK = new Map([
2
+ [5, 2],
3
+ [4, 3],
4
+ [3, 5],
5
+ [2, 10],
6
+ [1, 20],
7
+ [0, 30],
8
+ ]);
9
+
10
+ function toDateOnly(dateLike) {
11
+ const d = dateLike ? new Date(dateLike) : new Date();
12
+ if (Number.isNaN(d.getTime())) throw new Error(`Invalid date: ${dateLike}`);
13
+ return d;
14
+ }
15
+
16
+ function addDays(date, days) {
17
+ const d = new Date(date.getTime());
18
+ d.setUTCDate(d.getUTCDate() + days);
19
+ return d.toISOString().slice(0, 10);
20
+ }
21
+
22
+ function clampRisk(n) {
23
+ const v = Number(n ?? 0);
24
+ if (!Number.isFinite(v)) return 0;
25
+ return Math.max(0, Math.min(5, Math.round(v)));
26
+ }
27
+
28
+ export function intervalDaysForConcept(concept) {
29
+ const decay = clampRisk(concept.decay_risk);
30
+ const misconception = clampRisk(concept.misconception_risk);
31
+ const base = BASE_INTERVAL_BY_DECAY_RISK.get(decay) ?? 14;
32
+ let factor = 1;
33
+ if (misconception >= 4) factor = 0.5;
34
+ else if (misconception === 3) factor = 0.75;
35
+ return Math.max(1, Math.round(base * factor));
36
+ }
37
+
38
+ export function buildReviewSchedule(concepts, options = {}) {
39
+ if (!Array.isArray(concepts)) throw new TypeError('concepts must be an array');
40
+ const today = toDateOnly(options.today || new Date().toISOString().slice(0, 10));
41
+ return concepts.map((concept) => {
42
+ const first = intervalDaysForConcept(concept);
43
+ return {
44
+ concept: concept.concept || concept.label || concept.id,
45
+ first_review_date: addDays(today, first),
46
+ interval_days: [first, first * 2, first * 4].map((n) => Math.max(1, Math.round(n))),
47
+ reasons: {
48
+ decay_risk: clampRisk(concept.decay_risk),
49
+ misconception_risk: clampRisk(concept.misconception_risk),
50
+ },
51
+ promotion_requires: ['define from memory', 'new example', 'transfer', 'contrast', 'misconception repair', 'source check'],
52
+ };
53
+ });
54
+ }
@@ -0,0 +1,42 @@
1
+ export const DAG_STATE_FILES = [
2
+ 'run_manifest.json',
3
+ 'source_ledger.json',
4
+ 'claim_ledger.json',
5
+ 'kt_dag.json',
6
+ 'learner_state.json',
7
+ 'probe_results.json',
8
+ 'review_schedule.json',
9
+ 'output_manifest.json',
10
+ ];
11
+
12
+ export const OBSIDIAN_FILES = [
13
+ '00-map.md',
14
+ 'source-ledger.md',
15
+ 'concepts/<concept>.md',
16
+ 'misconceptions.md',
17
+ 'probes.md',
18
+ 'review-plan.md',
19
+ 'reflection-log.md',
20
+ 'manifest.md',
21
+ ];
22
+
23
+ export function storagePlan({ topic = 'topic', dagStateRoot = '.pi-gnosis/state', obsidianRoot = 'notes', manimRoot = 'manim' } = {}) {
24
+ const slug = String(topic).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'topic';
25
+ return {
26
+ canonical: {
27
+ root: `${dagStateRoot}/${slug}`,
28
+ role: 'machine-readable runtime state for Pi-GNOSIS and pi-circuitry',
29
+ files: DAG_STATE_FILES.map((name) => `${dagStateRoot}/${slug}/${name}`),
30
+ },
31
+ obsidian: {
32
+ root: `${obsidianRoot}/${slug}`,
33
+ role: 'learner-facing notes and study memory',
34
+ files: OBSIDIAN_FILES.map((name) => `${obsidianRoot}/${slug}/${name}`),
35
+ },
36
+ manim: {
37
+ root: `${manimRoot}/${slug}`,
38
+ role: 'video lecture project output',
39
+ files: ['plan.md', 'scene_spec.json', 'script.py', 'render.sh', 'README.md'].map((name) => `${manimRoot}/${slug}/${name}`),
40
+ },
41
+ };
42
+ }