This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
13
README.md
13
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
16
docs/MIGRATION-v1.5.7-to-v1.5.8.md
Normal file
16
docs/MIGRATION-v1.5.7-to-v1.5.8.md
Normal file
@@ -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`.
|
||||
20
docs/RELEASE-v1.5.8.md
Normal file
20
docs/RELEASE-v1.5.8.md
Normal file
@@ -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.
|
||||
@@ -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
|
||||
|
||||
525
patches/v1.5.7-to-v1.5.8.diff
Normal file
525
patches/v1.5.7-to-v1.5.8.diff
Normal file
@@ -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:
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user