This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
###############################################################################
|
||||
# GLPI NEUROFORGE MEGA v1.5.2 - VOLLSTÄNDIGE .ENV.example
|
||||
# GLPI NEUROFORGE MEGA v1.5.3 - 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.2
|
||||
IMAGE_TAG=1.5.3
|
||||
|
||||
CONTROL_HOST_PORT=8070
|
||||
AGENT_HOST_PORT=8080
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
27dc46be5cbb1b171deff7fbd2f28bff1be802dff403797535fd8968bb98c8eb ./.cbmignore
|
||||
db413beb17cc2a71be57ba39bdeeab1f380a51c806177b9df39881bb77f2d05f ./.env.example
|
||||
effc9563ca91cf7356ede825a2f6e58cfb3ab7982db7747f5cddc71cc7071f15 ./.env.example
|
||||
ed22fda7661db8203563611dc144998161cd024e161b0471d615eaf0defeb7db ./.gitea/workflows/release-tag.yml
|
||||
e1ff71187cc3411a85067b964264011db7bd109a585ef7b9ea5b08bda039d813 ./.gitignore
|
||||
ccfc4c139345a69d05cd2b809b0d09b4332d98e5b4f79aace4db0b648e3ea814 ./Makefile
|
||||
d3951624cd67488d07866d712a4294c6f5aac52b6e5c8e994a3fe179fd05a129 ./README.md
|
||||
f15ee2669a665104420b0bfd3219912f291272f06074c196d8bfa2b0031d2e5b ./README.md
|
||||
4858caa52c0fb6cf302a1c581d07d448e5e90e0daa797e5819610fd6223bd348 ./RELEASE-NOTES-v1.1.0.md
|
||||
01163462f46314f57660677fdef407c6c2884412ea850aab13a4f650e8c29f50 ./RELEASE-NOTES-v1.2.0.md
|
||||
4da388ce660aa3b7a0d8075ec066a025b5437960973397360fcb9a5d4cb58c96 ./RELEASE-NOTES-v1.3.0.md
|
||||
@@ -14,11 +14,11 @@ d3951624cd67488d07866d712a4294c6f5aac52b6e5c8e994a3fe179fd05a129 ./README.md
|
||||
61ceebe5a891388336795fda2fd0d1ad10373b4ebaae7d2e670f35766115c604 ./RELEASE-NOTES-v1.4.4.md
|
||||
15a3defdd5bbf07ebaec1a2e8626347a027a3622f10afb508f7a21c9b0bdfa71 ./RELEASE-NOTES-v1.4.5.md
|
||||
d10b1dbd87585d72144d816449800dc17b6d7f8faba2dfb4eca93ae3def2c631 ./RELEASE-NOTES-v1.5.0.md
|
||||
ded1b601e81d31ca1f65c4f3ccf2ebd7992b6390e37bc2a86f3837282f1e00cf ./VERSION
|
||||
c03e3824001f39b9518249b8d6522422fc4de7227e12a568c0164662b03b7758 ./VERSION
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ./backups/.gitkeep
|
||||
8127e9db5e5e0af1d88770dc8fa60b381de45dbcc843262698cf9501409b4d58 ./deploy/searxng/settings.yml
|
||||
3e153ad540d6cb1b20275775f2d7c129ec0216764cc9d0d27b963c634f347b43 ./docker-compose.dev.yml
|
||||
dcda7c439002ec6ff4254f38dbf5ee21bb39cc4b2fd74c6c37d6173531211c75 ./docker-compose.yml
|
||||
fb70b76fe7f599334aba85d9bf10c89ae065e4979127cbe313bf158740a216c0 ./docker-compose.yml
|
||||
0f2adaa0765ff00d9c3a1133840c7a768a011f9d8f820f8a340b43600c7649d8 ./docs/ARCHITECTURE.md
|
||||
34e2c7ac5992389cc51b40f740c1e206f20bffc67bdf19cb59d94b5df67a7082 ./docs/BACKUP-RESTORE-v1.5.0.md
|
||||
9467a3c0796a87bada0eab6ad191913e628b4abc05520cbb4b73fac76d334481 ./docs/CODEBASE-MEMORY-MCP.md
|
||||
@@ -38,12 +38,14 @@ b285d050223844f5fd05014c2278199eaccd7525fbbf8a6dae724ed87ced0ee8 ./docs/MIGRATI
|
||||
57858f4f2b6670106edf5a88ce705649d547c65c26407d6117040296122a0e26 ./docs/MIGRATION-v1.4.5-to-v1.5.0.md
|
||||
c905084e03788887894e37c040492b8ce2386ecc25ac3ab2e10bfd4b0a7cd167 ./docs/MIGRATION-v1.5.0-to-v1.5.1.md
|
||||
12619874bce206ed6e817685d2def62244d6bcceb6eeccbca9b0936fed74b8a2 ./docs/MIGRATION-v1.5.1-to-v1.5.2.md
|
||||
44219c5fe32a0d6d2c32719e243793a54f3b7624cb45a497cda78c4a443774ae ./docs/MIGRATION-v1.5.2-to-v1.5.3.md
|
||||
2a01fb10a3e04eae1800a7c7e0aafc31e9bbb23584cea54d849b004716ef81b4 ./docs/OBSIDIAN-EXPORT.md
|
||||
040010a807178d33797106e04716822f8d147d7d5833c1ba70865a1827f8484a ./docs/OPERATIONS.md
|
||||
69ea49bc76690ac489aea908f4e784a14293e92d62478a240eabea53c5f90820 ./docs/QUALITY-REPLAY-example.json
|
||||
bd2d3c43a09a89fefd0e844433064d7d7384a414179a754c6de94232d738fb04 ./docs/QUALITY-REPLAY.md
|
||||
6d72b49b5b0ce93b94597cf5688403c468cd3b82f4d87470d80f108c5a7ec531 ./docs/RELEASE-v1.5.1.md
|
||||
84b12cafd6320d781b99970cb48b7252da5aff94797d45af164f8a56dee94e5c ./docs/RELEASE-v1.5.2.md
|
||||
f0ab4fa6b353059acf6b41a1e5a484ebd698684102724eddf8f8b91d7ff321a3 ./docs/RELEASE-v1.5.3.md
|
||||
be749a09ddbcd4cf427316c6fe531f138e9bfaa6c104f4c11a08f7555231e8b2 ./docs/UNIFIED-GRAPH.md
|
||||
77d91565660789c621b05bd69fecfa3c2ef736f93855eca1f58b017e61c76f90 ./docs/VALIDATION-v1.5.0.md
|
||||
3ea1953b2b4eb8f966310a65a12a681fb44e4eb3857b843312e235759aacfce5 ./docs/VALIDATION.md
|
||||
@@ -153,8 +155,8 @@ cb3cde795233c0d4842dc7451c5855d1c071e459efbde3be0cabac32a837ba1b ./knowledge/14
|
||||
7f1d67faf4a6cea0c41c84d7b275d8a8979b19420b52db4925d8e2cd71ead3e0 ./knowledge/16_office-aktivierung-und-lizenzierung.json
|
||||
f1eab883370e0a40ef52a6b6d785a510d8ed48a19d25e0a2bc95f4f2cc8e329e ./knowledge/17_serienbriefe-und-dokumentfunktionen.json
|
||||
5a0b3d5d5bc712e30a67a4de3432f69070f7e43dd99931f675e72c213006b363 ./knowledge/example-vpn.json
|
||||
d6626ebcf2bf97ecfca640489ea589dc96ed0c7cfa6d41dbab1a3008d04fd34e ./mega-project.json
|
||||
565568c083ac68e28de7ce4d17625666bc009b8a2fb6bbcf8380c0cdb5ee2e20 ./patches/SHA256SUMS
|
||||
4e80435353ee4aaf577886eae6fb118e3a3b657b0f19e362f3f59a586a683a40 ./mega-project.json
|
||||
e96578cd54f549b8931dc389dd63477a2cd70a31f1dfbbd5bc75525b97633309 ./patches/SHA256SUMS
|
||||
47a6fa2c79bbba0c04af86dfa65d58529f492c698060fe586456c22a4eadb877 ./patches/glpi-agent-mega.diff
|
||||
9b411c90d96a86c80f088ee4637046eeefaf31c62059d11aa1a999d6eb08b5b4 ./patches/glpi-knowledge-mega.diff
|
||||
f0491de3cb6201f98ca6be8e865237adbba4772471f7fc7165d030e0c045fdb6 ./patches/neuroforge-mega.diff
|
||||
@@ -168,6 +170,7 @@ d90be604acf6ecf5d821890d5864307853caf0e0678a3249eeaa1d29748a6ada ./patches/v1.4
|
||||
01ba8cb6ea6ab0abd84295fb969fcfb93d841cd46ae1813fb0de9b23b2d5b22e ./patches/v1.4.4-to-v1.4.5.diff
|
||||
617b434dcdc0dd0be5cdb15740661f0a513b330b99835364b508e21467f86ae4 ./patches/v1.4.5-to-v1.5.0.diff
|
||||
b031ea42356a022d7bb03e7e2bda23c4ceddb66ccc59a5d1f0ebe787a790b829 ./patches/v1.5.1-to-v1.5.2.diff
|
||||
5c799db6701dce8019aeead5bccef6fc5fdc869e30785853eaae8d0eaf7ea43b ./patches/v1.5.2-to-v1.5.3.diff
|
||||
564817f8edabde0c4e4a1a427a3aa5418aae7bf12e9463044a7e6e0f13973657 ./platform/neuroforge/.env.example
|
||||
39319b6f2058e4c8d6656a9cf01675374f81a04075b956b093a2befb5e05ada4 ./platform/neuroforge/.gitignore
|
||||
189486a885c7fb78e0eb878d93cda0c70ca6d7ff9bfdfb3f5f32487cf03a9688 ./platform/neuroforge/BENCHMARK-v0.5.0.md
|
||||
@@ -208,12 +211,12 @@ fc993dc95fa49802ecb62994e4140dff18a27438e8a4f3c6352229c79b041710 ./platform/neu
|
||||
995aa439cd162029b3e545d9270b3951285771553021ab3474256d59bee9d933 ./platform/neuroforge/internal/brain/brain.go
|
||||
976288422c0c4116d8c98af8a9b164ac670f9caf03eddc2d5e2e48747456d3b4 ./platform/neuroforge/internal/brain/consolidation_test.go
|
||||
7bea749edea9bfbe8cca9515aa05bc4d44eb6285a6e20a5b7276a73e55fab646 ./platform/neuroforge/internal/brain/goal_progress.go
|
||||
5418ac804bfc517a2e143536e8a8b0ae2a393576f58ad5dac5fe98287c3c5462 ./platform/neuroforge/internal/brain/goal_progress_test.go
|
||||
14731dde4df09f25dc677914db00570acc85e4b67a93de76df56587c51076d6f ./platform/neuroforge/internal/brain/goal_progress_test.go
|
||||
359955653c647125559afd6dc3ebe69aa5ca19ff7e825ce801b7bc24e5fbcfcb ./platform/neuroforge/internal/brain/policy.go
|
||||
27e87af473d2d71ba94ffb9bf7a70934776f8c23ce45496ca0998ad3000fc156 ./platform/neuroforge/internal/brain/policy_test.go
|
||||
367e4964aee4c3b7dcda6a01ad105b3b909d102c1899b34b567144b9fcbac22d ./platform/neuroforge/internal/brain/research_quality.go
|
||||
9022a797aa1d49af20ced5d8c3e0b903be740fbaa7251e8a80d43cd04d2f68fe ./platform/neuroforge/internal/brain/research_quality.go
|
||||
0a3c8f7d149e814e091595982dbaa69467f6bf11eec631471d133a9b21585ae4 ./platform/neuroforge/internal/brain/research_trace.go
|
||||
0a2518c8a87406372dfed6093080945d785ef55a3bc2705d8ac22ad26a7c01da ./platform/neuroforge/internal/brain/staging.go
|
||||
f4f6ef6f643ab35c117ac2e4346a1292e9a47e2501d469602b7105d5574a5b9f ./platform/neuroforge/internal/brain/staging.go
|
||||
d4fed8b68d31f6fbd1992bde76abd4e7b2f9d211e53bf8b234369a6a53692fe8 ./platform/neuroforge/internal/brain/v3.go
|
||||
3ae13251512ecad1423a33ce09889f961130fefaed9342170b2d5cc6b3b51893 ./platform/neuroforge/internal/brain/v3_test.go
|
||||
4bc58463b659bd7e51db4c7dbeba053de90fcb41f392a7d6e62a8cd84ddaa092 ./platform/neuroforge/internal/brain/v4.go
|
||||
@@ -227,8 +230,9 @@ cf6e43d8daa0461ec38dc725197aa851332caa466bf9e0414c02fa88c446e064 ./platform/neu
|
||||
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
|
||||
eb2b8c599e4e0838d920296391ecaacdba1578c4f3a6a0a9e8469fdba31217ed ./platform/neuroforge/internal/httpapi/goal_duplicate_test.go
|
||||
1e2cbeecce139a32d4a12137dea59d5071ed9dbebd26a2d9933c1737ae6a851a ./platform/neuroforge/internal/httpapi/httpapi.go
|
||||
668eec30df3bc92fccbdacbd77effeab985189480fb39c48e3af0832e352de24 ./platform/neuroforge/internal/httpapi/index.html
|
||||
680b7fdcb20ac76c9305dfb94e4ec7474526be7a44b56afb3db7e4a66c8e2c38 ./platform/neuroforge/internal/httpapi/index.html
|
||||
20f4a6cc30d5ce84ceed4fbdfc9f5c57ab5f82274e0c54464817d571e0e881b0 ./platform/neuroforge/internal/httpapi/integration.go
|
||||
c346dd0205548f7cdb00ea026f1afd82d59f87824eb983ebe84abfa7cb078974 ./platform/neuroforge/internal/httpapi/integration_api_test.go
|
||||
f8cfcc7a2bc781394231c25e36e75c41563f5250a1387e5687880d2591efd93b ./platform/neuroforge/internal/httpapi/integration_graph.go
|
||||
@@ -244,7 +248,7 @@ c40412c77dd8635bbd4bf9c6d7eb11b0d7902aee896bdd1ee99e5cdfb72c7453 ./platform/neu
|
||||
b3b1f5cfd6b0ec04341978898d72856af03a95b8922daf9864caeb27e37f1925 ./platform/neuroforge/internal/httpapi/research_live.go
|
||||
b37f7ec3be867666999a9ec314139dd1bc4c74efb53edfa224b4fe8119858aac ./platform/neuroforge/internal/httpapi/research_live_test.go
|
||||
4e65ab0a760369a6d6c4932c180ff6c67f0768d91e08ccf471f53c6ecd5d09be ./platform/neuroforge/internal/httpapi/status_dashboard_test.go
|
||||
cccfebd3fdac137cc7334b7059c8ecb53951056499e5b8895f2efa38584178ee ./platform/neuroforge/internal/httpapi/v3.go
|
||||
3eb47f1b666336efe952961c19a1f46a7475acff8fe4dd7a59ade000aa1b9466 ./platform/neuroforge/internal/httpapi/v3.go
|
||||
b1587e066b56ef72f17162583698614d31c413e2ac7e75359b731c2e882a50ee ./platform/neuroforge/internal/httpapi/v4.go
|
||||
1ef5cacb0652f2a6301c57f8987dd848fbe590abb73cab44899d9152ccce45d0 ./platform/neuroforge/internal/httpapi/v5.go
|
||||
1d534726d7a75f21f7ad28a959f73a3402c021f7f4a1b554f0600cf3abf669d1 ./platform/neuroforge/internal/httpapi/v6.go
|
||||
@@ -262,6 +266,7 @@ e890897c662b070997c3b18134bafbee5bf9b3fa5220045ac09dc6e3dfd4ac41 ./platform/neu
|
||||
8e7a029a904ca1c22821c5e4871c72958f0d2645786871d75f3e00c186b00778 ./platform/neuroforge/internal/store/cluster.go
|
||||
6637e541f61a58f624baca31d972095ea01a95491ddb30492d9ac055740213c5 ./platform/neuroforge/internal/store/diskann.go
|
||||
1858ed8045b16282de815a6668ff6027c7a3a3795e38659c753c504d0f2a8383 ./platform/neuroforge/internal/store/diskann_test.go
|
||||
366e3b2f29b4abb1997aceb9c2b934ebe8f4ee44605b54167c70b8ddad683c7c ./platform/neuroforge/internal/store/goal_duplicate_test.go
|
||||
d49925fda3cbe3ceeffde3369073bce5a5bc67a23defb6827e27cd4a7f4c59f5 ./platform/neuroforge/internal/store/index_segments.go
|
||||
478efaeb260cb5e970af8b3bb99f232ef846f54c6e4ff9deb5499df6e008e1c1 ./platform/neuroforge/internal/store/index_segments_test.go
|
||||
bea84ff9bd7812249ada186b97be58766a37d319f4d986252a0fd43e399a3dc2 ./platform/neuroforge/internal/store/knowledge.go
|
||||
@@ -282,7 +287,7 @@ ea968456538367ed536c212707209a173098ec65823152a0813422bc8071c952 ./platform/neu
|
||||
61cd82b106e74f2aa0a7591d757a0872491a635dc1fccc78c66d41c052d0c9d2 ./platform/neuroforge/internal/store/sqar_vector.go
|
||||
d8f9d45c6675fdd6dbc04b8c1ca5173c2e013024676ccab3a6594572f30a51d7 ./platform/neuroforge/internal/store/store.go
|
||||
f86b648e8b4845ab83e672ae3fcfb0bcb0dc1bf891abf4bd30d380304f0b259a ./platform/neuroforge/internal/store/tiering.go
|
||||
55f8e410258a37b4da1175753342017b94481a77142f9015a8b1f9a99b34d36a ./platform/neuroforge/internal/store/v3.go
|
||||
2138e467981a3c814d1e88842dfbd470b76333898614543cafb4f93ea8b9beaa ./platform/neuroforge/internal/store/v3.go
|
||||
091e4c9cacfc76786a76f6afc4eb2b697e5a641e611475b6dab97e99371360ea ./platform/neuroforge/internal/store/v3_test.go
|
||||
7f0bca5419a895258b7921e940c3fd69cce04dedcb0b9d8f1127cd24b9c9e201 ./platform/neuroforge/internal/store/v5_test.go
|
||||
b3a0115907719a8a7aa9e6b244eec55be71a2535ba20db65907987d5fb78dc77 ./platform/neuroforge/internal/store/vector_journal.go
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# GLPI NeuroForge Mega v1.5.2
|
||||
# GLPI NeuroForge Mega v1.5.3
|
||||
|
||||
> Release: **v1.5.2** · Research-Query-Hardening mit kompakten deterministischen Suchanfragen und automatischem SearXNG-Kategorie-Fallback.
|
||||
> Release: **v1.5.3** · Research-Relevanz-Hardening, nachvollziehbare Draft-Belegzahlen und Schutz vor doppelten aktiven Goals.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ services:
|
||||
cap_drop:
|
||||
- ALL
|
||||
neuroforge:
|
||||
image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.2}
|
||||
image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.3}
|
||||
command:
|
||||
- -data
|
||||
- /app/data
|
||||
@@ -99,7 +99,7 @@ services:
|
||||
start_period: 15s
|
||||
stop_grace_period: 35s
|
||||
neuroforge-worker:
|
||||
image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.2}
|
||||
image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.3}
|
||||
command:
|
||||
- -server
|
||||
- http://neuroforge:8080
|
||||
@@ -119,7 +119,7 @@ services:
|
||||
cap_drop:
|
||||
- ALL
|
||||
agent-data-init:
|
||||
image: git.send.nrw/sendnrw/glpi-neuroforge-mega-agent-data-init:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.2}
|
||||
image: git.send.nrw/sendnrw/glpi-neuroforge-mega-agent-data-init:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.3}
|
||||
restart: 'no'
|
||||
user: 0:0
|
||||
volumes:
|
||||
@@ -132,7 +132,7 @@ services:
|
||||
- CHOWN
|
||||
- FOWNER
|
||||
agent:
|
||||
image: git.send.nrw/sendnrw/glpi-neuroforge-mega-agent:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.2}
|
||||
image: git.send.nrw/sendnrw/glpi-neuroforge-mega-agent:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.3}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
AI_CONTENT_LABEL_ENABLED: ${AI_CONTENT_LABEL_ENABLED:-}
|
||||
@@ -353,7 +353,7 @@ services:
|
||||
start_period: 10s
|
||||
stop_grace_period: 20s
|
||||
knowledge:
|
||||
image: git.send.nrw/sendnrw/glpi-neuroforge-mega-knowledge:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.2}
|
||||
image: git.send.nrw/sendnrw/glpi-neuroforge-mega-knowledge:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.3}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
APP_MODE: ${KB_APP_MODE:-editor}
|
||||
@@ -394,7 +394,7 @@ services:
|
||||
- ALL
|
||||
stop_grace_period: 35s
|
||||
control:
|
||||
image: git.send.nrw/sendnrw/glpi-neuroforge-mega-control:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.2}
|
||||
image: git.send.nrw/sendnrw/glpi-neuroforge-mega-control:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.3}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
CONTROL_ADDR: :8070
|
||||
|
||||
13
docs/MIGRATION-v1.5.2-to-v1.5.3.md
Normal file
13
docs/MIGRATION-v1.5.2-to-v1.5.3.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Migration v1.5.2 → v1.5.3
|
||||
|
||||
v1.5.3 is a drop-in quality-gate update. Persistent volumes and existing secrets are retained.
|
||||
|
||||
1. Build/publish the v1.5.3 images through the normal Gitea pipeline.
|
||||
2. Set `IMAGE_TAG=1.5.3`.
|
||||
3. Run `docker compose --profile research pull`.
|
||||
4. Recreate NeuroForge and its worker with `docker compose --profile research up -d --force-recreate neuroforge neuroforge-worker`.
|
||||
5. Remove/reject any previously generated staging draft whose sources are not actually relevant to the goal. The upgrade intentionally does not delete human-review artifacts automatically.
|
||||
6. If duplicate active goals already exist, keep the desired goal and delete the duplicate through the UI/API. New identical active goals are rejected with HTTP 409.
|
||||
7. Re-run the narrow FortiClient SSLVPN 7200 test. Sources that merely contain `sslvpn`, or a longer identifier containing `7200` as a substring, must be rejected before evidence ingestion.
|
||||
|
||||
Expected audit behavior: staging `research_evidence` and `research_sources` match the actual `research_evidence_ids` / `research_source_uris` used for the draft; aggregate goal totals are available in the new `research_goal_*` fields.
|
||||
21
docs/RELEASE-v1.5.3.md
Normal file
21
docs/RELEASE-v1.5.3.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# GLPI NeuroForge Mega v1.5.3
|
||||
|
||||
v1.5.3 hardens the research-quality gate after a live FortiClient SSLVPN 7200 test exposed two remaining failure modes: generic Docker Hub pages could pass relevance because a single goal anchor or a numeric substring was sufficient, and two identical active goals could create two independent staging drafts because their integration keys correctly differ by goal ID.
|
||||
|
||||
## Fixes
|
||||
|
||||
- Goal relevance is token-aware instead of unrestricted substring matching.
|
||||
- Numeric anchors such as error code `7200` must occur as exact normalized tokens; values such as `72007bf3` no longer match.
|
||||
- A goal containing a numeric/error-code anchor also requires at least one lexical product/topic anchor.
|
||||
- Multi-anchor nonnumeric goals require at least two independent title anchors instead of any single generic overlap.
|
||||
- `SSLVPN` still matches source wording such as `SSL VPN` through bounded adjacent-token compaction.
|
||||
- Identical non-completed goal fingerprints (title + description + target) are rejected atomically by the store; the HTTP API returns `409 Conflict`.
|
||||
- The Goal UI disables the create button while the POST is in flight, closing the common double-click path in addition to the backend guard.
|
||||
- Staging metadata now distinguishes the evidence actually supplied to the synthesizer from aggregate goal history: `research_evidence` and `research_sources` describe the selected draft inputs, while `research_goal_evidence` / `research_goal_sources` preserve the totals.
|
||||
- Regression coverage includes the observed Docker Hub false positives and duplicate-goal creation.
|
||||
|
||||
## Validation
|
||||
|
||||
NeuroForge passes its complete test suite after the changes. The full Mega release gate additionally covers test/vet/build for all four Go modules, targeted race checks, static Compose preflight and secret scanning before packaging.
|
||||
|
||||
Existing bad staging drafts are not deleted automatically. Human-review content remains operator-controlled; delete/reject drafts that were generated by older relevance rules.
|
||||
@@ -28,7 +28,7 @@
|
||||
"schema": "Wiki/Schema.md",
|
||||
"glpi_relations": "KnowbaseItem_Item when exposed by GLPI OpenAPI"
|
||||
},
|
||||
"version": "1.5.2",
|
||||
"version": "1.5.3",
|
||||
"controlled_learning": {
|
||||
"raw_chat_auto_learning": false,
|
||||
"validated_outcomes": [
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
47a6fa2c79bbba0c04af86dfa65d58529f492c698060fe586456c22a4eadb877 patches/glpi-agent-mega.diff
|
||||
9b411c90d96a86c80f088ee4637046eeefaf31c62059d11aa1a999d6eb08b5b4 patches/glpi-knowledge-mega.diff
|
||||
f0491de3cb6201f98ca6be8e865237adbba4772471f7fc7165d030e0c045fdb6 patches/neuroforge-mega.diff
|
||||
576e75ca9191bbef7f136abde90c10a1c6bf2e36450a421fd3796e988bd2af00 patches/v1.1.0-to-v1.2.0.diff
|
||||
7a5e0ae1d09b268d3ac62a51f92bfedf6771be26b909605a347f26d47d6fa8cb patches/v1.2.0-to-v1.3.0.diff
|
||||
163a8a990aacd986bacb385494c2b2062ac10a9a8f0a456d19a6d1746c2fc500 patches/v1.3.0-to-v1.4.0.diff
|
||||
654de4aac7cf318680872897c3de192afbafb61c7169acb8e7514116df86efde patches/v1.4.0-envfix-to-v1.4.1.diff
|
||||
1ebc306bd6870389500c86cf07dcd677b5bd0448c762812c7e1c0f8732f36eb1 patches/v1.4.1-to-v1.4.2.diff
|
||||
651c883facddfd0316f89e3cf3c24fd913371fcec2a5f447b8d105fa5dec463e patches/v1.4.2-to-v1.4.3.diff
|
||||
d90be604acf6ecf5d821890d5864307853caf0e0678a3249eeaa1d29748a6ada patches/v1.4.3-to-v1.4.4.diff
|
||||
01ba8cb6ea6ab0abd84295fb969fcfb93d841cd46ae1813fb0de9b23b2d5b22e patches/v1.4.4-to-v1.4.5.diff
|
||||
617b434dcdc0dd0be5cdb15740661f0a513b330b99835364b508e21467f86ae4 patches/v1.4.5-to-v1.5.0.diff
|
||||
b031ea42356a022d7bb03e7e2bda23c4ceddb66ccc59a5d1f0ebe787a790b829 patches/v1.5.1-to-v1.5.2.diff
|
||||
47a6fa2c79bbba0c04af86dfa65d58529f492c698060fe586456c22a4eadb877 glpi-agent-mega.diff
|
||||
9b411c90d96a86c80f088ee4637046eeefaf31c62059d11aa1a999d6eb08b5b4 glpi-knowledge-mega.diff
|
||||
f0491de3cb6201f98ca6be8e865237adbba4772471f7fc7165d030e0c045fdb6 neuroforge-mega.diff
|
||||
576e75ca9191bbef7f136abde90c10a1c6bf2e36450a421fd3796e988bd2af00 v1.1.0-to-v1.2.0.diff
|
||||
7a5e0ae1d09b268d3ac62a51f92bfedf6771be26b909605a347f26d47d6fa8cb v1.2.0-to-v1.3.0.diff
|
||||
163a8a990aacd986bacb385494c2b2062ac10a9a8f0a456d19a6d1746c2fc500 v1.3.0-to-v1.4.0.diff
|
||||
654de4aac7cf318680872897c3de192afbafb61c7169acb8e7514116df86efde v1.4.0-envfix-to-v1.4.1.diff
|
||||
1ebc306bd6870389500c86cf07dcd677b5bd0448c762812c7e1c0f8732f36eb1 v1.4.1-to-v1.4.2.diff
|
||||
651c883facddfd0316f89e3cf3c24fd913371fcec2a5f447b8d105fa5dec463e v1.4.2-to-v1.4.3.diff
|
||||
d90be604acf6ecf5d821890d5864307853caf0e0678a3249eeaa1d29748a6ada v1.4.3-to-v1.4.4.diff
|
||||
01ba8cb6ea6ab0abd84295fb969fcfb93d841cd46ae1813fb0de9b23b2d5b22e v1.4.4-to-v1.4.5.diff
|
||||
617b434dcdc0dd0be5cdb15740661f0a513b330b99835364b508e21467f86ae4 v1.4.5-to-v1.5.0.diff
|
||||
b031ea42356a022d7bb03e7e2bda23c4ceddb66ccc59a5d1f0ebe787a790b829 v1.5.1-to-v1.5.2.diff
|
||||
5c799db6701dce8019aeead5bccef6fc5fdc869e30785853eaae8d0eaf7ea43b v1.5.2-to-v1.5.3.diff
|
||||
|
||||
396
patches/v1.5.2-to-v1.5.3.diff
Normal file
396
patches/v1.5.2-to-v1.5.3.diff
Normal file
@@ -0,0 +1,396 @@
|
||||
diff --git a/.env.example b/.env.example
|
||||
index 29dccfe3cc0acceb42597be0217c2f7ddeae6752..3fd8d691392605c5b0971b3758c1d91286cadbe6 100644
|
||||
--- a/.env.example
|
||||
+++ b/.env.example
|
||||
@@ -1,5 +1,5 @@
|
||||
###############################################################################
|
||||
-# GLPI NEUROFORGE MEGA v1.5.2 - VOLLSTÄNDIGE .ENV.example
|
||||
+# GLPI NEUROFORGE MEGA v1.5.3 - 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.2
|
||||
+IMAGE_TAG=1.5.3
|
||||
|
||||
CONTROL_HOST_PORT=8070
|
||||
AGENT_HOST_PORT=8080
|
||||
diff --git a/README.md b/README.md
|
||||
index 432307f253bb5f4368a6da35e8209d1253d8e759..cd2ebb9bf588270fa3e5b02a12a8f96dc0091c16 100644
|
||||
--- a/README.md
|
||||
+++ b/README.md
|
||||
@@ -1,6 +1,6 @@
|
||||
-# GLPI NeuroForge Mega v1.5.2
|
||||
+# GLPI NeuroForge Mega v1.5.3
|
||||
|
||||
-> Release: **v1.5.2** · Research-Query-Hardening mit kompakten deterministischen Suchanfragen und automatischem SearXNG-Kategorie-Fallback.
|
||||
+> Release: **v1.5.3** · Research-Relevanz-Hardening, nachvollziehbare Draft-Belegzahlen und Schutz vor doppelten aktiven Goals.
|
||||
|
||||
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.
|
||||
|
||||
diff --git a/VERSION b/VERSION
|
||||
index 4cda8f19edc7ffa01ba13d5dbe4909a6dc2ce3c8..8af85beb5159909f7df13705826e8c937f2b050b 100644
|
||||
--- a/VERSION
|
||||
+++ b/VERSION
|
||||
@@ -1 +1 @@
|
||||
-1.5.2
|
||||
+1.5.3
|
||||
diff --git a/docker-compose.yml b/docker-compose.yml
|
||||
index 1e82ef5fff7ad747c8f335f457fc7e08d3b9e5ed..aca2e75cd7b856d95435ac477327313cd02a9ed9 100644
|
||||
--- a/docker-compose.yml
|
||||
+++ b/docker-compose.yml
|
||||
@@ -33,7 +33,7 @@ services:
|
||||
cap_drop:
|
||||
- ALL
|
||||
neuroforge:
|
||||
- image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.2}
|
||||
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.3}
|
||||
command:
|
||||
- -data
|
||||
- /app/data
|
||||
@@ -99,7 +99,7 @@ services:
|
||||
start_period: 15s
|
||||
stop_grace_period: 35s
|
||||
neuroforge-worker:
|
||||
- image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.2}
|
||||
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.3}
|
||||
command:
|
||||
- -server
|
||||
- http://neuroforge:8080
|
||||
@@ -119,7 +119,7 @@ services:
|
||||
cap_drop:
|
||||
- ALL
|
||||
agent-data-init:
|
||||
- image: git.send.nrw/sendnrw/glpi-neuroforge-mega-agent-data-init:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.2}
|
||||
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-agent-data-init:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.3}
|
||||
restart: 'no'
|
||||
user: 0:0
|
||||
volumes:
|
||||
@@ -132,7 +132,7 @@ services:
|
||||
- CHOWN
|
||||
- FOWNER
|
||||
agent:
|
||||
- image: git.send.nrw/sendnrw/glpi-neuroforge-mega-agent:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.2}
|
||||
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-agent:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.3}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
AI_CONTENT_LABEL_ENABLED: ${AI_CONTENT_LABEL_ENABLED:-}
|
||||
@@ -353,7 +353,7 @@ services:
|
||||
start_period: 10s
|
||||
stop_grace_period: 20s
|
||||
knowledge:
|
||||
- image: git.send.nrw/sendnrw/glpi-neuroforge-mega-knowledge:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.2}
|
||||
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-knowledge:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.3}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
APP_MODE: ${KB_APP_MODE:-editor}
|
||||
@@ -394,7 +394,7 @@ services:
|
||||
- ALL
|
||||
stop_grace_period: 35s
|
||||
control:
|
||||
- image: git.send.nrw/sendnrw/glpi-neuroforge-mega-control:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.2}
|
||||
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-control:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.5.3}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
CONTROL_ADDR: :8070
|
||||
diff --git a/mega-project.json b/mega-project.json
|
||||
index 9c840c1deb461108a879aba189bb844b0981a8ed..d12824a263e59d507a344f0751755775a2ba4b94 100644
|
||||
--- a/mega-project.json
|
||||
+++ b/mega-project.json
|
||||
@@ -28,7 +28,7 @@
|
||||
"schema": "Wiki/Schema.md",
|
||||
"glpi_relations": "KnowbaseItem_Item when exposed by GLPI OpenAPI"
|
||||
},
|
||||
- "version": "1.5.2",
|
||||
+ "version": "1.5.3",
|
||||
"controlled_learning": {
|
||||
"raw_chat_auto_learning": false,
|
||||
"validated_outcomes": [
|
||||
diff --git a/platform/neuroforge/internal/brain/goal_progress_test.go b/platform/neuroforge/internal/brain/goal_progress_test.go
|
||||
index 6c96722c8d50543db9820cf2284dcf87f76be854..19d5edb71c119233ff8390dd8a9214c179f9cb79 100644
|
||||
--- a/platform/neuroforge/internal/brain/goal_progress_test.go
|
||||
+++ b/platform/neuroforge/internal/brain/goal_progress_test.go
|
||||
@@ -76,6 +76,16 @@ func TestGoalResearchPublishesIdempotentHumanReviewDraft(t *testing.T) {
|
||||
if body["answer"] == "" {
|
||||
t.Fatalf("empty answer")
|
||||
}
|
||||
+ meta, ok := body["metadata"].(map[string]any)
|
||||
+ if !ok {
|
||||
+ t.Fatalf("missing metadata: %#v", body["metadata"])
|
||||
+ }
|
||||
+ if meta["research_evidence"] != float64(1) || meta["research_sources"] != float64(1) {
|
||||
+ t.Fatalf("draft counters must reflect selected evidence: %#v", meta)
|
||||
+ }
|
||||
+ if meta["research_goal_evidence"] != float64(13) || meta["research_goal_sources"] != float64(5) {
|
||||
+ t.Fatalf("goal totals must remain auditable: %#v", meta)
|
||||
+ }
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"staging": map[string]any{"key": "KB-AI-STAGING-1", "meta": map[string]any{"integration_action": "created"}}})
|
||||
}))
|
||||
@@ -103,7 +113,7 @@ func TestGoalResearchPublishesIdempotentHumanReviewDraft(t *testing.T) {
|
||||
|
||||
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: "NVIDIA", ResearchEvidence: 1, ResearchSources: 1, ResearchSourceIDs: []string{src.ID}}
|
||||
+ g := &core.Goal{ID: "goal-1", Title: "NVIDIA", ResearchEvidence: 13, ResearchSources: 5, ResearchSourceIDs: []string{src.ID}}
|
||||
e.maybePublishGoalDraft(context.Background(), g, ResearchResult{RunID: run.ID})
|
||||
if requests != 1 || g.StagingDraftsCreated != 1 || g.LastStagingDraftID == "" || g.LastStagingError != "" {
|
||||
t.Fatalf("goal=%#v requests=%d", g, requests)
|
||||
@@ -289,3 +299,45 @@ func TestGoalArticleTargetUsesCreatedStagingArticles(t *testing.T) {
|
||||
t.Fatalf("article target must count articles, got progress=%f reason=%q", g.Progress, g.ProgressReason)
|
||||
}
|
||||
}
|
||||
+
|
||||
+func TestResearchMaterialRelevanceRequiresExactErrorCodeAndSubjectAnchor(t *testing.T) {
|
||||
+ g := &core.Goal{Title: "FortiClient SSLVPN 7200"}
|
||||
+
|
||||
+ for _, tc := range []struct {
|
||||
+ name string
|
||||
+ parts []string
|
||||
+ relevant bool
|
||||
+ }{
|
||||
+ {
|
||||
+ name: "official fortinet title with exact code",
|
||||
+ parts: []string{"Troubleshooting Tip: FortiClient error 'Credentials or SSLVPN configuration is wrong. (-7200)'", "FortiClient SSL VPN authentication troubleshooting"},
|
||||
+ relevant: true,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "split ssl vpn plus exact code",
|
||||
+ parts: []string{"Technical Tip: Credential or SSL VPN configuration is wrong (-7200)", "SSL VPN authentication rule troubleshooting"},
|
||||
+ relevant: true,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "docker id only contains 7200 as substring",
|
||||
+ parts: []string{"Docker image", "https://hub.docker.com/r/cffork8s/72007bf3-214d-4f19-a618-f74b145ca3a5", "generic container image"},
|
||||
+ relevant: false,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "generic sslvpn without error code",
|
||||
+ parts: []string{"baselibrary/sslvpn", "VPN_TYPE=fortinet NET_ADMIN /dev/ppp"},
|
||||
+ relevant: false,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "exact code without product subject",
|
||||
+ parts: []string{"Build 7200 released", "unrelated software package"},
|
||||
+ relevant: false,
|
||||
+ },
|
||||
+ } {
|
||||
+ t.Run(tc.name, func(t *testing.T) {
|
||||
+ if got := researchMaterialRelevant(g, tc.parts...); got != tc.relevant {
|
||||
+ t.Fatalf("researchMaterialRelevant=%v want %v for %#v", got, tc.relevant, tc.parts)
|
||||
+ }
|
||||
+ })
|
||||
+ }
|
||||
+}
|
||||
diff --git a/platform/neuroforge/internal/brain/research_quality.go b/platform/neuroforge/internal/brain/research_quality.go
|
||||
index 328c000b75d270ace41b9170a2c76a39b870cb83..541a68d982368beef3eb2a7a755e87a451233a73 100644
|
||||
--- a/platform/neuroforge/internal/brain/research_quality.go
|
||||
+++ b/platform/neuroforge/internal/brain/research_quality.go
|
||||
@@ -66,13 +66,83 @@ func researchMaterialRelevant(goal *core.Goal, parts ...string) bool {
|
||||
if len(anchors) == 0 {
|
||||
return true
|
||||
}
|
||||
- haystack := strings.ToLower(strings.Join(parts, "\n"))
|
||||
- for _, tok := range anchors {
|
||||
- if strings.Contains(haystack, tok) {
|
||||
- return true
|
||||
+
|
||||
+ // Relevance is intentionally token-aware and requires coverage of more than
|
||||
+ // one anchor for multi-part goals. The previous substring-any rule allowed
|
||||
+ // false positives such as goal code "7200" matching a Docker repository id
|
||||
+ // beginning with "72007...", or a generic page containing only "sslvpn".
|
||||
+ // Numeric anchors (error codes, versions, model numbers) must match an exact
|
||||
+ // normalized token and, when a lexical anchor exists, be accompanied by at
|
||||
+ // least one subject/product anchor.
|
||||
+ words := normalizedResearchWords(parts...)
|
||||
+ wordSet := make(map[string]bool, len(words))
|
||||
+ for _, w := range words {
|
||||
+ wordSet[w] = true
|
||||
+ }
|
||||
+ // Also index compact adjacent word pairs/triples so an anchor such as
|
||||
+ // "sslvpn" matches source text written as "SSL VPN" without falling back to
|
||||
+ // unrestricted substring matching.
|
||||
+ compact := make(map[string]bool, len(words)*2)
|
||||
+ for i := range words {
|
||||
+ if i+1 < len(words) {
|
||||
+ compact[words[i]+words[i+1]] = true
|
||||
+ }
|
||||
+ if i+2 < len(words) {
|
||||
+ compact[words[i]+words[i+1]+words[i+2]] = true
|
||||
}
|
||||
}
|
||||
- return false
|
||||
+
|
||||
+ lexicalTotal, lexicalMatches := 0, 0
|
||||
+ numericTotal, numericMatches := 0, 0
|
||||
+ for _, anchor := range anchors {
|
||||
+ if allDigits(anchor) {
|
||||
+ numericTotal++
|
||||
+ if wordSet[anchor] {
|
||||
+ numericMatches++
|
||||
+ }
|
||||
+ continue
|
||||
+ }
|
||||
+ lexicalTotal++
|
||||
+ if wordSet[anchor] || compact[anchor] {
|
||||
+ lexicalMatches++
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ if numericTotal > 0 {
|
||||
+ if numericMatches == 0 {
|
||||
+ return false
|
||||
+ }
|
||||
+ if lexicalTotal > 0 && lexicalMatches == 0 {
|
||||
+ return false
|
||||
+ }
|
||||
+ return true
|
||||
+ }
|
||||
+ if lexicalTotal <= 1 {
|
||||
+ return lexicalMatches == lexicalTotal
|
||||
+ }
|
||||
+ return lexicalMatches >= 2
|
||||
+}
|
||||
+
|
||||
+func normalizedResearchWords(parts ...string) []string {
|
||||
+ normalized := strings.Map(func(r rune) rune {
|
||||
+ if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||||
+ return unicode.ToLower(r)
|
||||
+ }
|
||||
+ return ' '
|
||||
+ }, strings.Join(parts, "\n"))
|
||||
+ return strings.Fields(normalized)
|
||||
+}
|
||||
+
|
||||
+func allDigits(s string) bool {
|
||||
+ if s == "" {
|
||||
+ return false
|
||||
+ }
|
||||
+ for _, r := range s {
|
||||
+ if !unicode.IsDigit(r) {
|
||||
+ return false
|
||||
+ }
|
||||
+ }
|
||||
+ return true
|
||||
}
|
||||
|
||||
func goalIDFromTags(tags []string) string {
|
||||
diff --git a/platform/neuroforge/internal/brain/staging.go b/platform/neuroforge/internal/brain/staging.go
|
||||
index 801b1ec8957a08392ffaff47a6f7a8f447d503f3..fcea5df063d7c2ec098aae09ec6ddc0f5f0c216f 100644
|
||||
--- a/platform/neuroforge/internal/brain/staging.go
|
||||
+++ b/platform/neuroforge/internal/brain/staging.go
|
||||
@@ -138,14 +138,19 @@ func (e *Engine) maybePublishGoalDraft(ctx context.Context, goal *core.Goal, res
|
||||
}
|
||||
}
|
||||
draft.Metadata = map[string]any{
|
||||
- "research_goal_id": goal.ID,
|
||||
- "research_run_id": research.RunID,
|
||||
- "research_evidence": goal.ResearchEvidence,
|
||||
- "research_sources": goal.ResearchSources,
|
||||
- "research_corroborations": goal.ResearchCorroborations,
|
||||
- "research_evidence_ids": evidenceIDs,
|
||||
- "research_source_uris": sourceURIs,
|
||||
- "human_review_required": true,
|
||||
+ "research_goal_id": goal.ID,
|
||||
+ "research_run_id": research.RunID,
|
||||
+ // Draft-level counters describe the evidence actually supplied to the
|
||||
+ // synthesizer. Goal totals are preserved separately for audit/history.
|
||||
+ "research_evidence": len(evidenceIDs),
|
||||
+ "research_sources": len(sourceURIs),
|
||||
+ "research_corroborations": goal.ResearchCorroborations,
|
||||
+ "research_goal_evidence": goal.ResearchEvidence,
|
||||
+ "research_goal_sources": goal.ResearchSources,
|
||||
+ "research_goal_corroborations": goal.ResearchCorroborations,
|
||||
+ "research_evidence_ids": evidenceIDs,
|
||||
+ "research_source_uris": sourceURIs,
|
||||
+ "human_review_required": true,
|
||||
}
|
||||
body, _ := json.Marshal(draft)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, cfg.URL, bytes.NewReader(body))
|
||||
diff --git a/platform/neuroforge/internal/httpapi/index.html b/platform/neuroforge/internal/httpapi/index.html
|
||||
index 3fc4fd742602c5cdc907c38edd371ddd3cf2987f..2909d9bef560a7a3fc623efdd97f0ac9cb2dd2d2 100644
|
||||
--- a/platform/neuroforge/internal/httpapi/index.html
|
||||
+++ b/platform/neuroforge/internal/httpapi/index.html
|
||||
@@ -226,7 +226,7 @@ function openGoalResearch(id,title){researchWatch.goalId=id;researchWatch.goalTi
|
||||
$('liveCloseBtn').addEventListener('click',()=>{$('goalResearchLive').classList.remove('show');stopResearchPolling()});$('liveRefreshBtn').addEventListener('click',()=>{researchWatch.after=0;researchWatch.events=[];researchWatch.runId='';pollGoalResearch();loadResearchHistory()});
|
||||
async function loadGoals(){try{let xs=await req('/api/v1/goals');xs=Array.isArray(xs)?xs:[];$('goalsList').innerHTML=xs.length?xs.map(g=>{let active=g.status==='active',paused=g.status==='paused';return `<div class="goal ${paused?'goalPaused':''}"><div class="goalHead"><div><h3>${esc(g.title)}</h3><div class="meta"><span>Priorität ${g.priority}</span><span>${g.research_enabled?'⌕ Research':'ohne Research'}</span><span>${g.auto_run?'AUTO':'MANUELL'}</span></div></div><span class="pill ${active?'good':paused?'warn':''}">${paused?'PAUSIERT':esc(g.status)}</span></div><p class="muted small">${esc(g.description||'')}</p><div class="progress"><span style="width:${Math.round((g.progress||0)*100)}%"></span></div><div class="meta"><span>${Math.round((g.progress||0)*100)}%</span><span>${esc(g.progress_reason||'Fortschritt noch nicht messbar')}</span><span>Intervall ${g.interval_minutes||'–'} min</span><span>${paused?'Scheduler angehalten':('Nächster: '+(g.next_cycle_at?when(g.next_cycle_at):'–'))}</span></div>${g.research_enabled?`<div class="meta"><span>${g.research_evidence||0} Evidenzen</span><span>${g.research_sources||0} Quellen</span><span>${g.research_corroborations||0} bestätigt</span>${g.last_staging_draft_id?`<span class="pill good">Staging ${esc(g.last_staging_draft_id)}</span>`:''}</div>`:''}${g.next_action?`<p><b>Nächste Aktion:</b> ${esc(g.next_action)}</p>`:''}${g.last_staging_error?`<p class="error small"><b>Staging:</b> ${esc(g.last_staging_error)}</p>`:''}${g.last_error?`<p class="error small">${esc(g.last_error)}</p>`:''}<div class="goalActions">${active?`<button class="btn secondary" data-goal-action="cycle" data-goal-id="${esc(g.id)}" data-goal-title="${esc(g.title)}">Jetzt lernen</button><button class="btn ghost" data-goal-action="pause" data-goal-id="${esc(g.id)}">Pausieren</button>`:''}${paused?`<button class="btn secondary" data-goal-action="resume" data-goal-id="${esc(g.id)}">Fortsetzen</button>`:''}${g.research_enabled?`<button class="btn ghost" data-research-live="${esc(g.id)}" data-goal-title="${esc(g.title)}">Live Research</button>`:''}<button class="btn ghost" data-focus="${esc(g.id)}">Graph</button><button class="btn danger" data-goal-action="delete" data-goal-id="${esc(g.id)}" data-goal-title="${esc(g.title)}">Löschen</button></div></div>`}).join(''):'<p class="muted">Noch keine Ziele.</p>';$$('[data-goal-action]').forEach(b=>b.addEventListener('click',()=>goalAction(b)));$$('[data-research-live]').forEach(b=>b.addEventListener('click',()=>openGoalResearch(b.dataset.researchLive,b.dataset.goalTitle)));$$('[data-focus]').forEach(b=>b.addEventListener('click',()=>{switchView('knowledge');$('knowledgeQuery').value='goal:'+b.dataset.focus;searchKnowledge()}))}catch(e){toast(e.message,true)}}
|
||||
async function goalAction(b){let id=b.dataset.goalId,action=b.dataset.goalAction;if(action==='delete'&&!confirm(`Ziel „${b.dataset.goalTitle||id}“ wirklich löschen? Bereits gelerntes Wissen bleibt erhalten.`))return;if(action==='cycle')openGoalResearch(id,b.dataset.goalTitle||id);let old=b.textContent;try{b.disabled=true;b.textContent=action==='cycle'?'läuft …':action==='delete'?'lösche …':'speichere …';let path='/api/v1/goals/'+encodeURIComponent(id)+(action==='cycle'?'/cycle':action==='pause'?'/pause':action==='resume'?'/resume':'');let d=await req(path,{method:action==='delete'?'DELETE':'POST',body:action==='delete'?undefined:'{}'});if(action==='cycle')toast(`Zyklus fertig · Evaluation ${Number(d.evaluation).toFixed(2)} · ${d.sources_ingested||0} Quellen`);else if(action==='pause')toast('Ziel pausiert. Der Scheduler verarbeitet es nicht weiter.');else if(action==='resume')toast('Ziel wieder aktiv. Nächster Lauf wurde eingeplant.');else{toast('Ziel gelöscht. Bereits gelerntes Wissen bleibt erhalten.');if(researchWatch.goalId===id){$('goalResearchLive').classList.remove('show');stopResearchPolling();researchWatch.goalId=''}}await loadGoals();refreshAll()}catch(e){toast(e.message,true)}finally{b.disabled=false;b.textContent=old}}
|
||||
-$('goalCreateBtn').addEventListener('click',async()=>{let title=$('goalTitle').value.trim();if(!title)return toast('Titel fehlt.',true);try{let g=await req('/api/v1/goals',{method:'POST',body:JSON.stringify({title,description:$('goalDesc').value,target:$('goalTarget').value,priority:Number($('goalPriority').value||50),interval_minutes:Number($('goalInterval').value||10),status:'active'})});toast(`Ziel ${g.title} angelegt – erster Lauf ist eingeplant.`);$('goalTitle').value='';$('goalDesc').value='';loadGoals();refreshAll()}catch(e){toast(e.message,true)}});$('runAutonomyBtn').addEventListener('click',async()=>{try{let d=await req('/admin/api/autonomy',{method:'POST',body:'{}'});toast(`${d.cycles?.length||0} fällige Zyklen ausgeführt.`);loadGoals()}catch(e){toast(e.message,true)}});
|
||||
+$('goalCreateBtn').addEventListener('click',async()=>{let title=$('goalTitle').value.trim();if(!title)return toast('Titel fehlt.',true);let b=$('goalCreateBtn'),old=b.textContent;try{b.disabled=true;b.textContent='lege an …';let g=await req('/api/v1/goals',{method:'POST',body:JSON.stringify({title,description:$('goalDesc').value,target:$('goalTarget').value,priority:Number($('goalPriority').value||50),interval_minutes:Number($('goalInterval').value||10),status:'active'})});toast(`Ziel ${g.title} angelegt – erster Lauf ist eingeplant.`);$('goalTitle').value='';$('goalDesc').value='';await loadGoals();refreshAll()}catch(e){toast(e.message,true)}finally{b.disabled=false;b.textContent=old}});$('runAutonomyBtn').addEventListener('click',async()=>{try{let d=await req('/admin/api/autonomy',{method:'POST',body:'{}'});toast(`${d.cycles?.length||0} fällige Zyklen ausgeführt.`);loadGoals()}catch(e){toast(e.message,true)}});
|
||||
|
||||
// ---- Chat ----
|
||||
$('chatBtn').addEventListener('click',async()=>{let input=document.getElementById('chatPrompt').value.trim();if(!input)return toast('Nachricht fehlt.',true);chatAbort=new AbortController();$('chatAbortBtn').disabled=false;$('chatBtn').disabled=true;let start=Date.now(),timer=setInterval(()=>$('chatElapsed').textContent=Math.round((Date.now()-start)/1000)+' s',1000);$('chatOutput').textContent='Ollama / NeuroForge arbeitet …';try{let r=await fetch('/api/v1/chat',{method:'POST',headers:tokenHeaders(),signal:chatAbort.signal,body:JSON.stringify({session_id:$('chatSession').value,input,provider:$('chatProvider').value})}),txt=await r.text(),d;try{d=JSON.parse(txt)}catch{d=txt}if(!r.ok)throw new Error(d.error||d);$('chatOutput').textContent=(d.answer||'')+'\n\n— '+(d.provider||'')+' · '+(d.model||'')+' · Recall '+(d.recalled?.length||0)+' · Reward '+Number(d.auto_reward||0).toFixed(3);refreshAll()}catch(e){$('chatOutput').textContent=e.name==='AbortError'?'Abgebrochen.':e.message}finally{clearInterval(timer);$('chatElapsed').textContent='';$('chatAbortBtn').disabled=true;$('chatBtn').disabled=false;chatAbort=null}});$('chatAbortBtn').addEventListener('click',()=>chatAbort?.abort());
|
||||
diff --git a/platform/neuroforge/internal/httpapi/v3.go b/platform/neuroforge/internal/httpapi/v3.go
|
||||
index 8c2b8b4f5edfd46f1e355a63bd81c622654f4cad..e01f17d73e493b205c609085fa57637007ba3bb3 100644
|
||||
--- a/platform/neuroforge/internal/httpapi/v3.go
|
||||
+++ b/platform/neuroforge/internal/httpapi/v3.go
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"neuroforge/internal/brain"
|
||||
"neuroforge/internal/core"
|
||||
+ "neuroforge/internal/store"
|
||||
)
|
||||
|
||||
func (s *Server) goalsList(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -22,6 +23,10 @@ func (s *Server) goalsCreate(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if err := s.store.UpsertGoal(&g); err != nil {
|
||||
+ if errors.Is(err, store.ErrDuplicateGoal) {
|
||||
+ s.err(w, http.StatusConflict, err)
|
||||
+ return
|
||||
+ }
|
||||
s.err(w, 400, err)
|
||||
return
|
||||
}
|
||||
diff --git a/platform/neuroforge/internal/store/v3.go b/platform/neuroforge/internal/store/v3.go
|
||||
index f0e1ceaf4d21e99616e4e4ab756f801463652cef..033be4b7ca8cf7dda411e18b62730dc0c1ca9ce6 100644
|
||||
--- a/platform/neuroforge/internal/store/v3.go
|
||||
+++ b/platform/neuroforge/internal/store/v3.go
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"neuroforge/internal/vector"
|
||||
)
|
||||
|
||||
+var ErrDuplicateGoal = errors.New("duplicate goal")
|
||||
+
|
||||
func (s *Store) resolveConflictLocked(in *core.Memory) []core.Memory {
|
||||
key := strings.TrimSpace(in.TruthKey)
|
||||
if key == "" {
|
||||
@@ -173,6 +175,17 @@ func (s *Store) UpsertGoal(g *core.Goal) error {
|
||||
defer s.mu.Unlock()
|
||||
now := time.Now().UTC()
|
||||
newGoal := g.ID == "" || s.state.Goals[g.ID] == nil
|
||||
+ if newGoal {
|
||||
+ fingerprint := goalFingerprint(g)
|
||||
+ for id, existing := range s.state.Goals {
|
||||
+ if existing == nil || existing.Status == core.GoalCompleted {
|
||||
+ continue
|
||||
+ }
|
||||
+ if goalFingerprint(existing) == fingerprint {
|
||||
+ return fmt.Errorf("%w: existing goal %s", ErrDuplicateGoal, id)
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
if g.ID == "" {
|
||||
g.ID = NewID("goal")
|
||||
}
|
||||
@@ -206,6 +219,16 @@ func (s *Store) UpsertGoal(g *core.Goal) error {
|
||||
return s.commitLocked("goal.upsert", cp)
|
||||
}
|
||||
|
||||
+func goalFingerprint(g *core.Goal) string {
|
||||
+ if g == nil {
|
||||
+ return ""
|
||||
+ }
|
||||
+ normalize := func(v string) string {
|
||||
+ return strings.ToLower(strings.Join(strings.Fields(strings.TrimSpace(v)), " "))
|
||||
+ }
|
||||
+ return normalize(g.Title) + "\x00" + normalize(g.Description) + "\x00" + normalize(g.Target)
|
||||
+}
|
||||
+
|
||||
func (s *Store) GetGoal(id string) (*core.Goal, bool) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
@@ -76,6 +76,16 @@ func TestGoalResearchPublishesIdempotentHumanReviewDraft(t *testing.T) {
|
||||
if body["answer"] == "" {
|
||||
t.Fatalf("empty answer")
|
||||
}
|
||||
meta, ok := body["metadata"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("missing metadata: %#v", body["metadata"])
|
||||
}
|
||||
if meta["research_evidence"] != float64(1) || meta["research_sources"] != float64(1) {
|
||||
t.Fatalf("draft counters must reflect selected evidence: %#v", meta)
|
||||
}
|
||||
if meta["research_goal_evidence"] != float64(13) || meta["research_goal_sources"] != float64(5) {
|
||||
t.Fatalf("goal totals must remain auditable: %#v", meta)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"staging": map[string]any{"key": "KB-AI-STAGING-1", "meta": map[string]any{"integration_action": "created"}}})
|
||||
}))
|
||||
@@ -103,7 +113,7 @@ func TestGoalResearchPublishesIdempotentHumanReviewDraft(t *testing.T) {
|
||||
|
||||
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: "NVIDIA", ResearchEvidence: 1, ResearchSources: 1, ResearchSourceIDs: []string{src.ID}}
|
||||
g := &core.Goal{ID: "goal-1", Title: "NVIDIA", ResearchEvidence: 13, ResearchSources: 5, ResearchSourceIDs: []string{src.ID}}
|
||||
e.maybePublishGoalDraft(context.Background(), g, ResearchResult{RunID: run.ID})
|
||||
if requests != 1 || g.StagingDraftsCreated != 1 || g.LastStagingDraftID == "" || g.LastStagingError != "" {
|
||||
t.Fatalf("goal=%#v requests=%d", g, requests)
|
||||
@@ -289,3 +299,45 @@ func TestGoalArticleTargetUsesCreatedStagingArticles(t *testing.T) {
|
||||
t.Fatalf("article target must count articles, got progress=%f reason=%q", g.Progress, g.ProgressReason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResearchMaterialRelevanceRequiresExactErrorCodeAndSubjectAnchor(t *testing.T) {
|
||||
g := &core.Goal{Title: "FortiClient SSLVPN 7200"}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
parts []string
|
||||
relevant bool
|
||||
}{
|
||||
{
|
||||
name: "official fortinet title with exact code",
|
||||
parts: []string{"Troubleshooting Tip: FortiClient error 'Credentials or SSLVPN configuration is wrong. (-7200)'", "FortiClient SSL VPN authentication troubleshooting"},
|
||||
relevant: true,
|
||||
},
|
||||
{
|
||||
name: "split ssl vpn plus exact code",
|
||||
parts: []string{"Technical Tip: Credential or SSL VPN configuration is wrong (-7200)", "SSL VPN authentication rule troubleshooting"},
|
||||
relevant: true,
|
||||
},
|
||||
{
|
||||
name: "docker id only contains 7200 as substring",
|
||||
parts: []string{"Docker image", "https://hub.docker.com/r/cffork8s/72007bf3-214d-4f19-a618-f74b145ca3a5", "generic container image"},
|
||||
relevant: false,
|
||||
},
|
||||
{
|
||||
name: "generic sslvpn without error code",
|
||||
parts: []string{"baselibrary/sslvpn", "VPN_TYPE=fortinet NET_ADMIN /dev/ppp"},
|
||||
relevant: false,
|
||||
},
|
||||
{
|
||||
name: "exact code without product subject",
|
||||
parts: []string{"Build 7200 released", "unrelated software package"},
|
||||
relevant: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := researchMaterialRelevant(g, tc.parts...); got != tc.relevant {
|
||||
t.Fatalf("researchMaterialRelevant=%v want %v for %#v", got, tc.relevant, tc.parts)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,13 +66,83 @@ func researchMaterialRelevant(goal *core.Goal, parts ...string) bool {
|
||||
if len(anchors) == 0 {
|
||||
return true
|
||||
}
|
||||
haystack := strings.ToLower(strings.Join(parts, "\n"))
|
||||
for _, tok := range anchors {
|
||||
if strings.Contains(haystack, tok) {
|
||||
return true
|
||||
|
||||
// Relevance is intentionally token-aware and requires coverage of more than
|
||||
// one anchor for multi-part goals. The previous substring-any rule allowed
|
||||
// false positives such as goal code "7200" matching a Docker repository id
|
||||
// beginning with "72007...", or a generic page containing only "sslvpn".
|
||||
// Numeric anchors (error codes, versions, model numbers) must match an exact
|
||||
// normalized token and, when a lexical anchor exists, be accompanied by at
|
||||
// least one subject/product anchor.
|
||||
words := normalizedResearchWords(parts...)
|
||||
wordSet := make(map[string]bool, len(words))
|
||||
for _, w := range words {
|
||||
wordSet[w] = true
|
||||
}
|
||||
// Also index compact adjacent word pairs/triples so an anchor such as
|
||||
// "sslvpn" matches source text written as "SSL VPN" without falling back to
|
||||
// unrestricted substring matching.
|
||||
compact := make(map[string]bool, len(words)*2)
|
||||
for i := range words {
|
||||
if i+1 < len(words) {
|
||||
compact[words[i]+words[i+1]] = true
|
||||
}
|
||||
if i+2 < len(words) {
|
||||
compact[words[i]+words[i+1]+words[i+2]] = true
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
lexicalTotal, lexicalMatches := 0, 0
|
||||
numericTotal, numericMatches := 0, 0
|
||||
for _, anchor := range anchors {
|
||||
if allDigits(anchor) {
|
||||
numericTotal++
|
||||
if wordSet[anchor] {
|
||||
numericMatches++
|
||||
}
|
||||
continue
|
||||
}
|
||||
lexicalTotal++
|
||||
if wordSet[anchor] || compact[anchor] {
|
||||
lexicalMatches++
|
||||
}
|
||||
}
|
||||
|
||||
if numericTotal > 0 {
|
||||
if numericMatches == 0 {
|
||||
return false
|
||||
}
|
||||
if lexicalTotal > 0 && lexicalMatches == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if lexicalTotal <= 1 {
|
||||
return lexicalMatches == lexicalTotal
|
||||
}
|
||||
return lexicalMatches >= 2
|
||||
}
|
||||
|
||||
func normalizedResearchWords(parts ...string) []string {
|
||||
normalized := strings.Map(func(r rune) rune {
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||||
return unicode.ToLower(r)
|
||||
}
|
||||
return ' '
|
||||
}, strings.Join(parts, "\n"))
|
||||
return strings.Fields(normalized)
|
||||
}
|
||||
|
||||
func allDigits(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range s {
|
||||
if !unicode.IsDigit(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func goalIDFromTags(tags []string) string {
|
||||
|
||||
@@ -138,14 +138,19 @@ func (e *Engine) maybePublishGoalDraft(ctx context.Context, goal *core.Goal, res
|
||||
}
|
||||
}
|
||||
draft.Metadata = map[string]any{
|
||||
"research_goal_id": goal.ID,
|
||||
"research_run_id": research.RunID,
|
||||
"research_evidence": goal.ResearchEvidence,
|
||||
"research_sources": goal.ResearchSources,
|
||||
"research_corroborations": goal.ResearchCorroborations,
|
||||
"research_evidence_ids": evidenceIDs,
|
||||
"research_source_uris": sourceURIs,
|
||||
"human_review_required": true,
|
||||
"research_goal_id": goal.ID,
|
||||
"research_run_id": research.RunID,
|
||||
// Draft-level counters describe the evidence actually supplied to the
|
||||
// synthesizer. Goal totals are preserved separately for audit/history.
|
||||
"research_evidence": len(evidenceIDs),
|
||||
"research_sources": len(sourceURIs),
|
||||
"research_corroborations": goal.ResearchCorroborations,
|
||||
"research_goal_evidence": goal.ResearchEvidence,
|
||||
"research_goal_sources": goal.ResearchSources,
|
||||
"research_goal_corroborations": goal.ResearchCorroborations,
|
||||
"research_evidence_ids": evidenceIDs,
|
||||
"research_source_uris": sourceURIs,
|
||||
"human_review_required": true,
|
||||
}
|
||||
body, _ := json.Marshal(draft)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, cfg.URL, bytes.NewReader(body))
|
||||
|
||||
28
platform/neuroforge/internal/httpapi/goal_duplicate_test.go
Normal file
28
platform/neuroforge/internal/httpapi/goal_duplicate_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGoalsCreateReturnsConflictForDuplicateActiveGoal(t *testing.T) {
|
||||
srv, _ := newMetricsTestServer(t)
|
||||
payload := `{"title":"FortiClient SSLVPN 7200","description":"Create one support article","target":"1 hochwertiger Wissensartikel","status":"active"}`
|
||||
|
||||
post := func() *httptest.ResponseRecorder {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/goals", strings.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+srv.store.Secrets().AppAPIKey)
|
||||
rr := httptest.NewRecorder()
|
||||
srv.Handler().ServeHTTP(rr, req)
|
||||
return rr
|
||||
}
|
||||
if rr := post(); rr.Code != http.StatusCreated {
|
||||
t.Fatalf("first create status=%d body=%s", rr.Code, rr.Body.String())
|
||||
}
|
||||
if rr := post(); rr.Code != http.StatusConflict {
|
||||
t.Fatalf("duplicate create status=%d body=%s", rr.Code, rr.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -226,7 +226,7 @@ function openGoalResearch(id,title){researchWatch.goalId=id;researchWatch.goalTi
|
||||
$('liveCloseBtn').addEventListener('click',()=>{$('goalResearchLive').classList.remove('show');stopResearchPolling()});$('liveRefreshBtn').addEventListener('click',()=>{researchWatch.after=0;researchWatch.events=[];researchWatch.runId='';pollGoalResearch();loadResearchHistory()});
|
||||
async function loadGoals(){try{let xs=await req('/api/v1/goals');xs=Array.isArray(xs)?xs:[];$('goalsList').innerHTML=xs.length?xs.map(g=>{let active=g.status==='active',paused=g.status==='paused';return `<div class="goal ${paused?'goalPaused':''}"><div class="goalHead"><div><h3>${esc(g.title)}</h3><div class="meta"><span>Priorität ${g.priority}</span><span>${g.research_enabled?'⌕ Research':'ohne Research'}</span><span>${g.auto_run?'AUTO':'MANUELL'}</span></div></div><span class="pill ${active?'good':paused?'warn':''}">${paused?'PAUSIERT':esc(g.status)}</span></div><p class="muted small">${esc(g.description||'')}</p><div class="progress"><span style="width:${Math.round((g.progress||0)*100)}%"></span></div><div class="meta"><span>${Math.round((g.progress||0)*100)}%</span><span>${esc(g.progress_reason||'Fortschritt noch nicht messbar')}</span><span>Intervall ${g.interval_minutes||'–'} min</span><span>${paused?'Scheduler angehalten':('Nächster: '+(g.next_cycle_at?when(g.next_cycle_at):'–'))}</span></div>${g.research_enabled?`<div class="meta"><span>${g.research_evidence||0} Evidenzen</span><span>${g.research_sources||0} Quellen</span><span>${g.research_corroborations||0} bestätigt</span>${g.last_staging_draft_id?`<span class="pill good">Staging ${esc(g.last_staging_draft_id)}</span>`:''}</div>`:''}${g.next_action?`<p><b>Nächste Aktion:</b> ${esc(g.next_action)}</p>`:''}${g.last_staging_error?`<p class="error small"><b>Staging:</b> ${esc(g.last_staging_error)}</p>`:''}${g.last_error?`<p class="error small">${esc(g.last_error)}</p>`:''}<div class="goalActions">${active?`<button class="btn secondary" data-goal-action="cycle" data-goal-id="${esc(g.id)}" data-goal-title="${esc(g.title)}">Jetzt lernen</button><button class="btn ghost" data-goal-action="pause" data-goal-id="${esc(g.id)}">Pausieren</button>`:''}${paused?`<button class="btn secondary" data-goal-action="resume" data-goal-id="${esc(g.id)}">Fortsetzen</button>`:''}${g.research_enabled?`<button class="btn ghost" data-research-live="${esc(g.id)}" data-goal-title="${esc(g.title)}">Live Research</button>`:''}<button class="btn ghost" data-focus="${esc(g.id)}">Graph</button><button class="btn danger" data-goal-action="delete" data-goal-id="${esc(g.id)}" data-goal-title="${esc(g.title)}">Löschen</button></div></div>`}).join(''):'<p class="muted">Noch keine Ziele.</p>';$$('[data-goal-action]').forEach(b=>b.addEventListener('click',()=>goalAction(b)));$$('[data-research-live]').forEach(b=>b.addEventListener('click',()=>openGoalResearch(b.dataset.researchLive,b.dataset.goalTitle)));$$('[data-focus]').forEach(b=>b.addEventListener('click',()=>{switchView('knowledge');$('knowledgeQuery').value='goal:'+b.dataset.focus;searchKnowledge()}))}catch(e){toast(e.message,true)}}
|
||||
async function goalAction(b){let id=b.dataset.goalId,action=b.dataset.goalAction;if(action==='delete'&&!confirm(`Ziel „${b.dataset.goalTitle||id}“ wirklich löschen? Bereits gelerntes Wissen bleibt erhalten.`))return;if(action==='cycle')openGoalResearch(id,b.dataset.goalTitle||id);let old=b.textContent;try{b.disabled=true;b.textContent=action==='cycle'?'läuft …':action==='delete'?'lösche …':'speichere …';let path='/api/v1/goals/'+encodeURIComponent(id)+(action==='cycle'?'/cycle':action==='pause'?'/pause':action==='resume'?'/resume':'');let d=await req(path,{method:action==='delete'?'DELETE':'POST',body:action==='delete'?undefined:'{}'});if(action==='cycle')toast(`Zyklus fertig · Evaluation ${Number(d.evaluation).toFixed(2)} · ${d.sources_ingested||0} Quellen`);else if(action==='pause')toast('Ziel pausiert. Der Scheduler verarbeitet es nicht weiter.');else if(action==='resume')toast('Ziel wieder aktiv. Nächster Lauf wurde eingeplant.');else{toast('Ziel gelöscht. Bereits gelerntes Wissen bleibt erhalten.');if(researchWatch.goalId===id){$('goalResearchLive').classList.remove('show');stopResearchPolling();researchWatch.goalId=''}}await loadGoals();refreshAll()}catch(e){toast(e.message,true)}finally{b.disabled=false;b.textContent=old}}
|
||||
$('goalCreateBtn').addEventListener('click',async()=>{let title=$('goalTitle').value.trim();if(!title)return toast('Titel fehlt.',true);try{let g=await req('/api/v1/goals',{method:'POST',body:JSON.stringify({title,description:$('goalDesc').value,target:$('goalTarget').value,priority:Number($('goalPriority').value||50),interval_minutes:Number($('goalInterval').value||10),status:'active'})});toast(`Ziel ${g.title} angelegt – erster Lauf ist eingeplant.`);$('goalTitle').value='';$('goalDesc').value='';loadGoals();refreshAll()}catch(e){toast(e.message,true)}});$('runAutonomyBtn').addEventListener('click',async()=>{try{let d=await req('/admin/api/autonomy',{method:'POST',body:'{}'});toast(`${d.cycles?.length||0} fällige Zyklen ausgeführt.`);loadGoals()}catch(e){toast(e.message,true)}});
|
||||
$('goalCreateBtn').addEventListener('click',async()=>{let title=$('goalTitle').value.trim();if(!title)return toast('Titel fehlt.',true);let b=$('goalCreateBtn'),old=b.textContent;try{b.disabled=true;b.textContent='lege an …';let g=await req('/api/v1/goals',{method:'POST',body:JSON.stringify({title,description:$('goalDesc').value,target:$('goalTarget').value,priority:Number($('goalPriority').value||50),interval_minutes:Number($('goalInterval').value||10),status:'active'})});toast(`Ziel ${g.title} angelegt – erster Lauf ist eingeplant.`);$('goalTitle').value='';$('goalDesc').value='';await loadGoals();refreshAll()}catch(e){toast(e.message,true)}finally{b.disabled=false;b.textContent=old}});$('runAutonomyBtn').addEventListener('click',async()=>{try{let d=await req('/admin/api/autonomy',{method:'POST',body:'{}'});toast(`${d.cycles?.length||0} fällige Zyklen ausgeführt.`);loadGoals()}catch(e){toast(e.message,true)}});
|
||||
|
||||
// ---- Chat ----
|
||||
$('chatBtn').addEventListener('click',async()=>{let input=document.getElementById('chatPrompt').value.trim();if(!input)return toast('Nachricht fehlt.',true);chatAbort=new AbortController();$('chatAbortBtn').disabled=false;$('chatBtn').disabled=true;let start=Date.now(),timer=setInterval(()=>$('chatElapsed').textContent=Math.round((Date.now()-start)/1000)+' s',1000);$('chatOutput').textContent='Ollama / NeuroForge arbeitet …';try{let r=await fetch('/api/v1/chat',{method:'POST',headers:tokenHeaders(),signal:chatAbort.signal,body:JSON.stringify({session_id:$('chatSession').value,input,provider:$('chatProvider').value})}),txt=await r.text(),d;try{d=JSON.parse(txt)}catch{d=txt}if(!r.ok)throw new Error(d.error||d);$('chatOutput').textContent=(d.answer||'')+'\n\n— '+(d.provider||'')+' · '+(d.model||'')+' · Recall '+(d.recalled?.length||0)+' · Reward '+Number(d.auto_reward||0).toFixed(3);refreshAll()}catch(e){$('chatOutput').textContent=e.name==='AbortError'?'Abgebrochen.':e.message}finally{clearInterval(timer);$('chatElapsed').textContent='';$('chatAbortBtn').disabled=true;$('chatBtn').disabled=false;chatAbort=null}});$('chatAbortBtn').addEventListener('click',()=>chatAbort?.abort());
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"neuroforge/internal/brain"
|
||||
"neuroforge/internal/core"
|
||||
"neuroforge/internal/store"
|
||||
)
|
||||
|
||||
func (s *Server) goalsList(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -22,6 +23,10 @@ func (s *Server) goalsCreate(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if err := s.store.UpsertGoal(&g); err != nil {
|
||||
if errors.Is(err, store.ErrDuplicateGoal) {
|
||||
s.err(w, http.StatusConflict, err)
|
||||
return
|
||||
}
|
||||
s.err(w, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
42
platform/neuroforge/internal/store/goal_duplicate_test.go
Normal file
42
platform/neuroforge/internal/store/goal_duplicate_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"neuroforge/internal/core"
|
||||
)
|
||||
|
||||
func TestUpsertGoalRejectsDuplicateActiveGoalFingerprint(t *testing.T) {
|
||||
s, err := New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
first := &core.Goal{Title: "FortiClient SSLVPN 7200", Description: "Create one support article", Target: "1 hochwertiger Wissensartikel", Status: core.GoalActive}
|
||||
if err := s.UpsertGoal(first); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
duplicate := &core.Goal{Title: " forticlient sslvpn 7200 ", Description: " Create one support article ", Target: "1 hochwertiger Wissensartikel", Status: core.GoalActive}
|
||||
if err := s.UpsertGoal(duplicate); !errors.Is(err, ErrDuplicateGoal) {
|
||||
t.Fatalf("duplicate goal error=%v, want %v", err, ErrDuplicateGoal)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpsertGoalAllowsSameFingerprintAfterCompletion(t *testing.T) {
|
||||
s, err := New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
first := &core.Goal{Title: "FortiClient SSLVPN 7200", Description: "Create one support article", Target: "1 hochwertiger Wissensartikel", Status: core.GoalCompleted}
|
||||
if err := s.UpsertGoal(first); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second := &core.Goal{Title: first.Title, Description: first.Description, Target: first.Target, Status: core.GoalActive}
|
||||
if err := s.UpsertGoal(second); err != nil {
|
||||
t.Fatalf("completed historical goal must not block a new run: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"neuroforge/internal/vector"
|
||||
)
|
||||
|
||||
var ErrDuplicateGoal = errors.New("duplicate goal")
|
||||
|
||||
func (s *Store) resolveConflictLocked(in *core.Memory) []core.Memory {
|
||||
key := strings.TrimSpace(in.TruthKey)
|
||||
if key == "" {
|
||||
@@ -173,6 +175,17 @@ func (s *Store) UpsertGoal(g *core.Goal) error {
|
||||
defer s.mu.Unlock()
|
||||
now := time.Now().UTC()
|
||||
newGoal := g.ID == "" || s.state.Goals[g.ID] == nil
|
||||
if newGoal {
|
||||
fingerprint := goalFingerprint(g)
|
||||
for id, existing := range s.state.Goals {
|
||||
if existing == nil || existing.Status == core.GoalCompleted {
|
||||
continue
|
||||
}
|
||||
if goalFingerprint(existing) == fingerprint {
|
||||
return fmt.Errorf("%w: existing goal %s", ErrDuplicateGoal, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
if g.ID == "" {
|
||||
g.ID = NewID("goal")
|
||||
}
|
||||
@@ -206,6 +219,16 @@ func (s *Store) UpsertGoal(g *core.Goal) error {
|
||||
return s.commitLocked("goal.upsert", cp)
|
||||
}
|
||||
|
||||
func goalFingerprint(g *core.Goal) string {
|
||||
if g == nil {
|
||||
return ""
|
||||
}
|
||||
normalize := func(v string) string {
|
||||
return strings.ToLower(strings.Join(strings.Fields(strings.TrimSpace(v)), " "))
|
||||
}
|
||||
return normalize(g.Title) + "\x00" + normalize(g.Description) + "\x00" + normalize(g.Target)
|
||||
}
|
||||
|
||||
func (s *Store) GetGoal(id string) (*core.Goal, bool) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
Reference in New Issue
Block a user