docs: add Contract sections + tag tests to contract items (Tier 2 — 10 files)
Some checks failed
Deploy (Gitea) / deploy (push) Failing after 2s

This commit is contained in:
2026-05-05 12:19:25 -04:00
parent ea1150f38e
commit dcb5a1f1a6
20 changed files with 168 additions and 52 deletions

View File

@@ -129,19 +129,20 @@
(in-suite communication-protocol-suite) (in-suite communication-protocol-suite)
(test test-framing (test test-framing
"Contract 1: frame-message produces correct hex length prefix."
(let* ((msg '(:type :EVENT :payload (:action :handshake))) (let* ((msg '(:type :EVENT :payload (:action :handshake)))
(framed (frame-message msg))) (framed (frame-message msg)))
(is (string= "00002C" (string-upcase (subseq framed 0 6)))))) (is (string= "00002C" (string-upcase (subseq framed 0 6))))))
(test test-framing-round-trip (test test-framing-round-trip
"A message should survive frame → read-frame without loss." "Contract 3: frame → read-frame preserves message identity."
(let* ((msg '(:type :EVENT :payload (:action :handshake :version "1.0") :meta (:source :tui))) (let* ((msg '(:type :EVENT :payload (:action :handshake :version "1.0") :meta (:source :tui)))
(framed (frame-message msg)) (framed (frame-message msg))
(unframed (read-framed-message (make-string-input-stream framed)))) (unframed (read-framed-message (make-string-input-stream framed))))
(is (equal msg unframed)))) (is (equal msg unframed))))
(test test-framing-empty-message (test test-framing-empty-message
"An empty or simple message should still frame correctly." "Contract 1: simple messages frame with valid hex length."
(let* ((msg '(:type :ping)) (let* ((msg '(:type :ping))
(framed (frame-message msg))) (framed (frame-message msg)))
(is (> (length framed) 5)) (is (> (length framed) 5))

View File

@@ -166,6 +166,7 @@ Privacy-filtered objects (matching bouncer-privacy-tags) are excluded."
(in-suite vision-suite) (in-suite vision-suite)
(test test-foveal-rendering (test test-foveal-rendering
"Contract 1: foveal content inline, peripheral content title-only."
(clrhash passepartout::*memory-store*) (clrhash passepartout::*memory-store*)
(let* ((ast '(:type :HEADLINE :properties (:ID "proj-root" :TITLE "Project" :TAGS ("project")) (let* ((ast '(:type :HEADLINE :properties (:ID "proj-root" :TITLE "Project" :TAGS ("project"))
:contents ((:type :HEADLINE :properties (:ID "node-foveal" :TITLE "Foveal Node") :contents ((:type :HEADLINE :properties (:ID "node-foveal" :TITLE "Foveal Node")
@@ -179,6 +180,7 @@ Privacy-filtered objects (matching bouncer-privacy-tags) are excluded."
(is (not (search "PERIPHERAL CONTENT" output)))))) (is (not (search "PERIPHERAL CONTENT" output))))))
(test test-awareness-budget (test test-awareness-budget
"Contract 1: all active projects appear in awareness output."
(clrhash passepartout::*memory-store*) (clrhash passepartout::*memory-store*)
(ingest-ast '(:type :HEADLINE :properties (:ID "p1" :TITLE "Project 1" :TAGS ("project")) :contents nil)) (ingest-ast '(:type :HEADLINE :properties (:ID "p1" :TITLE "Project 1" :TAGS ("project")) :contents nil))
(ingest-ast '(:type :HEADLINE :properties (:ID "p2" :TITLE "Project 2" :TAGS ("project")) :contents nil)) (ingest-ast '(:type :HEADLINE :properties (:ID "p2" :TITLE "Project 2" :TAGS ("project")) :contents nil))
@@ -187,14 +189,14 @@ Privacy-filtered objects (matching bouncer-privacy-tags) are excluded."
(is (search "Project 2" output)))) (is (search "Project 2" output))))
(test test-context-empty-memory (test test-context-empty-memory
"An empty memory should produce a clean awareness output without errors." "Contract 1: empty memory produces clean output without error."
(clrhash passepartout::*memory-store*) (clrhash passepartout::*memory-store*)
(let ((output (context-awareness-assemble))) (let ((output (context-awareness-assemble)))
(is (stringp output)) (is (stringp output))
(is (search "MEMEX" output :test #'char-equal)))) (is (search "MEMEX" output :test #'char-equal))))
(test test-context-no-foveal-focus (test test-context-no-foveal-focus
"Without a foveal focus, all content should be peripheral (no inline content)." "Contract 2: without foveal focus, no inline content appears."
(clrhash passepartout::*memory-store*) (clrhash passepartout::*memory-store*)
(let* ((ast '(:type :HEADLINE :properties (:ID "root" :TITLE "Root" :TAGS ("project")) (let* ((ast '(:type :HEADLINE :properties (:ID "root" :TITLE "Root" :TAGS ("project"))
:contents ((:type :HEADLINE :properties (:ID "child" :TITLE "Child Node") :contents ((:type :HEADLINE :properties (:ID "child" :TITLE "Child Node")

View File

@@ -173,6 +173,7 @@ For approval-required actions, creates a Flight Plan instead of executing."
(in-suite pipeline-act-suite) (in-suite pipeline-act-suite)
(test test-loop-gate-act-basic (test test-loop-gate-act-basic
"Contract 1: approved action reaches :acted status via loop-gate-act."
(clrhash passepartout::*skill-registry*) (clrhash passepartout::*skill-registry*)
(let* ((signal (list :type :EVENT :status nil :depth 0 :approved-action '(:target :cli :payload (:text "Hello")))) (let* ((signal (list :type :EVENT :status nil :depth 0 :approved-action '(:target :cli :payload (:text "Hello"))))
(result (loop-gate-act signal))) (result (loop-gate-act signal)))
@@ -180,14 +181,14 @@ For approval-required actions, creates a Flight Plan instead of executing."
(is (null result)))) (is (null result))))
(test test-loop-gate-act-no-approved-action (test test-loop-gate-act-no-approved-action
"When no approved-action is set, the signal should still reach :acted status." "Contract 1: signal with no approved-action still reaches :acted status."
(clrhash passepartout::*skill-registry*) (clrhash passepartout::*skill-registry*)
(let* ((signal (list :type :EVENT :status nil :depth 0))) (let* ((signal (list :type :EVENT :status nil :depth 0)))
(loop-gate-act signal) (loop-gate-act signal)
(is (eq :acted (getf signal :status))))) (is (eq :acted (getf signal :status)))))
(test test-loop-gate-act-last-mile-reject (test test-loop-gate-act-last-mile-reject
"When the last-mile cognitive-verify rejects with :LOG, status should not reach :acted." "Contract 1: last-mile cognitive-verify rejection blocks approved-action."
(clrhash passepartout::*skill-registry*) (clrhash passepartout::*skill-registry*)
(passepartout::defskill :mock-blocker (passepartout::defskill :mock-blocker
:priority 50 :priority 50
@@ -202,7 +203,7 @@ For approval-required actions, creates a Flight Plan instead of executing."
(is (null (getf signal :approved-action))))) (is (null (getf signal :approved-action)))))
(test test-loop-gate-act-preserves-meta (test test-loop-gate-act-preserves-meta
"Signal metadata should not be mutated by the act gate." "Contract 1: signal metadata is not mutated by loop-gate-act."
(clrhash passepartout::*skill-registry*) (clrhash passepartout::*skill-registry*)
(let* ((meta '(:source :tui :session "s1")) (let* ((meta '(:source :tui :session "s1"))
(signal (list :type :EVENT :status nil :depth 0 :meta meta (signal (list :type :EVENT :status nil :depth 0 :meta meta

View File

@@ -123,6 +123,7 @@ FN receives (signal) and returns T if consumed, nil to continue."
(in-suite pipeline-perceive-suite) (in-suite pipeline-perceive-suite)
(test test-loop-gate-perceive (test test-loop-gate-perceive
"Contract 1: :buffer-update ingests AST and sets :perceived status."
(clrhash passepartout::*memory-store*) (clrhash passepartout::*memory-store*)
(let* ((signal (list :type :EVENT :payload (list :sensor :buffer-update :ast (list :type :HEADLINE :properties (list :ID "test-node" :TITLE "Test") :contents nil)))) (let* ((signal (list :type :EVENT :payload (list :sensor :buffer-update :ast (list :type :HEADLINE :properties (list :ID "test-node" :TITLE "Test") :contents nil))))
(result (loop-gate-perceive signal))) (result (loop-gate-perceive signal)))
@@ -130,24 +131,25 @@ FN receives (signal) and returns T if consumed, nil to continue."
(is (not (null (gethash "test-node" passepartout::*memory-store*)))))) (is (not (null (gethash "test-node" passepartout::*memory-store*))))))
(test test-depth-limiting (test test-depth-limiting
"Edge: depth 11 signals are rejected by the pipeline."
(let ((runaway-signal (list :type :EVENT :depth 11 :payload (list :sensor :heartbeat)))) (let ((runaway-signal (list :type :EVENT :depth 11 :payload (list :sensor :heartbeat))))
(is (null (process-signal runaway-signal))))) (is (null (process-signal runaway-signal)))))
(test test-loop-gate-perceive-unknown-sensor (test test-loop-gate-perceive-unknown-sensor
"Unknown sensors should pass through and still reach :perceived status." "Contract 1: unknown sensors pass through and reach :perceived."
(let* ((signal (list :type :EVENT :depth 0 :payload (list :sensor :custom-metric))) (let* ((signal (list :type :EVENT :depth 0 :payload (list :sensor :custom-metric)))
(result (loop-gate-perceive signal))) (result (loop-gate-perceive signal)))
(is (eq :perceived (getf result :status))))) (is (eq :perceived (getf result :status)))))
(test test-loop-gate-perceive-no-ast (test test-loop-gate-perceive-no-ast
"A :buffer-update with no AST should not crash and reach :perceived." "Contract 1: :buffer-update without AST doesn't crash, reaches :perceived."
(clrhash passepartout::*memory-store*) (clrhash passepartout::*memory-store*)
(let* ((signal (list :type :EVENT :depth 0 :payload (list :sensor :buffer-update))) (let* ((signal (list :type :EVENT :depth 0 :payload (list :sensor :buffer-update)))
(result (loop-gate-perceive signal))) (result (loop-gate-perceive signal)))
(is (eq :perceived (getf result :status))))) (is (eq :perceived (getf result :status)))))
(test test-depth-limiting-normal (test test-depth-limiting-normal
"Signals at depth 10 or below should not be rejected by the depth guard." "Contract 1: signals at normal depth pass through without rejection."
(let ((normal-signal (list :type :EVENT :depth 5 :payload (list :sensor :heartbeat)))) (let ((normal-signal (list :type :EVENT :depth 5 :payload (list :sensor :heartbeat))))
(is (not (eq :rejected (getf normal-signal :status))) (is (not (eq :rejected (getf normal-signal :status)))
"Signal at normal depth should not be rejected"))) "Signal at normal depth should not be rejected")))

View File

@@ -199,6 +199,7 @@ sorted by priority (highest first). Returns a rejection plist or the action."
(in-suite pipeline-reason-suite) (in-suite pipeline-reason-suite)
(test test-decide-gate-safety (test test-decide-gate-safety
"Contract 1: cognitive-verify blocks unsafe actions with :LOG rejection."
(clrhash passepartout::*skill-registry*) (clrhash passepartout::*skill-registry*)
(passepartout::defskill :mock-safety (passepartout::defskill :mock-safety
:priority 50 :priority 50
@@ -214,7 +215,7 @@ sorted by priority (highest first). Returns a rejection plist or the action."
(is (eq :LOG (getf result :type))))) (is (eq :LOG (getf result :type)))))
(test test-cognitive-verify-pass-through (test test-cognitive-verify-pass-through
"Safe actions should pass through cognitive-verify unchanged." "Contract 1: safe actions pass through cognitive-verify unchanged."
(clrhash passepartout::*skill-registry*) (clrhash passepartout::*skill-registry*)
(passepartout::defskill :mock-passthrough (passepartout::defskill :mock-passthrough
:priority 50 :priority 50
@@ -228,7 +229,7 @@ sorted by priority (highest first). Returns a rejection plist or the action."
(is (equal candidate result)))) (is (equal candidate result))))
(test test-cognitive-verify-empty-registry (test test-cognitive-verify-empty-registry
"When no gates are registered, the action passes through unchanged." "Contract 1: with no gates registered, action passes through unchanged."
(clrhash passepartout::*skill-registry*) (clrhash passepartout::*skill-registry*)
(let* ((candidate '(:type :REQUEST :payload (:action :shell :cmd "ls"))) (let* ((candidate '(:type :REQUEST :payload (:action :shell :cmd "ls")))
(signal '(:type :EVENT :payload (:sensor :user-input))) (signal '(:type :EVENT :payload (:sensor :user-input)))
@@ -236,7 +237,7 @@ sorted by priority (highest first). Returns a rejection plist or the action."
(is (equal candidate result)))) (is (equal candidate result))))
(test test-cognitive-verify-approval-required (test test-cognitive-verify-approval-required
"A gate returning :level :approval-required should produce an approval event." "Contract 1: gate returning :approval-required produces an approval event."
(clrhash passepartout::*skill-registry*) (clrhash passepartout::*skill-registry*)
(passepartout::defskill :mock-approval (passepartout::defskill :mock-approval
:priority 50 :priority 50

View File

@@ -152,7 +152,7 @@
(in-suite immune-suite) (in-suite immune-suite)
(test loop-error-injection (test loop-error-injection
"Verify that a crash in think/decide triggers a :loop-error stimulus." "Contract 1: a crash in think/decide triggers :loop-error stimulus."
(clrhash passepartout::*skill-registry*) (clrhash passepartout::*skill-registry*)
(passepartout:defskill :evil-skill (passepartout:defskill :evil-skill
:priority 100 :priority 100
@@ -164,7 +164,7 @@
(is (not (null (find-if (lambda (line) (search "CRITICAL BRAIN FAILURE" line)) logs)))))) (is (not (null (find-if (lambda (line) (search "CRITICAL BRAIN FAILURE" line)) logs))))))
(test test-process-signal-normal-path (test test-process-signal-normal-path
"A valid signal should pass through the pipeline without error." "Contract 1: a valid signal passes through the pipeline without crash."
(clrhash passepartout::*skill-registry*) (clrhash passepartout::*skill-registry*)
(handler-case (handler-case
(let ((signal (list :type :EVENT :depth 0 :payload (list :sensor :heartbeat)))) (let ((signal (list :type :EVENT :depth 0 :payload (list :sensor :heartbeat))))
@@ -174,6 +174,6 @@
(fail "Pipeline crashed on normal signal: ~a" c)))) (fail "Pipeline crashed on normal signal: ~a" c))))
(test test-loop-process-returns-nil-on-deep (test test-loop-process-returns-nil-on-deep
"Processing a signal at depth > 10 should return nil." "Contract 1: depth > 10 returns nil from loop-process."
(let ((result (loop-process '(:type :EVENT :depth 11 :payload (:sensor :heartbeat))))) (let ((result (loop-process '(:type :EVENT :depth 11 :payload (:sensor :heartbeat)))))
(is (null result)))) (is (null result))))

View File

@@ -157,6 +157,7 @@
(in-suite memory-suite) (in-suite memory-suite)
(test merkle-hash-consistency (test merkle-hash-consistency
"Contract 2: identical ASTs produce identical Merkle hashes."
(let* ((ast1 '(:type :HEADLINE :properties (:ID "test-1" :TITLE "Node 1") :contents nil))) (let* ((ast1 '(:type :HEADLINE :properties (:ID "test-1" :TITLE "Node 1") :contents nil)))
(clrhash passepartout::*memory-store*) (clrhash passepartout::*memory-store*)
(let ((id1 (ingest-ast ast1))) (let ((id1 (ingest-ast ast1)))
@@ -166,7 +167,7 @@
(is (equal hash1 (memory-object-hash (memory-object-get id2))))))))) (is (equal hash1 (memory-object-hash (memory-object-get id2)))))))))
(test merkle-hash-different (test merkle-hash-different
"Different ASTs should produce different hashes." "Contract 2: distinct ASTs produce different Merkle hashes."
(clrhash passepartout::*memory-store*) (clrhash passepartout::*memory-store*)
(let* ((ast1 '(:type :HEADLINE :properties (:ID "a" :TITLE "Alpha") :contents nil)) (let* ((ast1 '(:type :HEADLINE :properties (:ID "a" :TITLE "Alpha") :contents nil))
(ast2 '(:type :HEADLINE :properties (:ID "b" :TITLE "Beta") :contents nil)) (ast2 '(:type :HEADLINE :properties (:ID "b" :TITLE "Beta") :contents nil))

View File

@@ -295,6 +295,7 @@
(in-suite boot-suite) (in-suite boot-suite)
(test test-topological-sort-basic (test test-topological-sort-basic
"Contract 2: dependency ordering puts dependencies before dependents."
(let ((tmp-dir "/tmp/passepartout-boot-test/")) (let ((tmp-dir "/tmp/passepartout-boot-test/"))
(uiop:ensure-all-directories-exist (list tmp-dir)) (uiop:ensure-all-directories-exist (list tmp-dir))
(with-open-file (out (merge-pathnames "org-skill-a.org" tmp-dir) :direction :output :if-exists :supersede) (with-open-file (out (merge-pathnames "org-skill-a.org" tmp-dir) :direction :output :if-exists :supersede)
@@ -309,9 +310,9 @@
(uiop:delete-directory-tree (uiop:ensure-directory-pathname tmp-dir) :validate t)))) (uiop:delete-directory-tree (uiop:ensure-directory-pathname tmp-dir) :validate t))))
(test test-lisp-syntax-validate-valid (test test-lisp-syntax-validate-valid
"Valid Lisp code should pass syntax validation." "Contract 1: valid Lisp code passes syntax validation."
(is (eq t (lisp-syntax-validate "(+ 1 2)")))) (is (eq t (lisp-syntax-validate "(+ 1 2)"))))
(test test-lisp-syntax-validate-invalid (test test-lisp-syntax-validate-invalid
"Unbalanced Lisp code should fail syntax validation." "Contract 1: unbalanced Lisp code fails syntax validation."
(is (null (lisp-syntax-validate "(+ 1 2")))) (is (null (lisp-syntax-validate "(+ 1 2"))))

View File

@@ -185,12 +185,12 @@
(in-suite diagnostics-suite) (in-suite diagnostics-suite)
(test test-diagnostics-dependency-fail (test test-diagnostics-dependency-fail
"Verify that missing binaries are correctly identified as failures." "Contract 1: missing binaries cause diagnostics-dependencies-check to return nil."
(let ((passepartout::*diagnostics-binaries* '("non-existent-binary-123"))) (let ((passepartout::*diagnostics-binaries* '("non-existent-binary-123")))
(is (null (diagnostics-dependencies-check))))) (is (null (diagnostics-dependencies-check)))))
(test test-diagnostics-env-fail (test test-diagnostics-env-fail
"Verify that an invalid MEMEX_DIR triggers a critical failure." "Contract 2: invalid MEMEX_DIR causes diagnostics-env-check to return nil."
(let ((old-m (uiop:getenv "MEMEX_DIR")) (let ((old-m (uiop:getenv "MEMEX_DIR"))
(old-d (uiop:getenv "PASSEPARTOUT_DATA_DIR"))) (old-d (uiop:getenv "PASSEPARTOUT_DATA_DIR")))
(unwind-protect (unwind-protect
@@ -201,7 +201,7 @@
(setf (uiop:getenv "PASSEPARTOUT_DATA_DIR") (or old-d ""))))) (setf (uiop:getenv "PASSEPARTOUT_DATA_DIR") (or old-d "")))))
(test test-diagnostics-dependency-success (test test-diagnostics-dependency-success
"When all binaries exist, diagnostics should pass." "Contract 1: all binaries present returns T."
(let ((passepartout::*diagnostics-binaries* '("ls"))) (let ((passepartout::*diagnostics-binaries* '("ls")))
(is (eq t (diagnostics-dependencies-check))))) (is (eq t (diagnostics-dependencies-check)))))

View File

@@ -130,12 +130,12 @@ If API-KEY is nil, reads from environment."
(fiveam:in-suite llm-gateway-suite) (fiveam:in-suite llm-gateway-suite)
(fiveam:test test-provider-rejects-bad-keyword (fiveam:test test-provider-rejects-bad-keyword
"Verify that provider-openai-request returns :error for an unregistered provider." "Edge: provider-openai-request returns :error for unregistered provider."
(let ((result (provider-openai-request "hello" "test" :provider :not-a-real-provider))) (let ((result (provider-openai-request "hello" "test" :provider :not-a-real-provider)))
(fiveam:is (eq (getf result :status) :error)))) (fiveam:is (eq (getf result :status) :error))))
(fiveam:test test-provider-config-registered (fiveam:test test-provider-config-registered
"A registered provider should return its configuration plist." "Contract: provider-config returns configuration plist for registered provider."
(let ((config (provider-config :openrouter))) (let ((config (provider-config :openrouter)))
(fiveam:is (listp config)) (fiveam:is (listp config))
(fiveam:is (getf config :base-url)))) (fiveam:is (getf config :base-url))))

View File

@@ -29,6 +29,16 @@ The length prefix solves all three problems. The reader reads exactly 6 characte
The 6-character hex length supports messages up to ~16MB (0xFFFFFF bytes). This is sufficient for any single message the agent would produce. Larger payloads should be split across multiple messages. The 6-character hex length supports messages up to ~16MB (0xFFFFFF bytes). This is sufficient for any single message the agent would produce. Larger payloads should be split across multiple messages.
** Contract
1. (frame-message msg): serializes a plist message to a length-prefixed
string. The first 6 characters are the hex-encoded payload length.
2. (read-framed-message stream): reads a framed message from a stream,
returning the deserialized plist. Consumes exactly the length-prefixed
bytes.
3. Round-trip invariant: ~(read-framed-message (make-string-input-stream
(frame-message msg)))~ equals ~msg~.
* Implementation * Implementation
** Package Context ** Package Context
@@ -261,19 +271,20 @@ Verifies that the framing protocol correctly serializes and deserializes message
(in-suite communication-protocol-suite) (in-suite communication-protocol-suite)
(test test-framing (test test-framing
"Contract 1: frame-message produces correct hex length prefix."
(let* ((msg '(:type :EVENT :payload (:action :handshake))) (let* ((msg '(:type :EVENT :payload (:action :handshake)))
(framed (frame-message msg))) (framed (frame-message msg)))
(is (string= "00002C" (string-upcase (subseq framed 0 6)))))) (is (string= "00002C" (string-upcase (subseq framed 0 6))))))
(test test-framing-round-trip (test test-framing-round-trip
"A message should survive frame → read-frame without loss." "Contract 3: frame → read-frame preserves message identity."
(let* ((msg '(:type :EVENT :payload (:action :handshake :version "1.0") :meta (:source :tui))) (let* ((msg '(:type :EVENT :payload (:action :handshake :version "1.0") :meta (:source :tui)))
(framed (frame-message msg)) (framed (frame-message msg))
(unframed (read-framed-message (make-string-input-stream framed)))) (unframed (read-framed-message (make-string-input-stream framed))))
(is (equal msg unframed)))) (is (equal msg unframed))))
(test test-framing-empty-message (test test-framing-empty-message
"An empty or simple message should still frame correctly." "Contract 1: simple messages frame with valid hex length."
(let* ((msg '(:type :ping)) (let* ((msg '(:type :ping))
(framed (frame-message msg))) (framed (frame-message msg)))
(is (> (length framed) 5)) (is (> (length framed) 5))

View File

@@ -24,6 +24,15 @@ A naive implementation that serializes every ~org-object~ to text would produce
The semantic threshold is configurable via ~CONTEXT_SEMANTIC_THRESHOLD~ env var (default 0.75). Lower values include more peripherally related content; higher values restrict to tightly related content. The semantic threshold is configurable via ~CONTEXT_SEMANTIC_THRESHOLD~ env var (default 0.75). Lower values include more peripherally related content; higher values restrict to tightly related content.
** Contract
1. (context-awareness-assemble &optional signal): produces a skeletal
outline of current Memory for the LLM. If ~:foveal-focus~ is set,
the foveal node gets inline rendering; peripheral nodes get title-only.
Privacy-filtered objects are excluded.
2. (context-assemble-global-awareness): zero-arg wrapper — calls
~context-awareness-assemble~ without foveal focus.
* Implementation * Implementation
** Package Context ** Package Context
@@ -300,6 +309,7 @@ Verifies that the Foveal-Peripheral rendering correctly distinguishes between fo
(in-suite vision-suite) (in-suite vision-suite)
(test test-foveal-rendering (test test-foveal-rendering
"Contract 1: foveal content inline, peripheral content title-only."
(clrhash passepartout::*memory-store*) (clrhash passepartout::*memory-store*)
(let* ((ast '(:type :HEADLINE :properties (:ID "proj-root" :TITLE "Project" :TAGS ("project")) (let* ((ast '(:type :HEADLINE :properties (:ID "proj-root" :TITLE "Project" :TAGS ("project"))
:contents ((:type :HEADLINE :properties (:ID "node-foveal" :TITLE "Foveal Node") :contents ((:type :HEADLINE :properties (:ID "node-foveal" :TITLE "Foveal Node")
@@ -313,6 +323,7 @@ Verifies that the Foveal-Peripheral rendering correctly distinguishes between fo
(is (not (search "PERIPHERAL CONTENT" output)))))) (is (not (search "PERIPHERAL CONTENT" output))))))
(test test-awareness-budget (test test-awareness-budget
"Contract 1: all active projects appear in awareness output."
(clrhash passepartout::*memory-store*) (clrhash passepartout::*memory-store*)
(ingest-ast '(:type :HEADLINE :properties (:ID "p1" :TITLE "Project 1" :TAGS ("project")) :contents nil)) (ingest-ast '(:type :HEADLINE :properties (:ID "p1" :TITLE "Project 1" :TAGS ("project")) :contents nil))
(ingest-ast '(:type :HEADLINE :properties (:ID "p2" :TITLE "Project 2" :TAGS ("project")) :contents nil)) (ingest-ast '(:type :HEADLINE :properties (:ID "p2" :TITLE "Project 2" :TAGS ("project")) :contents nil))
@@ -321,14 +332,14 @@ Verifies that the Foveal-Peripheral rendering correctly distinguishes between fo
(is (search "Project 2" output)))) (is (search "Project 2" output))))
(test test-context-empty-memory (test test-context-empty-memory
"An empty memory should produce a clean awareness output without errors." "Contract 1: empty memory produces clean output without error."
(clrhash passepartout::*memory-store*) (clrhash passepartout::*memory-store*)
(let ((output (context-awareness-assemble))) (let ((output (context-awareness-assemble)))
(is (stringp output)) (is (stringp output))
(is (search "MEMEX" output :test #'char-equal)))) (is (search "MEMEX" output :test #'char-equal))))
(test test-context-no-foveal-focus (test test-context-no-foveal-focus
"Without a foveal focus, all content should be peripheral (no inline content)." "Contract 2: without foveal focus, no inline content appears."
(clrhash passepartout::*memory-store*) (clrhash passepartout::*memory-store*)
(let* ((ast '(:type :HEADLINE :properties (:ID "root" :TITLE "Root" :TAGS ("project")) (let* ((ast '(:type :HEADLINE :properties (:ID "root" :TITLE "Root" :TAGS ("project"))
:contents ((:type :HEADLINE :properties (:ID "child" :TITLE "Child Node") :contents ((:type :HEADLINE :properties (:ID "child" :TITLE "Child Node")

View File

@@ -22,6 +22,16 @@ The Reason stage already ran every proposed action through the deterministic eng
Because a skill's deterministic gate runs during Reason, but between Reason and Act, the action might have been transformed by the pipeline (metadata added, format normalized). The last-mile verification catches any transformation that might have introduced an unsafe property. It's the same philosophy as "trust but verify" — the second check is cheap and catches a class of bugs that would otherwise be silent data corruption. Because a skill's deterministic gate runs during Reason, but between Reason and Act, the action might have been transformed by the pipeline (metadata added, format normalized). The last-mile verification catches any transformation that might have introduced an unsafe property. It's the same philosophy as "trust but verify" — the second check is cheap and catches a class of bugs that would otherwise be silent data corruption.
** Contract
1. (loop-gate-act signal): the final pipeline stage. Handles HITL
~:approval-required~ (suspends action), runs last-mile
~cognitive-verify~ on approved actions, dispatches via
~action-dispatch~, sets ~:status :acted~, returns feedback.
2. (act-gate signal): thin alias for ~loop-gate-act~.
3. (action-dispatch approved signal): routes approved actions to
registered actuators by ~:target~ keyword.
* Implementation * Implementation
** Package Context ** Package Context
@@ -289,6 +299,7 @@ Verifies that the act gate correctly processes an approved action and sets the s
(in-suite pipeline-act-suite) (in-suite pipeline-act-suite)
(test test-loop-gate-act-basic (test test-loop-gate-act-basic
"Contract 1: approved action reaches :acted status via loop-gate-act."
(clrhash passepartout::*skill-registry*) (clrhash passepartout::*skill-registry*)
(let* ((signal (list :type :EVENT :status nil :depth 0 :approved-action '(:target :cli :payload (:text "Hello")))) (let* ((signal (list :type :EVENT :status nil :depth 0 :approved-action '(:target :cli :payload (:text "Hello"))))
(result (loop-gate-act signal))) (result (loop-gate-act signal)))
@@ -296,14 +307,14 @@ Verifies that the act gate correctly processes an approved action and sets the s
(is (null result)))) (is (null result))))
(test test-loop-gate-act-no-approved-action (test test-loop-gate-act-no-approved-action
"When no approved-action is set, the signal should still reach :acted status." "Contract 1: signal with no approved-action still reaches :acted status."
(clrhash passepartout::*skill-registry*) (clrhash passepartout::*skill-registry*)
(let* ((signal (list :type :EVENT :status nil :depth 0))) (let* ((signal (list :type :EVENT :status nil :depth 0)))
(loop-gate-act signal) (loop-gate-act signal)
(is (eq :acted (getf signal :status))))) (is (eq :acted (getf signal :status)))))
(test test-loop-gate-act-last-mile-reject (test test-loop-gate-act-last-mile-reject
"When the last-mile cognitive-verify rejects with :LOG, status should not reach :acted." "Contract 1: last-mile cognitive-verify rejection blocks approved-action."
(clrhash passepartout::*skill-registry*) (clrhash passepartout::*skill-registry*)
(passepartout::defskill :mock-blocker (passepartout::defskill :mock-blocker
:priority 50 :priority 50
@@ -318,7 +329,7 @@ Verifies that the act gate correctly processes an approved action and sets the s
(is (null (getf signal :approved-action))))) (is (null (getf signal :approved-action)))))
(test test-loop-gate-act-preserves-meta (test test-loop-gate-act-preserves-meta
"Signal metadata should not be mutated by the act gate." "Contract 1: signal metadata is not mutated by loop-gate-act."
(clrhash passepartout::*skill-registry*) (clrhash passepartout::*skill-registry*)
(let* ((meta '(:source :tui :session "s1")) (let* ((meta '(:source :tui :session "s1"))
(signal (list :type :EVENT :status nil :depth 0 :meta meta (signal (list :type :EVENT :status nil :depth 0 :meta meta

View File

@@ -27,6 +27,14 @@ The `*loop-async-sensors*` list defines which sensor types are processed in dedi
The depth limit prevents runaway recursive loops. A signal that generates another signal that generates another signal can infinite-loop. If depth exceeds a threshold (10), the signal is silently dropped rather than processed. This is the metabolic loop's circuit breaker. The depth limit prevents runaway recursive loops. A signal that generates another signal that generates another signal can infinite-loop. If depth exceeds a threshold (10), the signal is silently dropped rather than processed. This is the metabolic loop's circuit breaker.
** Contract
1. (loop-gate-perceive signal): normalizes sensory input. Routes by
sensor type (~:buffer-update~, ~:point-update~, ~:interrupt~,
~:approval-required~) and signal type (~:EVENT~, ~:RESPONSE~).
Sets ~:status :perceived~ on completion. Returns the signal.
2. (perceive-gate signal): thin alias for ~loop-gate-perceive~.
* Implementation * Implementation
** Package Context ** Package Context
@@ -253,6 +261,7 @@ Verifies that the perceive gate correctly ingests AST nodes into memory and that
(in-suite pipeline-perceive-suite) (in-suite pipeline-perceive-suite)
(test test-loop-gate-perceive (test test-loop-gate-perceive
"Contract 1: :buffer-update ingests AST and sets :perceived status."
(clrhash passepartout::*memory-store*) (clrhash passepartout::*memory-store*)
(let* ((signal (list :type :EVENT :payload (list :sensor :buffer-update :ast (list :type :HEADLINE :properties (list :ID "test-node" :TITLE "Test") :contents nil)))) (let* ((signal (list :type :EVENT :payload (list :sensor :buffer-update :ast (list :type :HEADLINE :properties (list :ID "test-node" :TITLE "Test") :contents nil))))
(result (loop-gate-perceive signal))) (result (loop-gate-perceive signal)))
@@ -260,24 +269,25 @@ Verifies that the perceive gate correctly ingests AST nodes into memory and that
(is (not (null (gethash "test-node" passepartout::*memory-store*)))))) (is (not (null (gethash "test-node" passepartout::*memory-store*))))))
(test test-depth-limiting (test test-depth-limiting
"Edge: depth 11 signals are rejected by the pipeline."
(let ((runaway-signal (list :type :EVENT :depth 11 :payload (list :sensor :heartbeat)))) (let ((runaway-signal (list :type :EVENT :depth 11 :payload (list :sensor :heartbeat))))
(is (null (process-signal runaway-signal))))) (is (null (process-signal runaway-signal)))))
(test test-loop-gate-perceive-unknown-sensor (test test-loop-gate-perceive-unknown-sensor
"Unknown sensors should pass through and still reach :perceived status." "Contract 1: unknown sensors pass through and reach :perceived."
(let* ((signal (list :type :EVENT :depth 0 :payload (list :sensor :custom-metric))) (let* ((signal (list :type :EVENT :depth 0 :payload (list :sensor :custom-metric)))
(result (loop-gate-perceive signal))) (result (loop-gate-perceive signal)))
(is (eq :perceived (getf result :status))))) (is (eq :perceived (getf result :status)))))
(test test-loop-gate-perceive-no-ast (test test-loop-gate-perceive-no-ast
"A :buffer-update with no AST should not crash and reach :perceived." "Contract 1: :buffer-update without AST doesn't crash, reaches :perceived."
(clrhash passepartout::*memory-store*) (clrhash passepartout::*memory-store*)
(let* ((signal (list :type :EVENT :depth 0 :payload (list :sensor :buffer-update))) (let* ((signal (list :type :EVENT :depth 0 :payload (list :sensor :buffer-update)))
(result (loop-gate-perceive signal))) (result (loop-gate-perceive signal)))
(is (eq :perceived (getf result :status))))) (is (eq :perceived (getf result :status)))))
(test test-depth-limiting-normal (test test-depth-limiting-normal
"Signals at depth 10 or below should not be rejected by the depth guard." "Contract 1: signals at normal depth pass through without rejection."
(let ((normal-signal (list :type :EVENT :depth 5 :payload (list :sensor :heartbeat)))) (let ((normal-signal (list :type :EVENT :depth 5 :payload (list :sensor :heartbeat))))
(is (not (eq :rejected (getf normal-signal :status))) (is (not (eq :rejected (getf normal-signal :status)))
"Signal at normal depth should not be rejected"))) "Signal at normal depth should not be rejected")))

View File

@@ -36,6 +36,22 @@ A plist is simultaneously:
This is not a cosmetic choice. It means the reasoning pipeline can generate, modify, and execute its own communication protocol without external parsing libraries. There is no JSON encoder, no schema validator, no serialization layer between the two engines. They speak the same language because they *are* the same language. This is not a cosmetic choice. It means the reasoning pipeline can generate, modify, and execute its own communication protocol without external parsing libraries. There is no JSON encoder, no schema validator, no serialization layer between the two engines. They speak the same language because they *are* the same language.
** Contract
1. (cognitive-verify proposed-action context): runs all registered
deterministic gates sorted by priority. Returns a rejection plist
(~:LOG~ or ~:EVENT~) if any gate blocks the action, an
~:approval-required~ event if a gate requires HITL, or the action
(potentially modified) if it passes.
2. (loop-gate-reason signal): the full reason pipeline — only processes
~:user-input~ and ~:chat-message~ sensors. Runs ~think~ to generate
a candidate, then ~cognitive-verify~ to gate it. Retries up to 3
times on rejection. Sets ~:status :reasoned~ on completion.
3. (reason-gate signal): thin alias for ~loop-gate-reason~.
4. (backend-cascade-call prompt): iterates ~*provider-cascade*~ calling
each backend's handler until one succeeds. Returns the LLM content
string, or a ~:LOG~ failure if all backends are exhausted.
* Implementation * Implementation
** Package Context ** Package Context
@@ -360,6 +376,7 @@ Verifies that the deterministic engine correctly rejects unsafe actions (like ~r
(in-suite pipeline-reason-suite) (in-suite pipeline-reason-suite)
(test test-decide-gate-safety (test test-decide-gate-safety
"Contract 1: cognitive-verify blocks unsafe actions with :LOG rejection."
(clrhash passepartout::*skill-registry*) (clrhash passepartout::*skill-registry*)
(passepartout::defskill :mock-safety (passepartout::defskill :mock-safety
:priority 50 :priority 50
@@ -375,7 +392,7 @@ Verifies that the deterministic engine correctly rejects unsafe actions (like ~r
(is (eq :LOG (getf result :type))))) (is (eq :LOG (getf result :type)))))
(test test-cognitive-verify-pass-through (test test-cognitive-verify-pass-through
"Safe actions should pass through cognitive-verify unchanged." "Contract 1: safe actions pass through cognitive-verify unchanged."
(clrhash passepartout::*skill-registry*) (clrhash passepartout::*skill-registry*)
(passepartout::defskill :mock-passthrough (passepartout::defskill :mock-passthrough
:priority 50 :priority 50
@@ -389,7 +406,7 @@ Verifies that the deterministic engine correctly rejects unsafe actions (like ~r
(is (equal candidate result)))) (is (equal candidate result))))
(test test-cognitive-verify-empty-registry (test test-cognitive-verify-empty-registry
"When no gates are registered, the action passes through unchanged." "Contract 1: with no gates registered, action passes through unchanged."
(clrhash passepartout::*skill-registry*) (clrhash passepartout::*skill-registry*)
(let* ((candidate '(:type :REQUEST :payload (:action :shell :cmd "ls"))) (let* ((candidate '(:type :REQUEST :payload (:action :shell :cmd "ls")))
(signal '(:type :EVENT :payload (:sensor :user-input))) (signal '(:type :EVENT :payload (:sensor :user-input)))
@@ -397,7 +414,7 @@ Verifies that the deterministic engine correctly rejects unsafe actions (like ~r
(is (equal candidate result)))) (is (equal candidate result))))
(test test-cognitive-verify-approval-required (test test-cognitive-verify-approval-required
"A gate returning :level :approval-required should produce an approval event." "Contract 1: gate returning :approval-required produces an approval event."
(clrhash passepartout::*skill-registry*) (clrhash passepartout::*skill-registry*)
(passepartout::defskill :mock-approval (passepartout::defskill :mock-approval
:priority 50 :priority 50

View File

@@ -33,6 +33,15 @@ The three-tier error recovery model:
2. **Critical errors** (undefined functions, malformed data) — require memory rollback to the last snapshot 2. **Critical errors** (undefined functions, malformed data) — require memory rollback to the last snapshot
3. **Recursive loops** (signals generating more signals indefinitely) — depth limit enforcement 3. **Recursive loops** (signals generating more signals indefinitely) — depth limit enforcement
** Contract
1. (loop-process signal): the full pipeline loop — Perceive → Reason
→ Act. Enforces depth limit (10). Catches errors with rollback and
~:loop-error~ re-injection on non-terminal errors below depth 2.
2. (process-signal signal): thin alias for ~loop-process~.
3. (diagnostics-startup-run): runs health check on startup, sets
~*system-health*~ to ~:healthy~, ~:degraded~, or ~:unhealthy~.
* Implementation * Implementation
** Package Context ** Package Context
@@ -311,7 +320,7 @@ Verifies that the immune system (error handling) correctly catches and reports e
(in-suite immune-suite) (in-suite immune-suite)
(test loop-error-injection (test loop-error-injection
"Verify that a crash in think/decide triggers a :loop-error stimulus." "Contract 1: a crash in think/decide triggers :loop-error stimulus."
(clrhash passepartout::*skill-registry*) (clrhash passepartout::*skill-registry*)
(passepartout:defskill :evil-skill (passepartout:defskill :evil-skill
:priority 100 :priority 100
@@ -323,7 +332,7 @@ Verifies that the immune system (error handling) correctly catches and reports e
(is (not (null (find-if (lambda (line) (search "CRITICAL BRAIN FAILURE" line)) logs)))))) (is (not (null (find-if (lambda (line) (search "CRITICAL BRAIN FAILURE" line)) logs))))))
(test test-process-signal-normal-path (test test-process-signal-normal-path
"A valid signal should pass through the pipeline without error." "Contract 1: a valid signal passes through the pipeline without crash."
(clrhash passepartout::*skill-registry*) (clrhash passepartout::*skill-registry*)
(handler-case (handler-case
(let ((signal (list :type :EVENT :depth 0 :payload (list :sensor :heartbeat)))) (let ((signal (list :type :EVENT :depth 0 :payload (list :sensor :heartbeat))))
@@ -333,7 +342,7 @@ Verifies that the immune system (error handling) correctly catches and reports e
(fail "Pipeline crashed on normal signal: ~a" c)))) (fail "Pipeline crashed on normal signal: ~a" c))))
(test test-loop-process-returns-nil-on-deep (test test-loop-process-returns-nil-on-deep
"Processing a signal at depth > 10 should return nil." "Contract 1: depth > 10 returns nil from loop-process."
(let ((result (loop-process '(:type :EVENT :depth 11 :payload (:sensor :heartbeat))))) (let ((result (loop-process '(:type :EVENT :depth 11 :payload (:sensor :heartbeat)))))
(is (null result)))) (is (null result))))
#+end_src #+end_src

View File

@@ -34,6 +34,17 @@ Git tracks changes to files. Passepartout tracks changes to live memory state. T
The tradeoff is memory usage: each snapshot is a deep copy of every object in active memory. 20 snapshots means 20x the active memory size. For a typical knowledge base of 10,000 objects, this is manageable (~100MB for 20 snapshots). The tradeoff is memory usage: each snapshot is a deep copy of every object in active memory. 20 snapshots means 20x the active memory size. For a typical knowledge base of 10,000 objects, this is manageable (~100MB for 20 snapshots).
** Contract
1. (ingest-ast ast &key scope): stores AST nodes in ~*memory-store*~.
Detaches children, gives each an ID, computes Merkle hash. Returns the
root ID string.
2. (memory-object-hash object): returns the SHA-256 Merkle hash of the
object's content. Hash is deterministic — same content → same hash.
3. (memory-object-get id): retrieves a stored object by ID, or nil.
4. (snapshot-memory): deep-copies ~*memory-store*~ to ~*memory-snapshots*~.
5. (rollback-memory snap-index): restores ~*memory-store*~ from a snapshot.
* Implementation * Implementation
** Package Context ** Package Context
@@ -358,6 +369,7 @@ Verifies that the Merkle hash is deterministic and consistent across independent
(in-suite memory-suite) (in-suite memory-suite)
(test merkle-hash-consistency (test merkle-hash-consistency
"Contract 2: identical ASTs produce identical Merkle hashes."
(let* ((ast1 '(:type :HEADLINE :properties (:ID "test-1" :TITLE "Node 1") :contents nil))) (let* ((ast1 '(:type :HEADLINE :properties (:ID "test-1" :TITLE "Node 1") :contents nil)))
(clrhash passepartout::*memory-store*) (clrhash passepartout::*memory-store*)
(let ((id1 (ingest-ast ast1))) (let ((id1 (ingest-ast ast1)))
@@ -367,7 +379,7 @@ Verifies that the Merkle hash is deterministic and consistent across independent
(is (equal hash1 (memory-object-hash (memory-object-get id2))))))))) (is (equal hash1 (memory-object-hash (memory-object-get id2)))))))))
(test merkle-hash-different (test merkle-hash-different
"Different ASTs should produce different hashes." "Contract 2: distinct ASTs produce different Merkle hashes."
(clrhash passepartout::*memory-store*) (clrhash passepartout::*memory-store*)
(let* ((ast1 '(:type :HEADLINE :properties (:ID "a" :TITLE "Alpha") :contents nil)) (let* ((ast1 '(:type :HEADLINE :properties (:ID "a" :TITLE "Alpha") :contents nil))
(ast2 '(:type :HEADLINE :properties (:ID "b" :TITLE "Beta") :contents nil)) (ast2 '(:type :HEADLINE :properties (:ID "b" :TITLE "Beta") :contents nil))

View File

@@ -25,6 +25,14 @@ After loading, the engine exports the skill's public symbols into the ~passepart
This is how the "thin org, fat skills" principle works in practice: the org provides the loading infrastructure; the skills provide all the intelligence. This is how the "thin org, fat skills" principle works in practice: the org provides the loading infrastructure; the skills provide all the intelligence.
** Contract
1. (lisp-syntax-validate code-string): returns T if the Lisp code is
structurally valid, nil if reader errors are detected.
2. (skill-topological-sort dir): reads org files in a directory, parses
~#+DEPENDS_ON:~ declarations, returns files sorted such that
dependencies come before dependents.
* Implementation * Implementation
** Package Context ** Package Context
@@ -425,6 +433,7 @@ Verifies that the topological sorter correctly orders skills by their ~#+DEPENDS
(in-suite boot-suite) (in-suite boot-suite)
(test test-topological-sort-basic (test test-topological-sort-basic
"Contract 2: dependency ordering puts dependencies before dependents."
(let ((tmp-dir "/tmp/passepartout-boot-test/")) (let ((tmp-dir "/tmp/passepartout-boot-test/"))
(uiop:ensure-all-directories-exist (list tmp-dir)) (uiop:ensure-all-directories-exist (list tmp-dir))
(with-open-file (out (merge-pathnames "org-skill-a.org" tmp-dir) :direction :output :if-exists :supersede) (with-open-file (out (merge-pathnames "org-skill-a.org" tmp-dir) :direction :output :if-exists :supersede)
@@ -439,10 +448,10 @@ Verifies that the topological sorter correctly orders skills by their ~#+DEPENDS
(uiop:delete-directory-tree (uiop:ensure-directory-pathname tmp-dir) :validate t)))) (uiop:delete-directory-tree (uiop:ensure-directory-pathname tmp-dir) :validate t))))
(test test-lisp-syntax-validate-valid (test test-lisp-syntax-validate-valid
"Valid Lisp code should pass syntax validation." "Contract 1: valid Lisp code passes syntax validation."
(is (eq t (lisp-syntax-validate "(+ 1 2)")))) (is (eq t (lisp-syntax-validate "(+ 1 2)"))))
(test test-lisp-syntax-validate-invalid (test test-lisp-syntax-validate-invalid
"Unbalanced Lisp code should fail syntax validation." "Contract 1: unbalanced Lisp code fails syntax validation."
(is (null (lisp-syntax-validate "(+ 1 2")))) (is (null (lisp-syntax-validate "(+ 1 2"))))
#+end_src #+end_src

View File

@@ -14,10 +14,15 @@ The Doctor transforms opaque startup failures into actionable engineering report
** Detection Invariant ** Detection Invariant
Binary detection must use shell probing (`which`) to account for varying `$PATH` inheritance between interactive and headless sessions. Binary detection must use shell probing (`which`) to account for varying `$PATH` inheritance between interactive and headless sessions.
* Phase B: Protocol (Success Criteria) * Phase B: Contract
- Dependency check passes when all required binaries are found
- Environment check passes when XDG directories exist and are accessible 1. (diagnostics-dependencies-check): probes PATH for every binary in
- LLM check passes when at least one provider is configured or Ollama is running locally ~*diagnostics-binaries*~. Returns T if all found, NIL if any missing.
Side-effect: populates ~*doctor-missing-deps*~.
2. (diagnostics-env-check): validates XDG directories exist. Returns T
if all critical dirs present, NIL otherwise.
3. (diagnostics-run-all &key auto-install): orchestrates 1-3. Returns
a plist with ~:deps~, ~:env~, ~:llm~ keys. Respects ~:auto-install nil~.
* Phase C: Implementation (Build) * Phase C: Implementation (Build)
@@ -257,12 +262,12 @@ The doctor checks all supported LLM providers and detects local Ollama instances
(in-suite diagnostics-suite) (in-suite diagnostics-suite)
(test test-diagnostics-dependency-fail (test test-diagnostics-dependency-fail
"Verify that missing binaries are correctly identified as failures." "Contract 1: missing binaries cause diagnostics-dependencies-check to return nil."
(let ((passepartout::*diagnostics-binaries* '("non-existent-binary-123"))) (let ((passepartout::*diagnostics-binaries* '("non-existent-binary-123")))
(is (null (diagnostics-dependencies-check))))) (is (null (diagnostics-dependencies-check)))))
(test test-diagnostics-env-fail (test test-diagnostics-env-fail
"Verify that an invalid MEMEX_DIR triggers a critical failure." "Contract 2: invalid MEMEX_DIR causes diagnostics-env-check to return nil."
(let ((old-m (uiop:getenv "MEMEX_DIR")) (let ((old-m (uiop:getenv "MEMEX_DIR"))
(old-d (uiop:getenv "PASSEPARTOUT_DATA_DIR"))) (old-d (uiop:getenv "PASSEPARTOUT_DATA_DIR")))
(unwind-protect (unwind-protect
@@ -273,7 +278,7 @@ The doctor checks all supported LLM providers and detects local Ollama instances
(setf (uiop:getenv "PASSEPARTOUT_DATA_DIR") (or old-d ""))))) (setf (uiop:getenv "PASSEPARTOUT_DATA_DIR") (or old-d "")))))
(test test-diagnostics-dependency-success (test test-diagnostics-dependency-success
"When all binaries exist, diagnostics should pass." "Contract 1: all binaries present returns T."
(let ((passepartout::*diagnostics-binaries* '("ls"))) (let ((passepartout::*diagnostics-binaries* '("ls")))
(is (eq t (diagnostics-dependencies-check))))) (is (eq t (diagnostics-dependencies-check)))))
#+end_src #+end_src

View File

@@ -13,6 +13,18 @@ Providers register themselves at boot. No API key? That provider doesn't registe
=*provider-cascade*= defaults to cloud-only (all providers except ~:local~ and ~:ollama~). If you want a local fallback, set ~LOCAL_BASE_URL~ in your env and add ~:local~ to the ~PROVIDER_CASCADE~ list. =*provider-cascade*= defaults to cloud-only (all providers except ~:local~ and ~:ollama~). If you want a local fallback, set ~LOCAL_BASE_URL~ in your env and add ~:local~ to the ~PROVIDER_CASCADE~ list.
** Contract
1. (provider-config provider): returns the configuration plist for a
provider keyword, or nil if unregistered.
2. (provider-available-p provider): returns T if the provider's API key
or base URL is configured.
3. (provider-openai-request prompt system-prompt &key model provider):
executes an OpenAI-compatible /v1/chat/completions request. Returns
~(:status :success :content ...)~ or ~(:status :error :message ...)~.
4. (provider-cascade-initialize): reads ~PROVIDER_CASCADE~ from env and
sets ~*provider-cascade*~.
* Implementation * Implementation
** Provider registry ** Provider registry
@@ -177,12 +189,12 @@ If API-KEY is nil, reads from environment."
(fiveam:in-suite llm-gateway-suite) (fiveam:in-suite llm-gateway-suite)
(fiveam:test test-provider-rejects-bad-keyword (fiveam:test test-provider-rejects-bad-keyword
"Verify that provider-openai-request returns :error for an unregistered provider." "Edge: provider-openai-request returns :error for unregistered provider."
(let ((result (provider-openai-request "hello" "test" :provider :not-a-real-provider))) (let ((result (provider-openai-request "hello" "test" :provider :not-a-real-provider)))
(fiveam:is (eq (getf result :status) :error)))) (fiveam:is (eq (getf result :status) :error))))
(fiveam:test test-provider-config-registered (fiveam:test test-provider-config-registered
"A registered provider should return its configuration plist." "Contract: provider-config returns configuration plist for registered provider."
(let ((config (provider-config :openrouter))) (let ((config (provider-config :openrouter)))
(fiveam:is (listp config)) (fiveam:is (listp config))
(fiveam:is (getf config :base-url)))) (fiveam:is (getf config :base-url))))