From 4902388ef19fe96070b0ab1f95072c6aa5fe7573 Mon Sep 17 00:00:00 2001 From: jbergner Date: Thu, 27 Aug 2026 14:06:37 +0200 Subject: [PATCH] 1.5.8 --- .env.example | 4 +- MANIFEST.sha256 | 23 +- README.md | 13 +- VERSION | 2 +- docs/GO-LIVE-v1.5.0.md | 5 + docs/MIGRATION-v1.5.7-to-v1.5.8.md | 16 + docs/RELEASE-v1.5.8.md | 20 + patches/SHA256SUMS | 1 + patches/v1.5.7-to-v1.5.8.diff | 525 ++++++++++++++++++ .../internal/brain/goal_progress.go | 5 +- .../internal/brain/goal_progress_test.go | 97 +++- platform/neuroforge/internal/brain/staging.go | 93 +++- platform/neuroforge/internal/core/types.go | 63 ++- platform/neuroforge/openapi.yaml | 6 + 14 files changed, 810 insertions(+), 63 deletions(-) create mode 100644 docs/MIGRATION-v1.5.7-to-v1.5.8.md create mode 100644 docs/RELEASE-v1.5.8.md create mode 100644 patches/v1.5.7-to-v1.5.8.diff diff --git a/.env.example b/.env.example index daf61f7..7138009 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ ############################################################################### -# GLPI NEUROFORGE MEGA v1.5.7 - VOLLSTÄNDIGE .ENV.example +# GLPI NEUROFORGE MEGA v1.5.8 - VOLLSTÄNDIGE .ENV.example # # Diese Datei ist die zentrale Konfiguration für docker compose. # Sie enthält: @@ -29,7 +29,7 @@ # 01. MEGA STACK - RELEASE / HOST PORTS / PFADE ############################################################################### # Immutable Registry-Tag der sechs Projekt-Images. "latest" ist produktiv verboten. -IMAGE_TAG=1.5.7 +IMAGE_TAG=1.5.8 CONTROL_HOST_PORT=8070 AGENT_HOST_PORT=8080 diff --git a/MANIFEST.sha256 b/MANIFEST.sha256 index d4b6925..bc80c37 100644 --- a/MANIFEST.sha256 +++ b/MANIFEST.sha256 @@ -1,9 +1,9 @@ 27dc46be5cbb1b171deff7fbd2f28bff1be802dff403797535fd8968bb98c8eb ./.cbmignore -67c5365c186a4ced2140c819ae40e7a32dbd97a15d53b56ce253beabbabb21b4 ./.env.example +b976311e7c8e34b7a095e4061f4caab9803396670e029437f5a0ec100a753b14 ./.env.example ed22fda7661db8203563611dc144998161cd024e161b0471d615eaf0defeb7db ./.gitea/workflows/release-tag.yml e1ff71187cc3411a85067b964264011db7bd109a585ef7b9ea5b08bda039d813 ./.gitignore ccfc4c139345a69d05cd2b809b0d09b4332d98e5b4f79aace4db0b648e3ea814 ./Makefile -1d6f45fb84bef654faf2ef691d6edaea4e2232ef7b27cfe474a792dee0186dc2 ./README.md +8c90cb40d654821bf4ef4493142b5babdd41e2e69ff405cde9a0237628179815 ./README.md 4858caa52c0fb6cf302a1c581d07d448e5e90e0daa797e5819610fd6223bd348 ./RELEASE-NOTES-v1.1.0.md 01163462f46314f57660677fdef407c6c2884412ea850aab13a4f650e8c29f50 ./RELEASE-NOTES-v1.2.0.md 4da388ce660aa3b7a0d8075ec066a025b5437960973397360fcb9a5d4cb58c96 ./RELEASE-NOTES-v1.3.0.md @@ -14,7 +14,7 @@ ccfc4c139345a69d05cd2b809b0d09b4332d98e5b4f79aace4db0b648e3ea814 ./Makefile 61ceebe5a891388336795fda2fd0d1ad10373b4ebaae7d2e670f35766115c604 ./RELEASE-NOTES-v1.4.4.md 15a3defdd5bbf07ebaec1a2e8626347a027a3622f10afb508f7a21c9b0bdfa71 ./RELEASE-NOTES-v1.4.5.md d10b1dbd87585d72144d816449800dc17b6d7f8faba2dfb4eca93ae3def2c631 ./RELEASE-NOTES-v1.5.0.md -2ba0c4acefd378988cd7a510eb0ab4a45857306a5f54c3627a688d7e48240a3e ./VERSION +f6d79598472df80cb52c923e0909c952c4c067bb8beb68303046cfb63ed7ead8 ./VERSION e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ./backups/.gitkeep 8127e9db5e5e0af1d88770dc8fa60b381de45dbcc843262698cf9501409b4d58 ./deploy/searxng/settings.yml 3e153ad540d6cb1b20275775f2d7c129ec0216764cc9d0d27b963c634f347b43 ./docker-compose.dev.yml @@ -27,7 +27,7 @@ ae0c2cfc2e92ade260c82f5c27ba0ccb65cb8aab1e9c35a8bab3936fca8f0d1a ./docker-compo 323402668da4d8e1d97a29c45db753834f6c3a16739eebebf7e016f06af160bf ./docs/CONTROLLED-AUTONOMY.md 51b4a8d7daff787f099b77bd5a33ebf8038c0812bd3056f0ee6afe17e103681d ./docs/ENV-MIGRATION-FROM-LEGACY.md 7a58f9f63e8bed40e12553e3ff1418688084ead1e0e83f78167809ddb73383fc ./docs/ENVIRONMENT.md -679b3bfbac8a266811a51e799a86ef6d486193003dbbbbe9bc13e67530ee1ecc ./docs/GO-LIVE-v1.5.0.md +af80223b35e4c2f8dc3e09cc28601c869a1af64ffa816c49ab20dd470f399b6b ./docs/GO-LIVE-v1.5.0.md 2465c837c243ef03856ad1297b540d6df90005c74b9f4a9bba223066a612009f ./docs/IMPLEMENTED.md 9c38807cc12fba6f94cadc4694996de58e6d2d5396e55fc0c4bd404bb13f53ad ./docs/MIGRATION-CUTOVER.md a71d28353529906cee08cf90f35e0b0cf97bbc94ccfcef830145a2d561b21a9c ./docs/MIGRATION-MANIFEST.md @@ -43,6 +43,7 @@ fd78b77e18277a8bb81cf6ad33e67c0e7fae3977dc1bbf74787b0cb0cb9a4b4e ./docs/MIGRATI 4e2e595c0c1b78271a971f6e039c48416114ad9916ad7afc5293c2cff4ec8f9d ./docs/MIGRATION-v1.5.4-to-v1.5.5.md b38aaf9d352ad70a9701566fc24d73d26c50c407291fe1c38b08b1afad9435bc ./docs/MIGRATION-v1.5.5-to-v1.5.6.md b57e66a589cff7331d547afc0e031accd1d576c46c013143f76311a84eacbcfe ./docs/MIGRATION-v1.5.6-to-v1.5.7.md +6c234e8bfcfa63f879f0748ccf9ca006595f5ee905b97d316f685154a4a5a2b9 ./docs/MIGRATION-v1.5.7-to-v1.5.8.md 2a01fb10a3e04eae1800a7c7e0aafc31e9bbb23584cea54d849b004716ef81b4 ./docs/OBSIDIAN-EXPORT.md 040010a807178d33797106e04716822f8d147d7d5833c1ba70865a1827f8484a ./docs/OPERATIONS.md 69ea49bc76690ac489aea908f4e784a14293e92d62478a240eabea53c5f90820 ./docs/QUALITY-REPLAY-example.json @@ -54,6 +55,7 @@ bc9067063b13788393fc3389ac60c0db50e35cc9d7d733311fca2fc6923834f1 ./docs/RELEASE 677bf7eacb64adce080dcc1184dca0d7c088727b1c49242df14a7694eabbb593 ./docs/RELEASE-v1.5.5.md 4cde69f2875d5f97544fb85ff066eb671a40d9670302c81b2a9d9b9a95a9bee3 ./docs/RELEASE-v1.5.6.md f7bbf78e76592762fe6e2a08eff9110a273962ae3145da287c5875355672db7f ./docs/RELEASE-v1.5.7.md +5757dd833f1f8b7b0a7b90b09324cb8989496ea6326d652d659a59f215a3172d ./docs/RELEASE-v1.5.8.md be749a09ddbcd4cf427316c6fe531f138e9bfaa6c104f4c11a08f7555231e8b2 ./docs/UNIFIED-GRAPH.md 77d91565660789c621b05bd69fecfa3c2ef736f93855eca1f58b017e61c76f90 ./docs/VALIDATION-v1.5.0.md 3ea1953b2b4eb8f966310a65a12a681fb44e4eb3857b843312e235759aacfce5 ./docs/VALIDATION.md @@ -164,7 +166,7 @@ cb3cde795233c0d4842dc7451c5855d1c071e459efbde3be0cabac32a837ba1b ./knowledge/14 f1eab883370e0a40ef52a6b6d785a510d8ed48a19d25e0a2bc95f4f2cc8e329e ./knowledge/17_serienbriefe-und-dokumentfunktionen.json 5a0b3d5d5bc712e30a67a4de3432f69070f7e43dd99931f675e72c213006b363 ./knowledge/example-vpn.json 49c48fb45c8387b020bb6a4bde2c81e544ac82b74403e453e28a85f987052978 ./mega-project.json -a13876d5d6dbc2845d5c34cafae2465dfe7d4f574a16435570df05f01626f821 ./patches/SHA256SUMS +7430333f6dcddbd0212a7d87c69c08dcb4f6a383a25718f6d069dc8fa900754f ./patches/SHA256SUMS 47a6fa2c79bbba0c04af86dfa65d58529f492c698060fe586456c22a4eadb877 ./patches/glpi-agent-mega.diff 9b411c90d96a86c80f088ee4637046eeefaf31c62059d11aa1a999d6eb08b5b4 ./patches/glpi-knowledge-mega.diff f0491de3cb6201f98ca6be8e865237adbba4772471f7fc7165d030e0c045fdb6 ./patches/neuroforge-mega.diff @@ -184,6 +186,7 @@ b031ea42356a022d7bb03e7e2bda23c4ceddb66ccc59a5d1f0ebe787a790b829 ./patches/v1.5 7d0c4c85ee6cb89c6200cf35f23ea92c93ff5fdd9f2887c179c83fd5e5657281 ./patches/v1.5.4-to-v1.5.5.diff 405b41cbbd1ec48f316b1d935e67bc8d002fa1a3bd583fb813bf0875bd30a2e0 ./patches/v1.5.5-to-v1.5.6.diff 257a78b8f6613f92086a04bf53455f12e9bd1ab94775add31819ada465582163 ./patches/v1.5.6-to-v1.5.7.diff +53ab438b31a31590edbfe00eea6ba9d9ebff2b4c6255d7ac2056c86757f65c3a ./patches/v1.5.7-to-v1.5.8.diff 564817f8edabde0c4e4a1a427a3aa5418aae7bf12e9463044a7e6e0f13973657 ./platform/neuroforge/.env.example 39319b6f2058e4c8d6656a9cf01675374f81a04075b956b093a2befb5e05ada4 ./platform/neuroforge/.gitignore 189486a885c7fb78e0eb878d93cda0c70ca6d7ff9bfdfb3f5f32487cf03a9688 ./platform/neuroforge/BENCHMARK-v0.5.0.md @@ -223,13 +226,13 @@ dbbaa7fd4430b9cdb5302144b6d40f7ac4ab9f73e81da4ddb51e788dd373d95e ./platform/neu fc993dc95fa49802ecb62994e4140dff18a27438e8a4f3c6352229c79b041710 ./platform/neuroforge/go.mod 585f5d02cbd11a2ebdc8e9fff536b5bd2dbd46237fa3ee23414dadda4f55a73a ./platform/neuroforge/internal/brain/brain.go 976288422c0c4116d8c98af8a9b164ac670f9caf03eddc2d5e2e48747456d3b4 ./platform/neuroforge/internal/brain/consolidation_test.go -f231a9ee8ffcbea8477e828d4111760e52fd85bb027e5670830e6df065a9ec75 ./platform/neuroforge/internal/brain/goal_progress.go -d2ca691799b7aa6e9244767d9534c14323e1e4aeaf88ad0931036e63afcd476d ./platform/neuroforge/internal/brain/goal_progress_test.go +0eb3cb86d1c235ac21527aae79de7c26ce204412814c5ea84dd9389e0b0fca2b ./platform/neuroforge/internal/brain/goal_progress.go +2541000c961784bb8c26c6ca2192a2be1baf9d7ecf2d8d40400a4f33fec73277 ./platform/neuroforge/internal/brain/goal_progress_test.go 359955653c647125559afd6dc3ebe69aa5ca19ff7e825ce801b7bc24e5fbcfcb ./platform/neuroforge/internal/brain/policy.go 27e87af473d2d71ba94ffb9bf7a70934776f8c23ce45496ca0998ad3000fc156 ./platform/neuroforge/internal/brain/policy_test.go 9022a797aa1d49af20ced5d8c3e0b903be740fbaa7251e8a80d43cd04d2f68fe ./platform/neuroforge/internal/brain/research_quality.go 0a3c8f7d149e814e091595982dbaa69467f6bf11eec631471d133a9b21585ae4 ./platform/neuroforge/internal/brain/research_trace.go -530bd1eed2a1a157b4fd4468cbc51560fc94d6047de7593ade515fde0ad4a300 ./platform/neuroforge/internal/brain/staging.go +ba9183d3e084cb17448376fbf2af43d612deffe97fe1543d20ddfa70b1565ce1 ./platform/neuroforge/internal/brain/staging.go 7f58e1b413836e6b0fb4ed4f272d82509df1d3d9e2ab48722c6901427496e67c ./platform/neuroforge/internal/brain/staging_quality.go d4fed8b68d31f6fbd1992bde76abd4e7b2f9d211e53bf8b234369a6a53692fe8 ./platform/neuroforge/internal/brain/v3.go 3ae13251512ecad1423a33ce09889f961130fefaed9342170b2d5cc6b3b51893 ./platform/neuroforge/internal/brain/v3_test.go @@ -240,7 +243,7 @@ a9619d9571f6ab6363b36af9fdf1690f9f88e9333eb6f2773ae68580b19666d8 ./platform/neu cd8f1281e25ce42ae8918abbed16d4bf57ada9c3a3cc1212edd28f77ccc9328f ./platform/neuroforge/internal/brain/v6.go 312e7578b5800ccb06755c19d7ef7026a362443804ff78cb7a11a7443af2aab5 ./platform/neuroforge/internal/brain/v8.go 31420af7145686a2c7eb42fc18ca1299604afcb5942cfdfd132bef5c5f05d980 ./platform/neuroforge/internal/brain/v8_test.go -cf6e43d8daa0461ec38dc725197aa851332caa466bf9e0414c02fa88c446e064 ./platform/neuroforge/internal/core/types.go +4e0f6a74fbbd54666cea2733542fbb0ddd19fad024e3fc37487f7808eb63b069 ./platform/neuroforge/internal/core/types.go 65a8b8196343e7cfd9444ca314da4a83c81bc046d9478b8b93217d9cc68ba562 ./platform/neuroforge/internal/cost/cost.go b9bb2934e01ed2bfb6b16e4139e10e51f1387112eb8654f7f692779a2fd2d273 ./platform/neuroforge/internal/cost/cost_test.go fb66ce4ab760b979eacd4f7f17dedc41916f93c53ba582be572d250f99654695 ./platform/neuroforge/internal/httpapi/admin_app_auth_test.go @@ -315,7 +318,7 @@ c854f70f4141344dc2c6fbec4419afe5dfe25fd1bc2da11e41781730a21c8834 ./platform/neu 32f55e82419e4b043d15682c82ade1c4bc9694bfa372f925c131c2611f48d744 ./platform/neuroforge/internal/vector/snapshot_test.go c5463c525f5ea703ea1f6df74cabc30f937f3846b1f75f237adeee2700baa977 ./platform/neuroforge/internal/vector/vector.go 276cb34ddd9f87abdac17be110e2bf15135ab83ba66bf91663d59d61fbd81125 ./platform/neuroforge/neuroforge-v0.7.3-rtx4090-example.json -5199b597b3e6b83654c7912b6ee7b22173ff21f77cc6ba26e385d335a0e6acf4 ./platform/neuroforge/openapi.yaml +9cdc135b2baa4edd699e7782b7075d09f6599c27fd26aeda136c64e12fc127ec ./platform/neuroforge/openapi.yaml 3e9a34479e5106cd7551c5bbe0eb1f6b206421ef9fbb99d7f7dd0f153f54dfeb ./scripts/backup-data.sh df684b1b487762248516aebb82742e5777395b504cbde42ebda85b999ae353be ./scripts/check-compose-env.py 6f1d21b7aea3265a088256801aa37a00931157523d8bd834aa4f473e99511f04 ./scripts/codebase-memory-ui.sh diff --git a/README.md b/README.md index 9bfff20..b5a7271 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# GLPI NeuroForge Mega v1.5.7 +# GLPI NeuroForge Mega v1.5.8 -> Release: **v1.5.7** · Identifier-Grounding-Hardening: Slash-Komposita und URL-Pfade werden nicht mehr als CLI-Switches fehlklassifiziert; echte code-markierte Command-Switches bleiben source-verifiziert. +> Release: **v1.5.8** · Staging-Revalidation-Hardening: persistierte Quality-Fehler werden versionsbewusst neu geprüft; nur ein unter dem aktuellen Gate validierter Draft erfüllt ein Artikelziel. Ein kontrolliertes Monorepo aus **GLPI AI Agent**, **GLPI AI Knowledgebase** und **NeuroForge + SQAR**. Ziel ist nicht ein untrennbarer Monolith, sondern eine gemeinsame Plattform mit klaren Zuständigkeiten, getrennten Credentials und nachvollziehbaren Failure-Modi. @@ -155,6 +155,15 @@ Die Zielgröße `research_corroborations` zählt v1.5.5 unabhängige Source-Orig Staging-Synthese, Claim-Verifikation und Grounding-Rewrite fordern bei Ollama jetzt provider-nativ `format: "json"` an. Zusätzlich validiert NeuroForge die erwarteten JSON-Schemata strikt, lehnt unbekannte Felder ab und repariert ausschließlich syntaktisch eindeutig ungültige Backslash-Escapes innerhalb von JSON-Strings (z. B. Windows-/Registry-Pfade). Gültige JSON-Escapes und Daten außerhalb von Strings werden nicht verändert. Andere Syntaxfehler bleiben fail-closed bzw. durchlaufen höchstens den bereits begrenzten syntax-only Repair-Pass. + +### Staging Revalidation Hardening (v1.5.8) + +- Persistierte `last_staging_error`-Werte aus älteren Quality-Gates werden nicht mehr endlos angezeigt, nur weil ein späterer Research-Cycle keine neue Evidenz enthält. +- Ein bestehender Draft wird nach einem Quality-Gate-Upgrade mit seiner vorhandenen relevanten Evidence einmal erneut validiert. +- Deterministisch fehlgeschlagene, unveränderte Drafts werden über eine Evidence-Signatur nicht in jedem Scheduler-Tick erneut teuer synthetisiert. +- Der Goal-State speichert, ob der aktuelle Staging-Draft unter der aktuellen Quality-Gate-Version validiert wurde. +- Artikelziele zählen nur einen aktuell validierten Draft; historische Draft-Erzeugungen allein erzeugen keine 100-%-Anzeige mehr. + ### Identifier Grounding Hardening (v1.5.7) Der deterministische Identifier-Guard unterscheidet Slash-prefixed CLI-Switches jetzt von normaler Prosa. Konstruktionen wie `BIOS-/UEFI-Konfiguration`, `Web-/Portal-Konfiguration` und URL-Pfade blockieren Staging nicht mehr als vermeintlich erfundene Command-Switches. Slash-Switches werden nur noch aus explizit als Code markierten Markdown-Spans/Fences extrahiert; dort bleiben erfundene Optionen wie `DISM /MagicRepair` weiterhin fail-closed source-verifiziert. Fehlercodes, CVEs, KB-Nummern und Versionsnummern werden unverändert global geprüft. diff --git a/VERSION b/VERSION index f01291b..1cc9c18 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.5.7 +1.5.8 diff --git a/docs/GO-LIVE-v1.5.0.md b/docs/GO-LIVE-v1.5.0.md index ce05239..3b85c88 100644 --- a/docs/GO-LIVE-v1.5.0.md +++ b/docs/GO-LIVE-v1.5.0.md @@ -51,3 +51,8 @@ Für einen realen Research→Staging-Smoke-Test zusätzlich verifizieren: ## v1.5.7 Identifier-Grounding Zusatzgate Bei Windows-/Vendor-Artikeln dürfen normale Slash-Komposita oder URL-Pfade kein `source-unverified identifiers` auslösen. Echte CLI-Switches in Code-Spans/Fences bleiben source-verifiziert. Vor Go-Live mindestens einen Goal-Lauf mit `BIOS-/UEFI`-ähnlicher Prosa und einen Lauf mit einem belegten Slash-Command prüfen. + + +## v1.5.8 Staging-Revalidation Zusatzgate + +Nach einem Upgrade auf v1.5.8 aktive Goals mindestens einen Scheduler-Cycle durchlaufen lassen. Ein Artikelziel darf erst dann als erfüllt gelten, wenn der Goal-State `staging_draft_validated=true` und `staging_quality_gate_version=staging-v3` meldet. Persistierte Fehler aus älteren Gates dürfen nicht als aktueller Fehlerstatus interpretiert werden. diff --git a/docs/MIGRATION-v1.5.7-to-v1.5.8.md b/docs/MIGRATION-v1.5.7-to-v1.5.8.md new file mode 100644 index 0000000..ad46591 --- /dev/null +++ b/docs/MIGRATION-v1.5.7-to-v1.5.8.md @@ -0,0 +1,16 @@ +# Migration v1.5.7 → v1.5.8 + +v1.5.8 is a non-destructive goal/staging state migration. No volumes must be deleted. + +1. Build/publish the v1.5.8 images. +2. Set `IMAGE_TAG=1.5.8`. +3. Pull and recreate NeuroForge and its worker: + +```bash +docker compose --profile research pull neuroforge neuroforge-worker +docker compose --profile research up -d --force-recreate neuroforge neuroforge-worker +``` + +4. Keep all existing goals and volumes. +5. Let each active goal run once. Existing legacy drafts will either be revalidated under the current gate, remain below the current evidence threshold, or receive a current quality error. +6. Article goals should only return to 100% after `staging_draft_validated=true`. diff --git a/docs/RELEASE-v1.5.8.md b/docs/RELEASE-v1.5.8.md new file mode 100644 index 0000000..2fb1238 --- /dev/null +++ b/docs/RELEASE-v1.5.8.md @@ -0,0 +1,20 @@ +# GLPI NeuroForge Mega v1.5.8 + +v1.5.8 closes a production-state consistency bug found while revalidating existing FortiClient, DISM and BitLocker goals after the v1.5.7 identifier-grounding fix. Older `last_staging_error` values could remain visible forever because a goal with an existing draft returned early whenever the newest research run contributed no new evidence. The old draft also continued to satisfy an article target even if it had never passed the current quality gate. + +## Staging revalidation + +- Goals persist `staging_draft_validated`, `staging_quality_gate_version` and a hashed `last_staging_attempt_signature`. +- Existing drafts from an older gate are revalidated once against the current gate using already persisted relevant evidence. +- Unchanged, already validated drafts are not regenerated on empty scheduler cycles. +- Unchanged deterministic failures are not retried every cycle; the evidence signature prevents expensive LLM retry loops. +- Transient transport/publisher failures remain retryable. +- If current evidence no longer satisfies the quality threshold, the old draft is marked not validated and the stale error is replaced by the current quality state. + +## Goal progress semantics + +For article/draft targets, historical `staging_drafts_created` is audit history only. Progress is now based on whether the current stable staging draft exists and is validated under the current quality-gate version. This prevents `1/1 = 100%` for legacy or invalidated drafts. + +## Compatibility + +No destructive data migration is required. New fields are optional JSON fields and are populated by the next goal cycle. Existing goals, memories, synapses, research runs and staging drafts are retained. diff --git a/patches/SHA256SUMS b/patches/SHA256SUMS index 63555bd..c310b97 100644 --- a/patches/SHA256SUMS +++ b/patches/SHA256SUMS @@ -17,3 +17,4 @@ b031ea42356a022d7bb03e7e2bda23c4ceddb66ccc59a5d1f0ebe787a790b829 v1.5.1-to-v1.5 7d0c4c85ee6cb89c6200cf35f23ea92c93ff5fdd9f2887c179c83fd5e5657281 v1.5.4-to-v1.5.5.diff 405b41cbbd1ec48f316b1d935e67bc8d002fa1a3bd583fb813bf0875bd30a2e0 v1.5.5-to-v1.5.6.diff 257a78b8f6613f92086a04bf53455f12e9bd1ab94775add31819ada465582163 v1.5.6-to-v1.5.7.diff +53ab438b31a31590edbfe00eea6ba9d9ebff2b4c6255d7ac2056c86757f65c3a v1.5.7-to-v1.5.8.diff diff --git a/patches/v1.5.7-to-v1.5.8.diff b/patches/v1.5.7-to-v1.5.8.diff new file mode 100644 index 0000000..317c67f --- /dev/null +++ b/patches/v1.5.7-to-v1.5.8.diff @@ -0,0 +1,525 @@ +diff --git a/.env.example b/.env.example +index daf61f7..7138009 100644 +--- a/.env.example ++++ b/.env.example +@@ -1,5 +1,5 @@ + ############################################################################### +-# GLPI NEUROFORGE MEGA v1.5.7 - VOLLSTÄNDIGE .ENV.example ++# GLPI NEUROFORGE MEGA v1.5.8 - VOLLSTÄNDIGE .ENV.example + # + # Diese Datei ist die zentrale Konfiguration für docker compose. + # Sie enthält: +@@ -29,7 +29,7 @@ + # 01. MEGA STACK - RELEASE / HOST PORTS / PFADE + ############################################################################### + # Immutable Registry-Tag der sechs Projekt-Images. "latest" ist produktiv verboten. +-IMAGE_TAG=1.5.7 ++IMAGE_TAG=1.5.8 + + CONTROL_HOST_PORT=8070 + AGENT_HOST_PORT=8080 +diff --git a/README.md b/README.md +index 9bfff20..b5a7271 100644 +--- a/README.md ++++ b/README.md +@@ -1,6 +1,6 @@ +-# GLPI NeuroForge Mega v1.5.7 ++# GLPI NeuroForge Mega v1.5.8 + +-> Release: **v1.5.7** · Identifier-Grounding-Hardening: Slash-Komposita und URL-Pfade werden nicht mehr als CLI-Switches fehlklassifiziert; echte code-markierte Command-Switches bleiben source-verifiziert. ++> Release: **v1.5.8** · Staging-Revalidation-Hardening: persistierte Quality-Fehler werden versionsbewusst neu geprüft; nur ein unter dem aktuellen Gate validierter Draft erfüllt ein Artikelziel. + + Ein kontrolliertes Monorepo aus **GLPI AI Agent**, **GLPI AI Knowledgebase** und **NeuroForge + SQAR**. Ziel ist nicht ein untrennbarer Monolith, sondern eine gemeinsame Plattform mit klaren Zuständigkeiten, getrennten Credentials und nachvollziehbaren Failure-Modi. + +@@ -155,6 +155,15 @@ Die Zielgröße `research_corroborations` zählt v1.5.5 unabhängige Source-Orig + + Staging-Synthese, Claim-Verifikation und Grounding-Rewrite fordern bei Ollama jetzt provider-nativ `format: "json"` an. Zusätzlich validiert NeuroForge die erwarteten JSON-Schemata strikt, lehnt unbekannte Felder ab und repariert ausschließlich syntaktisch eindeutig ungültige Backslash-Escapes innerhalb von JSON-Strings (z. B. Windows-/Registry-Pfade). Gültige JSON-Escapes und Daten außerhalb von Strings werden nicht verändert. Andere Syntaxfehler bleiben fail-closed bzw. durchlaufen höchstens den bereits begrenzten syntax-only Repair-Pass. + ++ ++### Staging Revalidation Hardening (v1.5.8) ++ ++- Persistierte `last_staging_error`-Werte aus älteren Quality-Gates werden nicht mehr endlos angezeigt, nur weil ein späterer Research-Cycle keine neue Evidenz enthält. ++- Ein bestehender Draft wird nach einem Quality-Gate-Upgrade mit seiner vorhandenen relevanten Evidence einmal erneut validiert. ++- Deterministisch fehlgeschlagene, unveränderte Drafts werden über eine Evidence-Signatur nicht in jedem Scheduler-Tick erneut teuer synthetisiert. ++- Der Goal-State speichert, ob der aktuelle Staging-Draft unter der aktuellen Quality-Gate-Version validiert wurde. ++- Artikelziele zählen nur einen aktuell validierten Draft; historische Draft-Erzeugungen allein erzeugen keine 100-%-Anzeige mehr. ++ + ### Identifier Grounding Hardening (v1.5.7) + + Der deterministische Identifier-Guard unterscheidet Slash-prefixed CLI-Switches jetzt von normaler Prosa. Konstruktionen wie `BIOS-/UEFI-Konfiguration`, `Web-/Portal-Konfiguration` und URL-Pfade blockieren Staging nicht mehr als vermeintlich erfundene Command-Switches. Slash-Switches werden nur noch aus explizit als Code markierten Markdown-Spans/Fences extrahiert; dort bleiben erfundene Optionen wie `DISM /MagicRepair` weiterhin fail-closed source-verifiziert. Fehlercodes, CVEs, KB-Nummern und Versionsnummern werden unverändert global geprüft. +diff --git a/VERSION b/VERSION +index f01291b..1cc9c18 100644 +--- a/VERSION ++++ b/VERSION +@@ -1 +1 @@ +-1.5.7 ++1.5.8 +diff --git a/docs/GO-LIVE-v1.5.0.md b/docs/GO-LIVE-v1.5.0.md +index ce05239..3b85c88 100644 +--- a/docs/GO-LIVE-v1.5.0.md ++++ b/docs/GO-LIVE-v1.5.0.md +@@ -51,3 +51,8 @@ Für einen realen Research→Staging-Smoke-Test zusätzlich verifizieren: + ## v1.5.7 Identifier-Grounding Zusatzgate + + Bei Windows-/Vendor-Artikeln dürfen normale Slash-Komposita oder URL-Pfade kein `source-unverified identifiers` auslösen. Echte CLI-Switches in Code-Spans/Fences bleiben source-verifiziert. Vor Go-Live mindestens einen Goal-Lauf mit `BIOS-/UEFI`-ähnlicher Prosa und einen Lauf mit einem belegten Slash-Command prüfen. ++ ++ ++## v1.5.8 Staging-Revalidation Zusatzgate ++ ++Nach einem Upgrade auf v1.5.8 aktive Goals mindestens einen Scheduler-Cycle durchlaufen lassen. Ein Artikelziel darf erst dann als erfüllt gelten, wenn der Goal-State `staging_draft_validated=true` und `staging_quality_gate_version=staging-v3` meldet. Persistierte Fehler aus älteren Gates dürfen nicht als aktueller Fehlerstatus interpretiert werden. +diff --git a/docs/MIGRATION-v1.5.7-to-v1.5.8.md b/docs/MIGRATION-v1.5.7-to-v1.5.8.md +new file mode 100644 +index 0000000..ad46591 +--- /dev/null ++++ b/docs/MIGRATION-v1.5.7-to-v1.5.8.md +@@ -0,0 +1,16 @@ ++# Migration v1.5.7 → v1.5.8 ++ ++v1.5.8 is a non-destructive goal/staging state migration. No volumes must be deleted. ++ ++1. Build/publish the v1.5.8 images. ++2. Set `IMAGE_TAG=1.5.8`. ++3. Pull and recreate NeuroForge and its worker: ++ ++```bash ++docker compose --profile research pull neuroforge neuroforge-worker ++docker compose --profile research up -d --force-recreate neuroforge neuroforge-worker ++``` ++ ++4. Keep all existing goals and volumes. ++5. Let each active goal run once. Existing legacy drafts will either be revalidated under the current gate, remain below the current evidence threshold, or receive a current quality error. ++6. Article goals should only return to 100% after `staging_draft_validated=true`. +diff --git a/docs/RELEASE-v1.5.8.md b/docs/RELEASE-v1.5.8.md +new file mode 100644 +index 0000000..2fb1238 +--- /dev/null ++++ b/docs/RELEASE-v1.5.8.md +@@ -0,0 +1,20 @@ ++# GLPI NeuroForge Mega v1.5.8 ++ ++v1.5.8 closes a production-state consistency bug found while revalidating existing FortiClient, DISM and BitLocker goals after the v1.5.7 identifier-grounding fix. Older `last_staging_error` values could remain visible forever because a goal with an existing draft returned early whenever the newest research run contributed no new evidence. The old draft also continued to satisfy an article target even if it had never passed the current quality gate. ++ ++## Staging revalidation ++ ++- Goals persist `staging_draft_validated`, `staging_quality_gate_version` and a hashed `last_staging_attempt_signature`. ++- Existing drafts from an older gate are revalidated once against the current gate using already persisted relevant evidence. ++- Unchanged, already validated drafts are not regenerated on empty scheduler cycles. ++- Unchanged deterministic failures are not retried every cycle; the evidence signature prevents expensive LLM retry loops. ++- Transient transport/publisher failures remain retryable. ++- If current evidence no longer satisfies the quality threshold, the old draft is marked not validated and the stale error is replaced by the current quality state. ++ ++## Goal progress semantics ++ ++For article/draft targets, historical `staging_drafts_created` is audit history only. Progress is now based on whether the current stable staging draft exists and is validated under the current quality-gate version. This prevents `1/1 = 100%` for legacy or invalidated drafts. ++ ++## Compatibility ++ ++No destructive data migration is required. New fields are optional JSON fields and are populated by the next goal cycle. Existing goals, memories, synapses, research runs and staging drafts are retained. +diff --git a/platform/neuroforge/internal/brain/goal_progress.go b/platform/neuroforge/internal/brain/goal_progress.go +index 0dfa763..9fee91a 100644 +--- a/platform/neuroforge/internal/brain/goal_progress.go ++++ b/platform/neuroforge/internal/brain/goal_progress.go +@@ -112,7 +112,10 @@ func (e *Engine) refreshGoalResearchProgress(goal *core.Goal, evaluation float64 + if n, err := strconv.Atoi(m[1]); err == nil && n > 0 { + current, label := goal.ResearchEvidence, "quellengebundene Evidenzen" + if strings.Contains(target, "artikel") || strings.Contains(target, "article") || strings.Contains(target, "draft") || strings.Contains(target, "entwurf") { +- current, label = goal.StagingDraftsCreated, "Staging-Artikel" ++ current, label = 0, "validierte Staging-Artikel" ++ if goal.LastStagingDraftID != "" && goal.StagingDraftValidated && goal.StagingQualityGateVersion == stagingQualityGateVersion { ++ current = 1 ++ } + } else { + evidenceTarget := strings.Contains(target, "wissensein") || strings.Contains(target, "evidenz") || strings.Contains(target, "claim") || strings.Contains(target, "eintr") + if !evidenceTarget && (strings.Contains(target, "quelle") || strings.Contains(target, "source")) { +diff --git a/platform/neuroforge/internal/brain/goal_progress_test.go b/platform/neuroforge/internal/brain/goal_progress_test.go +index 63f4ad2..2ad18ff 100644 +--- a/platform/neuroforge/internal/brain/goal_progress_test.go ++++ b/platform/neuroforge/internal/brain/goal_progress_test.go +@@ -286,17 +286,17 @@ func TestResearchMaterialRelevanceRejectsOffTopicWebRTCForNVIDIA(t *testing.T) { + } + } + +-func TestGoalArticleTargetUsesCreatedStagingArticles(t *testing.T) { ++func TestGoalArticleTargetUsesCurrentValidatedStagingDraft(t *testing.T) { + s, err := store.New(t.TempDir()) + if err != nil { + t.Fatal(err) + } + defer s.Close() +- g := &core.Goal{ID: "goal-articles", Title: "NVIDIA", Target: "20 hochwertige Wissensartikel", StagingDraftsCreated: 1} ++ g := &core.Goal{ID: "goal-articles", Title: "NVIDIA", Target: "20 hochwertige Wissensartikel", StagingDraftsCreated: 1, LastStagingDraftID: "KB-1", StagingDraftValidated: true, StagingQualityGateVersion: stagingQualityGateVersion} + e := &Engine{store: s} + e.refreshGoalResearchProgress(g, 0) +- if g.Progress < .049 || g.Progress > .051 || !strings.Contains(g.ProgressReason, "1/20 Staging-Artikel") { +- t.Fatalf("article target must count articles, got progress=%f reason=%q", g.Progress, g.ProgressReason) ++ if g.Progress < .049 || g.Progress > .051 || !strings.Contains(g.ProgressReason, "1/20 validierte Staging-Artikel") { ++ t.Fatalf("article target must count only a current validated draft, got progress=%f reason=%q", g.Progress, g.ProgressReason) + } + } + +@@ -630,3 +630,92 @@ func TestAuthoritativeDomainConfigurationRejectsOverbroadValues(t *testing.T) { + t.Fatalf("unsafe authority domains were not sanitized: %#v", cfg.AuthoritativeDomains) + } + } ++ ++func TestStagingRetriesOldQualityFailureOnceAfterGateUpgrade(t *testing.T) { ++ requests := 0 ++ kb := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ requests++ ++ w.Header().Set("Content-Type", "application/json") ++ _ = json.NewEncoder(w).Encode(map[string]any{"staging": map[string]any{"key": "KB-AI-STAGING-OLD", "meta": map[string]any{"integration_action": "updated"}}}) ++ })) ++ defer kb.Close() ++ ++ s, err := store.New(t.TempDir()) ++ if err != nil { ++ t.Fatal(err) ++ } ++ defer s.Close() ++ src := &core.KnowledgeSource{ID: "src-1", Type: "web", Title: "Fortinet 7200", URI: "https://community.fortinet.com/fortigate/7200", Trust: .9, Status: "ready"} ++ if err := s.UpsertSource(src); err != nil { ++ t.Fatal(err) ++ } ++ mem := &core.Memory{ID: "mem-1", Kind: "evidence", MemoryType: core.MemorySemantic, Text: "FortiClient SSLVPN error 7200 troubleshooting evidence.", Confidence: .8, Status: core.MemoryActive, Provenance: core.MemoryProvenance{Source: "web.page", GoalID: "goal-1", SourceID: src.ID}} ++ if err := s.AddMemory(mem); err != nil { ++ t.Fatal(err) ++ } ++ run, err := s.StartResearchRun("goal-1", "FortiClient SSLVPN 7200") ++ if err != nil { ++ t.Fatal(err) ++ } ++ _, _ = s.FinishResearchRun(run.ID, "completed", "") ++ ++ e := &Engine{store: s, http: kb.Client()} ++ e.ConfigureStagingPublisher(StagingPublisherConfig{Enabled: true, URL: kb.URL, Token: "secret", MinEvidence: 1, MinSources: 1, SynthesisMode: "evidence"}) ++ g := &core.Goal{ ++ ID: "goal-1", Title: "FortiClient SSLVPN 7200", ResearchEvidence: 1, ResearchSources: 1, ++ LastStagingDraftID: "KB-AI-STAGING-OLD", ++ LastStagingError: "staging synthesis introduced source-unverified identifiers: /portal-konfiguration", ++ } ++ ++ e.maybePublishGoalDraft(context.Background(), g, ResearchResult{RunID: run.ID}) ++ if requests != 1 || g.LastStagingError != "" || !g.StagingDraftValidated || g.StagingQualityGateVersion != stagingQualityGateVersion { ++ t.Fatalf("old quality failure was not revalidated: requests=%d goal=%#v", requests, g) ++ } ++ ++ // The same unchanged, already validated evidence must not be synthesized again. ++ e.maybePublishGoalDraft(context.Background(), g, ResearchResult{RunID: run.ID}) ++ if requests != 1 { ++ t.Fatalf("validated unchanged draft was republished: requests=%d", requests) ++ } ++} ++ ++func TestArticleProgressRequiresCurrentValidatedDraft(t *testing.T) { ++ s, err := store.New(t.TempDir()) ++ if err != nil { ++ t.Fatal(err) ++ } ++ defer s.Close() ++ e := &Engine{store: s} ++ g := &core.Goal{ID: "g1", Title: "Test", Target: "1 hochwertiger Wissensartikel", LastStagingDraftID: "KB-OLD", StagingDraftsCreated: 1} ++ ++ e.refreshGoalResearchProgress(g, 0) ++ if g.Progress != 0 || !strings.Contains(g.ProgressReason, "0/1 validierte Staging-Artikel") { ++ t.Fatalf("unvalidated legacy draft must not satisfy article target: %#v", g) ++ } ++ ++ g.StagingDraftValidated = true ++ g.StagingQualityGateVersion = stagingQualityGateVersion ++ e.refreshGoalResearchProgress(g, 0) ++ if g.Progress != 1 || !strings.Contains(g.ProgressReason, "1/1 validierte Staging-Artikel") { ++ t.Fatalf("current validated draft should satisfy target: %#v", g) ++ } ++} ++ ++func TestStagingBelowThresholdInvalidatesLegacyDraftAndReplacesStaleError(t *testing.T) { ++ s, err := store.New(t.TempDir()) ++ if err != nil { ++ t.Fatal(err) ++ } ++ defer s.Close() ++ e := &Engine{store: s} ++ e.ConfigureStagingPublisher(StagingPublisherConfig{Enabled: true, URL: "http://knowledge.invalid", Token: "secret", MinEvidence: 4, MinSources: 2}) ++ g := &core.Goal{ ++ ID: "g1", LastStagingDraftID: "KB-OLD", StagingDraftValidated: true, ++ StagingQualityGateVersion: "staging-v2", ++ LastStagingError: "staging synthesis introduced source-unverified identifiers: /portal-konfiguration", ++ } ++ e.maybePublishGoalDraft(context.Background(), g, ResearchResult{}) ++ if g.StagingDraftValidated || g.StagingQualityGateVersion != stagingQualityGateVersion || !strings.Contains(g.LastStagingError, "quality gate not satisfied") { ++ t.Fatalf("legacy draft state was not invalidated/reconciled: %#v", g) ++ } ++} +diff --git a/platform/neuroforge/internal/brain/staging.go b/platform/neuroforge/internal/brain/staging.go +index fc82db1..4ae981e 100644 +--- a/platform/neuroforge/internal/brain/staging.go ++++ b/platform/neuroforge/internal/brain/staging.go +@@ -3,6 +3,7 @@ package brain + import ( + "bytes" + "context" ++ "crypto/sha256" + "encoding/json" + "errors" + "fmt" +@@ -14,6 +15,8 @@ import ( + "neuroforge/internal/core" + ) + ++const stagingQualityGateVersion = "staging-v3" ++ + // StagingPublisherConfig configures the one-way governance bridge from + // autonomous research into the human-review knowledge staging area. + type StagingPublisherConfig struct { +@@ -120,25 +123,25 @@ func (e *Engine) maybePublishGoalDraft(ctx context.Context, goal *core.Goal, res + return + } + if strings.TrimSpace(cfg.URL) == "" || strings.TrimSpace(cfg.Token) == "" { ++ goal.StagingDraftValidated = false ++ goal.StagingQualityGateVersion = stagingQualityGateVersion + goal.LastStagingError = "staging publisher enabled but URL/token is missing" + _ = e.store.AddKnowledgeEvent(core.KnowledgeEvent{Type: "staging.publish_error", Summary: "Research draft could not be published", Reason: goal.LastStagingError, Actor: "goal-learning", Metadata: map[string]string{"goal_id": goal.ID}}) + return + } +- if goal.LastStagingDraftID != "" && research.RunID != "" { +- if run, ok := e.store.GetResearchRun(research.RunID); ok && run.Stats.NewEvidence == 0 && run.Stats.Corroborations == 0 { +- // Avoid rewriting the same active staging draft every scheduler tick when +- // this cycle contributed no new information. +- return +- } +- } + if goal.ResearchEvidence < cfg.MinEvidence || goal.ResearchSources < cfg.MinSources || goal.ResearchCorroborations < cfg.MinCorroborations { +- goal.LastStagingError = "" +- _ = e.store.AddKnowledgeEvent(core.KnowledgeEvent{Type: "staging.not_ready", Summary: "Research has not reached staging quality gate", Reason: fmt.Sprintf("evidence=%d/%d sources=%d/%d corroborations=%d/%d", goal.ResearchEvidence, cfg.MinEvidence, goal.ResearchSources, cfg.MinSources, goal.ResearchCorroborations, cfg.MinCorroborations), Actor: "goal-learning", Metadata: map[string]string{"goal_id": goal.ID}}) ++ goal.StagingDraftValidated = false ++ goal.StagingQualityGateVersion = stagingQualityGateVersion ++ goal.LastStagingAttemptSignature = "" ++ goal.LastStagingError = fmt.Sprintf("staging quality gate not satisfied: evidence=%d/%d sources=%d/%d corroborations=%d/%d", goal.ResearchEvidence, cfg.MinEvidence, goal.ResearchSources, cfg.MinSources, goal.ResearchCorroborations, cfg.MinCorroborations) ++ _ = e.store.AddKnowledgeEvent(core.KnowledgeEvent{Type: "staging.not_ready", Summary: "Research has not reached staging quality gate", Reason: goal.LastStagingError, Actor: "goal-learning", Metadata: map[string]string{"goal_id": goal.ID}}) + return + } + + evidence := e.collectGoalDraftEvidence(goal, cfg) + if len(evidence) == 0 { ++ goal.StagingDraftValidated = false ++ goal.StagingQualityGateVersion = stagingQualityGateVersion + goal.LastStagingError = "no active, goal-relevant source-backed evidence available for staging" + return + } +@@ -158,15 +161,39 @@ func (e *Engine) maybePublishGoalDraft(ctx context.Context, goal *core.Goal, res + } + } + if len(selectedSources) < cfg.MinSources { ++ goal.StagingDraftValidated = false ++ goal.StagingQualityGateVersion = stagingQualityGateVersion + goal.LastStagingError = fmt.Sprintf("staging evidence diversity below threshold: relevant_sources=%d/%d", len(selectedSources), cfg.MinSources) + return + } + authoritativeSources, independentOrigins, sourceAudit := summarizeEvidenceAuthority(cfg, evidence) + if cfg.RequireAuthoritativeSource && authoritativeSources < cfg.MinAuthoritativeSources { ++ goal.StagingDraftValidated = false ++ goal.StagingQualityGateVersion = stagingQualityGateVersion + goal.LastStagingError = fmt.Sprintf("staging source authority below threshold: authoritative_sources=%d/%d", authoritativeSources, cfg.MinAuthoritativeSources) + _ = e.store.AddKnowledgeEvent(core.KnowledgeEvent{Type: "staging.not_ready", Summary: "Research draft lacks authoritative sources", Reason: goal.LastStagingError, Actor: "goal-learning", Metadata: map[string]string{"goal_id": goal.ID}}) + return + } ++ ++ attemptSignature := stagingEvidenceSignature(evidence) ++ noNewInformation := false ++ if research.RunID != "" { ++ if run, ok := e.store.GetResearchRun(research.RunID); ok { ++ noNewInformation = run.Stats.NewEvidence == 0 && run.Stats.Corroborations == 0 ++ } ++ } ++ if goal.LastStagingDraftID != "" && noNewInformation { ++ if goal.StagingDraftValidated && goal.StagingQualityGateVersion == stagingQualityGateVersion && strings.TrimSpace(goal.LastStagingError) == "" { ++ return ++ } ++ if goal.StagingQualityGateVersion == stagingQualityGateVersion && goal.LastStagingAttemptSignature == attemptSignature && deterministicStagingFailure(goal.LastStagingError) { ++ return ++ } ++ } ++ goal.StagingDraftValidated = false ++ goal.StagingQualityGateVersion = stagingQualityGateVersion ++ goal.LastStagingAttemptSignature = attemptSignature ++ + draft, err := e.synthesizeGoalDraft(ctx, goal, evidence) + if err != nil { + goal.LastStagingError = err.Error() +@@ -205,7 +232,7 @@ func (e *Engine) maybePublishGoalDraft(ctx context.Context, goal *core.Goal, res + "research_evidence_ids": evidenceIDs, + "research_source_uris": sourceURIs, + "source_authority": sourceAudit, +- "quality_gate_version": "staging-v2", ++ "quality_gate_version": stagingQualityGateVersion, + "human_review_required": true, + } + if draft.Quality != nil && draft.Quality.Verification != nil { +@@ -245,9 +272,49 @@ func (e *Engine) maybePublishGoalDraft(ctx context.Context, goal *core.Goal, res + } + goal.LastStagingDraftID = out.Staging.Key + goal.LastStagingError = "" ++ goal.StagingDraftValidated = true ++ goal.StagingQualityGateVersion = stagingQualityGateVersion ++ goal.LastStagingAttemptSignature = attemptSignature + _ = e.store.AddKnowledgeEvent(core.KnowledgeEvent{Type: "staging.draft_" + firstNonEmpty(action, "created"), Summary: "Research proposal sent to human-review staging", Reason: "research quality gate satisfied", Actor: "goal-learning", Metadata: map[string]string{"goal_id": goal.ID, "staging_id": out.Staging.Key, "action": action}}) + } + ++func stagingEvidenceSignature(evidence []draftEvidence) string { ++ parts := make([]string, 0, len(evidence)*2+1) ++ parts = append(parts, stagingQualityGateVersion) ++ for _, ev := range evidence { ++ parts = append(parts, ev.Memory.ID, ev.Memory.Provenance.SourceID) ++ for _, src := range ev.CorroboratingSources { ++ if src != nil { ++ parts = append(parts, src.ID) ++ } ++ } ++ } ++ sort.Strings(parts[1:]) ++ sum := sha256.Sum256([]byte(strings.Join(parts, "\x00"))) ++ return fmt.Sprintf("%s:%x", stagingQualityGateVersion, sum[:12]) ++} ++ ++func deterministicStagingFailure(message string) bool { ++ m := strings.ToLower(strings.TrimSpace(message)) ++ if m == "" { ++ return false ++ } ++ for _, prefix := range []string{ ++ "staging synthesis ", ++ "invalid staging synthesis ", ++ "claim verification ", ++ "staging source authority ", ++ "staging evidence diversity ", ++ "no active, goal-relevant ", ++ "staging quality gate ", ++ } { ++ if strings.HasPrefix(m, prefix) { ++ return true ++ } ++ } ++ return strings.Contains(m, "source-unverified identifiers") ++} ++ + func (e *Engine) collectGoalDraftEvidence(goal *core.Goal, cfg StagingPublisherConfig) []draftEvidence { + if goal == nil { + return nil +@@ -412,7 +479,7 @@ func (e *Engine) synthesizeGoalDraft(ctx context.Context, goal *core.Goal, evide + Text: "Automatisch recherchiertes Evidence-Bundle. Keine Artikelsynthese; menschliche Prüfung ist zwingend erforderlich.\n\n" + evidencePack, + Answer: answer, Categories: []string{"Research", goal.Title}, Keywords: goalKeywords(goal), MinScore: .85, + IntegrationKey: "neuroforge-goal:" + goal.ID, +- Quality: &stagingQualityMetadata{GateVersion: "staging-v2", AuthoritativeSources: auth, IndependentOrigins: origins, SourceAudit: audit}, ++ Quality: &stagingQualityMetadata{GateVersion: stagingQualityGateVersion, AuthoritativeSources: auth, IndependentOrigins: origins, SourceAudit: audit}, + }, nil + } + if cfg.SynthesisMode != "llm" { +@@ -473,7 +540,7 @@ func (e *Engine) synthesizeGoalDraft(ctx context.Context, goal *core.Goal, evide + return stagingDraftPayload{}, err + } + auth, origins, audit := summarizeEvidenceAuthority(cfg, evidence) +- draft.Quality = &stagingQualityMetadata{GateVersion: "staging-v2", AuthoritativeSources: auth, IndependentOrigins: origins, SourceAudit: audit} ++ draft.Quality = &stagingQualityMetadata{GateVersion: stagingQualityGateVersion, AuthoritativeSources: auth, IndependentOrigins: origins, SourceAudit: audit} + if !cfg.VerifyClaims { + return draft, nil + } +@@ -485,7 +552,7 @@ func (e *Engine) synthesizeGoalDraft(ctx context.Context, goal *core.Goal, evide + repairedReport, secondErr := e.verifyDraftClaims(ctx, goal, evidence, repairedDraft) + if secondErr == nil { + repairedReport.RepairApplied = true +- repairedDraft.Quality = &stagingQualityMetadata{GateVersion: "staging-v2", AuthoritativeSources: auth, IndependentOrigins: origins, SourceAudit: audit, Verification: &repairedReport} ++ repairedDraft.Quality = &stagingQualityMetadata{GateVersion: stagingQualityGateVersion, AuthoritativeSources: auth, IndependentOrigins: origins, SourceAudit: audit, Verification: &repairedReport} + return repairedDraft, nil + } + verifyErr = fmt.Errorf("%v; grounded repair verification failed: %w", verifyErr, secondErr) +diff --git a/platform/neuroforge/internal/core/types.go b/platform/neuroforge/internal/core/types.go +index ce7d682..5cd39f3 100644 +--- a/platform/neuroforge/internal/core/types.go ++++ b/platform/neuroforge/internal/core/types.go +@@ -523,36 +523,39 @@ type MaintenanceStatus struct { + } + + type Goal struct { +- ID string `json:"id"` +- Title string `json:"title"` +- Description string `json:"description"` +- Status string `json:"status"` +- Priority int `json:"priority"` +- Progress float64 `json:"progress"` +- Target string `json:"target,omitempty"` +- Prediction string `json:"prediction,omitempty"` +- NextAction string `json:"next_action,omitempty"` +- LastEvaluation float64 `json:"last_evaluation,omitempty"` +- ProgressReason string `json:"progress_reason,omitempty"` +- ResearchEvidence int `json:"research_evidence,omitempty"` +- ResearchSources int `json:"research_sources,omitempty"` +- ResearchCorroborations int `json:"research_corroborations,omitempty"` +- ResearchSourceIDs []string `json:"research_source_ids,omitempty"` +- StagingDraftsCreated int `json:"staging_drafts_created,omitempty"` +- StagingDraftsUpdated int `json:"staging_drafts_updated,omitempty"` +- LastStagingDraftID string `json:"last_staging_draft_id,omitempty"` +- LastStagingError string `json:"last_staging_error,omitempty"` +- MemoryIDs []string `json:"memory_ids,omitempty"` +- Tags []string `json:"tags,omitempty"` +- CreatedAt time.Time `json:"created_at"` +- UpdatedAt time.Time `json:"updated_at"` +- LastCycleAt time.Time `json:"last_cycle_at,omitempty"` +- AutoRun bool `json:"auto_run"` +- IntervalMinutes int `json:"interval_minutes,omitempty"` +- NextCycleAt time.Time `json:"next_cycle_at,omitempty"` +- ResearchEnabled bool `json:"research_enabled"` +- ConsecutiveErrors int `json:"consecutive_errors,omitempty"` +- LastError string `json:"last_error,omitempty"` ++ ID string `json:"id"` ++ Title string `json:"title"` ++ Description string `json:"description"` ++ Status string `json:"status"` ++ Priority int `json:"priority"` ++ Progress float64 `json:"progress"` ++ Target string `json:"target,omitempty"` ++ Prediction string `json:"prediction,omitempty"` ++ NextAction string `json:"next_action,omitempty"` ++ LastEvaluation float64 `json:"last_evaluation,omitempty"` ++ ProgressReason string `json:"progress_reason,omitempty"` ++ ResearchEvidence int `json:"research_evidence,omitempty"` ++ ResearchSources int `json:"research_sources,omitempty"` ++ ResearchCorroborations int `json:"research_corroborations,omitempty"` ++ ResearchSourceIDs []string `json:"research_source_ids,omitempty"` ++ StagingDraftsCreated int `json:"staging_drafts_created,omitempty"` ++ StagingDraftsUpdated int `json:"staging_drafts_updated,omitempty"` ++ LastStagingDraftID string `json:"last_staging_draft_id,omitempty"` ++ LastStagingError string `json:"last_staging_error,omitempty"` ++ StagingDraftValidated bool `json:"staging_draft_validated,omitempty"` ++ StagingQualityGateVersion string `json:"staging_quality_gate_version,omitempty"` ++ LastStagingAttemptSignature string `json:"last_staging_attempt_signature,omitempty"` ++ MemoryIDs []string `json:"memory_ids,omitempty"` ++ Tags []string `json:"tags,omitempty"` ++ CreatedAt time.Time `json:"created_at"` ++ UpdatedAt time.Time `json:"updated_at"` ++ LastCycleAt time.Time `json:"last_cycle_at,omitempty"` ++ AutoRun bool `json:"auto_run"` ++ IntervalMinutes int `json:"interval_minutes,omitempty"` ++ NextCycleAt time.Time `json:"next_cycle_at,omitempty"` ++ ResearchEnabled bool `json:"research_enabled"` ++ ConsecutiveErrors int `json:"consecutive_errors,omitempty"` ++ LastError string `json:"last_error,omitempty"` + } + + type LearningCycle struct { +diff --git a/platform/neuroforge/openapi.yaml b/platform/neuroforge/openapi.yaml +index e914aca..031fc51 100644 +--- a/platform/neuroforge/openapi.yaml ++++ b/platform/neuroforge/openapi.yaml +@@ -456,6 +456,12 @@ components: + type: string + last_staging_error: + type: string ++ staging_draft_validated: ++ type: boolean ++ staging_quality_gate_version: ++ type: string ++ last_staging_attempt_signature: ++ type: string + memory_ids: + type: array + items: diff --git a/platform/neuroforge/internal/brain/goal_progress.go b/platform/neuroforge/internal/brain/goal_progress.go index 0dfa763..9fee91a 100644 --- a/platform/neuroforge/internal/brain/goal_progress.go +++ b/platform/neuroforge/internal/brain/goal_progress.go @@ -112,7 +112,10 @@ func (e *Engine) refreshGoalResearchProgress(goal *core.Goal, evaluation float64 if n, err := strconv.Atoi(m[1]); err == nil && n > 0 { current, label := goal.ResearchEvidence, "quellengebundene Evidenzen" if strings.Contains(target, "artikel") || strings.Contains(target, "article") || strings.Contains(target, "draft") || strings.Contains(target, "entwurf") { - current, label = goal.StagingDraftsCreated, "Staging-Artikel" + current, label = 0, "validierte Staging-Artikel" + if goal.LastStagingDraftID != "" && goal.StagingDraftValidated && goal.StagingQualityGateVersion == stagingQualityGateVersion { + current = 1 + } } else { evidenceTarget := strings.Contains(target, "wissensein") || strings.Contains(target, "evidenz") || strings.Contains(target, "claim") || strings.Contains(target, "eintr") if !evidenceTarget && (strings.Contains(target, "quelle") || strings.Contains(target, "source")) { diff --git a/platform/neuroforge/internal/brain/goal_progress_test.go b/platform/neuroforge/internal/brain/goal_progress_test.go index 63f4ad2..2ad18ff 100644 --- a/platform/neuroforge/internal/brain/goal_progress_test.go +++ b/platform/neuroforge/internal/brain/goal_progress_test.go @@ -286,17 +286,17 @@ func TestResearchMaterialRelevanceRejectsOffTopicWebRTCForNVIDIA(t *testing.T) { } } -func TestGoalArticleTargetUsesCreatedStagingArticles(t *testing.T) { +func TestGoalArticleTargetUsesCurrentValidatedStagingDraft(t *testing.T) { s, err := store.New(t.TempDir()) if err != nil { t.Fatal(err) } defer s.Close() - g := &core.Goal{ID: "goal-articles", Title: "NVIDIA", Target: "20 hochwertige Wissensartikel", StagingDraftsCreated: 1} + g := &core.Goal{ID: "goal-articles", Title: "NVIDIA", Target: "20 hochwertige Wissensartikel", StagingDraftsCreated: 1, LastStagingDraftID: "KB-1", StagingDraftValidated: true, StagingQualityGateVersion: stagingQualityGateVersion} e := &Engine{store: s} e.refreshGoalResearchProgress(g, 0) - if g.Progress < .049 || g.Progress > .051 || !strings.Contains(g.ProgressReason, "1/20 Staging-Artikel") { - t.Fatalf("article target must count articles, got progress=%f reason=%q", g.Progress, g.ProgressReason) + if g.Progress < .049 || g.Progress > .051 || !strings.Contains(g.ProgressReason, "1/20 validierte Staging-Artikel") { + t.Fatalf("article target must count only a current validated draft, got progress=%f reason=%q", g.Progress, g.ProgressReason) } } @@ -630,3 +630,92 @@ func TestAuthoritativeDomainConfigurationRejectsOverbroadValues(t *testing.T) { t.Fatalf("unsafe authority domains were not sanitized: %#v", cfg.AuthoritativeDomains) } } + +func TestStagingRetriesOldQualityFailureOnceAfterGateUpgrade(t *testing.T) { + requests := 0 + kb := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests++ + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{"staging": map[string]any{"key": "KB-AI-STAGING-OLD", "meta": map[string]any{"integration_action": "updated"}}}) + })) + defer kb.Close() + + s, err := store.New(t.TempDir()) + if err != nil { + t.Fatal(err) + } + defer s.Close() + src := &core.KnowledgeSource{ID: "src-1", Type: "web", Title: "Fortinet 7200", URI: "https://community.fortinet.com/fortigate/7200", Trust: .9, Status: "ready"} + if err := s.UpsertSource(src); err != nil { + t.Fatal(err) + } + mem := &core.Memory{ID: "mem-1", Kind: "evidence", MemoryType: core.MemorySemantic, Text: "FortiClient SSLVPN error 7200 troubleshooting evidence.", Confidence: .8, Status: core.MemoryActive, Provenance: core.MemoryProvenance{Source: "web.page", GoalID: "goal-1", SourceID: src.ID}} + if err := s.AddMemory(mem); err != nil { + t.Fatal(err) + } + run, err := s.StartResearchRun("goal-1", "FortiClient SSLVPN 7200") + if err != nil { + t.Fatal(err) + } + _, _ = s.FinishResearchRun(run.ID, "completed", "") + + e := &Engine{store: s, http: kb.Client()} + e.ConfigureStagingPublisher(StagingPublisherConfig{Enabled: true, URL: kb.URL, Token: "secret", MinEvidence: 1, MinSources: 1, SynthesisMode: "evidence"}) + g := &core.Goal{ + ID: "goal-1", Title: "FortiClient SSLVPN 7200", ResearchEvidence: 1, ResearchSources: 1, + LastStagingDraftID: "KB-AI-STAGING-OLD", + LastStagingError: "staging synthesis introduced source-unverified identifiers: /portal-konfiguration", + } + + e.maybePublishGoalDraft(context.Background(), g, ResearchResult{RunID: run.ID}) + if requests != 1 || g.LastStagingError != "" || !g.StagingDraftValidated || g.StagingQualityGateVersion != stagingQualityGateVersion { + t.Fatalf("old quality failure was not revalidated: requests=%d goal=%#v", requests, g) + } + + // The same unchanged, already validated evidence must not be synthesized again. + e.maybePublishGoalDraft(context.Background(), g, ResearchResult{RunID: run.ID}) + if requests != 1 { + t.Fatalf("validated unchanged draft was republished: requests=%d", requests) + } +} + +func TestArticleProgressRequiresCurrentValidatedDraft(t *testing.T) { + s, err := store.New(t.TempDir()) + if err != nil { + t.Fatal(err) + } + defer s.Close() + e := &Engine{store: s} + g := &core.Goal{ID: "g1", Title: "Test", Target: "1 hochwertiger Wissensartikel", LastStagingDraftID: "KB-OLD", StagingDraftsCreated: 1} + + e.refreshGoalResearchProgress(g, 0) + if g.Progress != 0 || !strings.Contains(g.ProgressReason, "0/1 validierte Staging-Artikel") { + t.Fatalf("unvalidated legacy draft must not satisfy article target: %#v", g) + } + + g.StagingDraftValidated = true + g.StagingQualityGateVersion = stagingQualityGateVersion + e.refreshGoalResearchProgress(g, 0) + if g.Progress != 1 || !strings.Contains(g.ProgressReason, "1/1 validierte Staging-Artikel") { + t.Fatalf("current validated draft should satisfy target: %#v", g) + } +} + +func TestStagingBelowThresholdInvalidatesLegacyDraftAndReplacesStaleError(t *testing.T) { + s, err := store.New(t.TempDir()) + if err != nil { + t.Fatal(err) + } + defer s.Close() + e := &Engine{store: s} + e.ConfigureStagingPublisher(StagingPublisherConfig{Enabled: true, URL: "http://knowledge.invalid", Token: "secret", MinEvidence: 4, MinSources: 2}) + g := &core.Goal{ + ID: "g1", LastStagingDraftID: "KB-OLD", StagingDraftValidated: true, + StagingQualityGateVersion: "staging-v2", + LastStagingError: "staging synthesis introduced source-unverified identifiers: /portal-konfiguration", + } + e.maybePublishGoalDraft(context.Background(), g, ResearchResult{}) + if g.StagingDraftValidated || g.StagingQualityGateVersion != stagingQualityGateVersion || !strings.Contains(g.LastStagingError, "quality gate not satisfied") { + t.Fatalf("legacy draft state was not invalidated/reconciled: %#v", g) + } +} diff --git a/platform/neuroforge/internal/brain/staging.go b/platform/neuroforge/internal/brain/staging.go index fc82db1..4ae981e 100644 --- a/platform/neuroforge/internal/brain/staging.go +++ b/platform/neuroforge/internal/brain/staging.go @@ -3,6 +3,7 @@ package brain import ( "bytes" "context" + "crypto/sha256" "encoding/json" "errors" "fmt" @@ -14,6 +15,8 @@ import ( "neuroforge/internal/core" ) +const stagingQualityGateVersion = "staging-v3" + // StagingPublisherConfig configures the one-way governance bridge from // autonomous research into the human-review knowledge staging area. type StagingPublisherConfig struct { @@ -120,25 +123,25 @@ func (e *Engine) maybePublishGoalDraft(ctx context.Context, goal *core.Goal, res return } if strings.TrimSpace(cfg.URL) == "" || strings.TrimSpace(cfg.Token) == "" { + goal.StagingDraftValidated = false + goal.StagingQualityGateVersion = stagingQualityGateVersion goal.LastStagingError = "staging publisher enabled but URL/token is missing" _ = e.store.AddKnowledgeEvent(core.KnowledgeEvent{Type: "staging.publish_error", Summary: "Research draft could not be published", Reason: goal.LastStagingError, Actor: "goal-learning", Metadata: map[string]string{"goal_id": goal.ID}}) return } - if goal.LastStagingDraftID != "" && research.RunID != "" { - if run, ok := e.store.GetResearchRun(research.RunID); ok && run.Stats.NewEvidence == 0 && run.Stats.Corroborations == 0 { - // Avoid rewriting the same active staging draft every scheduler tick when - // this cycle contributed no new information. - return - } - } if goal.ResearchEvidence < cfg.MinEvidence || goal.ResearchSources < cfg.MinSources || goal.ResearchCorroborations < cfg.MinCorroborations { - goal.LastStagingError = "" - _ = e.store.AddKnowledgeEvent(core.KnowledgeEvent{Type: "staging.not_ready", Summary: "Research has not reached staging quality gate", Reason: fmt.Sprintf("evidence=%d/%d sources=%d/%d corroborations=%d/%d", goal.ResearchEvidence, cfg.MinEvidence, goal.ResearchSources, cfg.MinSources, goal.ResearchCorroborations, cfg.MinCorroborations), Actor: "goal-learning", Metadata: map[string]string{"goal_id": goal.ID}}) + goal.StagingDraftValidated = false + goal.StagingQualityGateVersion = stagingQualityGateVersion + goal.LastStagingAttemptSignature = "" + goal.LastStagingError = fmt.Sprintf("staging quality gate not satisfied: evidence=%d/%d sources=%d/%d corroborations=%d/%d", goal.ResearchEvidence, cfg.MinEvidence, goal.ResearchSources, cfg.MinSources, goal.ResearchCorroborations, cfg.MinCorroborations) + _ = e.store.AddKnowledgeEvent(core.KnowledgeEvent{Type: "staging.not_ready", Summary: "Research has not reached staging quality gate", Reason: goal.LastStagingError, Actor: "goal-learning", Metadata: map[string]string{"goal_id": goal.ID}}) return } evidence := e.collectGoalDraftEvidence(goal, cfg) if len(evidence) == 0 { + goal.StagingDraftValidated = false + goal.StagingQualityGateVersion = stagingQualityGateVersion goal.LastStagingError = "no active, goal-relevant source-backed evidence available for staging" return } @@ -158,15 +161,39 @@ func (e *Engine) maybePublishGoalDraft(ctx context.Context, goal *core.Goal, res } } if len(selectedSources) < cfg.MinSources { + goal.StagingDraftValidated = false + goal.StagingQualityGateVersion = stagingQualityGateVersion goal.LastStagingError = fmt.Sprintf("staging evidence diversity below threshold: relevant_sources=%d/%d", len(selectedSources), cfg.MinSources) return } authoritativeSources, independentOrigins, sourceAudit := summarizeEvidenceAuthority(cfg, evidence) if cfg.RequireAuthoritativeSource && authoritativeSources < cfg.MinAuthoritativeSources { + goal.StagingDraftValidated = false + goal.StagingQualityGateVersion = stagingQualityGateVersion goal.LastStagingError = fmt.Sprintf("staging source authority below threshold: authoritative_sources=%d/%d", authoritativeSources, cfg.MinAuthoritativeSources) _ = e.store.AddKnowledgeEvent(core.KnowledgeEvent{Type: "staging.not_ready", Summary: "Research draft lacks authoritative sources", Reason: goal.LastStagingError, Actor: "goal-learning", Metadata: map[string]string{"goal_id": goal.ID}}) return } + + attemptSignature := stagingEvidenceSignature(evidence) + noNewInformation := false + if research.RunID != "" { + if run, ok := e.store.GetResearchRun(research.RunID); ok { + noNewInformation = run.Stats.NewEvidence == 0 && run.Stats.Corroborations == 0 + } + } + if goal.LastStagingDraftID != "" && noNewInformation { + if goal.StagingDraftValidated && goal.StagingQualityGateVersion == stagingQualityGateVersion && strings.TrimSpace(goal.LastStagingError) == "" { + return + } + if goal.StagingQualityGateVersion == stagingQualityGateVersion && goal.LastStagingAttemptSignature == attemptSignature && deterministicStagingFailure(goal.LastStagingError) { + return + } + } + goal.StagingDraftValidated = false + goal.StagingQualityGateVersion = stagingQualityGateVersion + goal.LastStagingAttemptSignature = attemptSignature + draft, err := e.synthesizeGoalDraft(ctx, goal, evidence) if err != nil { goal.LastStagingError = err.Error() @@ -205,7 +232,7 @@ func (e *Engine) maybePublishGoalDraft(ctx context.Context, goal *core.Goal, res "research_evidence_ids": evidenceIDs, "research_source_uris": sourceURIs, "source_authority": sourceAudit, - "quality_gate_version": "staging-v2", + "quality_gate_version": stagingQualityGateVersion, "human_review_required": true, } if draft.Quality != nil && draft.Quality.Verification != nil { @@ -245,9 +272,49 @@ func (e *Engine) maybePublishGoalDraft(ctx context.Context, goal *core.Goal, res } goal.LastStagingDraftID = out.Staging.Key goal.LastStagingError = "" + goal.StagingDraftValidated = true + goal.StagingQualityGateVersion = stagingQualityGateVersion + goal.LastStagingAttemptSignature = attemptSignature _ = e.store.AddKnowledgeEvent(core.KnowledgeEvent{Type: "staging.draft_" + firstNonEmpty(action, "created"), Summary: "Research proposal sent to human-review staging", Reason: "research quality gate satisfied", Actor: "goal-learning", Metadata: map[string]string{"goal_id": goal.ID, "staging_id": out.Staging.Key, "action": action}}) } +func stagingEvidenceSignature(evidence []draftEvidence) string { + parts := make([]string, 0, len(evidence)*2+1) + parts = append(parts, stagingQualityGateVersion) + for _, ev := range evidence { + parts = append(parts, ev.Memory.ID, ev.Memory.Provenance.SourceID) + for _, src := range ev.CorroboratingSources { + if src != nil { + parts = append(parts, src.ID) + } + } + } + sort.Strings(parts[1:]) + sum := sha256.Sum256([]byte(strings.Join(parts, "\x00"))) + return fmt.Sprintf("%s:%x", stagingQualityGateVersion, sum[:12]) +} + +func deterministicStagingFailure(message string) bool { + m := strings.ToLower(strings.TrimSpace(message)) + if m == "" { + return false + } + for _, prefix := range []string{ + "staging synthesis ", + "invalid staging synthesis ", + "claim verification ", + "staging source authority ", + "staging evidence diversity ", + "no active, goal-relevant ", + "staging quality gate ", + } { + if strings.HasPrefix(m, prefix) { + return true + } + } + return strings.Contains(m, "source-unverified identifiers") +} + func (e *Engine) collectGoalDraftEvidence(goal *core.Goal, cfg StagingPublisherConfig) []draftEvidence { if goal == nil { return nil @@ -412,7 +479,7 @@ func (e *Engine) synthesizeGoalDraft(ctx context.Context, goal *core.Goal, evide Text: "Automatisch recherchiertes Evidence-Bundle. Keine Artikelsynthese; menschliche Prüfung ist zwingend erforderlich.\n\n" + evidencePack, Answer: answer, Categories: []string{"Research", goal.Title}, Keywords: goalKeywords(goal), MinScore: .85, IntegrationKey: "neuroforge-goal:" + goal.ID, - Quality: &stagingQualityMetadata{GateVersion: "staging-v2", AuthoritativeSources: auth, IndependentOrigins: origins, SourceAudit: audit}, + Quality: &stagingQualityMetadata{GateVersion: stagingQualityGateVersion, AuthoritativeSources: auth, IndependentOrigins: origins, SourceAudit: audit}, }, nil } if cfg.SynthesisMode != "llm" { @@ -473,7 +540,7 @@ func (e *Engine) synthesizeGoalDraft(ctx context.Context, goal *core.Goal, evide return stagingDraftPayload{}, err } auth, origins, audit := summarizeEvidenceAuthority(cfg, evidence) - draft.Quality = &stagingQualityMetadata{GateVersion: "staging-v2", AuthoritativeSources: auth, IndependentOrigins: origins, SourceAudit: audit} + draft.Quality = &stagingQualityMetadata{GateVersion: stagingQualityGateVersion, AuthoritativeSources: auth, IndependentOrigins: origins, SourceAudit: audit} if !cfg.VerifyClaims { return draft, nil } @@ -485,7 +552,7 @@ func (e *Engine) synthesizeGoalDraft(ctx context.Context, goal *core.Goal, evide repairedReport, secondErr := e.verifyDraftClaims(ctx, goal, evidence, repairedDraft) if secondErr == nil { repairedReport.RepairApplied = true - repairedDraft.Quality = &stagingQualityMetadata{GateVersion: "staging-v2", AuthoritativeSources: auth, IndependentOrigins: origins, SourceAudit: audit, Verification: &repairedReport} + repairedDraft.Quality = &stagingQualityMetadata{GateVersion: stagingQualityGateVersion, AuthoritativeSources: auth, IndependentOrigins: origins, SourceAudit: audit, Verification: &repairedReport} return repairedDraft, nil } verifyErr = fmt.Errorf("%v; grounded repair verification failed: %w", verifyErr, secondErr) diff --git a/platform/neuroforge/internal/core/types.go b/platform/neuroforge/internal/core/types.go index ce7d682..5cd39f3 100644 --- a/platform/neuroforge/internal/core/types.go +++ b/platform/neuroforge/internal/core/types.go @@ -523,36 +523,39 @@ type MaintenanceStatus struct { } type Goal struct { - ID string `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - Status string `json:"status"` - Priority int `json:"priority"` - Progress float64 `json:"progress"` - Target string `json:"target,omitempty"` - Prediction string `json:"prediction,omitempty"` - NextAction string `json:"next_action,omitempty"` - LastEvaluation float64 `json:"last_evaluation,omitempty"` - ProgressReason string `json:"progress_reason,omitempty"` - ResearchEvidence int `json:"research_evidence,omitempty"` - ResearchSources int `json:"research_sources,omitempty"` - ResearchCorroborations int `json:"research_corroborations,omitempty"` - ResearchSourceIDs []string `json:"research_source_ids,omitempty"` - StagingDraftsCreated int `json:"staging_drafts_created,omitempty"` - StagingDraftsUpdated int `json:"staging_drafts_updated,omitempty"` - LastStagingDraftID string `json:"last_staging_draft_id,omitempty"` - LastStagingError string `json:"last_staging_error,omitempty"` - MemoryIDs []string `json:"memory_ids,omitempty"` - Tags []string `json:"tags,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - LastCycleAt time.Time `json:"last_cycle_at,omitempty"` - AutoRun bool `json:"auto_run"` - IntervalMinutes int `json:"interval_minutes,omitempty"` - NextCycleAt time.Time `json:"next_cycle_at,omitempty"` - ResearchEnabled bool `json:"research_enabled"` - ConsecutiveErrors int `json:"consecutive_errors,omitempty"` - LastError string `json:"last_error,omitempty"` + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Status string `json:"status"` + Priority int `json:"priority"` + Progress float64 `json:"progress"` + Target string `json:"target,omitempty"` + Prediction string `json:"prediction,omitempty"` + NextAction string `json:"next_action,omitempty"` + LastEvaluation float64 `json:"last_evaluation,omitempty"` + ProgressReason string `json:"progress_reason,omitempty"` + ResearchEvidence int `json:"research_evidence,omitempty"` + ResearchSources int `json:"research_sources,omitempty"` + ResearchCorroborations int `json:"research_corroborations,omitempty"` + ResearchSourceIDs []string `json:"research_source_ids,omitempty"` + StagingDraftsCreated int `json:"staging_drafts_created,omitempty"` + StagingDraftsUpdated int `json:"staging_drafts_updated,omitempty"` + LastStagingDraftID string `json:"last_staging_draft_id,omitempty"` + LastStagingError string `json:"last_staging_error,omitempty"` + StagingDraftValidated bool `json:"staging_draft_validated,omitempty"` + StagingQualityGateVersion string `json:"staging_quality_gate_version,omitempty"` + LastStagingAttemptSignature string `json:"last_staging_attempt_signature,omitempty"` + MemoryIDs []string `json:"memory_ids,omitempty"` + Tags []string `json:"tags,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LastCycleAt time.Time `json:"last_cycle_at,omitempty"` + AutoRun bool `json:"auto_run"` + IntervalMinutes int `json:"interval_minutes,omitempty"` + NextCycleAt time.Time `json:"next_cycle_at,omitempty"` + ResearchEnabled bool `json:"research_enabled"` + ConsecutiveErrors int `json:"consecutive_errors,omitempty"` + LastError string `json:"last_error,omitempty"` } type LearningCycle struct { diff --git a/platform/neuroforge/openapi.yaml b/platform/neuroforge/openapi.yaml index e914aca..031fc51 100644 --- a/platform/neuroforge/openapi.yaml +++ b/platform/neuroforge/openapi.yaml @@ -456,6 +456,12 @@ components: type: string last_staging_error: type: string + staging_draft_validated: + type: boolean + staging_quality_gate_version: + type: string + last_staging_attempt_signature: + type: string memory_ids: type: array items: