Compare commits

...
Author SHA1 Message Date
m 2ee5da8114 docs: record compat production deploy blocker
CI / test (push) Failing after 29s
2026-08-30 13:37:42 +03:00
m 91f0c9e799 sync gen2 integration with promoted main
CI / test (push) Failing after 29s
2026-08-30 13:26:37 +03:00
m cb66a60db2 merge compat observation deployment safeguards
CI / test (push) Failing after 35s
2026-08-30 13:16:30 +03:00
m 6a4f973e38 docs: record compat observation deployment blocker
CI / test (push) Failing after 34s
2026-08-30 13:13:47 +03:00
m 87ec7efd0f fix: allow bounded candidate startup discovery
CI / test (push) Failing after 37s
2026-08-30 13:10:38 +03:00
m 51e33e7283 fix: start candidate release set in dependency order
CI / test (push) Failing after 38s
2026-08-30 13:06:33 +03:00
m e8f35f6393 chore: support candidate-only compat observation deploy
CI / test (push) Failing after 36s
2026-08-30 13:02:23 +03:00
m 8babf6ae1f merge compat observation deployment guard
CI / test (push) Failing after 37s
2026-08-30 13:01:46 +03:00
m d0210e5aed fix: guard stable containers during compat observation deploy
CI / test (push) Failing after 35s
2026-08-30 13:01:32 +03:00
m ebdfaa0769 merge ReleaseSet production drill evidence
CI / test (push) Failing after 29s
2026-08-30 12:31:48 +03:00
m 9e0faa7c47 docs: record ReleaseSet production drill 2026-08-30 12:31:47 +03:00
m 7f8a05ccfb merge creator ReleaseSet readiness fix
CI / test (push) Failing after 31s
2026-08-30 12:21:50 +03:00
m c94d04acd0 fix: use creator health as ReleaseSet readiness 2026-08-30 12:21:48 +03:00
m bb866cd2ab merge ReleaseSet readiness rollback fix
CI / test (push) Failing after 30s
2026-08-30 12:16:37 +03:00
m 892266a4c8 fix: preserve ReleaseSet rollback and readiness budget 2026-08-30 12:16:35 +03:00
m ac531c3ec3 merge candidate ReleaseSet command fix
CI / test (push) Failing after 31s
2026-08-30 12:07:53 +03:00
m 4cd50a0937 fix: close candidate ReleaseSet heredoc assignment 2026-08-30 12:07:52 +03:00
m 8b751257e8 merge candidate health script fix
CI / test (push) Failing after 30s
2026-08-30 12:07:26 +03:00
m cbbb60e081 fix: simplify candidate health shell assignments 2026-08-30 12:07:24 +03:00
m 46e3cfc445 merge stable ReleaseSet command fix
CI / test (push) Failing after 30s
2026-08-30 12:06:57 +03:00
m 5832c49ad8 fix: close stable ReleaseSet command substitution 2026-08-30 12:06:55 +03:00
m b13d12583b merge stable ReleaseSet script fix
CI / test (push) Failing after 30s
2026-08-30 12:06:29 +03:00
m 9d5473d260 fix: close stable ReleaseSet heredoc assignment 2026-08-30 12:06:28 +03:00
m 50b508b641 merge ReleaseSet observation generation fix
CI / test (push) Failing after 30s
2026-08-30 12:06:01 +03:00
m 8400cdd92d fix: generate observation JSON without heredoc 2026-08-30 12:06:00 +03:00
m ec1860d441 merge ReleaseSet payload generation fix
CI / test (push) Failing after 30s
2026-08-30 12:05:11 +03:00
m 134ba26e08 fix: simplify ReleaseSet payload generation 2026-08-30 12:05:09 +03:00
m b82b8b874f merge ReleaseSet script quoting fix
CI / test (push) Failing after 45s
2026-08-30 12:04:33 +03:00
m 5c9d4663f8 fix: avoid nested shell quoting in observation 2026-08-30 12:04:31 +03:00
m ec9d434650 merge ReleaseSet observation script fix
CI / test (push) Failing after 44s
2026-08-30 12:03:48 +03:00
m a8a60ce29e fix: quote ReleaseSet observation payload 2026-08-30 12:03:46 +03:00
m 96cf66a876 merge ReleaseSet canary validation fix
CI / test (push) Failing after 44s
2026-08-30 12:03:03 +03:00
m 381b52d06d fix: validate complete candidate service health 2026-08-30 12:03:01 +03:00
m 4ceec5a316 merge GEN2-DEPLOY-002 supervised release set
CI / test (push) Failing after 31s
2026-08-30 11:57:07 +03:00
m 93a9c1a3bd feat: supervise complete Domovoy release sets
CI / test (push) Failing after 30s
2026-08-30 10:39:36 +03:00
m f3ad7d682b merge compatibility observation blocker evidence
CI / test (push) Failing after 30s
2026-08-30 10:29:12 +03:00
m 387de89efe docs: record compatibility observation deployment blocker 2026-08-30 10:29:11 +03:00
m 346e0acb6b merge GEN2-COMPAT-OBS-001 instrumentation
CI / test (push) Failing after 30s
2026-08-30 10:25:36 +03:00
m 3efcbbded8 feat(telemetry): observe V1 compatibility paths safely 2026-08-30 10:25:35 +03:00
m a0b742b1d5 merge GEN2-030 final acceptance
CI / test (push) Failing after 29s
2026-08-30 10:02:24 +03:00
m 86aacea99e docs(gen2): record final acceptance and retirement gap 2026-08-30 10:02:23 +03:00
m 2a8e4e1b81 merge GEN2-029 retirement decision
CI / test (push) Failing after 30s
2026-08-30 09:59:44 +03:00
m d64080ef2b docs(gen2): record V1 retirement decision 2026-08-30 09:59:42 +03:00
m 59d32143bf merge GEN2-028 compatibility inventory
CI / test (push) Failing after 28s
2026-08-30 09:58:07 +03:00
m 031ba7666c docs(gen2): inventory V1 compatibility retirement readiness 2026-08-30 09:58:06 +03:00
m 93feb9bf2d merge M6 task scope revisions
CI / test (push) Failing after 30s
2026-08-30 09:53:51 +03:00
m a957172d3c governance: revise Gen2 M6 task scopes 2026-08-30 09:53:50 +03:00
m 3182092d5c merge final core m5 evidence
CI / test (push) Failing after 30s
2026-08-30 02:35:29 +03:00
m df8a275eb1 record final core m5 production evidence
CI / test (push) Failing after 29s
2026-08-30 02:35:23 +03:00
m 8aa5179d47 merge final core medium readiness remediation
CI / test (push) Failing after 31s
2026-08-30 02:30:43 +03:00
m 7b62781087 separate medium health and model readiness
CI / test (push) Failing after 31s
2026-08-30 02:30:39 +03:00
m 5c95916648 merge bounded medium invocation recovery
CI / test (push) Failing after 31s
2026-08-30 02:10:59 +03:00
m 8caa49f429 allow bounded medium invocation recovery
CI / test (push) Failing after 31s
2026-08-30 02:10:55 +03:00
m 5b5875d0f4 merge local medium readiness remediation follow-up
CI / test (push) Failing after 29s
2026-08-30 01:59:58 +03:00
m f7ffaee10d extend local medium readiness budget
CI / test (push) Failing after 29s
2026-08-30 01:59:42 +03:00
m 550738f3ca merge Local MEDIUM readiness remediation
CI / test (push) Failing after 29s
2026-08-30 01:52:02 +03:00
m e8b4c4fdab bound cognitive provider readiness and fleet probes
CI / test (push) Failing after 30s
2026-08-30 01:51:18 +03:00
m 39f6fdee7b record core M5 acceptance amendment
CI / test (push) Failing after 27s
2026-08-30 01:45:21 +03:00
m 711a734fee fix continuity recovery harness request forwarding
CI / test (push) Failing after 33s
2026-08-30 01:09:02 +03:00
m ad851a98aa merge retrieved memory fast path
CI / test (push) Failing after 33s
2026-08-30 00:54:12 +03:00
m 1e3f29b791 short circuit cognitive work for retrieved memory
CI / test (push) Failing after 31s
2026-08-30 00:54:08 +03:00
m 62fd265bb7 merge deterministic memory review fix
CI / test (push) Failing after 33s
2026-08-30 00:48:30 +03:00
m 2a52306f90 skip review for deterministic memory replies
CI / test (push) Failing after 28s
2026-08-30 00:48:25 +03:00
m e1c2e5f59c merge memory subject normalization
CI / test (push) Failing after 32s
2026-08-30 00:43:11 +03:00
m 49b3165caa normalize marked memory subjects
CI / test (push) Failing after 28s
2026-08-30 00:43:07 +03:00
m 41b5c57d05 merge memory continuation priority fix
CI / test (push) Failing after 32s
2026-08-30 00:38:39 +03:00
m 31dd628519 prioritize memory retrieval over stale travel continuation
CI / test (push) Failing after 29s
2026-08-30 00:38:35 +03:00
m d0103bbf8e merge acknowledgement routing fix
CI / test (push) Failing after 33s
2026-08-30 00:34:34 +03:00
m 29217ca935 classify stable acknowledgements before continuation
CI / test (push) Failing after 30s
2026-08-30 00:34:30 +03:00
m e9c3df26ae merge stable response review fix
CI / test (push) Failing after 32s
2026-08-30 00:30:05 +03:00
m 70ece6ed5c avoid redundant review for stable chat
CI / test (push) Failing after 32s
2026-08-30 00:30:00 +03:00
m 916b93517a merge stable chat routing fix
CI / test (push) Failing after 31s
2026-08-30 00:26:36 +03:00
m cf68786d0c skip cognitive interpretation for stable chat
CI / test (push) Failing after 30s
2026-08-30 00:26:31 +03:00
m 44ac0203a9 merge bounded chat response fix
CI / test (push) Failing after 32s
2026-08-30 00:22:25 +03:00
m 8571e3fe59 bound deterministic chat response path
CI / test (push) Failing after 33s
2026-08-30 00:22:20 +03:00
m 0af6a9b9a5 merge conversational context fix into gen2 integration
CI / test (push) Failing after 34s
2026-08-29 23:42:57 +03:00
m 6b9d5be299 preserve stable conversational replies across stale context
CI / test (push) Failing after 26s
2026-08-29 23:42:52 +03:00
m 8b3d6f9176 merge cognitive readiness budget fix
CI / test (push) Failing after 27s
2026-08-29 23:31:29 +03:00
m b4ab280950 align cognitive readiness probe budgets
CI / test (push) Failing after 30s
2026-08-29 23:31:25 +03:00
m 9cb69d1663 merge Gen2 household acceptance fixes
CI / test (push) Failing after 38s
2026-08-29 23:28:15 +03:00
m 5387010a15 harden Gen2 household introspection and readiness
CI / test (push) Failing after 33s
2026-08-29 23:28:10 +03:00
m 66fd4bfc75 merge memory recall routing fix
CI / test (push) Failing after 36s
2026-08-29 23:14:19 +03:00
m 8ac4525569 route recalled memory through local cognitive review
CI / test (push) Failing after 27s
2026-08-29 23:14:15 +03:00
m 40c610945f merge stable local answer routing fix
CI / test (push) Failing after 34s
2026-08-29 23:10:11 +03:00
m 99458a6ced avoid redundant review of stable local answers
CI / test (push) Failing after 29s
2026-08-29 23:10:07 +03:00
m 25c62e38fc merge bounded cognitive response budgets into integration
CI / test (push) Failing after 36s
2026-08-29 23:05:57 +03:00
m 5c0c6b3e42 bound local cognitive household response budgets
CI / test (push) Failing after 29s
2026-08-29 23:05:43 +03:00
m 788137b4ba merge bounded Gen2 memory projection
CI / test (push) Failing after 33s
2026-08-29 22:59:43 +03:00
m 2ef639eba9 bound cognitive personal-memory projection
CI / test (push) Failing after 36s
2026-08-29 22:59:38 +03:00
m d4ed85dec2 merge cognitive head persistence fix
CI / test (push) Failing after 35s
2026-08-29 22:44:04 +03:00
m c1c2110322 preserve governed cognitive head across discovery refresh
CI / test (push) Failing after 31s
2026-08-29 22:43:59 +03:00
m d83fcb2176 merge local cognitive metadata fix
CI / test (push) Failing after 35s
2026-08-29 22:40:58 +03:00
m 041fb1556d fix local cognitive model metadata qualification
CI / test (push) Failing after 32s
2026-08-29 22:40:54 +03:00
m a3baade40b merge main into gen2-integration before GEN2-027 acceptance
CI / test (push) Failing after 30s
2026-08-29 22:19:32 +03:00
m 12104210a3 synchronize discovery head protection
CI / test (push) Failing after 29s
2026-08-29 21:54:03 +03:00
m 40dcc36b06 synchronize governed binding head fix
CI / test (push) Failing after 36s
2026-08-29 21:53:51 +03:00
m 433cb2de2c prevent discovery from rolling back governed binding head
CI / test (push) Failing after 27s
2026-08-29 21:53:46 +03:00
m b49ff6cb71 synchronize cognitive activation fix
CI / test (push) Failing after 32s
2026-08-29 21:51:02 +03:00
m 4bb6728dfe load canary revision for cognitive activation
CI / test (push) Failing after 29s
2026-08-29 21:50:58 +03:00
m b00130ac15 synchronize cognitive remediation branches
CI / test (push) Failing after 29s
2026-08-29 21:47:28 +03:00
m fc356eb0a3 synchronize medium startup discovery fix
CI / test (push) Failing after 35s
2026-08-29 21:47:14 +03:00
m 0f880896fe allow bounded medium startup discovery
CI / test (push) Failing after 28s
2026-08-29 21:47:09 +03:00
m 460a82e020 synchronize cognitive lifecycle remediation
CI / test (push) Failing after 31s
2026-08-29 21:44:01 +03:00
m e27ead1a75 promote durable task evaluations through cognitive lifecycle
CI / test (push) Failing after 29s
2026-08-29 21:43:57 +03:00
m 64bc8bca25 merge governed cognitive promotion wiring
CI / test (push) Failing after 31s
2026-08-29 21:40:34 +03:00
m 67b585af94 load evaluation evidence for cognitive promotion
CI / test (push) Failing after 28s
2026-08-29 21:40:27 +03:00
m e47ea7b48e synchronize cognitive remediation timeout fix
CI / test (push) Failing after 31s
2026-08-29 21:36:20 +03:00
m 75042e090f use bounded medium client timeout for discovery
CI / test (push) Failing after 30s
2026-08-29 21:36:08 +03:00
m 1168c00caf merge cognitive remediation into main
CI / test (push) Failing after 32s
2026-08-29 21:33:23 +03:00
m 6e873ae3af merge medium discovery timeout remediation
CI / test (push) Failing after 29s
2026-08-29 21:32:42 +03:00
m b7210dc0ad fix cognitive medium discovery timeout
CI / test (push) Failing after 30s
2026-08-29 21:32:26 +03:00
m c0704dd28e merge cognitive qualification fix into main
CI / test (push) Failing after 29s
2026-08-29 21:17:18 +03:00
m 198f6cfe58 merge cognitive task evaluation locale fix
CI / test (push) Failing after 30s
2026-08-29 21:17:13 +03:00
m 49608599c4 fix cognitive task evaluation locale 2026-08-29 21:17:12 +03:00
m 4f6d205a60 merge cognitive route remediation into main
CI / test (push) Failing after 29s
2026-08-29 21:05:32 +03:00
m bcb833f15e merge GEN2-COGNITIVE-001 remediation task
CI / test (push) Failing after 30s
2026-08-29 21:05:16 +03:00
m 4f6f2a1f48 remediate local medium cognitive route 2026-08-29 21:05:10 +03:00
m f709c026d0 merge: repair cognitive discovery head persistence
CI / test (push) Failing after 28s
2026-08-29 19:35:27 +03:00
m 92ad3516ea fix: advance cognitive discovery binding heads
CI / test (push) Failing after 26s
2026-08-29 19:35:23 +03:00
m e1beed0391 merge: wire local medium cognitive route
CI / test (push) Failing after 39s
2026-08-29 19:28:43 +03:00
m 6b297c7749 fix: wire cognitive medium to local chat gateway
CI / test (push) Failing after 33s
2026-08-29 19:28:37 +03:00
m 91b8da2240 merge: integrate frozen Domovoy UI and identity auth
CI / test (push) Failing after 28s
2026-08-29 17:31:41 +03:00
m 8924f5a48a Move creator passwords into identity accounts
CI / test (push) Failing after 27s
2026-08-29 17:02:42 +03:00
m 636fb904f2 refactor: fix secret file paths in code
CI / test (push) Failing after 27s
2026-08-29 16:51:50 +03:00
m 598a97cd1a fix: keep sessions during cognitive probe failures
CI / test (push) Failing after 26s
2026-08-29 16:22:05 +03:00
m e9ef635933 fix: simplify chat composer actions
CI / test (push) Failing after 27s
2026-08-29 16:10:59 +03:00
m c516754d63 fix: keep attachment tools neutral
CI / test (push) Failing after 28s
2026-08-29 16:00:09 +03:00
m 5c965ef69c fix: make Domovoy chat-first
CI / test (push) Failing after 29s
2026-08-29 15:58:19 +03:00
m eab7d11bdd fix: refine chat composer controls
CI / test (push) Failing after 26s
2026-08-29 15:42:52 +03:00
m 51a49ba0ef fix: make chat composer compact and keyboard friendly
CI / test (push) Failing after 25s
2026-08-29 15:38:28 +03:00
m 60ff22882c fix: enable starting a fresh chat
CI / test (push) Failing after 28s
2026-08-29 15:28:38 +03:00
m 9896e4f716 feat: redesign Domovoy chat as calm dark cockpit
CI / test (push) Failing after 28s
2026-08-29 15:26:54 +03:00
m ff0d0e585b merge: deploy context retrieval remediation
CI / test (push) Failing after 27s
2026-08-29 15:08:04 +03:00
m 13f3165ba0 merge: integrate context retrieval remediation
CI / test (push) Failing after 28s
2026-08-29 15:07:59 +03:00
m 55f2f9affa fix: project explicit remembered assertions
CI / test (push) Failing after 26s
2026-08-29 15:07:47 +03:00
m e47e033dda merge: synchronize integration with current main
CI / test (push) Failing after 31s
2026-08-29 14:52:15 +03:00
m 2dc5587fd1 Clarify chat response progress
CI / test (push) Failing after 30s
2026-08-29 14:31:03 +03:00
m 992c84240b Preserve chat responsive guardrail 2026-08-29 14:30:29 +03:00
m cfeb6aeba7 Put chat first on mobile screens 2026-08-29 14:30:10 +03:00
m f0d45ca9f1 Clarify chat message roles and timing 2026-08-29 14:29:39 +03:00
m e20c9fdfc0 Improve Domovoy chat composer usability 2026-08-29 14:28:32 +03:00
m cb50a120e5 merge: record Gen2 deployment recovery evidence
CI / test (push) Failing after 29s
2026-08-29 12:49:59 +03:00
m b30eb38eca docs: record Gen2 deployment recovery evidence
CI / test (push) Failing after 28s
2026-08-29 12:49:51 +03:00
m 2083096aed merge: preserve stable rollback target
CI / test (push) Failing after 28s
2026-08-29 12:44:33 +03:00
m 671ecb4968 fix(supervisor): preserve stable rollback target
CI / test (push) Failing after 32s
2026-08-29 12:44:25 +03:00
m af9d5809e9 merge: stabilize supervisor control contract
CI / test (push) Failing after 27s
2026-08-29 12:41:30 +03:00
m 990928618d fix(supervisor): stabilize control JSON contract
CI / test (push) Failing after 31s
2026-08-29 12:41:22 +03:00
m 9fac8284f5 merge: initialize supervisor state volume permissions
CI / test (push) Failing after 27s
2026-08-29 12:39:01 +03:00
m 7d9a5edee5 fix(deploy): initialize supervisor volume permissions
CI / test (push) Failing after 32s
2026-08-29 12:38:53 +03:00
m 32d3f318a5 merge: independent supervisor control plane
CI / test (push) Failing after 31s
2026-08-29 12:33:54 +03:00
m 3a563a29ee feat(deploy): add independent supervisor control plane
CI / test (push) Failing after 28s
2026-08-29 12:33:44 +03:00
m 734db5d779 merge: synchronize integration with current main
CI / test (push) Failing after 28s
2026-08-29 12:25:26 +03:00
m 36fc8ec8c6 merge: resume Gen2 controlled canary
CI / test (push) Failing after 27s
2026-08-29 12:16:21 +03:00
m b8c001be4c chore(gen2): resume controlled production canary
CI / test (push) Failing after 26s
2026-08-29 12:16:09 +03:00
m eeeba6b7c0 merge: Gen2 database provisioning remediation
CI / test (push) Failing after 31s
2026-08-29 12:12:43 +03:00
m fce3ec569c fix(database): provision Gen2 owner roles before migrations
CI / test (push) Failing after 27s
2026-08-29 12:12:39 +03:00
m a925d787dc GEN2-027: prepare controlled production canary
CI / test (push) Failing after 26s
2026-08-29 12:01:37 +03:00
m 43556e1603 GEN2-026: route household context through scoped Gen2 projection
CI / test (push) Failing after 28s
2026-08-29 12:00:04 +03:00
m dd00dabf32 GEN2-025: normalize multimodal ingress provenance
CI / test (push) Failing after 27s
2026-08-29 11:58:34 +03:00
m 33365ce7ee GEN2-024: establish principal realms and source trust
CI / test (push) Failing after 26s
2026-08-29 11:57:19 +03:00
m 5017a854ee GEN2-023: make conversational configuration intent explicit
CI / test (push) Failing after 29s
2026-08-29 11:55:38 +03:00
m 839b35f310 GEN2 M4 remediation: durable recovery and governed evolution
CI / test (push) Failing after 26s
2026-08-29 11:47:05 +03:00
m 96687140ce GEN2-022: govern self evolution through development gates
CI / test (push) Failing after 27s
2026-08-29 11:31:45 +03:00
m b282038949 GEN2-021: add independent supervisor lifeboat
CI / test (push) Failing after 27s
2026-08-29 11:30:49 +03:00
m 4de506a2af GEN2-020: separate execution receipts from world outcomes
CI / test (push) Failing after 26s
2026-08-29 11:30:05 +03:00
m 36272b9036 GEN2-019: add advisory consequence assessment
CI / test (push) Failing after 29s
2026-08-29 11:28:52 +03:00
m a779568622 GEN2-018: add bounded perspectives over Work Graph
CI / test (push) Failing after 27s
2026-08-29 11:28:05 +03:00
m 243a11490b GEN2-017: add durable long-running investigations
CI / test (push) Failing after 26s
2026-08-29 11:27:21 +03:00
m 26207d93ef GEN2-016: add intelligence strategy and benchmark evidence
CI / test (push) Failing after 26s
2026-08-29 11:16:07 +03:00
m 42871f2e47 GEN2-015: add evidence-backed SELF ledger
CI / test (push) Failing after 28s
2026-08-29 11:13:38 +03:00
m 6ea1d4b729 GEN2-014: add reflex lifecycle and escalation
CI / test (push) Failing after 28s
2026-08-29 11:12:42 +03:00
m c6d1ebf36f GEN2-013: add versioned Skill V2 contracts
CI / test (push) Failing after 27s
2026-08-29 11:11:50 +03:00
m 25d8d6d219 GEN2-012: add typed scoped configuration
CI / test (push) Failing after 33s
2026-08-29 11:09:33 +03:00
m 6cbef9af41 GEN2-012: add scoped configuration bootstrap boundary
CI / test (push) Failing after 34s
2026-08-29 11:08:53 +03:00
m 2fe9802e5a GEN2-011: cut PersonalMemory to compatibility fallback
CI / test (push) Failing after 37s
2026-08-29 02:41:23 +03:00
m 0882df3ad2 GEN2-010: add bounded Context Compiler V2 trace
CI / test (push) Successful in 53s
2026-08-29 02:39:58 +03:00
m 0acbe53fad GEN2-009: add rebuildable retrieval projections
CI / test (push) Successful in 50s
2026-08-29 02:38:49 +03:00
m e634bb946a GEN2-008: promote experience and episode projections
CI / test (push) Successful in 55s
2026-08-29 02:37:28 +03:00
m c7ad996cae GEN2-007: add task-relative freshness assessment
CI / test (push) Successful in 52s
2026-08-29 02:35:54 +03:00
m a065382512 GEN2-006: normalize epistemic propositions and assessments
CI / test (push) Successful in 51s
2026-08-29 02:33:09 +03:00
m 8a8e571905 GEN2-005: govern identity resolution and version bindings
CI / test (push) Failing after 38s
2026-08-29 02:20:02 +03:00
m 301dbdcfb2 GEN2-004: add ContextScope and FocusStack V2
CI / test (push) Successful in 50s
2026-08-29 02:17:36 +03:00
m 8037e66998 GEN2-003: add typed World referent foundation
CI / test (push) Successful in 50s
2026-08-29 02:11:36 +03:00
m 29815c49b8 GEN2-002: add shared semantic primitives
CI / test (push) Successful in 52s
2026-08-29 02:08:20 +03:00
m c14ad7fd0b GEN2-001: accept owner registry and dependency rules
CI / test (push) Successful in 49s
2026-08-29 02:05:35 +03:00
m 939a5152e1 ARCH-GEN2-000: adopt Gen2 architecture governance
CI / test (push) Successful in 48s
2026-08-29 01:53:27 +03:00
m 4e54ace76b Route local place searches to relevant evidence
CI / test (pull_request) Successful in 1m19s
CI / test (push) Successful in 47s
2026-08-26 10:40:15 +03:00
m f329224d12 Merge pull request 'Keep nearest flight reply focused' (#405) from codex/household-text-cognition-path into main
CI / test (push) Successful in 1m21s
Reviewed-on: #405
2026-08-26 07:33:36 +00:00
m bb76171af4 Merge remote-tracking branch 'origin/main' into codex/household-text-cognition-path
CI / test (push) Successful in 1m20s
CI / test (pull_request) Successful in 1m23s
2026-08-26 10:30:15 +03:00
m 213f06661d Keep nearest flight reply focused
CI / test (push) Failing after 59s
CI / test (pull_request) Failing after 58s
2026-08-26 10:27:54 +03:00
m d350b9a565 Merge pull request 'Allow reply-only cognition for text directives' (#404) from codex/household-text-cognition-path into main
CI / test (push) Successful in 1m24s
Reviewed-on: #404
2026-08-26 07:21:07 +00:00
m f862c3ac13 Allow reply-only cognition for text directives
CI / test (push) Successful in 1m22s
CI / test (pull_request) Successful in 1m17s
2026-08-26 10:17:51 +03:00
m 5e18eb6082 Merge pull request 'Route safe text directives through cognition' (#403) from codex/household-text-directives into main
CI / test (push) Successful in 1m21s
Reviewed-on: #403
2026-08-26 07:12:00 +00:00
m ec86c983e3 Route safe text directives through cognition
CI / test (push) Successful in 1m20s
CI / test (pull_request) Successful in 1m16s
2026-08-26 10:08:42 +03:00
m ea854d5278 Merge pull request 'Allow review of backed research answers' (#402) from codex/household-research-review into main
CI / test (push) Successful in 1m17s
Reviewed-on: #402
2026-08-26 07:01:37 +00:00
218 changed files with 23003 additions and 334 deletions
+13
View File
@@ -72,3 +72,16 @@ State:
- assumptions;
- findings not changed;
- gaps or limitations.
## Domovoy Gen2 execution governance
When the active task is a Gen2 packet, read the canonical documents before editing:
- `docs/DOMOVOY_CONSTITUTION.md`
- `architecture/GEN2_TARGET_ARCHITECTURE.md`
- `architecture/GEN2_CANONICAL_DATA_MODEL.md`
- `development/GEN2_MIGRATION_MAP.md`
- `development/GEN2_MASTER_PLAN.md`
- `development/GEN2_EXECUTION_CONTRACT.md`
The active task packet and `development/GEN2_TASK_INDEX.md` define execution order. `PRE-GEN2-000` freezes the V1 baseline; `ARCH-GEN2-000` accepts Gen2 governance; later `GEN2-*` packets must not be started automatically. Gen2 architecture and ownership changes are permitted only within the active packet's declared scope and authority. Default validation is FAST; MILESTONE or FULL is required only when the selected packet explicitly says so.
+11 -1
View File
@@ -25,6 +25,13 @@ type OpenAICompatibleLocalAdapter struct {
apiKey string
}
// compatibleDefaultContextBytes is a conservative bound used when an
// OpenAI-compatible local endpoint omits n_ctx from /v1/models. The adapter
// is a chat-completions adapter, so advisory capability is known from the
// adapter contract; quality and activation still require governed
// qualification/evaluation in the capability registry.
const compatibleDefaultContextBytes int64 = 32 << 10
func NewOpenAICompatibleLocalAdapter(raw string, transport http.RoundTripper) (*OpenAICompatibleLocalAdapter, error) {
return NewOpenAICompatibleLocalAdapterWithAPIKey(raw, "", transport)
}
@@ -90,7 +97,10 @@ func (a *OpenAICompatibleLocalAdapter) Discover(ctx context.Context) ([]Discover
stableMetadata := fmt.Sprintf("%s\x00%s\x00%d\x00%d\x00%d\x00%d\x00%s", item.ID, item.OwnedBy, item.Meta.Context, item.Meta.TrainingContext, item.Meta.Parameters, item.Meta.Size, item.Meta.FileType)
digest := sha256.Sum256([]byte(stableMetadata))
contextBytes := item.Meta.Context * 4
models = append(models, DiscoveredModel{ProviderModelID: capability.ProviderModelID(item.ID), Fingerprint: capability.ModelFingerprint(hex.EncodeToString(digest[:])), MaxContextBytes: contextBytes, Modalities: []capability.ModelModality{capability.ModalityText}, Capabilities: []capability.ModelCapability{capability.CapabilityUnknown}, SizeBytes: item.Meta.Size, Runtime: item.OwnedBy, Quantization: item.Meta.FileType})
if contextBytes <= 0 {
contextBytes = compatibleDefaultContextBytes
}
models = append(models, DiscoveredModel{ProviderModelID: capability.ProviderModelID(item.ID), Fingerprint: capability.ModelFingerprint(hex.EncodeToString(digest[:])), MaxContextBytes: contextBytes, Modalities: []capability.ModelModality{capability.ModalityText}, Capabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, SizeBytes: item.Meta.Size, Runtime: item.OwnedBy, Quantization: item.Meta.FileType})
}
return models, ProviderProbe{Health: capability.HealthAvailable}
}
@@ -18,6 +18,34 @@ type captureRoundTripper struct {
body []byte
}
type modelsMetadataRoundTripper struct{}
func (modelsMetadataRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{"data":[{"id":"medium-model","owned_by":"llama.cpp","meta":{}}]}`)),
Header: make(http.Header),
Request: request,
}, nil
}
func TestOpenAICompatibleLocalAdapterNormalizesMinimalModelMetadata(t *testing.T) {
adapter, err := NewOpenAICompatibleLocalAdapter("http://gateway/v1", modelsMetadataRoundTripper{})
if err != nil {
t.Fatal(err)
}
models, probe := adapter.Discover(context.Background())
if probe.Health != capability.HealthAvailable || len(models) != 1 {
t.Fatalf("unexpected discovery result: probe=%+v models=%+v", probe, models)
}
if models[0].MaxContextBytes != compatibleDefaultContextBytes {
t.Fatalf("context bound=%d, want %d", models[0].MaxContextBytes, compatibleDefaultContextBytes)
}
if len(models[0].Capabilities) != 1 || models[0].Capabilities[0] != capability.CapabilityAdvisory {
t.Fatalf("capabilities=%v, want advisory", models[0].Capabilities)
}
}
func TestOpenAICompatibleLocalAdapterUsesConfiguredTimeout(t *testing.T) {
adapter, err := NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout("http://gateway/v1", "", &captureRoundTripper{}, 47*time.Second)
if err != nil {
+18
View File
@@ -0,0 +1,18 @@
package multimodal
import (
"crypto/sha256"
"encoding/hex"
"git.cin.su/m/Assistant/contracts/artifact"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/interaction"
)
func NormalizeIngress(id string, modality interaction.IngressModality, sourceRef, deviceRef, locationRef, principalID, contentType string, data []byte, response capability.MultimodalResponse, at int64) (interaction.IngressEnvelope, artifact.MediaArtifact) {
hash := sha256.Sum256(data)
artifactValue := artifact.MediaArtifact{ID: artifact.ArtifactID("artifact:media:" + hex.EncodeToString(hash[:8])), ContentType: contentType, SizeBytes: int64(len(data)), ContentDigest: hex.EncodeToString(hash[:]), SourceRef: sourceRef, RecordedAtUnixMillis: at}
if len(data) == 0 {
return interaction.IngressEnvelope{ID: id, Modality: modality, SourceRef: sourceRef, DeviceRef: deviceRef, LocationRef: locationRef, PrincipalID: principalID, Degraded: true, RecordedAtUnixMillis: at}, artifact.MediaArtifact{}
}
return interaction.IngressEnvelope{ID: id, Modality: modality, SourceRef: sourceRef, DeviceRef: deviceRef, LocationRef: locationRef, PrincipalID: principalID, ArtifactRef: string(artifactValue.ID), ObservationRef: response.EvidenceRef, ProviderRef: response.Provider, ModelRef: response.Model, ModelVersion: response.ModelVersion, ConfidenceMillis: response.ConfidenceMillis, RecordedAtUnixMillis: at}, artifactValue
}
+22
View File
@@ -0,0 +1,22 @@
package multimodal_test
import (
"git.cin.su/m/Assistant/adapters/multimodal"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/interaction"
"testing"
)
func TestIngressNormalizesMediaArtifactAndObservationProvenance(t *testing.T) {
e, a := multimodal.NormalizeIngress("ingress-1", interaction.IngressImage, "camera:street", "device:cam", "world:street", "principal", "image/png", []byte("image"), capability.MultimodalResponse{Provider: "local", Model: "vision", ModelVersion: "1", EvidenceRef: "observation-1", ConfidenceMillis: 800}, 1)
if !e.Valid() || !a.Valid() || e.ArtifactRef == "" || e.ObservationRef != "observation-1" || e.DeviceRef == "" || e.LocationRef == "" {
t.Fatalf("envelope=%+v artifact=%+v", e, a)
}
}
func TestUnavailableIngressDegradesWithoutFabricatingObservation(t *testing.T) {
e, a := multimodal.NormalizeIngress("ingress-1", interaction.IngressAudio, "mic", "device:mic", "", "principal", "audio/wav", nil, capability.MultimodalResponse{}, 1)
if !e.Degraded || e.ObservationRef != "" || a.Valid() {
t.Fatalf("envelope=%+v artifact=%+v", e, a)
}
}
+14 -5
View File
@@ -181,10 +181,6 @@ func (r *CapabilityRepository) SaveBinding(ctx context.Context, binding capabili
if err != nil {
return err
}
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_binding_revision_heads (binding_id,revision) VALUES ($1,$2) ON CONFLICT DO NOTHING`, binding.ID, binding.Revision)
if err != nil {
return err
}
}
return tx.Commit(ctx)
}
@@ -437,6 +433,14 @@ func (r *CapabilityRepository) LoadCognitiveCandidatesForProfile(ctx context.Con
return r.loadCognitiveCandidates(ctx, capabilityID, owner, profileID)
}
func (r *CapabilityRepository) LoadCognitiveCandidatesForEvaluation(ctx context.Context, capabilityID capability.CapabilityID, owner identity.PrincipalID, evaluationID capability.EvaluationID) ([]capability.ResolvedCognitiveBinding, error) {
var profileID capability.TaskEvaluationProfileID
if err := r.pool.QueryRow(ctx, `SELECT task_profile_id FROM capability.cognitive_task_evaluations WHERE evaluation_id=$1 AND capability_id=$2`, evaluationID, capabilityID).Scan(&profileID); err != nil {
return nil, err
}
return r.loadCognitiveCandidates(ctx, capabilityID, owner, profileID)
}
func (r *CapabilityRepository) LoadCognitiveCandidate(ctx context.Context, bindingID capability.CapabilityBindingID, revision int64) (capability.ResolvedCognitiveBinding, error) {
var capabilityID capability.CapabilityID
var owner identity.PrincipalID
@@ -564,7 +568,12 @@ func (r *CapabilityRepository) SaveCognitiveDiscovery(ctx context.Context, value
if err != nil {
return err
}
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_binding_revision_heads (binding_id,revision) VALUES ($1,$2) ON CONFLICT DO NOTHING`, binding.ID, binding.Revision)
// Discovery may advance a head, but must never roll back a governed
// CANARY/ACTIVE revision to an older discovered snapshot after restart.
// Discovery may advance an inactive head even when its deterministic
// revision number is lower, but it must never overwrite a governed canary
// or active head during restart/startup refresh.
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_binding_revision_heads (binding_id,revision) VALUES ($1,$2) ON CONFLICT (binding_id) DO UPDATE SET revision=EXCLUDED.revision,updated_at=CURRENT_TIMESTAMP WHERE NOT EXISTS (SELECT 1 FROM capability.cognitive_bindings current_binding WHERE current_binding.binding_id=capability.cognitive_binding_revision_heads.binding_id AND current_binding.revision=capability.cognitive_binding_revision_heads.revision AND current_binding.activation IN ('CANARY','ACTIVE'))`, binding.ID, binding.Revision)
if err != nil {
return err
}
@@ -6,6 +6,7 @@ import (
"time"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/platform/telemetry"
"github.com/jackc/pgx/v5/pgxpool"
)
@@ -36,6 +37,11 @@ func (r *EpistemicsRepository) Ping(ctx context.Context) error {
// TruthRuntimeStatus is a read-only projection of durable truth bundles.
// It does not promote claims, revise beliefs, or mutate epistemic state.
func (r *EpistemicsRepository) TruthRuntimeStatus(ctx context.Context) (TruthRuntimeStatus, error) {
started := time.Now()
outcome := "SUCCESS"
defer func() {
telemetry.RecordCompatibility("truth-bundles-status-reader", "epistemics-repository", "read", "research-status", true, false, true, outcome, "truth-status", "epistemics", started)
}()
status := TruthRuntimeStatus{
ClaimsByVerification: map[string]int64{},
BeliefsByState: map[string]int64{},
@@ -43,6 +49,7 @@ func (r *EpistemicsRepository) TruthRuntimeStatus(ctx context.Context) (TruthRun
ObservedAtUnixMillis: time.Now().UnixMilli(),
}
if err := r.pool.QueryRow(ctx, `SELECT count(*) FROM epistemics.truth_bundles`).Scan(&status.BundleCount); err != nil {
outcome = "FAILURE"
return TruthRuntimeStatus{}, err
}
queries := []struct {
@@ -56,18 +63,21 @@ func (r *EpistemicsRepository) TruthRuntimeStatus(ctx context.Context) (TruthRun
for _, item := range queries {
rows, err := r.pool.Query(ctx, item.query)
if err != nil {
outcome = "FAILURE"
return TruthRuntimeStatus{}, err
}
for rows.Next() {
var state string
var count int64
if err := rows.Scan(&state, &count); err != nil {
outcome = "FAILURE"
rows.Close()
return TruthRuntimeStatus{}, err
}
item.destination[state] = count
}
if err := rows.Err(); err != nil {
outcome = "FAILURE"
rows.Close()
return TruthRuntimeStatus{}, err
}
@@ -218,7 +228,13 @@ func (r *EpistemicsRepository) SaveReflectionCandidate(ctx context.Context, valu
}
func (r *EpistemicsRepository) SaveTruthBundle(ctx context.Context, value epistemics.TruthBundle, recordedAtUnixMillis int64) error {
started := time.Now()
outcome := "SUCCESS"
defer func() {
telemetry.RecordCompatibility("truth-bundles-writer", "epistemics-repository", "write", "persisted-research", true, false, true, outcome, string(value.Claim.ID), "epistemics", started)
}()
if value.Source.ID == "" || value.Snapshot.ID == "" || value.Excerpt.ID == "" || value.Claim.ID == "" || value.Belief.ID == "" || value.Citation.ID == "" || recordedAtUnixMillis < 1 {
outcome = "INVALID"
return fmt.Errorf("invalid truth bundle")
}
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.truth_bundles (
@@ -232,5 +248,8 @@ func (r *EpistemicsRepository) SaveTruthBundle(ctx context.Context, value episte
value.Excerpt.ID, value.Excerpt.SnapshotID, value.Excerpt.Text, value.Excerpt.Start, value.Excerpt.End,
value.Observation.ID, value.Observation.ObservedAt, value.Claim.ID, value.Claim.Statement, value.Claim.ObservationID, value.Claim.CitationID, value.Claim.Verification,
value.Belief.ID, value.Belief.ClaimID, value.Belief.State, value.Citation.ID, value.Citation.ExcerptID, value.Citation.Evidence, value.Citation.Coverage, value.Citation.Conflict, value.Citation.ResolvedAtUnixMillis, recordedAtUnixMillis)
if err != nil {
outcome = "FAILURE"
}
return err
}
+36
View File
@@ -26,3 +26,39 @@ func (r *IdentityRepository) LoadBinding(ctx context.Context, actor identity.Act
}
return binding, nil
}
func (r *IdentityRepository) LoadCreatorAccount(ctx context.Context, username string) (identity.CreatorAccount, error) {
var account identity.CreatorAccount
err := r.pool.QueryRow(ctx, `SELECT username, actor_id, principal_id, password_hash, revision FROM identity.creator_accounts WHERE username=$1`, username).
Scan(&account.Username, &account.ActorID, &account.PrincipalID, &account.PasswordHash, &account.Revision)
if err != nil {
return identity.CreatorAccount{}, fmt.Errorf("load creator account: %w", err)
}
return account, nil
}
func (r *IdentityRepository) LoadCreatorAccountByBinding(ctx context.Context, actor identity.ActorID, principal identity.PrincipalID) (identity.CreatorAccount, error) {
var account identity.CreatorAccount
err := r.pool.QueryRow(ctx, `SELECT username, actor_id, principal_id, password_hash, revision FROM identity.creator_accounts WHERE actor_id=$1 AND principal_id=$2`, actor, principal).
Scan(&account.Username, &account.ActorID, &account.PrincipalID, &account.PasswordHash, &account.Revision)
if err != nil {
return identity.CreatorAccount{}, fmt.Errorf("load creator account by binding: %w", err)
}
return account, nil
}
func (r *IdentityRepository) CreateCreatorAccount(ctx context.Context, account identity.CreatorAccount) error {
_, err := r.pool.Exec(ctx, `INSERT INTO identity.creator_accounts (username, actor_id, principal_id, password_hash) VALUES ($1,$2,$3,$4) ON CONFLICT (username) DO NOTHING`, account.Username, account.ActorID, account.PrincipalID, account.PasswordHash)
return err
}
func (r *IdentityRepository) UpdateCreatorPassword(ctx context.Context, actor identity.ActorID, principal identity.PrincipalID, revision int64, passwordHash string) error {
result, err := r.pool.Exec(ctx, `UPDATE identity.creator_accounts SET password_hash=$1, revision=revision+1, password_changed_at=CURRENT_TIMESTAMP WHERE actor_id=$2 AND principal_id=$3 AND revision=$4`, passwordHash, actor, principal, revision)
if err != nil {
return fmt.Errorf("update creator password: %w", err)
}
if result.RowsAffected() != 1 {
return fmt.Errorf("update creator password: revision conflict")
}
return nil
}
+33
View File
@@ -2,6 +2,7 @@ package postgres
import (
"context"
"encoding/json"
"strings"
"git.cin.su/m/Assistant/contracts/identity"
@@ -18,6 +19,38 @@ func OpenSituation(ctx context.Context, config ContinuityConfig) (*SituationRepo
}
return &SituationRepository{pool: pool}, nil
}
func (r *SituationRepository) SaveContextScope(ctx context.Context, scope situation.ContextScope) error {
if err := scope.Validate(); err != nil {
return err
}
bindings, err := json.Marshal(scope.Bindings)
if err != nil {
return err
}
_, err = r.pool.Exec(ctx, `INSERT INTO situation.context_scopes(context_scope_id,principal_id,revision,bindings,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5) ON CONFLICT(context_scope_id) DO UPDATE SET revision=EXCLUDED.revision, bindings=EXCLUDED.bindings, recorded_at_unix_millis=EXCLUDED.recorded_at_unix_millis`, scope.ID, scope.PrincipalID, scope.Revision, bindings, scope.RecordedAt)
return err
}
func (r *SituationRepository) SaveFocusFrame(ctx context.Context, frame situation.FocusFrame) error {
if err := frame.Validate(); err != nil {
return err
}
ref, err := json.Marshal(frame.ContextScopeRef)
if err != nil {
return err
}
_, err = r.pool.Exec(ctx, `INSERT INTO situation.focus_frames(focus_frame_id,principal_id,actor_id,revision,authority_scope_ref,context_scope_ref,goal_ref,task_ref,source_utterance_ref,provenance,confidence_millis,state,parent_frame_ref,activated_at_unix_millis,suspended_at_unix_millis,expired_at_unix_millis,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17) ON CONFLICT(focus_frame_id) DO UPDATE SET revision=EXCLUDED.revision, context_scope_ref=EXCLUDED.context_scope_ref, state=EXCLUDED.state, suspended_at_unix_millis=EXCLUDED.suspended_at_unix_millis, expired_at_unix_millis=EXCLUDED.expired_at_unix_millis, recorded_at_unix_millis=EXCLUDED.recorded_at_unix_millis`, frame.ID, frame.PrincipalID, frame.ActorID, frame.Revision, frame.AuthorityScopeRef, ref, frame.GoalRef, frame.TaskRef, frame.SourceUtteranceRef, frame.Provenance, frame.ConfidenceMillis, frame.State, frame.ParentFrameRef, frame.ActivatedAt, frame.SuspendedAt, frame.ExpiredAt, frame.RecordedAt)
return err
}
func (r *SituationRepository) AppendFocusTransition(ctx context.Context, transition situation.FocusTransition) error {
if err := transition.Validate(); err != nil {
return err
}
_, err := r.pool.Exec(ctx, `INSERT INTO situation.focus_transitions(transition_id,focus_frame_id,from_revision,to_revision,operation,cause_ref,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7)`, transition.TransitionID, transition.FrameID, transition.FromRevision, transition.ToRevision, transition.Operation, transition.CauseRef, transition.RecordedAt)
return err
}
func (r *SituationRepository) Close() { r.pool.Close() }
func (r *SituationRepository) Save(ctx context.Context, binding situation.FocusBinding) error {
_, err := r.pool.Exec(ctx, `INSERT INTO situation.focus_bindings (focus_binding_id, actor_id, session_id, workspace_id, project_id, goal_id, scope_id, active) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT (focus_binding_id) DO UPDATE SET active=EXCLUDED.active`, binding.ID, binding.ActorID, binding.SessionID, binding.Workspace, binding.ProjectID, binding.GoalID, binding.ScopeID, binding.Active)
+115
View File
@@ -25,6 +25,121 @@ var (
ErrDevelopmentCandidateConflict = errors.New("development candidate conflict")
)
func (r *WorkRepository) SaveInvestigation(ctx context.Context, value work.Investigation, expectedRevision int64) error {
if !value.Valid() || expectedRevision < 0 {
return errors.New("invalid investigation revision")
}
attempts := make([]string, 0, len(value.AttemptRefs))
for _, item := range value.AttemptRefs {
attempts = append(attempts, string(item))
}
hypotheses := make([]string, 0, len(value.HypothesisRefs))
for _, item := range value.HypothesisRefs {
hypotheses = append(hypotheses, string(item))
}
findings := make([]string, 0, len(value.FindingRefs))
for _, item := range value.FindingRefs {
findings = append(findings, string(item))
}
blockers := append([]string(nil), value.Blockers...)
checkpoints := make([]string, 0, len(value.CheckpointRefs))
for _, item := range value.CheckpointRefs {
checkpoints = append(checkpoints, string(item))
}
if expectedRevision == 0 {
result, err := r.pool.Exec(ctx, `INSERT INTO work.long_running_work(investigation_id,objective,hypothesis_refs,attempt_refs,finding_refs,blockers,checkpoint_refs,next_step,state,revision,time_budget_millis,cost_budget_micros,max_attempts,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT(investigation_id) DO NOTHING`, value.ID, value.Objective, hypotheses, attempts, findings, blockers, checkpoints, value.NextStep, value.State, value.Revision, value.TimeBudgetMillis, value.CostBudgetMicros, value.MaxAttempts, value.RecordedAtUnixMillis)
if err != nil {
return err
}
if result.RowsAffected() != 1 {
return errors.New("investigation already exists")
}
return nil
}
result, err := r.pool.Exec(ctx, `UPDATE work.long_running_work SET objective=$2,hypothesis_refs=$3,attempt_refs=$4,finding_refs=$5,blockers=$6,checkpoint_refs=$7,next_step=$8,state=$9,revision=$10,time_budget_millis=$11,cost_budget_micros=$12,max_attempts=$13,recorded_at_unix_millis=$14 WHERE investigation_id=$1 AND revision=$15`, value.ID, value.Objective, hypotheses, attempts, findings, blockers, checkpoints, value.NextStep, value.State, value.Revision, value.TimeBudgetMillis, value.CostBudgetMicros, value.MaxAttempts, value.RecordedAtUnixMillis, expectedRevision)
if err != nil {
return err
}
if result.RowsAffected() != 1 {
return errors.New("investigation revision conflict")
}
return nil
}
func (r *WorkRepository) LoadInvestigation(ctx context.Context, id work.LongRunningWorkID) (work.Investigation, error) {
var value work.Investigation
var hypotheses, attempts, findings, blockers, checkpoints []string
err := r.pool.QueryRow(ctx, `SELECT investigation_id,objective,hypothesis_refs,attempt_refs,finding_refs,blockers,checkpoint_refs,next_step,state,revision,time_budget_millis,cost_budget_micros,max_attempts,recorded_at_unix_millis FROM work.long_running_work WHERE investigation_id=$1`, id).Scan(&value.ID, &value.Objective, &hypotheses, &attempts, &findings, &blockers, &checkpoints, &value.NextStep, &value.State, &value.Revision, &value.TimeBudgetMillis, &value.CostBudgetMicros, &value.MaxAttempts, &value.RecordedAtUnixMillis)
if err != nil {
return work.Investigation{}, fmt.Errorf("load investigation: %w", err)
}
for _, item := range hypotheses {
value.HypothesisRefs = append(value.HypothesisRefs, work.EvidenceRef(item))
}
for _, item := range attempts {
value.AttemptRefs = append(value.AttemptRefs, work.AgentRunID(item))
}
for _, item := range findings {
value.FindingRefs = append(value.FindingRefs, work.EvidenceRef(item))
}
value.Blockers = append(value.Blockers, blockers...)
for _, item := range checkpoints {
value.CheckpointRefs = append(value.CheckpointRefs, work.AgentCheckpointID(item))
}
return value, nil
}
// SaveInvestigationCheckpoint atomically records the checkpoint and marks its
// attempts verified. The unique key makes retries idempotent.
func (r *WorkRepository) SaveInvestigationCheckpoint(ctx context.Context, checkpoint work.InvestigationCheckpoint) error {
if !checkpoint.Valid() {
return errors.New("invalid investigation checkpoint")
}
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err = tx.Exec(ctx, `INSERT INTO work.investigation_checkpoints(checkpoint_id,investigation_id,completed_attempt_refs,completed_finding_refs,next_step,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(checkpoint_id) DO NOTHING`, checkpoint.ID, checkpoint.InvestigationID, stringIDs(checkpoint.CompletedAttemptRefs), stringEvidence(checkpoint.CompletedFindingRefs), checkpoint.NextStep, checkpoint.RecordedAtUnixMillis); err != nil {
return err
}
for _, attempt := range checkpoint.CompletedAttemptRefs {
if _, err = tx.Exec(ctx, `INSERT INTO work.investigation_attempts(investigation_id,attempt_ref,verified_at_unix_millis) VALUES($1,$2,$3) ON CONFLICT(investigation_id,attempt_ref) DO NOTHING`, checkpoint.InvestigationID, attempt, checkpoint.RecordedAtUnixMillis); err != nil {
return err
}
}
return tx.Commit(ctx)
}
func (r *WorkRepository) UnverifiedInvestigationAttempts(ctx context.Context, id work.LongRunningWorkID, candidates []work.AgentRunID) ([]work.AgentRunID, error) {
result := make([]work.AgentRunID, 0, len(candidates))
for _, candidate := range candidates {
var exists bool
if err := r.pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM work.investigation_attempts WHERE investigation_id=$1 AND attempt_ref=$2)`, id, candidate).Scan(&exists); err != nil {
return nil, err
}
if !exists {
result = append(result, candidate)
}
}
return result, nil
}
func stringIDs(values []work.AgentRunID) []string {
result := make([]string, 0, len(values))
for _, value := range values {
result = append(result, string(value))
}
return result
}
func stringEvidence(values []work.EvidenceRef) []string {
result := make([]string, 0, len(values))
for _, value := range values {
result = append(result, string(value))
}
return result
}
func OpenWork(ctx context.Context, config ContinuityConfig) (*WorkRepository, error) {
pool, err := openPool(ctx, config)
if err != nil {
+108
View File
@@ -0,0 +1,108 @@
package postgres
import (
"context"
"encoding/json"
"fmt"
"git.cin.su/m/Assistant/contracts/world"
worlddomain "git.cin.su/m/Assistant/modules/world/domain"
"github.com/jackc/pgx/v5/pgxpool"
)
type WorldRepository struct{ pool *pgxpool.Pool }
func OpenWorld(ctx context.Context, config ContinuityConfig) (*WorldRepository, error) {
pool, err := openPool(ctx, config)
if err != nil {
return nil, err
}
return &WorldRepository{pool: pool}, nil
}
func (r *WorldRepository) Close() { r.pool.Close() }
func (r *WorldRepository) SaveKindDefinition(ctx context.Context, value world.WorldObjectKindDefinition) error {
if err := worlddomain.ValidateKindDefinition(value); err != nil {
return err
}
var parentOwner, parentKind, parentID string
var parentRevision *uint64
if value.ParentKindRef != nil {
parentOwner, parentKind, parentID = value.ParentKindRef.OwnerNamespace, value.ParentKindRef.SemanticKind, string(value.ParentKindRef.StableID)
if value.ParentKindRef.Revision != nil {
revision := uint64(*value.ParentKindRef.Revision)
parentRevision = &revision
}
}
_, err := r.pool.Exec(ctx, `INSERT INTO world.kind_definitions(kind_definition_id,kind_key,revision,display_name,parent_owner_namespace,parent_semantic_kind,parent_stable_id,parent_revision,lifecycle,definition_digest,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT(kind_definition_id) DO NOTHING`, value.ID, value.KindKey, value.Revision, value.DisplayName, parentOwner, parentKind, parentID, parentRevision, value.Lifecycle, value.DefinitionDigest, value.RecordedAt)
return err
}
func (r *WorldRepository) SaveObject(ctx context.Context, value world.WorldObject) error {
if err := worlddomain.ValidateObject(value); err != nil {
return err
}
_, err := r.pool.Exec(ctx, `INSERT INTO world.objects(world_object_id,principal_id,kind_owner_namespace,kind_semantic_kind,kind_stable_id,kind_revision,lifecycle,created_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(world_object_id) DO NOTHING`, value.ID, value.PrincipalID, value.KindDefinitionRef.OwnerNamespace, value.KindDefinitionRef.SemanticKind, value.KindDefinitionRef.StableID, value.KindRevision, value.Lifecycle, value.CreatedAt)
return err
}
func (r *WorldRepository) SaveAlias(ctx context.Context, value world.WorldAlias) error {
if err := worlddomain.ValidateAlias(value); err != nil {
return err
}
_, err := r.pool.Exec(ctx, `INSERT INTO world.aliases(world_alias_id,principal_id,world_object_id,alias,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5) ON CONFLICT(world_alias_id) DO NOTHING`, value.ID, value.PrincipalID, value.ObjectID, value.Alias, value.RecordedAt)
return err
}
func (r *WorldRepository) SaveFacetDefinition(ctx context.Context, value world.FacetDefinition) error {
if err := worlddomain.ValidateFacetDefinition(value); err != nil {
return err
}
payload, err := json.Marshal(value.ValueType)
if err != nil {
return fmt.Errorf("encode facet value type: %w", err)
}
_, err = r.pool.Exec(ctx, `INSERT INTO world.facet_definitions(facet_definition_id,kind_key,revision,facet_key,value_type,definition_digest,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(facet_definition_id) DO NOTHING`, value.ID, value.KindKey, value.Revision, value.FacetKey, payload, value.DefinitionDigest, value.RecordedAt)
return err
}
func (r *WorldRepository) SaveRelationDefinition(ctx context.Context, value world.RelationDefinition) error {
if err := worlddomain.ValidateRelationDefinition(value); err != nil {
return err
}
_, err := r.pool.Exec(ctx, `INSERT INTO world.relation_definitions(relation_definition_id,kind_key,revision,relation_key,target_kind_key,definition_digest,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(relation_definition_id) DO NOTHING`, value.ID, value.KindKey, value.Revision, value.RelationKey, value.TargetKindKey, value.DefinitionDigest, value.RecordedAt)
return err
}
func (r *WorldRepository) SaveResolution(ctx context.Context, value world.IdentityResolutionDecisionRecord) error {
if err := worlddomain.ValidateResolution(value); err != nil {
return err
}
candidates, err := json.Marshal(value.Candidates)
if err != nil {
return fmt.Errorf("encode resolution candidates: %w", err)
}
evidence, err := json.Marshal(value.Evidence)
if err != nil {
return fmt.Errorf("encode resolution evidence: %w", err)
}
_, err = r.pool.Exec(ctx, `INSERT INTO world.identity_resolution_decisions(resolution_decision_id,principal_id,candidates,decision,evidence,confidence_millis,decided_at_unix_millis,revision) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(resolution_decision_id) DO NOTHING`, value.ID, value.PrincipalID, candidates, value.Decision, evidence, value.ConfidenceMillis, value.DecidedAt, value.Revision)
return err
}
func (r *WorldRepository) SaveLineage(ctx context.Context, value world.WorldLineage) error {
if err := worlddomain.ValidateLineage(value); err != nil {
return err
}
sources, err := json.Marshal(value.SourceRefs)
if err != nil {
return fmt.Errorf("encode lineage sources: %w", err)
}
successors, err := json.Marshal(value.SuccessorRefs)
if err != nil {
return fmt.Errorf("encode lineage successors: %w", err)
}
_, err = r.pool.Exec(ctx, `INSERT INTO world.lineage(lineage_id,operation,source_refs,successor_refs,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5) ON CONFLICT(lineage_id) DO NOTHING`, value.ID, value.Operation, sources, successors, value.RecordedAt)
return err
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,45 @@
# ADR-0020 — Architecture Generation 2 governance adoption
**Status:** Accepted by `ARCH-GEN2-000`
**Date:** 2026-08-29
**Scope:** Architecture and governance only; no runtime or schema migration.
## Decision
Assistant adopts the documents under `docs/DOMOVOY_CONSTITUTION.md`,
`architecture/GEN2_TARGET_ARCHITECTURE.md`,
`architecture/GEN2_CANONICAL_DATA_MODEL.md`,
`development/GEN2_MIGRATION_MAP.md`,
`development/GEN2_MASTER_PLAN.md`, and the execution contract as the
canonical Architecture Generation 2 governance basis.
The existing V1 implementation remains the rollback and compatibility
baseline. This decision authorizes subsequent, separately accepted Gen2
packets to introduce the World, Configuration, and SELF bounded owners and
Gen2 migrations, subject to explicit ownership, schema, contract, and
rollback decisions in those packets. It does not create those owners or
change canonical runtime state in this task.
## Authority and execution order
The Gen2 constitution and accepted architecture decisions govern the Gen2
program. The target architecture, canonical data model, migration map,
master plan, execution contract, task index, and individual task packet are
interpreted in that order. A lower-level packet cannot silently weaken a
higher-level invariant or reorder the task index.
Every Gen2 task starts from `gen2-integration`, uses the active task pointer,
and passes the declared FAST/MILESTONE/FULL gate before merge. `ARCH-GEN2-000`
is the architecture adoption root accepted against the PRE baseline;
`GEN2-001` and later tasks are not accepted by this decision and require
separate execution.
## Invariants preserved
- No new canonical owner, cross-module dependency, privilege, database role,
third-party dependency, runtime writer, event, or migration is introduced.
- Existing Authority → Commit → Executor boundaries remain mandatory.
- Memory remains a rebuildable retrieval fabric, World is not a generic truth
owner, and SELF is not a monolithic orchestrator.
- The `domovoy-pre-gen2` baseline tag remains immutable and is the rollback
point until a later migration task explicitly establishes another one.
+53 -1
View File
@@ -1,7 +1,7 @@
{
"registry_version": 1,
"semantic_generation": 1,
"architecture_generation": 1,
"architecture_generation": 2,
"modules": {
"identity": {
"id": "mod.identity",
@@ -179,6 +179,30 @@
"database_schema": "interaction",
"boundary_class": "remote_capable",
"may_depend_on": ["contracts.identity", "contracts.situation", "contracts.epistemics", "contracts.cognitive", "platform.ids", "platform.events"]
},
"world": {
"id": "mod.world",
"owns": ["WorldObject", "WorldObjectKindDefinition", "WorldAlias", "FacetDefinition", "RelationDefinition", "IdentityResolutionCandidate", "IdentityResolutionDecision", "WorldLineage"],
"database_schema": "world",
"boundary_class": "remote_capable",
"may_depend_on": ["contracts.identity", "platform.ids", "platform.events"],
"forbidden": ["adapters.models", "adapters.temporal", "claims", "beliefs", "observations"]
},
"configuration": {
"id": "mod.configuration",
"owns": ["SettingDefinition", "SettingAssignment", "ConfigurationRevision", "ConfigurationChangeProvenance"],
"database_schema": "configuration",
"boundary_class": "remote_capable",
"may_depend_on": ["contracts.identity", "contracts.authority", "platform.ids", "platform.events", "platform.secrets"],
"forbidden": ["secret_values", "arbitrary_key_value_without_definition"]
},
"self": {
"id": "mod.self",
"owns": ["CompetenceAssessment", "WeaknessPattern", "DependencyAssessment", "LearningLedgerEntry", "ImprovementNeed", "SelfVersionView", "SelfEvaluationInterpretation"],
"database_schema": "self",
"boundary_class": "remote_capable",
"may_depend_on": ["contracts.capability", "contracts.epistemics", "contracts.artifact", "platform.ids", "platform.events"],
"forbidden": ["adapters.models", "tasks", "authority_grants", "code_release_switching"]
}
},
"platform": {
@@ -244,6 +268,26 @@
"cross_owner_write": "forbidden",
"cross_owner_application_join": "forbidden"
},
"runtime_topology_v2": {
"core_runtime": {
"modules": ["identity", "situation", "work", "epistemics", "capability", "artifact", "interaction", "world", "configuration", "self"]
},
"continuity_runtime": {"modules": ["continuity"]},
"commit_runtime": {"modules": ["authority", "commit"]},
"cognitive_workers": {"modules": [], "role": "replaceable reasoning/context workers; no canonical ownership"},
"supervisor_runtime": {"modules": [], "role": "release health, switch, rollback and recovery; not a semantic owner"}
},
"storage_topology_v2": {
"postgres_cluster": "assistant_primary",
"schemas": {
"identity": "identity_role", "authority": "authority_role", "situation": "situation_role", "work": "work_role",
"epistemics": "epistemics_role", "continuity": "continuity_role", "capability": "capability_role", "commit": "commit_role",
"artifact": "artifact_role", "interaction": "interaction_role", "world": "world_role", "configuration": "configuration_role", "self": "self_role"
},
"cross_owner_fk": "forbidden_by_default",
"cross_owner_write": "forbidden",
"cross_owner_application_join": "forbidden"
},
"invariants": [
{
"id": "INV-AUTH-001",
@@ -309,6 +353,14 @@
".gitea/workflows/ci.yml",
"cmd/devctl/**",
"dev/ACTIVE_TASK.json",
"docs/DOMOVOY_CONSTITUTION.md",
"docs/DOMOVOY_GEN2_GAP_MAP.md",
"architecture/GEN2_TARGET_ARCHITECTURE.md",
"architecture/GEN2_CANONICAL_DATA_MODEL.md",
"development/GEN2_MIGRATION_MAP.md",
"development/GEN2_MASTER_PLAN.md",
"development/GEN2_EXECUTION_CONTRACT.md",
"development/GEN2_TASK_INDEX.md",
"security/**"
],
"default_feature_task_authority": {
+67 -1
View File
@@ -1,6 +1,6 @@
registry_version: 1
semantic_generation: 1
architecture_generation: 1
architecture_generation: 2
modules:
identity:
@@ -75,6 +75,30 @@ modules:
boundary_class: remote_capable
may_depend_on: [contracts.identity, contracts.situation, contracts.epistemics, contracts.cognitive, platform.ids, platform.events]
world:
id: mod.world
owns: [WorldObject, WorldObjectKindDefinition, WorldAlias, FacetDefinition, RelationDefinition, IdentityResolutionCandidate, IdentityResolutionDecision, WorldLineage]
database_schema: world
boundary_class: remote_capable
may_depend_on: [contracts.identity, platform.ids, platform.events]
forbidden: [adapters.models, adapters.temporal, claims, beliefs, observations]
configuration:
id: mod.configuration
owns: [SettingDefinition, SettingAssignment, ConfigurationRevision, ConfigurationChangeProvenance]
database_schema: configuration
boundary_class: remote_capable
may_depend_on: [contracts.identity, contracts.authority, platform.ids, platform.events, platform.secrets]
forbidden: [secret_values, arbitrary_key_value_without_definition]
self:
id: mod.self
owns: [CompetenceAssessment, WeaknessPattern, DependencyAssessment, LearningLedgerEntry, ImprovementNeed, SelfVersionView, SelfEvaluationInterpretation]
database_schema: self
boundary_class: remote_capable
may_depend_on: [contracts.capability, contracts.epistemics, contracts.artifact, platform.ids, platform.events]
forbidden: [adapters.models, tasks, authority_grants, code_release_switching]
platform:
allowed_packages:
- platform.ids
@@ -98,6 +122,20 @@ runtime_topology_v1:
modules: []
role: "replaceable cognition/context compilation workers; no canonical ownership"
runtime_topology_v2:
core_runtime:
modules: [identity, situation, work, epistemics, capability, artifact, interaction, world, configuration, self]
continuity_runtime:
modules: [continuity]
commit_runtime:
modules: [authority, commit]
cognitive_workers:
modules: []
role: "replaceable reasoning/context workers; no canonical ownership"
supervisor_runtime:
modules: []
role: "release health, switch, rollback and recovery; not a semantic owner"
storage_topology_v1:
postgres_cluster: assistant_primary
schemas:
@@ -115,6 +153,26 @@ storage_topology_v1:
cross_owner_write: forbidden
cross_owner_application_join: forbidden
storage_topology_v2:
postgres_cluster: assistant_primary
schemas:
identity: identity_role
authority: authority_role
situation: situation_role
work: work_role
epistemics: epistemics_role
continuity: continuity_role
capability: capability_role
commit: commit_role
artifact: artifact_role
interaction: interaction_role
world: world_role
configuration: configuration_role
self: self_role
cross_owner_fk: forbidden_by_default
cross_owner_write: forbidden
cross_owner_application_join: forbidden
invariants:
- id: INV-AUTH-001
property: "No external mutation without current valid authority and target/revision checks"
@@ -149,6 +207,14 @@ protected_paths:
- .gitea/workflows/ci.yml
- cmd/devctl/**
- dev/ACTIVE_TASK.json
- docs/DOMOVOY_CONSTITUTION.md
- docs/DOMOVOY_GEN2_GAP_MAP.md
- architecture/GEN2_TARGET_ARCHITECTURE.md
- architecture/GEN2_CANONICAL_DATA_MODEL.md
- development/GEN2_MIGRATION_MAP.md
- development/GEN2_MASTER_PLAN.md
- development/GEN2_EXECUTION_CONTRACT.md
- development/GEN2_TASK_INDEX.md
- security/**
default_feature_task_authority:
+6 -1
View File
@@ -20,6 +20,7 @@ type fleetEndpoint struct {
URL string
Headers map[string]string
Client *http.Client
Timeout time.Duration
}
type fleetStatusResponse struct {
@@ -63,7 +64,11 @@ func probeFleetEndpoint(parent context.Context, fallbackClient *http.Client, end
if strings.TrimSpace(endpoint.URL) == "" {
return member
}
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
timeout := endpoint.Timeout
if timeout <= 0 {
timeout = 2 * time.Second
}
ctx, cancel := context.WithTimeout(parent, timeout)
defer cancel()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.URL, nil)
if err != nil {
+18
View File
@@ -68,3 +68,21 @@ func TestFleetReadinessSendsConfiguredProviderHeaders(t *testing.T) {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
}
func TestFleetReadinessUsesProviderSpecificTimeout(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(40 * time.Millisecond)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
handler := fleetReadinessHandler(server.Client(), capability.FleetTierMedium, []fleetEndpoint{{
ProviderID: "medium", ModelID: "medium-model", Tier: capability.FleetTierMedium,
URL: server.URL, Client: server.Client(), Timeout: 100 * time.Millisecond,
}})
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/v1/fleet/readiness", nil))
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"selected_tier":"MEDIUM"`) {
t.Fatalf("provider-specific timeout was not honored: status=%d body=%s", recorder.Code, recorder.Body.String())
}
}
+16 -6
View File
@@ -71,7 +71,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
mediumTimeout := durationEnv("LOCAL_MEDIUM_PROVIDER_TIMEOUT", 60*time.Second)
mediumTimeout := durationEnv("LOCAL_MEDIUM_PROVIDER_TIMEOUT", 120*time.Second)
mediumAdapter, adapterErr := models.NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout(strings.TrimRight(mediumURL, "/"), mediumAPIKey, nil, mediumTimeout)
if adapterErr != nil {
log.Fatal(adapterErr)
@@ -117,7 +117,8 @@ func main() {
mux.Handle("/healthz", health.Handler{Runtime: "cognitive-worker", SemanticGeneration: 1, ArchitectureGeneration: 1})
readinessClient := &http.Client{Timeout: 2 * time.Second}
cpuReadinessClient := &http.Client{Timeout: 10 * time.Second}
mediumReadinessClient := &http.Client{Timeout: 10 * time.Second}
mediumReadinessTimeout := 120 * time.Second
mediumReadinessClient := &http.Client{Timeout: mediumReadinessTimeout}
lifeboatURL := strings.TrimSuffix(strings.TrimRight(envOr("LOCAL_LIFEBOAT_PROVIDER_URL", "http://cognitive-lifeboat:8086/v1"), "/"), "/v1") + "/health"
fleetClient := &http.Client{Timeout: 2 * time.Second}
preferredFleet := capability.FleetTier(envOr("COGNITIVE_PREFERRED_FLEET_TIER", string(capability.FleetTierFast)))
@@ -131,14 +132,21 @@ func main() {
// readiness budget short; their detailed fleet state is reported separately.
cpuHealthProbe.Timeout = 2 * time.Second
mediumEndpoint := ""
mediumHealthEndpoint := ""
mediumHeaders := map[string]string{}
mediumHealthProbe := health.Probe{}
if mediumURL != "" {
mediumEndpoint = strings.TrimRight(mediumURL, "/") + "/models"
mediumHealthEndpoint = strings.TrimSuffix(strings.TrimRight(mediumURL, "/"), "/v1") + "/health"
if mediumAPIKey != "" {
mediumHeaders["Authorization"] = "Bearer " + mediumAPIKey
}
mediumHealthProbe = health.OptionalHTTPProbeWithHeaders(mediumReadinessClient, mediumEndpoint, mediumHeaders)
// Readiness reports provider discovery health quickly; fleet readiness
// separately reports authenticated model availability on /v1/models.
mediumHealthProbe = health.OptionalHTTPProbeWithHeaders(readinessClient, mediumHealthEndpoint, mediumHeaders)
// The local gateway may need several seconds to enumerate a loaded model.
// Keep this bounded, but do not classify a healthy MEDIUM provider as
// unavailable before its authenticated discovery response can arrive.
mediumHealthProbe.Timeout = 2 * time.Second
}
readinessProbes := []health.Probe{
@@ -157,8 +165,8 @@ func main() {
}
fleetEndpoints := []fleetEndpoint{
{ProviderID: "local-cpu-primary", ModelID: "local-cpu-model", Tier: capability.FleetTierFast, URL: cpuEndpoint, Headers: cpuFleetHeaders, Client: &http.Client{Timeout: 10 * time.Second}},
{ProviderID: "local-medium-primary", ModelID: "local-medium-model", Tier: capability.FleetTierMedium, URL: mediumEndpoint, Headers: mediumHeaders, Client: mediumReadinessClient},
{ProviderID: "local-cpu-primary", ModelID: "local-cpu-model", Tier: capability.FleetTierFast, URL: cpuEndpoint, Headers: cpuFleetHeaders, Client: &http.Client{Timeout: 10 * time.Second}, Timeout: 2 * time.Second},
{ProviderID: "local-medium-primary", ModelID: "local-medium-model", Tier: capability.FleetTierMedium, URL: mediumEndpoint, Headers: mediumHeaders, Client: mediumReadinessClient, Timeout: mediumReadinessTimeout},
{ProviderID: "cognitive-lifeboat", ModelID: "qwen3-8b-lifeboat", Tier: capability.FleetTierLifeboat, URL: lifeboatURL},
}
if gpuConfigured {
@@ -217,7 +225,9 @@ func durationEnv(name string, fallback time.Duration) time.Duration {
func refreshProviderDiscoveries(resolver bindingResolver, service discoveryService) {
for name := range service.providers {
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
// Keep startup discovery bounded while allowing the authenticated local
// gateway enough time to enumerate a loaded MEDIUM model.
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
registration, ok := service.providers[name]
if !ok || registration.Discovery == nil {
cancel()
+1 -1
View File
@@ -100,7 +100,7 @@ func (g cognitiveGateway) executeTaskEvaluationCases(ctx context.Context, reques
go func(index int, item capability.TaskEvaluationCase) {
defer wg.Done()
started := time.Now()
invocation := cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(fmt.Sprintf("task-evaluation:%s:%s", request.Profile.ID, item.CaseID)), ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: item.Prompt, MaxOutputBytes: int(request.MaxOutputBytes), ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ProviderModelID: selected.Model.ProviderModelID}
invocation := cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(fmt.Sprintf("task-evaluation:%s:%s", request.Profile.ID, item.CaseID)), ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: item.Prompt, Locale: "en-US", MaxOutputBytes: int(request.MaxOutputBytes), ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ProviderModelID: selected.Model.ProviderModelID}
response, invokeErr := g.invokeAdmitted(ctx, string(invocation.RequestID), provider, invocation, int(request.MaxOutputBytes))
latency := time.Since(started).Microseconds()
observations[index] = capability.TaskCaseObservation{CaseID: item.CaseID, Result: capability.EvaluationUnknown, CriticalFailure: invokeErr != nil || response.FinishReason != cognitive.FinishCompleted || strings.TrimSpace(response.AdvisoryText) == "", EvidenceArtifactRef: item.EvidenceArtifactRef, LatencyMicros: latency}
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
@@ -57,6 +58,30 @@ func TestTaskEvaluationHandlerInvokesEveryBoundedCase(t *testing.T) {
}
}
type localeCheckingProvider struct{ stubAdvisoryProvider }
func (p *localeCheckingProvider) Advise(ctx context.Context, request cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error) {
if request.Locale == "" {
return cognitive.AdvisoryResponse{}, fmt.Errorf("task evaluation omitted required locale")
}
return p.stubAdvisoryProvider.Advise(ctx, request)
}
func TestTaskEvaluationSuppliesLocaleToProviderAdapter(t *testing.T) {
selected := gatewaySelection()
provider := &localeCheckingProvider{stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, FinishReason: cognitive.FinishCompleted, AdvisoryText: "observed response"}}}
registry := &candidateRegistryStub{selected: selected}
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{selected.Provider.Kind: {{ProviderInstanceID: selected.Provider.ID, Kind: selected.Provider.Kind, EndpointRef: selected.Provider.EndpointRef, CredentialRef: selected.Provider.CredentialRef, Provider: provider}}}, now: time.Now}
profile := capability.TaskEvaluationProfile{ID: "profile-locale", Revision: 1, CapabilityID: selected.Binding.CapabilityID, Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "locale-v1", RequiredCaseIDs: []string{"case-locale"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100}
request := capability.TaskEvaluationExecutionRequest{Profile: profile, CapabilityID: selected.Binding.CapabilityID, BindingID: selected.Binding.ID, BindingRevision: selected.Binding.Revision, ActorID: identity.ActorID("actor-1"), PrincipalID: selected.Binding.OwnerPrincipalID, ScopeID: authority.ScopeID("scope-1"), MaxOutputBytes: 512, EgressAcknowledged: true, Cases: []capability.TaskEvaluationCase{{CaseID: "case-locale", Prompt: "one", EvidenceArtifactRef: "artifact:locale"}}}
payload, _ := json.Marshal(request)
recorder := httptest.NewRecorder()
gateway.taskEvaluationHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/task-evaluations/execute", bytes.NewReader(payload)))
if recorder.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
}
type taskEvaluationFailoverRegistry struct {
primary, fallback capability.ResolvedCognitiveBinding
resolveCalls int
+13 -4
View File
@@ -28,6 +28,15 @@ type cognitiveInterpretationClient struct {
media *http.Client
}
const (
// Household interpretation and response review are intentionally compact
// calls. Keeping their output bounded prevents a loaded local model from
// spending the whole request budget generating unnecessary prose.
responseReviewMaxOutputBytes = 512
stableKnowledgeMaxOutputBytes = 512
semanticInterpretMaxOutputBytes = 1024
)
// ReviewResponse asks the cognitive gateway to turn a locally assembled draft
// into a natural answer. The draft remains the safe fallback when cognition is
// unavailable; the model does not mutate canonical state or execute actions.
@@ -47,7 +56,7 @@ func (c cognitiveInterpretationClient) ReviewResponse(ctx context.Context, sessi
Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID),
ActorID: session.ActorID, PrincipalID: session.PrincipalID,
ScopeID: authority.ScopeID("domovoy-global-scope"), Message: prompt,
Context: context, Locale: "ru", MaxOutputBytes: 2048, DataPolicy: capability.DataPolicyLocalOnly,
Context: context, Locale: "ru", MaxOutputBytes: responseReviewMaxOutputBytes, DataPolicy: capability.DataPolicyLocalOnly,
Meaning: cognitive.TaskMeaning{
SpeechAct: "RESPONSE_REVIEW",
CapabilityHints: meaning.CapabilityHints,
@@ -91,7 +100,7 @@ func (c cognitiveInterpretationClient) AnswerStableKnowledge(ctx context.Context
if err != nil {
return "", err
}
prompt := "Ты отвечаешь как Домовой на обычный вопрос пользователя. Сначала пойми цель вопроса и верни короткий естественный ответ по устойчивому общеизвестному знанию. Начинай сразу с ответа и не представляйся, если пользователь не спросил об имени. Не называй модель или внутреннюю систему. Если истинность ответа зависит от текущего наблюдения, внешнего источника или инструмента, верни ровно NEEDS_VERIFIED_RESEARCH. Не придумывай факты.\n\nВопрос пользователя:\n" + userMessage
prompt := "Ты отвечаешь как Домовой на обычное сообщение пользователя. Сначала пойми цель: на вопрос дай короткий естественный ответ по устойчивому общеизвестному знанию, а безопасную просьбу создать текстовый или структурный результат выполни прямо в ответе. Начинай сразу с содержания и не представляйся, если пользователь не спросил об имени. Не называй модель или внутреннюю систему. Если истинность ответа зависит от текущего наблюдения, внешнего источника или инструмента, верни ровно NEEDS_VERIFIED_RESEARCH. Не придумывай факты и не утверждай выполнение внешнего действия.\n\nСообщение пользователя:\n" + userMessage
context := []cognitive.ContextExcerpt{}
if strings.TrimSpace(personalMemory) != "" {
context = append(context, cognitive.ContextExcerpt{ID: "personal-memory", SourceKind: "derived-personal-memory", SourceID: "interaction:human-utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: personalMemory})
@@ -101,7 +110,7 @@ func (c cognitiveInterpretationClient) AnswerStableKnowledge(ctx context.Context
payload, err := json.Marshal(cognitive.GatewayAdvisoryRequest{
Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID),
ActorID: session.ActorID, PrincipalID: session.PrincipalID, ScopeID: authority.ScopeID("domovoy-global-scope"),
Message: prompt, Context: context, Locale: "ru", MaxOutputBytes: 1024, DataPolicy: capability.DataPolicyLocalOnly,
Message: prompt, Context: context, Locale: "ru", MaxOutputBytes: stableKnowledgeMaxOutputBytes, DataPolicy: capability.DataPolicyLocalOnly,
Meaning: cognitive.TaskMeaning{SpeechAct: "STABLE_KNOWLEDGE_ANSWER", CapabilityHints: meaning.CapabilityHints, RequestedOutcome: meaning.RequestedOutcome, InformationRequestKind: meaning.InformationRequestKind, ConfidenceMillis: meaning.ConfidenceMillis},
})
if err != nil {
@@ -294,7 +303,7 @@ func buildGatewayInterpretationRequest(session identity.CreatorSession, utteranc
ScopeID: authority.ScopeID("domovoy-global-scope"),
Message: utterance.Content,
Locale: "ru",
MaxOutputBytes: 4096,
MaxOutputBytes: semanticInterpretMaxOutputBytes,
DataPolicy: capability.DataPolicyLocalOnly,
DeterministicSpeechAct: string(semantic.Interpretation.SpeechAct),
DeterministicSecondary: string(semantic.Interpretation.SecondarySpeechAct),
@@ -0,0 +1,46 @@
package main
import "testing"
func TestShouldInvokeHumanResponseSkipsDeterministicFastPaths(t *testing.T) {
tests := []struct {
name string
stableConversational bool
identityReply string
memoryInstructionReply string
want bool
}{
{name: "stable conversational", stableConversational: true},
{name: "identity", identityReply: "Меня зовут Домовой."},
{name: "memory instruction", memoryInstructionReply: "Запомнил."},
{name: "ordinary continuity", want: true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := shouldInvokeHumanResponse(test.stableConversational, test.identityReply, test.memoryInstructionReply); got != test.want {
t.Fatalf("shouldInvokeHumanResponse() = %t, want %t", got, test.want)
}
})
}
}
func TestShouldReviewResponseSkipsStableAnswers(t *testing.T) {
if shouldReviewResponse("Здравствуйте.", false, true, "", "", false) {
t.Fatal("stable conversational reply must not wait for response review")
}
if shouldReviewResponse("Меня зовут Домовой.", false, false, "Меня зовут Домовой.", "", false) {
t.Fatal("identity reply must not wait for response review")
}
if shouldReviewResponse("Запомнил.", false, false, "", "Запомнил.", false) {
t.Fatal("memory instruction reply must not wait for response review")
}
if shouldReviewResponse("Вы называли тестовый цвет: синий.", false, false, "", "", true) {
t.Fatal("deterministic memory reply must not wait for response review")
}
if !shouldReviewResponse("Ответ", false, false, "", "", false) {
t.Fatal("non-conversational answer remains reviewable")
}
if shouldReviewResponse("Ответ", true, false, "", "", false) {
t.Fatal("stable knowledge answer must not be reviewed twice")
}
}
+6
View File
@@ -13,6 +13,7 @@ import (
"git.cin.su/m/Assistant/contracts/interaction"
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
epistemicsdomain "git.cin.su/m/Assistant/modules/epistemics/domain"
"git.cin.su/m/Assistant/platform/telemetry"
)
const reminderResponseSkillID = capability.SkillProfileID("skill-obvious-reminder-response-v1")
@@ -160,6 +161,7 @@ func (r learningRecorder) recordFastPathDecline(ctx context.Context, input fastP
if r.epistemics == nil || input.Intent.ID == "" || input.Utterance.ID == "" {
return
}
telemetry.RecordCompatibility("fast-path-v1", "core-runtime", "decision", "decline-or-escalate", true, true, false, "OBSERVED", string(input.Utterance.CorrelationID), string(input.Utterance.PrincipalID), input.At)
experience, err := epistemicsdomain.FastPathDeclineExperience(
epistemics.OriginalIntentRef(input.Intent.ID),
epistemics.SourceUtteranceRef(input.Utterance.ID),
@@ -273,9 +275,11 @@ func (r learningRecorder) updateReminderSkill(ctx context.Context, experience ep
}
profile, err := r.capability.LoadSkillProfile(ctx, reminderResponseSkillID)
if err != nil {
telemetry.RecordCompatibility("skill-profile-v1", "core-runtime", "read", "fast-path-learning", true, false, true, "FAILURE", string(experience.OriginalIntentID), string(experience.SourceUtteranceID), time.UnixMilli(experience.RecordedAtUnixMillis))
log.Printf("learning skill profile unavailable: %v", err)
return
}
telemetry.RecordCompatibility("skill-profile-v1", "core-runtime", "read", "fast-path-learning", true, false, true, "SUCCESS", string(experience.OriginalIntentID), string(experience.SourceUtteranceID), time.UnixMilli(experience.RecordedAtUnixMillis))
next, err := capabilitydomain.ObserveSkillOutcome(profile, capability.SkillOutcomeObservation{
ExperienceRef: capability.SkillEvidenceRef("epistemics:experience:" + string(experience.ID)),
Outcome: outcome,
@@ -287,9 +291,11 @@ func (r learningRecorder) updateReminderSkill(ctx context.Context, experience ep
return
}
if err := r.capability.SaveSkillProfile(ctx, next); err != nil {
telemetry.RecordCompatibility("skill-profile-v1", "core-runtime", "write", "fast-path-learning", true, false, true, "FAILURE", string(experience.OriginalIntentID), string(experience.SourceUtteranceID), time.UnixMilli(experience.RecordedAtUnixMillis))
log.Printf("learning skill profile save unavailable: %v", err)
return
}
telemetry.RecordCompatibility("skill-profile-v1", "core-runtime", "write", "fast-path-learning", true, false, true, "SUCCESS", string(experience.OriginalIntentID), string(experience.SourceUtteranceID), time.UnixMilli(experience.RecordedAtUnixMillis))
}
func outcomeForHumanResponse(response continuity.HumanResponse) (epistemics.OutcomeClass, int64) {
+275 -53
View File
@@ -24,6 +24,7 @@ import (
workdomain "git.cin.su/m/Assistant/modules/work/domain"
"git.cin.su/m/Assistant/platform/ids"
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
"git.cin.su/m/Assistant/platform/telemetry"
"git.cin.su/m/Assistant/runtime/health"
"github.com/jackc/pgx/v5"
"io"
@@ -37,6 +38,14 @@ import (
"time"
)
const (
creatorPasswordPath = "/run/secrets/creator_password"
creatorBootstrapTokenPath = "/run/secrets/creator_bootstrap_token"
creatorSessionKeyPath = "/run/secrets/creator_session_signing_key"
epistemicsDBPasswordPath = "/run/secrets/epistemics_db_password"
interactionDBPasswordPath = "/run/secrets/interaction_db_password"
)
const maxCreatorCredentialsBytes = 4 << 10
func main() {
@@ -44,20 +53,21 @@ func main() {
if addr == "" {
addr = ":8080"
}
bootstrapToken, err := readRequiredSecret("CREATOR_BOOTSTRAP_TOKEN")
bootstrapToken, err := readRequiredSecretFile("CREATOR_BOOTSTRAP_TOKEN", creatorBootstrapTokenPath)
if err != nil {
log.Fatal(err)
}
sessionKeyValue, err := readRequiredSecret("CREATOR_SESSION_SIGNING_KEY")
if err != nil {
log.Fatal(err)
}
creatorPassword, err := readRequiredSecret("CREATOR_PASSWORD")
sessionKeyValue, err := readRequiredSecretFile("CREATOR_SESSION_SIGNING_KEY", creatorSessionKeyPath)
if err != nil {
log.Fatal(err)
}
sessionKey := []byte(sessionKeyValue)
creatorUsername := os.Getenv("CREATOR_USERNAME")
if creatorUsername == "" {
log.Fatal("CREATOR_USERNAME is required")
}
telemetry.RecordCompatibility("non-bootstrap-env-consumers", "core-runtime", "read", "runtime-startup-configuration", true, true, true, "OBSERVED", "startup", "core-runtime", time.Now())
telemetry.RecordCompatibility("migration-compatibility-switches", "core-runtime", "observe", "runtime-startup-switch-scan", true, false, false, "NOT_OBSERVED", "startup", "core-runtime", time.Now())
repository, err := postgres.OpenSituation(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("POSTGRES_USER"), Password: os.Getenv("POSTGRES_PASSWORD")})
if err != nil {
log.Fatal(err)
@@ -73,7 +83,7 @@ func main() {
log.Fatal(err)
}
defer capabilityRepository.Close()
epistemicsPassword, err := readCredentialRefSecret("EPISTEMICS_POSTGRES_PASSWORD", "EPISTEMICS_POSTGRES_CREDENTIAL_REF")
epistemicsPassword, err := readCredentialRefSecret("EPISTEMICS_POSTGRES_PASSWORD", "EPISTEMICS_POSTGRES_CREDENTIAL_REF", epistemicsDBPasswordPath)
if err != nil {
log.Fatal(err)
}
@@ -91,7 +101,10 @@ func main() {
log.Fatal(err)
}
defer identityRepository.Close()
interactionPassword, err := readCredentialRefSecret("INTERACTION_POSTGRES_PASSWORD", "INTERACTION_POSTGRES_CREDENTIAL_REF")
if err := bootstrapCreatorAccount(context.Background(), identityRepository, creatorUsername, creatorPasswordPath); err != nil {
log.Fatal(err)
}
interactionPassword, err := readCredentialRefSecret("INTERACTION_POSTGRES_PASSWORD", "INTERACTION_POSTGRES_CREDENTIAL_REF", interactionDBPasswordPath)
if err != nil {
log.Fatal(err)
}
@@ -144,10 +157,16 @@ func main() {
}
mux.Handle("/healthz", health.Handler{Runtime: "core-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
readinessClient := &http.Client{Timeout: 2 * time.Second}
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "core-runtime", Probes: []health.Probe{
cognitiveReadinessClient := &http.Client{Timeout: 135 * time.Second}
cognitiveReadinessProbe := health.HTTPProbe(cognitiveReadinessClient, strings.TrimRight(envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"), "/")+"/readyz")
cognitiveReadinessProbe.Timeout = 135 * time.Second
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "core-runtime", Timeout: 140 * time.Second, Probes: []health.Probe{
health.TCPProbe("postgres", envOr("POSTGRES_HOST", "postgres"), envOr("POSTGRES_PORT", "5432")),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), "/")+"/healthz"),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"), "/")+"/healthz"),
// A running worker is not necessarily ready to serve cognitive traffic.
// Keep deterministic paths alive, but do not advertise overall readiness
// while the cognitive dependency is still warming.
cognitiveReadinessProbe,
}})
admissionPolicy := capability.AdmissionPolicy{ID: envOr("COGNITIVE_ADMISSION_POLICY_ID", "creator-advisory-local-v1"), MaxConcurrent: envInt("COGNITIVE_ADMISSION_MAX_CONCURRENT", 1), MaxAcceptedPerHour: envInt("COGNITIVE_ADMISSION_MAX_ACCEPTED_PER_HOUR", 60), MaxInputBytes: envInt("COGNITIVE_ADMISSION_MAX_INPUT_BYTES", 80<<10), MaxOutputBytes: envInt("COGNITIVE_ADMISSION_MAX_OUTPUT_BYTES", 16<<10)}
mux.HandleFunc("POST /v1/internal/cognitive/admission", func(w http.ResponseWriter, r *http.Request) {
@@ -177,7 +196,39 @@ func main() {
}
w.WriteHeader(http.StatusNoContent)
})
mux.HandleFunc("POST /v1/creator/sessions", creatorSessionHandler(bootstrapToken, creatorUsername, creatorPassword, sessionKey, time.Now))
mux.HandleFunc("POST /v1/creator/sessions", creatorDatabaseSessionHandler(bootstrapToken, creatorUsername, identityRepository, sessionKey, time.Now))
mux.HandleFunc("POST /v1/creator/account/password", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
return
}
var request struct {
CurrentPassword string `json:"current_password"`
NewPassword string `json:"new_password"`
}
decoder := json.NewDecoder(io.LimitReader(r.Body, maxCreatorCredentialsBytes+1))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil || request.CurrentPassword == "" || request.NewPassword == "" {
http.Error(w, "invalid password change request", http.StatusBadRequest)
return
}
account, err := identityRepository.LoadCreatorAccountByBinding(r.Context(), session.ActorID, session.PrincipalID)
if err != nil || !identitydomain.VerifyCreatorPassword(account.PasswordHash, request.CurrentPassword) {
http.Error(w, "current password denied", http.StatusUnauthorized)
return
}
newHash, err := identitydomain.HashCreatorPassword(request.NewPassword)
if err != nil {
http.Error(w, "invalid new password", http.StatusBadRequest)
return
}
if err := identityRepository.UpdateCreatorPassword(r.Context(), session.ActorID, session.PrincipalID, account.Revision, newHash); err != nil {
http.Error(w, "password change conflict", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{"status": "changed"})
})
mux.HandleFunc("GET /v1/creator/sessions/current", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
@@ -505,9 +556,13 @@ func main() {
storedFocus, err := repository.ActiveProjectFocus(r.Context(), session.ActorID, session.PrincipalID)
if err == nil {
previousFocus = &storedFocus
telemetry.RecordCompatibility("active-project-focus-read", "core-runtime", "read", "chat-focus-load", true, false, true, "SUCCESS", utterance.CorrelationID, string(session.PrincipalID), now)
} else if !errors.Is(err, pgx.ErrNoRows) {
telemetry.RecordCompatibility("active-project-focus-read", "core-runtime", "read", "chat-focus-load", true, false, true, "FAILURE", utterance.CorrelationID, string(session.PrincipalID), now)
http.Error(w, "project focus unavailable", http.StatusServiceUnavailable)
return
} else {
telemetry.RecordCompatibility("active-project-focus-read", "core-runtime", "read", "chat-focus-load", true, false, true, "EMPTY", utterance.CorrelationID, string(session.PrincipalID), now)
}
var effectiveTaskMessage = interactiondomain.StripAssistantVocative(utterance.Content)
creativeUserRequest := interactiondomain.IsCreativeRequest(utterance.Content)
@@ -533,8 +588,16 @@ func main() {
log.Printf("interaction principal history loaded: %d utterances", len(recentUserHistory))
}
conversationHistory = append(conversationHistory, recentUserHistory...)
personalMemory := interactiondomain.CompilePersonalMemory(conversationHistory)
if !creativeUserRequest && locationReply == "" {
gen2Context := interactiondomain.BuildScopedGen2Context(conversationHistory, string(session.PrincipalID))
if gen2Context.UsedPersonalMemoryFallback {
telemetry.RecordCompatibility("personal-memory-fallback", "core-runtime", "read", "gen2-context-empty", true, true, false, "USED", utterance.CorrelationID, string(session.PrincipalID), now)
}
retrievedMemoryReply := interactiondomain.RecallPersonalPreference(conversationHistory, effectiveTaskMessage)
stableConversationalReply, stableConversational := interactiondomain.StableConversationalReply(
effectiveTaskMessage,
interactiondomain.ClassifySpeechAct(effectiveTaskMessage, utterance.CorrectsUtteranceID),
)
if !creativeUserRequest && locationReply == "" && !stableConversational && retrievedMemoryReply == "" {
if continuation := interactiondomain.ContinueLocalSearch(effectiveTaskMessage, conversationHistory); continuation != "" {
continuationMessage = continuation
effectiveTaskMessage = continuation
@@ -661,6 +724,14 @@ func main() {
semanticInterpretationRequested = false
semanticReflexAudit = false
}
if stableConversational {
semanticInterpretationRequested = false
semanticReflexAudit = false
}
if retrievedMemoryReply != "" {
semanticInterpretationRequested = false
semanticReflexAudit = false
}
if identityReply != "" || memoryInstructionReply != "" {
semanticReflexAudit = false
}
@@ -684,7 +755,7 @@ func main() {
cognitiveAvailable = false
}
}
if !creativeRequest {
if !creativeRequest && !stableConversational && retrievedMemoryReply == "" {
if travelReply := interactiondomain.TravelSearchClarification(effectiveTaskMessage, conversationHistory); travelReply != "" {
semantic.Interpretation.Clarification = interaction.ClarificationRequired
semantic.Interpretation.Applicability = interaction.SemanticCandidateClarify
@@ -702,12 +773,16 @@ func main() {
http.Error(w, "save chat semantics", http.StatusInternalServerError)
return
}
responseResult, responseErr := responseClient.Respond(r.Context(), r.Header.Get("Authorization"), continuity.HumanResponseRequest{
SourceUtteranceID: string(utterance.ID),
SourceIntentID: string(intent.ID),
Message: effectiveTaskMessage,
RequestedAtUnixMillis: now.UnixMilli(),
})
var responseResult continuity.HumanResponseResult
var responseErr error
if shouldInvokeHumanResponse(stableConversational, identityReply, memoryInstructionReply) {
responseResult, responseErr = responseClient.Respond(r.Context(), r.Header.Get("Authorization"), continuity.HumanResponseRequest{
SourceUtteranceID: string(utterance.ID),
SourceIntentID: string(intent.ID),
Message: effectiveTaskMessage,
RequestedAtUnixMillis: now.UnixMilli(),
})
}
if semantic.Interpretation.SpeechAct == interaction.SpeechActCorrection {
learning.recordCorrection(r.Context(), correctionLearningInput{Utterance: utterance, Intent: intent, At: now})
}
@@ -715,9 +790,11 @@ func main() {
activeFocusProvenance := situation.ProjectResolutionProvenance("")
if semantic.ActiveFocus != nil {
if err := repository.SaveActiveProjectFocus(r.Context(), *semantic.ActiveFocus); err != nil {
telemetry.RecordCompatibility("active-project-focus-write", "core-runtime", "write", "semantic-focus-update", true, false, true, "FAILURE", utterance.CorrelationID, string(session.PrincipalID), now)
http.Error(w, "save project focus", http.StatusInternalServerError)
return
}
telemetry.RecordCompatibility("active-project-focus-write", "core-runtime", "write", "semantic-focus-update", true, false, true, "SUCCESS", utterance.CorrelationID, string(session.PrincipalID), now)
activeFocusName = semantic.ActiveFocus.ProjectDisplayName
activeFocusProvenance = semantic.ActiveFocus.Provenance
} else if len(semantic.Projects) > 0 && semantic.Projects[0].Provenance == situation.ProjectResolutionInheritedFocus && previousFocus != nil {
@@ -733,7 +810,7 @@ func main() {
} else if len(semantic.Projects) > 0 && semantic.Projects[0].Provenance == situation.ProjectResolutionInheritedFocus {
projectID = string(previousFocus.ProjectID)
}
if responseErr == nil && responseResult.Handled {
if responseErr == nil && responseResult.Handled && !stableConversational {
planningReply = responseResult.Reply
needsCognition = false
learning.recordReminderResponse(r.Context(), reminderResponseLearningInput{Utterance: utterance, Intent: intent, Result: responseResult, At: now})
@@ -763,6 +840,12 @@ func main() {
ReceivedAtUnixMillis: utterance.ReceivedAtUnixMillis,
}
helpPath := workdomain.DetermineHelpPath(planningInput)
// Introspection requests are read-only household UX. A cognitive
// candidate must not reinterpret them as an executable delegation.
switch semantic.InformationRequestKind {
case interaction.InformationRequestCapabilityInventory, interaction.InformationRequestLocalCapabilityInventory, interaction.InformationRequestCognitiveModelInventory, interaction.InformationRequestCognitiveCurrentModel, interaction.InformationRequestCognitiveChannelStatus, interaction.InformationRequestCognitiveRecovery, interaction.InformationRequestMemoryInventory:
helpPath = work.HelpPath{}
}
if responseErr == nil && responseResult.Handled {
helpPath = work.HelpPath{}
}
@@ -800,6 +883,10 @@ func main() {
researchBacked := false
researchAttempted := false
stableKnowledgeReply := false
if retrievedMemoryReply != "" {
planningReply = retrievedMemoryReply
needsCognition = false
}
if hasChatRoute(routePlan, chatRouteResearch) && semantic.Interpretation.Clarification != interaction.ClarificationRequired && shouldRunGeneralResearch(semantic.InformationRequestKind) {
researchAttempted = true
if planID, idErr := ids.New(); idErr == nil {
@@ -829,7 +916,7 @@ func main() {
reply, backed := renderResearchReply(searchQuery, result, researchErr)
if !backed && cognitiveAvailable {
answerCtx, cancelAnswer := context.WithTimeout(r.Context(), 12*time.Second)
answer, answerErr := cognitiveClient.AnswerStableKnowledge(answerCtx, session, part, personalMemory.ContextText(), cognitive.TaskMeaning{
answer, answerErr := cognitiveClient.AnswerStableKnowledge(answerCtx, session, part, gen2Context.Text, cognitive.TaskMeaning{
SpeechAct: string(semantic.Interpretation.SpeechAct), CapabilityHints: toCapabilityKinds(semantic.CapabilityHints), RequestedOutcome: semantic.Interpretation.RequestedOutcome, InformationRequestKind: semantic.InformationRequestKind, ConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
})
cancelAnswer()
@@ -860,15 +947,16 @@ func main() {
planningReply = helpPath.Reply
needsCognition = false
}
if !creativeRequest {
if !creativeRequest && !stableConversational && retrievedMemoryReply == "" {
if travelReply := interactiondomain.TravelSearchClarification(effectiveTaskMessage, conversationHistory); travelReply != "" {
planningReply = travelReply
needsCognition = false
}
}
if planningReply == "" && needsCognition && semantic.Interpretation.SpeechAct == interaction.SpeechActQuestion && !hasChatRoute(routePlan, chatRouteResearch) && !creativeRequest {
canGenerateTextReply := shouldGenerateNonMutatingTextDirective(semantic, routePlan, helpPath, creativeRequest) && planningReply == deterministicReply
if (planningReply == "" && needsCognition && semantic.Interpretation.SpeechAct == interaction.SpeechActQuestion || canGenerateTextReply) && !hasChatRoute(routePlan, chatRouteResearch) && !creativeRequest {
answerCtx, cancelAnswer := context.WithTimeout(r.Context(), 12*time.Second)
answer, answerErr := cognitiveClient.AnswerStableKnowledge(answerCtx, session, effectiveTaskMessage, personalMemory.ContextText(), cognitive.TaskMeaning{
answer, answerErr := cognitiveClient.AnswerStableKnowledge(answerCtx, session, effectiveTaskMessage, gen2Context.Text, cognitive.TaskMeaning{
SpeechAct: string(semantic.Interpretation.SpeechAct), CapabilityHints: toCapabilityKinds(semantic.CapabilityHints), RequestedOutcome: semantic.Interpretation.RequestedOutcome, InformationRequestKind: semantic.InformationRequestKind, ConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
})
cancelAnswer()
@@ -989,9 +1077,13 @@ func main() {
planningReply = memoryInstructionReply
needsCognition = false
}
if strings.TrimSpace(planningReply) != "" {
// Stable-knowledge answers have already passed the local cognitive
// route with compact, source-aware context. Re-reviewing the same answer
// serially can exhaust the household request budget on a loaded local
// provider and turn a valid answer into a degraded response.
if shouldReviewResponse(planningReply, stableKnowledgeReply, stableConversational, identityReply, memoryInstructionReply, retrievedMemoryReply != "") {
reviewContext, cancelReview := context.WithTimeout(r.Context(), 12*time.Second)
reviewedReply, reviewErr := cognitiveClient.ReviewResponse(reviewContext, session, utterance.Content, planningReply, personalMemory.ContextText(), cognitive.TaskMeaning{
reviewedReply, reviewErr := cognitiveClient.ReviewResponse(reviewContext, session, utterance.Content, planningReply, gen2Context.Text, cognitive.TaskMeaning{
SpeechAct: string(semantic.Interpretation.SpeechAct),
CapabilityHints: toCapabilityKinds(semantic.CapabilityHints),
RequestedOutcome: semantic.Interpretation.RequestedOutcome,
@@ -1015,13 +1107,8 @@ func main() {
}
}
if !mediaAttempted && !mediaHandled && identityReply == "" && creativeReply == "" && creativeFallbackReply == "" {
stableAct := interactiondomain.ClassifySpeechAct(utterance.Content, utterance.CorrectsUtteranceID)
travelClarification := ""
if !creativeRequest {
travelClarification = interactiondomain.TravelSearchClarification(effectiveTaskMessage, conversationHistory)
}
if stableReply, stable := interactiondomain.StableConversationalReply(utterance.Content, stableAct); stable && travelClarification == "" {
planningReply = stableReply
if stableConversational {
planningReply = stableConversationalReply
needsCognition = false
}
}
@@ -1485,7 +1572,7 @@ func main() {
}
request.RequestedByPrincipalID = session.PrincipalID
request.CapabilityID = "creator-advisory"
candidates, err := capabilityRepository.LoadCognitiveCandidates(r.Context(), request.CapabilityID, session.PrincipalID)
candidates, err := capabilityRepository.LoadCognitiveCandidatesForEvaluation(r.Context(), request.CapabilityID, session.PrincipalID, request.EvaluationID)
if err != nil {
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
return
@@ -1524,18 +1611,11 @@ func main() {
}
request.RequestedByPrincipalID = session.PrincipalID
request.CapabilityID = "creator-advisory"
candidates, err := capabilityRepository.LoadCognitiveCandidates(r.Context(), request.CapabilityID, session.PrincipalID)
current, err := capabilityRepository.LoadCognitiveCandidate(r.Context(), request.BindingID, request.ExpectedBindingRevision)
if err != nil {
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
return
}
var current capability.ResolvedCognitiveBinding
for _, candidate := range candidates {
if candidate.Binding.ID == request.BindingID && candidate.Binding.Revision == request.ExpectedBindingRevision {
current = candidate
break
}
}
promotion, err := capabilitydomain.BuildActivationPromotion(request, current)
if err != nil {
http.Error(w, "cognitive activation precondition failed", http.StatusConflict)
@@ -1669,6 +1749,14 @@ func main() {
}
}
func shouldInvokeHumanResponse(stableConversational bool, identityReply, memoryInstructionReply string) bool {
return !stableConversational && identityReply == "" && memoryInstructionReply == ""
}
func shouldReviewResponse(planningReply string, stableKnowledgeReply, stableConversational bool, identityReply, memoryInstructionReply string, deterministicMemoryReply bool) bool {
return strings.TrimSpace(planningReply) != "" && !stableKnowledgeReply && !stableConversational && identityReply == "" && memoryInstructionReply == "" && !deterministicMemoryReply
}
func shouldRunGeneralResearch(kind interaction.InformationRequestKind) bool {
return kind == "" || kind == interaction.InformationRequestTimeDifference
}
@@ -1703,6 +1791,88 @@ func creatorSessionHandler(bootstrapToken, creatorUsername, creatorPassword stri
}
}
func creatorDatabaseSessionHandler(bootstrapToken, creatorUsername string, repository *postgres.IdentityRepository, sessionKey []byte, now func() time.Time) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if identitydomain.BootstrapMatches(bootstrapToken, bearerToken(r)) {
token, session, err := identitydomain.IssueCreatorSession(sessionKey, now(), 8*time.Hour)
if err != nil {
http.Error(w, "creator session unavailable", http.StatusServiceUnavailable)
return
}
writeCreatorSession(w, token, session)
return
}
var credentials identity.CreatorCredentials
decoder := json.NewDecoder(io.LimitReader(r.Body, maxCreatorCredentialsBytes+1))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&credentials); err != nil || decoder.Decode(&struct{}{}) != io.EOF {
http.Error(w, "creator authentication denied", http.StatusUnauthorized)
return
}
account, err := repository.LoadCreatorAccount(r.Context(), credentials.Username)
if err != nil || !identitydomain.VerifyCreatorPassword(account.PasswordHash, credentials.Password) {
http.Error(w, "creator authentication denied", http.StatusUnauthorized)
return
}
binding, err := repository.LoadBinding(r.Context(), account.ActorID, account.PrincipalID)
if err != nil || !binding.Active {
http.Error(w, "creator authentication denied", http.StatusUnauthorized)
return
}
token, session, err := identitydomain.IssueCreatorSessionForBinding(sessionKey, account.ActorID, account.PrincipalID, now(), 8*time.Hour)
if err != nil {
http.Error(w, "creator session unavailable", http.StatusServiceUnavailable)
return
}
writeCreatorSession(w, token, session)
}
}
func writeCreatorSession(w http.ResponseWriter, token string, session identity.CreatorSession) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(struct {
Token string
Session identity.CreatorSession
}{token, session})
}
func bootstrapCreatorAccount(ctx context.Context, repository *postgres.IdentityRepository, username, passwordPath string) error {
if account, err := repository.LoadCreatorAccount(ctx, username); err == nil {
if account.ActorID == "" || account.PrincipalID == "" {
return errors.New("creator account binding is incomplete")
}
return nil
} else if !errors.Is(unwrapIdentityRepositoryError(err), pgx.ErrNoRows) {
return err
}
password, err := readRequiredSecretFile("CREATOR_PASSWORD", passwordPath)
if err != nil {
return fmt.Errorf("creator account is missing and bootstrap password is unavailable: %w", err)
}
hash, err := identitydomain.HashCreatorPassword(password)
if err != nil {
return fmt.Errorf("hash creator bootstrap password: %w", err)
}
if err := repository.CreateCreatorAccount(ctx, identity.CreatorAccount{Username: username, ActorID: "domovoy-creator-actor", PrincipalID: "domovoy-creator-principal", PasswordHash: hash}); err != nil {
return fmt.Errorf("create creator account: %w", err)
}
return nil
}
func unwrapIdentityRepositoryError(err error) error {
for err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return pgx.ErrNoRows
}
unwrapped := errors.Unwrap(err)
if unwrapped == nil {
break
}
err = unwrapped
}
return err
}
func bearerToken(r *http.Request) string {
value := r.Header.Get("Authorization")
if !strings.HasPrefix(value, "Bearer ") {
@@ -1725,10 +1895,7 @@ func readRequiredSecret(name string) (string, error) {
if err != nil {
return "", fmt.Errorf("read %s_FILE: %w", name, err)
}
direct = strings.TrimRight(string(contents), "\r\n")
if strings.ContainsAny(direct, "\r\n") {
return "", fmt.Errorf("%s_FILE must contain one line", name)
}
return normalizeSecretFile(name, contents)
}
if direct == "" {
return "", fmt.Errorf("%s is required", name)
@@ -1736,13 +1903,28 @@ func readRequiredSecret(name string) (string, error) {
return direct, nil
}
func readCredentialRefSecret(name, refName string) (string, error) {
if _, directSet := os.LookupEnv(name); directSet {
return "", fmt.Errorf("%s direct environment secret is forbidden; use %s_FILE", name, name)
func readRequiredSecretFile(name, path string) (string, error) {
contents, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("read %s secret file: %w", name, err)
}
fileName := os.Getenv(name + "_FILE")
if fileName == "" {
return "", fmt.Errorf("%s_FILE is required", name)
return normalizeSecretFile(name, contents)
}
func normalizeSecretFile(name string, contents []byte) (string, error) {
value := strings.TrimRight(string(contents), "\r\n")
if strings.ContainsAny(value, "\r\n") {
return "", fmt.Errorf("%s secret file must contain one line", name)
}
if value == "" {
return "", fmt.Errorf("%s is required", name)
}
return value, nil
}
func readCredentialRefSecret(name, refName, fileName string) (string, error) {
if _, directSet := os.LookupEnv(name); directSet {
return "", fmt.Errorf("%s direct environment secret is forbidden; use the standard secret file", name)
}
ref := os.Getenv(refName)
if ref == "" {
@@ -1809,6 +1991,12 @@ func renderResearchReply(query string, result epistemics.ResearchResult, researc
excerpt = string([]rune(excerpt)[:limit]) + "…"
}
lines = append(lines, excerpt+"\nИсточник: "+researchSourceLabel(finding))
// A nearest-flight request needs one route-relevant schedule result.
// Appending weaker matches makes unrelated category pages look like
// alternatives even after the relevance check has accepted the route.
if nearestFlightQuery(query) {
break
}
if len(lines) == 2 {
break
}
@@ -1856,6 +2044,9 @@ func researchFindingMatchesIntent(query string, finding epistemics.ResearchFindi
if distanceResearchQuery(query) && !containsDistanceEvidence(finding.Excerpt.Text) {
return false
}
if localPlaceSearchQuery(query) && !containsLocalPlaceEvidence(finding.Excerpt.Text) {
return false
}
if !nearestFlightQuery(query) {
return true
}
@@ -1885,6 +2076,22 @@ func researchFindingMatchesIntent(query string, finding epistemics.ResearchFindi
return true
}
func localPlaceSearchQuery(query string) bool {
lower := strings.ToLower(query)
return (strings.Contains(lower, "ресторан") || strings.Contains(lower, "кафе")) &&
(strings.Contains(lower, "адрес") || strings.Contains(lower, "отзыв") || strings.Contains(lower, "меню"))
}
func containsLocalPlaceEvidence(value string) bool {
lower := strings.ToLower(value)
for _, term := range []string{"ресторан", "кафе", "адрес", "меню", "отзыв", "кухн"} {
if strings.Contains(lower, term) {
return true
}
}
return false
}
func distanceResearchQuery(query string) bool {
lower := strings.ToLower(query)
return strings.Contains(lower, "расстоян") || strings.Contains(lower, "километр")
@@ -2024,6 +2231,21 @@ func stableKnowledgeAnswer(answer string) bool {
return strings.TrimSpace(answer) != "" && strings.TrimSpace(answer) != "NEEDS_VERIFIED_RESEARCH"
}
// shouldGenerateNonMutatingTextDirective identifies a request that can be
// completed entirely in the reply. It excludes specialized routes and durable
// work, so cognition can produce content but cannot stand in for an executor.
func shouldGenerateNonMutatingTextDirective(semantic interactiondomain.SemanticResult, routes []chatRoute, helpPath work.HelpPath, creativeRequest bool) bool {
if semantic.Interpretation.SpeechAct != interaction.SpeechActDirective || creativeRequest || helpPath.NeedsDurableWork {
return false
}
// ANSWER and COGNITION are reply-only help paths. They do not execute an
// external action, unlike direct capabilities, gaps or durable work.
if helpPath.Kind != work.HelpPathAnswer && helpPath.Kind != work.HelpPathCognition && helpPath.Kind != "" {
return false
}
return !hasChatRoute(routes, chatRouteResearch) && !hasChatRoute(routes, chatRouteMedia) && !hasChatRoute(routes, chatRouteAgent)
}
func reviewPreservesResearchEvidence(reviewed, draft string) bool {
if strings.Contains(draft, "http://") || strings.Contains(draft, "https://") {
return strings.Contains(reviewed, "http://") || strings.Contains(reviewed, "https://")
+43
View File
@@ -13,6 +13,7 @@ import (
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/interaction"
"git.cin.su/m/Assistant/contracts/work"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
)
@@ -46,6 +47,17 @@ func TestNearestFlightReplyFiltersUnrelatedSearchSnippets(t *testing.T) {
}
}
func TestNearestFlightReplyDoesNotAppendWeakerSecondMatch(t *testing.T) {
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://schedule.example", Title: "Рейсы Москва — Казань"}, Excerpt: epistemics.SourceExcerpt{Text: "Расписание авиарейсов Москва — Казань, время вылета и прилёта."}},
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://category.example", Title: "Категория направлений"}, Excerpt: epistemics.SourceExcerpt{Text: "Список близлежащих городов и ориентировочные цены."}},
}}
reply, backed := renderResearchReply("ближайшие авиарейсы Москва Казань сегодня", result, nil)
if !backed || !strings.Contains(reply, "Рейсы Москва") || strings.Contains(reply, "Категория направлений") {
t.Fatalf("weaker second finding leaked into nearest-flight reply: %q", reply)
}
}
func TestNearestFlightEvidenceRequiresBothRouteEndpointsWhenKnown(t *testing.T) {
query := "ближайшие авиарейсы Москва Казань сегодня"
matched := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Рейсы Москва — Казань"}, Excerpt: epistemics.SourceExcerpt{Text: "Расписание авиарейсов из Москвы в Казань."}}
@@ -94,6 +106,18 @@ func TestResearchReplyRejectsGenericLandingPageForConcreteIdentifier(t *testing.
}
}
func TestLocalPlaceReplyRejectsDictionaryFinding(t *testing.T) {
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{
State: epistemics.FindingSupported,
Source: epistemics.Source{Locator: "https://dictionary.example", Title: "Ближайший — словарь"},
Excerpt: epistemics.SourceExcerpt{Text: "Ближайший — значение слова и пример употребления."},
}}}
reply, backed := renderResearchReply("места ресторан в Сочи адрес отзывы меню", result, nil)
if backed || !strings.Contains(reply, "Проверяемых данных") {
t.Fatalf("dictionary finding was presented as local place result: %q", reply)
}
}
func TestResearchReplyKeepsFindingContainingConcreteIdentifier(t *testing.T) {
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{
State: epistemics.FindingSupported,
@@ -145,6 +169,25 @@ func TestResponseReviewPreservesAttemptedResearchOutcome(t *testing.T) {
}
}
func TestNonMutatingTextDirectiveCanUseCognitiveReply(t *testing.T) {
semantic := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{SpeechAct: interaction.SpeechActDirective}}
if !shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{}, false) {
t.Fatal("ordinary formatting directive was not eligible for a cognitive reply")
}
if !shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{Kind: work.HelpPathCognition}, false) {
t.Fatal("reply-only cognition path was incorrectly rejected")
}
if shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{Kind: work.HelpPathDirectCapability}, false) {
t.Fatal("direct capability was incorrectly replaced by a textual reply")
}
if shouldGenerateNonMutatingTextDirective(semantic, []chatRoute{{Kind: chatRouteMedia}}, work.HelpPath{}, false) {
t.Fatal("media route was incorrectly replaced by a textual reply")
}
if shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{NeedsDurableWork: true}, false) {
t.Fatal("durable work was incorrectly replaced by a textual reply")
}
}
func TestBearerTokenRequiresBearerScheme(t *testing.T) {
request := httptest.NewRequest("GET", "/", nil)
request.Header.Set("Authorization", "Basic value")
+2
View File
@@ -841,6 +841,8 @@ func creatorRoute(method, path string) (routeTarget, string, bool) {
return targetCore, "/v1/creator/sessions", true
case method == http.MethodGet && path == "/api/session/current":
return targetCore, "/v1/creator/sessions/current", true
case method == http.MethodPost && path == "/api/account/password":
return targetCore, "/v1/creator/account/password", true
case method == http.MethodGet && path == "/api/focus":
return targetCore, "/v1/creator/focus", true
case method == http.MethodGet && strings.HasPrefix(path, "/api/tasks/"):
+260 -106
View File
@@ -1,131 +1,285 @@
:root {
--ink: #e9eee7;
--muted: #93a096;
--base: #0b100e;
--panel: #101714;
--line: #2a352f;
--signal: #c9a85f;
--bad: #de816f;
--good: #78c6a3;
--radius: 3px;
--sans: "Bahnschrift", "Aptos Narrow", "Segoe UI", sans-serif;
--serif: Georgia, "Times New Roman", serif;
--mono: "Cascadia Mono", Consolas, monospace;
--ink: #e6edf4;
--muted: #8494a4;
--faint: #5d6e7f;
--base: #0b1119;
--panel: #111b27;
--panel-raised: #172331;
--panel-soft: #0f1925;
--line: #263544;
--line-strong: #34485c;
--lime: #c5ef91;
--lime-ink: #142014;
--blue: #8fc7ef;
--yellow: #d8b86c;
--good: #8ed7ad;
--bad: #ee9292;
--radius: 8px;
--sans: "Manrope", "Segoe UI", sans-serif;
--mono: "DM Mono", "Cascadia Mono", Consolas, monospace;
}
* { box-sizing: border-box; }
[hidden] { display: none !important; }
body { margin: 0; min-width: 320px; min-height: 100vh; color: var(--ink); background: var(--base); font-family: var(--sans); }
button, input, textarea { font: inherit; }
button { cursor: pointer; color: inherit; border-radius: var(--radius); }
a { color: inherit; }
.chat-shell { min-height: 100vh; display: grid; }
.login-panel { width: min(100% - 32px, 420px); margin: auto; padding: 46px 40px; border: 1px solid var(--line); background: var(--panel); }
.brand-mark { width: 46px; height: 46px; display: grid; place-items: center; color: var(--base); background: var(--signal); font: 700 24px/1 var(--serif); clip-path: polygon(50% 0, 92% 22%, 92% 76%, 50% 100%, 8% 76%, 8% 22%); }
h1 { margin: 30px 0; font: 400 58px/.95 var(--serif); letter-spacing: -.02em; }
label { display: block; margin: 16px 0 8px; color: var(--muted); font: 600 10px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
input, textarea { width: 100%; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: #0b110e; }
input { height: 48px; padding: 0 12px; }
textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px; line-height: 1.45; }
.login-form button, .composer button { border: 0; color: #11150f; background: var(--signal); font-weight: 700; }
.login-form button { width: 100%; height: 50px; margin-top: 18px; }
.login-panel { width: min(100% - 32px, 420px); margin: auto; padding: 40px; border: 1px solid var(--line); border-radius: 12px; background: var(--panel); box-shadow: 0 24px 80px rgba(0,0,0,.28); }
.brand-mark { width: 42px; height: 42px; display: grid; place-items: center; color: var(--lime-ink); background: var(--lime); border-radius: 10px; font: 700 22px/1 var(--sans); }
h1, h2 { margin: 0; letter-spacing: -.03em; }
.login-panel h1 { margin: 26px 0; font-size: 42px; font-weight: 650; }
label { display: block; margin: 16px 0 8px; color: var(--muted); font: 600 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
input, textarea { width: 100%; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: var(--panel-soft); outline: none; }
input:focus, textarea:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(143,199,239,.12); }
input { height: 46px; padding: 0 12px; }
textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 13px; line-height: 1.45; }
.login-form button, .composer button { border: 0; color: var(--lime-ink); background: var(--lime); font-weight: 750; }
.login-form button { width: 100%; height: 46px; margin-top: 18px; }
#login-error { min-height: 20px; color: var(--bad); font-size: 13px; }
.chat-view { width: 100%; min-width: 0; height: 100vh; min-height: 0; display: grid; grid-template-rows: 64px minmax(0, 1fr); overflow: hidden; }
.chat-header { min-width: 0; display: flex; align-items: center; gap: 14px; padding: 0 22px; border-bottom: 1px solid var(--line); background: rgba(16, 23, 20, .98); }
.chat-header a { color: var(--ink); text-decoration: none; font: 400 24px/1 var(--serif); }
#active-focus { color: var(--good); }
.chat-view { width: 100%; min-width: 0; height: 100vh; min-height: 0; display: grid; grid-template-rows: 60px minmax(0, 1fr); overflow: hidden; }
.chat-header { min-width: 0; display: flex; align-items: center; gap: 14px; padding: 0 20px; border-bottom: 1px solid var(--line); background: rgba(17,27,39,.97); }
.chat-header > a:first-child { color: var(--ink); text-decoration: none; font-size: 18px; font-weight: 750; letter-spacing: -.02em; }
.cognitive-status { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; }
.chat-header .cognitive-status { width: auto; height: auto; padding: 0; border: 0; background: transparent; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
.chat-header .cognitive-status { max-width: min(30vw, 320px); margin-left: auto; overflow: hidden; text-overflow: ellipsis; }
.cognitive-status::before { content: ""; width: 7px; height: 7px; flex: 0 0 7px; border-radius: 50%; background: var(--signal); }
.chat-header .cognitive-status { width: auto; height: auto; max-width: min(30vw, 340px); margin-left: auto; padding: 0; overflow: hidden; border: 0; background: transparent; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .04em; text-overflow: ellipsis; text-transform: uppercase; }
.cognitive-status::before { content: ""; width: 7px; height: 7px; flex: 0 0 7px; border-radius: 50%; background: var(--yellow); }
.cognitive-status[data-state="available"] { color: var(--good); }
.cognitive-status[data-state="available"]::before { background: var(--good); }
.cognitive-status[data-state="unavailable"] { color: var(--bad); }
.cognitive-status[data-state="unavailable"]::before { background: var(--bad); }
.cognitive-console-link { color: var(--signal); font: 10px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; white-space: nowrap; }
.build-meta { display: grid; gap: 3px; flex: 0 0 auto; color: var(--muted); font: 9px/1 var(--mono); letter-spacing: .08em; text-align: right; text-transform: uppercase; white-space: nowrap; }
.build-meta time { color: #68776d; font-size: 8px; }
.chat-header button { width: 38px; height: 38px; border: 1px solid var(--line); background: transparent; }
.human-nav { display: grid; gap: 8px; }
.nav-tab, .operator-link { height: 34px; padding: 0 12px; display: inline-flex; align-items: center; border: 1px solid var(--line); color: var(--muted); background: #0b110e; text-decoration: none; font: 11px/1 var(--mono); text-transform: uppercase; letter-spacing: .08em; }
.chat-left .nav-tab { height: auto; min-height: 58px; padding: 10px 12px; display: grid; grid-template-columns: 28px 1fr; gap: 2px 8px; align-items: center; text-align: left; }
.chat-left .nav-tab b { grid-row: span 2; color: var(--signal); font: 11px/1 var(--mono); }
.chat-left .nav-tab span { color: var(--ink); font: 600 13px/1 var(--sans); text-transform: none; letter-spacing: 0; }
.chat-left .nav-tab small { color: var(--muted); font: 10px/1 var(--mono); text-transform: none; letter-spacing: 0; }
.nav-tab.active { color: #11150f; border-color: var(--signal); background: var(--signal); }
.operator-link { color: var(--good); }
.chat-header > .operator-link { flex: 0 0 auto; }
.chat-layout { min-height: 0; height: 100%; display: grid; grid-template-columns: minmax(220px, 280px) minmax(0, 1fr) minmax(250px, 320px); }
.chat-side { min-width: 0; min-height: 0; padding: 28px 20px; overflow-y: auto; background: #0e1512; }
.generated-image { display: block; max-width: min(100%, 640px); height: auto; margin-top: 12px; border: 1px solid var(--line); }
.cognitive-console-link { color: var(--blue); font: 10px/1 var(--mono); letter-spacing: .04em; white-space: nowrap; }
.build-meta { display: grid; gap: 3px; flex: 0 0 auto; color: var(--muted); font: 9px/1 var(--mono); letter-spacing: .04em; text-align: right; text-transform: uppercase; white-space: nowrap; }
.build-meta time { color: var(--faint); font-size: 8px; }
.chat-header button { width: 34px; height: 34px; border: 1px solid var(--line); background: transparent; }
.operator-link { height: 32px; padding: 0 11px; display: inline-flex; align-items: center; border: 1px solid var(--line); color: var(--blue); background: transparent; text-decoration: none; font: 10px/1 var(--mono); letter-spacing: .04em; text-transform: uppercase; }
.chat-layout { min-height: 0; height: 100%; display: grid; grid-template-columns: minmax(220px, 258px) minmax(0, 1fr) minmax(270px, 320px); }
.chat-side { min-width: 0; min-height: 0; padding: 22px 18px; overflow-y: auto; background: var(--panel); scrollbar-color: var(--line-strong) transparent; }
.chat-left { border-right: 1px solid var(--line); }
.chat-right { border-left: 1px solid var(--line); }
.side-intro h1, .side-intro h2 { margin: 12px 0; font: 400 clamp(28px, 2.5vw, 40px)/.95 var(--serif); letter-spacing: -.03em; }
.side-intro p:not(.side-index) { color: var(--muted); line-height: 1.5; }
.side-index, .side-focus span, .side-note span, .ai-info-card span { color: var(--signal); font: 10px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
.side-focus, .side-note, .ai-info-card { margin-top: 28px; padding: 14px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.side-focus strong, .side-note strong, .ai-info-card strong { display: block; margin-top: 9px; color: var(--ink); font: 600 14px/1.3 var(--sans); }
.side-note small, .ai-info-card p, .ai-data-form small { display: block; margin-top: 9px; color: var(--muted); line-height: 1.45; }
.chat-main { min-width: 0; min-height: 0; overflow: hidden; display: grid; }
.side-intro h1, .side-intro h2 { margin: 9px 0 10px; font-size: clamp(25px, 2.4vw, 34px); line-height: 1.05; font-weight: 680; }
.side-intro p:not(.side-index) { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.5; }
.side-index, .section-label, .ai-info-card span, .insight-card span { color: var(--faint); font: 600 9px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
.side-section, .side-note, .ai-info-card { margin-top: 20px; padding: 13px 0; border-top: 1px solid var(--line); }
.side-focus strong, .side-note strong { display: block; margin-top: 8px; color: var(--blue); font-size: 13px; font-weight: 650; line-height: 1.35; }
.side-note { border-bottom: 0; }
.side-note strong { color: var(--good); }
.side-note small, .ai-info-card p, .ai-data-form small { display: block; margin-top: 8px; color: var(--muted); font-size: 11px; line-height: 1.45; }
.context-list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.context-list span { padding: 5px 8px; border: 1px solid var(--line); border-radius: 6px; color: var(--muted); font-size: 11px; }
.quick-context { padding-bottom: 3px; }
.human-nav { display: grid; gap: 6px; margin-top: 8px; }
.nav-tab { width: 100%; min-height: 50px; padding: 8px 10px; display: grid; grid-template-columns: 26px 1fr; gap: 2px 7px; align-items: center; border: 1px solid transparent; color: var(--muted); background: transparent; text-align: left; }
.chat-left .nav-tab b { grid-row: span 2; color: var(--faint); font: 10px/1 var(--mono); }
.chat-left .nav-tab span { color: var(--ink); font-size: 12px; font-weight: 650; }
.chat-left .nav-tab small { color: var(--faint); font: 10px/1 var(--mono); }
.nav-tab:hover, .nav-tab:focus-visible { border-color: var(--line-strong); outline: none; }
.nav-tab.active { border-color: rgba(197,239,145,.42); background: rgba(197,239,145,.1); }
.nav-tab.active b { color: var(--lime); }
.quiet-state { margin: 9px 0 0; color: var(--faint); font-size: 11px; }
.new-conversation { width: 100%; min-height: 34px; margin-top: 9px; border: 1px solid var(--line-strong); color: var(--ink); background: transparent; font-size: 11px; }
.new-conversation:hover, .new-conversation:focus-visible { border-color: var(--lime); color: var(--lime); outline: none; }
.side-build { margin-top: 14px; padding-top: 14px; text-align: left; }
.chat-main { min-width: 0; min-height: 0; overflow: hidden; display: grid; background: var(--base); }
.surface-panel { min-height: 0; position: relative; overflow-y: auto; }
.active-panel { display: grid; grid-template-rows: minmax(0, 1fr) auto auto; min-height: 0; }
.messages { width: min(100%, 920px); min-height: 0; margin: 0 auto; padding: 28px 18px; display: flex; flex-direction: column; gap: 14px; overflow-y: auto; }
.message { max-width: min(720px, 92%); padding: 14px 16px; border: 1px solid var(--line); border-radius: var(--radius); line-height: 1.45; white-space: pre-wrap; }
.message.user { align-self: flex-end; background: #151e1a; }
.message.assistant { align-self: flex-start; background: #0f1613; }
.message small { display: block; margin-top: 8px; color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; }
.composer { width: min(100%, 920px); margin: 0 auto; padding: 16px 18px 10px; display: grid; grid-template-columns: minmax(0, 1fr) 130px; gap: 12px; background: linear-gradient(transparent, var(--base) 20%); }
.composer button { height: 48px; align-self: end; }
.composer button:disabled { cursor: wait; filter: saturate(.3); }
.media-tools { width: min(100%, 920px); margin: 0 auto; padding: 0 18px 10px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; letter-spacing: .06em; }
.media-tools label { display: inline-flex; align-items: center; gap: 6px; min-height: 30px; padding: 0 8px; border: 1px solid var(--line); color: var(--ink); cursor: pointer; }
.media-tools input { max-width: 150px; font: 10px var(--mono); }
.media-tools span { color: var(--signal); }
.conversation-starters { width: min(100%, 920px); margin: -4px auto 0; padding: 0 18px 18px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
.conversation-starters button { min-height: 32px; padding: 0 10px; border: 1px solid var(--line); color: var(--ink); background: #101814; font: 10px/1 var(--mono); }
.conversation-starters button:hover, .conversation-starters button:focus-visible { border-color: var(--signal); color: var(--signal); outline: none; }
.ai-data-form { margin-top: 24px; }
.ai-data-form textarea { min-height: 150px; resize: vertical; }
.ai-data-form button { width: 100%; min-height: 42px; margin-top: 12px; border: 0; color: #11150f; background: var(--signal); font-weight: 700; }
.ai-data-error { margin: 10px 0 0; color: var(--bad); font-size: 12px; line-height: 1.4; }
.side-console-link { display: flex; justify-content: space-between; margin-top: 26px; padding-top: 16px; border-top: 1px solid var(--line); color: var(--signal) !important; font: 11px/1.3 var(--mono) !important; text-transform: uppercase; letter-spacing: .06em; }
.attention-surface, .history-surface { width: min(100%, 920px); margin: 0 auto; padding: 34px 18px; align-content: start; gap: 18px; }
.surface-heading { display: flex; align-items: end; justify-content: space-between; gap: 16px; border-bottom: 1px solid var(--line); padding-bottom: 16px; }
.surface-heading p { margin: 0; font: 400 36px/.95 var(--serif); }
.surface-heading span, .attention-card small, .history-item span { color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; letter-spacing: .1em; }
.surface-heading button { height: 36px; padding: 0 12px; border: 1px solid var(--line); background: transparent; }
.attention-list, .history-list { display: grid; gap: 12px; }
.attention-card, .history-item { border: 1px solid var(--line); background: #0f1613; padding: 16px; }
.attention-card time { color: var(--signal); font: 12px/1 var(--mono); }
.attention-card h2 { margin: 10px 0 8px; font: 400 24px/1.1 var(--serif); }
.attention-card p, .history-item p { margin: 0 0 10px; color: var(--ink); line-height: 1.45; }
.attention-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.attention-actions button { height: 34px; padding: 0 12px; border: 1px solid var(--line); background: #151e1a; color: var(--ink); font: 11px/1 var(--mono); text-transform: uppercase; letter-spacing: .08em; }
.attention-actions button:first-child { border-color: var(--good); color: var(--good); }
.history-item.user { border-color: #34443c; background: #151e1a; }
.history-item strong { display: block; margin-bottom: 8px; color: var(--signal); }
.empty-state { margin: 24px 0 0; color: var(--muted); }
.messages { width: min(100%, 900px); min-height: 0; margin: 0 auto; padding: clamp(24px, 5vh, 52px) 22px 20px; display: flex; flex-direction: column; gap: 10px; overflow-y: auto; scrollbar-color: var(--line-strong) transparent; }
.message { max-width: min(700px, 92%); padding: 11px 14px 12px; border: 1px solid var(--line); border-radius: 10px; line-height: 1.5; }
.message.user { align-self: flex-end; background: #162231; border-color: #30465b; }
.message.assistant { align-self: flex-start; background: #121d2a; }
.message-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 6px; }
.message-role { color: var(--blue); font: 650 9px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
.message.user .message-role { color: var(--lime); }
.message-time { color: var(--faint); font: 9px/1 var(--mono); }
.message-content { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 13px; }
.message small { display: block; margin-top: 8px; color: var(--faint); font: 9px/1.2 var(--mono); letter-spacing: .03em; text-transform: none; }
.composer { width: min(100%, 900px); margin: 0 auto; padding: 10px 22px 7px; display: grid; grid-template-columns: minmax(0, 1fr) 106px; gap: 9px; border-top: 1px solid var(--line); background: var(--base); }
.composer-input-wrap { min-width: 0; border: 1px solid var(--line-strong); border-radius: 9px; background: var(--panel-soft); transition: border-color .15s ease, box-shadow .15s ease; }
.composer-input-wrap:focus-within { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(143,199,239,.1); }
.composer textarea { min-height: 46px; max-height: 200px; resize: none; border: 0; background: transparent; }
.composer-hint { display: flex; justify-content: space-between; gap: 12px; padding: 0 12px 7px; color: var(--faint); font: 9px/1 var(--mono); }
.composer-hint output { color: var(--muted); white-space: nowrap; }
.composer button { height: 46px; align-self: end; display: inline-flex; align-items: center; justify-content: space-between; gap: 7px; padding: 0 13px; }
.composer button[aria-busy="true"] { color: #384529; background: #a6ca7a; }
.composer button span:last-child { opacity: .65; font: 15px/1 var(--mono); }
.composer button:disabled { cursor: wait; filter: saturate(.35); }
.media-tools { width: min(100%, 900px); margin: 0 auto; padding: 0 22px 9px; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; color: var(--muted); font: 9px/1 var(--mono); letter-spacing: .03em; text-transform: uppercase; }
.media-tools label { display: inline-flex; align-items: center; gap: 5px; min-height: 28px; margin: 0; padding: 0 7px; border: 1px solid var(--line); border-radius: 6px; color: var(--muted); cursor: pointer; }
.media-tools input { max-width: 125px; font: 9px var(--mono); }
.media-tools span { color: var(--yellow); }
.insight-grid { display: grid; gap: 7px; margin-top: 19px; }
.insight-card { padding: 11px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-soft); }
.insight-card strong { display: block; margin-top: 7px; color: var(--ink); font-size: 12px; font-weight: 650; line-height: 1.3; }
.insight-card small { display: block; margin-top: 5px; color: var(--faint); font-size: 10px; line-height: 1.35; }
.ai-info-card { padding-bottom: 0; }
.ai-info-card strong { display: block; margin-top: 8px; font-size: 12px; }
.ai-data-form { margin-top: 18px; }
.ai-data-form textarea { min-height: 126px; }
.ai-data-form button { width: 100%; min-height: 38px; margin-top: 9px; border: 1px solid rgba(197,239,145,.3); border-radius: 7px; color: var(--lime); background: rgba(197,239,145,.1); font-size: 11px; font-weight: 700; }
.ai-data-form button:hover { background: rgba(197,239,145,.16); }
.ai-data-error { margin: 9px 0 0; color: var(--bad); font-size: 11px; line-height: 1.4; }
.side-console-link { display: flex; justify-content: space-between; margin-top: 20px; padding-top: 13px; border-top: 1px solid var(--line); color: var(--blue) !important; font: 10px/1.3 var(--mono) !important; text-decoration: none; letter-spacing: .03em; text-transform: uppercase; }
.attention-surface, .history-surface { width: min(100%, 900px); margin: 0 auto; padding: 28px 22px; align-content: start; gap: 15px; }
.surface-heading { display: flex; align-items: end; justify-content: space-between; gap: 16px; border-bottom: 1px solid var(--line); padding-bottom: 13px; }
.surface-heading p { margin: 0; font-size: 27px; font-weight: 680; }
.surface-heading span, .attention-card small, .history-item span { color: var(--muted); font: 9px/1 var(--mono); text-transform: uppercase; letter-spacing: .08em; }
.surface-heading button { height: 31px; padding: 0 10px; border: 1px solid var(--line); background: transparent; font-size: 11px; }
.attention-list, .history-list { display: grid; gap: 9px; }
.attention-card, .history-item { border: 1px solid var(--line); border-radius: 8px; background: var(--panel); padding: 13px; }
.attention-card time { color: var(--yellow); font: 10px/1 var(--mono); }
.attention-card h2 { margin: 8px 0 6px; font-size: 18px; font-weight: 650; }
.attention-card p, .history-item p { margin: 0 0 8px; color: var(--ink); font-size: 12px; line-height: 1.5; }
.attention-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 11px; }
.attention-actions button { height: 30px; padding: 0 10px; border: 1px solid var(--line); background: var(--panel-raised); color: var(--ink); font: 10px/1 var(--mono); }
.attention-actions button:first-child { border-color: rgba(142,215,173,.5); color: var(--good); }
.history-item.user { border-color: #30465b; background: #162231; }
.history-item strong { display: block; margin-bottom: 6px; color: var(--blue); font-size: 12px; }
.empty-state { margin: 20px 0 0; color: var(--muted); font-size: 12px; }
.empty-state.error { color: var(--bad); }
@media (max-width: 640px) {
@media (max-width: 780px) {
.chat-view { height: auto; min-height: 100vh; overflow: visible; display: block; }
.composer { grid-template-columns: 1fr; }
.chat-header { position: sticky; top: 0; z-index: 3; min-height: 58px; padding: 0 12px; }
.chat-header .cognitive-status { flex: 1 1 100px; min-width: 0; max-width: 150px; }
.chat-header .cognitive-console-link, .chat-header > .operator-link, .chat-header .build-meta { display: none; }
.chat-header > #logout { flex: 0 0 34px; }
.chat-layout { height: auto; display: flex; flex-direction: column; }
.chat-main { order: 1; height: 72vh; min-height: 530px; }
.chat-side { min-height: auto; max-height: none; overflow: visible; }
.chat-left { order: 2; border: 0; border-top: 1px solid var(--line); }
.chat-right { order: 3; border: 0; border-top: 1px solid var(--line); }
.messages { padding-inline: 12px; }
.composer { grid-template-columns: 1fr; padding-inline: 12px; }
.composer button { width: 100%; }
.conversation-starters { align-items: stretch; }
.conversation-starters span { width: 100%; }
.chat-header { gap: 10px; padding: 0 12px; }
.chat-header .cognitive-status { display: inline-flex; flex: 1 1 110px; min-width: 0; max-width: 110px; overflow: hidden; text-overflow: ellipsis; }
.chat-header .cognitive-console-link { flex: 0 1 110px; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.chat-header > .operator-link, .build-meta { display: none; }
.chat-header > #logout { flex: 0 0 38px; }
.composer-hint { font-size: 8px; }
.media-tools { padding-inline: 12px; }
.side-build { display: none; }
}
@media (max-width: 640px) {
.chat-layout { height: auto; display: block; }
.chat-side, .chat-main { min-height: auto; max-height: none; overflow: visible; }
.chat-left, .chat-right { border: 0; border-bottom: 1px solid var(--line); }
.chat-main { height: 70vh; min-height: 520px; }
.chat-left { padding-bottom: 20px; }
.chat-right { padding-top: 20px; }
.operator-link, .nav-tab { padding: 0 9px; }
.chat-header > .operator-link, .build-meta { display: none; }
.chat-header .cognitive-console-link { flex: 0 1 110px; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
}
@media (min-width: 641px) and (max-width: 1100px) {
.chat-layout { grid-template-columns: 210px minmax(0, 1fr) 245px; }
@media (min-width: 781px) and (max-width: 1100px) {
.chat-layout { grid-template-columns: 218px minmax(0, 1fr) 260px; }
.chat-side { padding-inline: 14px; }
.side-intro h1, .side-intro h2 { font-size: 30px; }
}
/* Chat-first pass: navigation and context stay out of the conversation's way. */
.chat-header { gap: 10px; padding-inline: 18px; }
.header-icon, .header-control, .profile-button { height: 32px; border: 1px solid var(--line); background: transparent; font: 10px/1 var(--mono); }
.header-icon { width: 32px; padding: 0; color: var(--muted); font-size: 15px; }
.header-control, .profile-button { padding: 0 10px; color: var(--muted); }
.header-control { color: var(--ink); }
.header-icon:hover, .header-icon:focus-visible, .header-control:hover, .header-control:focus-visible, .profile-button:hover, .profile-button:focus-visible { border-color: var(--blue); color: var(--blue); outline: none; }
.header-focus { min-width: 0; overflow: hidden; color: var(--muted); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
.chat-header .cognitive-status { margin-left: auto; max-width: 250px; }
.chat-header .cognitive-console-link { display: none; }
.chat-layout { grid-template-columns: 242px minmax(0, 1fr); }
.chat-layout.nav-collapsed { grid-template-columns: 0 minmax(0, 1fr); }
.chat-left { padding: 24px 16px; transition: opacity .18s ease, padding .18s ease, transform .18s ease; }
.chat-layout.nav-collapsed .chat-left { padding-inline: 0; overflow: hidden; opacity: 0; border-right: 0; }
.side-intro h1 { font-size: 26px; }
.side-section { margin-top: 18px; }
.human-nav { margin-top: 12px; }
.nav-tab { grid-template-columns: 1fr; min-height: 44px; padding-inline: 10px; }
.chat-left .nav-tab b { display: none; }
.new-conversation { margin-top: 16px; }
.chat-main { min-width: 0; }
.messages { width: min(100%, 1040px); padding: 32px 30px 22px; gap: 14px; }
.message { max-width: min(760px, 88%); padding: 14px 17px 15px; border-radius: 12px; }
.message-content { font-size: 15px; line-height: 1.55; }
.message-role { font-size: 10px; }
.message-time { font-size: 10px; }
.message small { font-size: 10px; }
.composer { width: min(100%, 1040px); padding-inline: 30px; }
.composer-input-wrap { min-height: 64px; padding: 0 12px 36px; border-radius: 14px; }
.composer textarea { min-height: 56px; max-height: 180px; padding: 13px 0 8px; font-size: 15px; }
.composer-actions { right: 9px; bottom: 8px; left: 9px; height: 28px; }
.composer-tool { width: 29px; height: 29px; padding: 0; border: 0; color: var(--muted); background: transparent !important; }
.composer-tool svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.8; }
.composer-tool:hover, .composer-tool:focus-visible { border: 0; color: var(--blue); box-shadow: none; }
.composer-hint { font-size: 10px; }
.send-button { width: 33px; height: 33px; flex-basis: 33px; }
.composer-runtime { height: 0; min-height: 0; padding: 0; overflow: visible; }
.attachment-menu { bottom: 42px; left: 8px; }
.context-indicator { display: none; }
.technical-meta { display: none !important; }
.context-backdrop { position: fixed; z-index: 8; inset: 60px 0 0; border: 0; background: rgba(4,9,15,.58); }
.context-drawer { position: fixed; z-index: 9; top: 60px; right: 0; bottom: 0; width: min(360px, 92vw); padding: 24px 20px; border-left: 1px solid var(--line-strong); box-shadow: -20px 0 50px rgba(0,0,0,.28); transform: translateX(105%); transition: transform .2s ease; }
.password-dialog { width: min(420px, calc(100vw - 32px)); padding: 0; color: var(--ink); background: var(--panel); border: 1px solid var(--line-strong); border-radius: var(--radius); box-shadow: 0 24px 80px rgba(0,0,0,.45); }
.password-dialog::backdrop { background: rgba(3,8,14,.68); }
.password-dialog form { padding: 22px; }
.dialog-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
.dialog-heading h2 { margin: 0; font-size: 20px; }
.dialog-heading button { width: 30px; height: 30px; border: 1px solid var(--line); background: transparent; }
.password-dialog label { display: block; margin: 14px 0 7px; color: var(--muted); font: 11px/1 var(--mono); }
.password-dialog input { width: 100%; height: 42px; padding: 0 12px; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: var(--panel-raised); }
.password-dialog input:focus { outline: 2px solid var(--blue); outline-offset: 1px; }
.dialog-actions { display: flex; justify-content: space-between; gap: 10px; margin-top: 20px; }
.dialog-actions button { min-height: 38px; padding: 0 14px; border: 1px solid var(--line); background: transparent; }
.dialog-actions button:first-child { border-color: var(--lime); color: var(--lime-ink); background: var(--lime); }
.dialog-actions button:disabled { opacity: .55; cursor: wait; }
#password-status { min-height: 18px; margin: 12px 0 0; color: var(--muted); font-size: 12px; }
.context-drawer.drawer-open { transform: translateX(0); }
.drawer-heading { display: flex; align-items: start; justify-content: space-between; gap: 12px; }
.drawer-heading h2 { margin-top: 9px; font-size: 26px; }
.drawer-copy { margin: 12px 0 0; color: var(--muted); font-size: 12px; line-height: 1.5; }
.drawer-close { width: 30px; height: 30px; border: 1px solid var(--line); color: var(--muted); background: transparent; font-size: 20px; line-height: 1; }
.drawer-close:hover, .drawer-close:focus-visible { border-color: var(--blue); color: var(--blue); outline: none; }
.insight-grid { margin-top: 20px; }
@media (max-width: 780px) {
.chat-header { padding-inline: 10px; }
.header-focus { display: none; }
.chat-header .cognitive-status { max-width: 145px; }
.profile-button { width: 32px; overflow: hidden; padding: 0; font-size: 0; }
.profile-button::before { content: "●"; font-size: 12px; }
.chat-layout { display: block; }
.chat-main { height: calc(100vh - 58px); min-height: 520px; }
.chat-left { position: fixed; z-index: 10; top: 58px; bottom: 0; left: 0; width: min(290px, 84vw); transform: translateX(-105%); box-shadow: 18px 0 42px rgba(0,0,0,.3); }
.chat-layout.nav-open .chat-left { transform: translateX(0); opacity: 1; overflow-y: auto; }
.chat-layout.nav-collapsed .chat-left { padding-inline: 16px; }
.messages { padding: 22px 12px 16px; }
.message { max-width: 94%; }
.message-content { font-size: 14px; }
.composer { width: 100%; padding-inline: 12px; }
.composer-input-wrap { padding-right: 10px; }
.composer-hint { display: none; }
.attachment-menu { bottom: 42px; left: 5px; }
.context-backdrop { inset: 58px 0 0; }
.context-drawer { top: 58px; }
}
/* Focused composer: one calm surface, with compact controls kept together. */
.active-panel { grid-template-rows: minmax(0, 1fr) auto auto auto; }
.composer { width: min(100%, 900px); margin: 0 auto; padding: 0 22px 7px; display: block; border-top: 0; background: transparent; }
.composer-input-wrap { position: relative; min-height: 62px; padding: 0 58px 32px 12px; border: 1px solid var(--line-strong); border-radius: 12px; background: var(--panel); }
.composer textarea { display: block; min-height: 54px; max-height: 178px; height: 54px; padding: 12px 0 8px; overflow-y: hidden; line-height: 1.4; }
.composer-actions { position: absolute; right: 8px; bottom: 7px; left: 8px; height: 27px; display: flex; align-items: center; gap: 8px; }
.composer-hint { order: 3; padding: 0; color: var(--faint); font: 9px/1 var(--mono); }
.composer-tool { width: 27px; height: 27px; flex: 0 0 27px; border: 1px solid var(--line-strong); border-radius: 7px; color: var(--muted); background: transparent; font-size: 18px; line-height: 1; }
.composer-tool:has(.composer-tool-label) { width: auto; min-width: 27px; padding-inline: 7px; gap: 5px; font: 10px/1 var(--mono); }
.composer-tool-label { font-size: 10px; }
.composer-tool:hover, .composer-tool:focus-visible { border-color: var(--blue); color: var(--blue); outline: none; }
.context-indicator { display: inline-flex; align-items: center; gap: 5px; color: var(--faint); font: 9px/1 var(--mono); white-space: nowrap; }
.context-indicator output { color: var(--muted); }
.send-button { order: 5; width: 32px; height: 32px; flex: 0 0 32px; margin-left: auto; display: grid; place-items: center; padding: 0; border: 1px solid rgba(197,239,145,.48); border-radius: 50%; color: var(--lime-ink); background: var(--lime); font-size: 17px; line-height: 1; }
.send-button:hover, .send-button:focus-visible { box-shadow: 0 0 0 3px rgba(197,239,145,.12); outline: none; }
.send-button[aria-busy="true"] { border-color: rgba(238,146,146,.55); color: var(--bad); background: rgba(238,146,146,.12); }
.send-button[aria-busy="true"]:hover { background: rgba(238,146,146,.2); }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
.composer-toolbar { width: min(100%, 900px); min-height: 30px; margin: 0 auto; padding: 0 22px 8px; position: relative; display: flex; align-items: center; gap: 6px; }
.toolbar-button { min-height: 27px; padding: 0 9px; border: 1px solid var(--line); border-radius: 7px; color: var(--muted); background: transparent; font: 9px/1 var(--mono); text-transform: uppercase; }
.toolbar-button:hover, .toolbar-button:focus-visible { border-color: var(--blue); color: var(--blue); outline: none; }
.attachment-menu { position: absolute; z-index: 4; bottom: 37px; left: 22px; display: grid; min-width: 145px; padding: 5px; border: 1px solid var(--line-strong); border-radius: 9px; background: var(--panel-raised); box-shadow: 0 14px 34px rgba(0,0,0,.35); }
.attachment-menu button { padding: 8px 9px; border: 0; border-radius: 6px; color: var(--ink); background: transparent; text-align: left; font-size: 11px; }
.attachment-menu button:hover, .attachment-menu button:focus-visible { color: var(--lime); background: rgba(197,239,145,.1); outline: none; }
.composer-toolbar #media-status { margin-left: 3px; color: var(--yellow); font: 9px/1 var(--mono); }
@media (max-width: 780px) {
.composer { width: 100%; padding-inline: 12px; }
.composer-input-wrap { padding-right: 50px; }
.composer-toolbar { width: 100%; padding-inline: 12px; }
.attachment-menu { left: 12px; }
.composer-hint { display: none; }
.composer-tool:has(.composer-tool-label) { padding-inline: 5px; }
.composer-tool-label { font-size: 9px; }
.context-indicator { font-size: 8px; }
}
+168 -24
View File
@@ -6,23 +6,69 @@ const messages = document.querySelector("#messages");
const chatForm = document.querySelector("#chat-form");
const messageInput = document.querySelector("#message");
const sendButton = document.querySelector("#send");
const sendLabel = document.querySelector("#send-label");
const messageCount = document.querySelector("#message-count");
const logoutButton = document.querySelector("#logout");
const passwordDialog = document.querySelector("#password-dialog");
const passwordForm = document.querySelector("#password-form");
const passwordClose = document.querySelector("#password-close");
const passwordLogout = document.querySelector("#password-logout");
const passwordStatus = document.querySelector("#password-status");
const activeFocus = document.querySelector("#active-focus");
const rightFocus = document.querySelector("#right-focus");
const headerFocus = document.querySelector("#header-focus");
const navToggle = document.querySelector("#nav-toggle");
const chatLayout = document.querySelector(".chat-layout");
const contextToggle = document.querySelector("#context-toggle");
const contextClose = document.querySelector("#context-close");
const contextBackdrop = document.querySelector("#context-backdrop");
const contextDrawer = document.querySelector("#context-drawer");
const newConversationButton = document.querySelector(".new-conversation");
const cognitiveStatus = document.querySelector("#cognitive-status");
const cognitiveConsoleLink = document.querySelector("#cognitive-console-link");
const todayList = document.querySelector("#today-list");
const refreshToday = document.querySelector("#refresh-today");
const historyList = document.querySelector("#history-list");
const navTabs = Array.from(document.querySelectorAll(".nav-tab"));
const conversationStarters = Array.from(document.querySelectorAll("[data-starter]"));
const aiDataForm = document.querySelector("#ai-data-form");
const aiDataInput = document.querySelector("#ai-data");
const aiDataError = document.querySelector("#ai-data-error");
const attachToggle = document.querySelector("#attach-toggle");
const attachmentMenu = document.querySelector("#attachment-menu");
const photoTrigger = document.querySelector("#photo-trigger");
const micTrigger = document.querySelector("#mic-trigger");
const sendIcon = document.querySelector("#send-icon");
const stopIcon = document.querySelector("#stop-icon");
let token = sessionStorage.getItem("domovoy.chat.token") || "";
let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || "";
let localHistory = [];
let cognitiveStatusTimer = null;
let activeRequestController = null;
let composingMessage = false;
function resizeComposerInput() {
if (!messageInput) return;
messageInput.style.height = "auto";
const maxHeight = 178;
messageInput.style.height = `${Math.min(messageInput.scrollHeight, maxHeight)}px`;
messageInput.style.overflowY = messageInput.scrollHeight > maxHeight ? "auto" : "hidden";
if (messageCount) messageCount.textContent = `${messageInput.value.length.toLocaleString("ru-RU")} / 16 384`;
}
function isChatBusy() {
return sendButton?.getAttribute("aria-busy") === "true";
}
function setChatBusy(busy) {
if (!sendButton) return;
sendButton.setAttribute("aria-busy", busy ? "true" : "false");
sendButton.setAttribute("aria-label", busy ? "Остановить обработку" : "Отправить");
sendButton.title = busy ? "Остановить обработку" : "Отправить";
if (sendLabel) sendLabel.textContent = busy ? "Остановить обработку" : "Отправить";
if (sendIcon) sendIcon.hidden = busy;
if (stopIcon) stopIcon.hidden = !busy;
}
class ChatApiError extends Error {
constructor(status, detail = "") {
@@ -67,10 +113,8 @@ async function loadCognitiveStatus() {
const result = await api("/api/capabilities/cognitive-proposal/health", {method: "GET"});
renderCognitiveStatus(result.State || result.state || "UNKNOWN");
} catch (error) {
if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
expireSession("Сессия устарела. Войдите снова, чтобы продолжить разговор.");
return;
}
// Cognitive health is an auxiliary probe. Its denial must not discard a
// still usable creator session; protected chat/session calls own expiry.
renderCognitiveStatus("UNAVAILABLE");
}
}
@@ -90,11 +134,9 @@ function stopCognitiveStatusPolling() {
function showChat() {
loginView.hidden = true;
chatView.hidden = false;
if (!messages.children.length) {
appendMessage("assistant", "Я рядом. Напишите обычное сообщение, без внутренних идентификаторов.");
}
startCognitiveStatusPolling();
messageInput.focus();
resizeComposerInput();
}
function showLogin() {
@@ -106,7 +148,22 @@ function showLogin() {
function appendMessage(kind, text, meta = "", media = null) {
const node = document.createElement("div");
node.className = `message ${kind}`;
node.textContent = text;
const head = document.createElement("div");
head.className = "message-head";
const role = document.createElement("strong");
role.className = "message-role";
role.textContent = kind === "user" ? "Вы" : "Домовой";
const time = document.createElement("time");
time.className = "message-time";
const now = new Date();
time.dateTime = now.toISOString();
time.textContent = now.toLocaleTimeString("ru-RU", {hour: "2-digit", minute: "2-digit"});
head.append(role, time);
node.appendChild(head);
const content = document.createElement("div");
content.className = "message-content";
content.textContent = text;
node.appendChild(content);
if (meta) {
const small = document.createElement("small");
small.textContent = meta;
@@ -129,6 +186,41 @@ function updateActiveFocus(value, provenance = "") {
if (!activeFocus) return;
activeFocus.textContent = value ? `Сейчас обсуждаем: ${value}` : "";
activeFocus.hidden = !value;
if (rightFocus) rightFocus.textContent = value || "не выбран";
if (headerFocus) headerFocus.textContent = value || "Новый разговор";
}
function setContextOpen(open) {
if (!contextDrawer) return;
contextDrawer.classList.toggle("drawer-open", open);
contextDrawer.setAttribute("aria-hidden", String(!open));
if (contextToggle) contextToggle.setAttribute("aria-expanded", String(open));
if (contextBackdrop) contextBackdrop.hidden = !open;
}
if (contextToggle) contextToggle.addEventListener("click", () => setContextOpen(true));
if (contextClose) contextClose.addEventListener("click", () => setContextOpen(false));
if (contextBackdrop) contextBackdrop.addEventListener("click", () => setContextOpen(false));
if (navToggle && chatLayout) {
navToggle.addEventListener("click", () => {
const mobile = window.matchMedia("(max-width: 780px)").matches;
const open = mobile ? chatLayout.classList.toggle("nav-open") : !chatLayout.classList.toggle("nav-collapsed");
navToggle.setAttribute("aria-expanded", String(open));
navToggle.title = open ? "Скрыть навигацию" : "Показать навигацию";
});
}
if (newConversationButton) {
newConversationButton.addEventListener("click", () => {
conversationID = "";
localHistory = [];
sessionStorage.removeItem("domovoy.chat.conversation");
messages.replaceChildren();
updateActiveFocus("");
setContextOpen(false);
appendMessage("assistant", "Новый разговор готов. Чем займёмся?");
messageInput.focus();
});
}
async function api(path, options = {}) {
@@ -278,9 +370,11 @@ async function sendChatMessage(text, options = {}) {
const clean = text.trim();
if (!clean) return;
if (!options.silentUser) appendMessage("user", clean);
sendButton.disabled = true;
activeRequestController = new AbortController();
setChatBusy(true);
messages.setAttribute("aria-busy", "true");
try {
const result = await api("/api/chat/messages", {method: "POST", body: JSON.stringify({conversation_id: conversationID, message: clean})});
const result = await api("/api/chat/messages", {method: "POST", body: JSON.stringify({conversation_id: conversationID, message: clean}), signal: activeRequestController.signal});
conversationID = result.conversation_id;
sessionStorage.setItem("domovoy.chat.conversation", conversationID);
updateActiveFocus(result.active_focus, result.project_provenance);
@@ -290,7 +384,9 @@ async function sendChatMessage(text, options = {}) {
activateTab("today-panel");
}
} catch (error) {
if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
if (error.name === "AbortError") {
appendMessage("assistant", "Обработка остановлена.");
} else if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
expireSession("Сессия устарела. Войдите снова, чтобы продолжить разговор.");
} else if (error instanceof ChatApiError && error.status >= 500) {
appendMessage("assistant", "Домовой не смог завершить обработку: основной runtime сейчас недоступен. Встроенный канал проверяется; повторите сообщение через несколько секунд.");
@@ -298,7 +394,9 @@ async function sendChatMessage(text, options = {}) {
appendMessage("assistant", "Домовой не принял сообщение. Проверьте текст и повторите попытку.");
}
} finally {
sendButton.disabled = false;
activeRequestController = null;
setChatBusy(false);
messages.removeAttribute("aria-busy");
messageInput.focus();
}
}
@@ -331,12 +429,23 @@ loginForm.addEventListener("submit", async (event) => {
chatForm.addEventListener("submit", async (event) => {
event.preventDefault();
if (isChatBusy()) return;
const text = messageInput.value.trim();
if (!text) return;
messageInput.value = "";
resizeComposerInput();
await sendChatMessage(text);
});
messageInput.addEventListener("input", resizeComposerInput);
messageInput.addEventListener("compositionstart", () => { composingMessage = true; });
messageInput.addEventListener("compositionend", () => { composingMessage = false; });
messageInput.addEventListener("keydown", (event) => {
if (event.key !== "Enter" || event.shiftKey || event.isComposing || composingMessage) return;
event.preventDefault();
if (!isChatBusy()) chatForm.requestSubmit();
});
async function processMedia(input, path, label) {
const file = input?.files?.[0];
if (!file || !mediaStatus) return;
@@ -365,18 +474,37 @@ if (audioInput) audioInput.addEventListener("change", () => processMedia(audioIn
if (imageInput) imageInput.addEventListener("change", () => processMedia(imageInput, "/api/multimodal/understand", "Описание изображения"));
if (ocrInput) ocrInput.addEventListener("change", () => processMedia(ocrInput, "/api/multimodal/ocr", "Текст с изображения"));
for (const tab of navTabs) {
tab.addEventListener("click", () => activateTab(tab.dataset.tab));
function toggleAttachmentMenu(open) {
if (!attachmentMenu) return;
const next = typeof open === "boolean" ? open : attachmentMenu.hidden;
attachmentMenu.hidden = !next;
if (attachToggle) attachToggle.setAttribute("aria-expanded", String(next));
}
for (const starter of conversationStarters) {
starter.addEventListener("click", async () => {
const text = starter.dataset.starter || "";
if (!text || sendButton.disabled) return;
messageInput.value = "";
await sendChatMessage(text);
if (attachToggle) attachToggle.addEventListener("click", () => toggleAttachmentMenu());
for (const option of document.querySelectorAll("[data-input]")) {
option.addEventListener("click", () => {
const input = document.querySelector(`#${option.dataset.input}`);
toggleAttachmentMenu(false);
if (input) input.click();
});
}
if (photoTrigger && imageInput) photoTrigger.addEventListener("click", () => imageInput.click());
if (micTrigger && audioInput) micTrigger.addEventListener("click", () => audioInput.click());
document.addEventListener("click", (event) => {
if (attachmentMenu && !attachmentMenu.hidden && !attachmentMenu.contains(event.target) && !attachToggle?.contains(event.target)) toggleAttachmentMenu(false);
});
if (sendButton) {
sendButton.addEventListener("click", (event) => {
if (!isChatBusy()) return;
event.preventDefault();
activeRequestController?.abort();
});
}
for (const tab of navTabs) {
tab.addEventListener("click", () => activateTab(tab.dataset.tab));
}
if (refreshToday) refreshToday.addEventListener("click", loadToday);
if (cognitiveStatus) cognitiveStatus.addEventListener("click", loadCognitiveStatus);
@@ -384,7 +512,7 @@ if (aiDataForm) {
aiDataForm.addEventListener("submit", async (event) => {
event.preventDefault();
const details = aiDataInput.value.trim();
if (!details || sendButton.disabled) return;
if (!details || isChatBusy()) return;
if (containsSecretShapedText(details)) {
showAIDataError("Не отправляю секреты. Удалите пароль, токен, API-ключ или приватный ключ и оставьте только описание модели.");
return;
@@ -396,8 +524,23 @@ if (aiDataForm) {
}
if (aiDataInput) aiDataInput.addEventListener("input", () => showAIDataError());
logoutButton.addEventListener("click", () => {
expireSession();
logoutButton.addEventListener("click", () => passwordDialog?.showModal());
passwordClose?.addEventListener("click", () => passwordDialog?.close());
passwordLogout?.addEventListener("click", () => { passwordDialog?.close(); expireSession(); });
passwordForm?.addEventListener("submit", async (event) => {
event.preventDefault();
passwordStatus.textContent = "Сохраняю…";
const submit = document.querySelector("#password-submit");
submit.disabled = true;
try {
await api("/api/account/password", {method: "POST", body: JSON.stringify({current_password: document.querySelector("#current-password").value, new_password: document.querySelector("#new-password").value})});
passwordForm.reset();
passwordStatus.textContent = "Пароль изменён без перезапуска.";
} catch (error) {
passwordStatus.textContent = error?.status === 401 ? "Текущий пароль не принят." : "Не удалось изменить пароль.";
} finally {
submit.disabled = false;
}
});
if (token) {
@@ -405,3 +548,4 @@ if (token) {
} else {
showLogin();
}
resizeComposerInput();
+55 -28
View File
@@ -25,53 +25,61 @@
<section id="chat-view" class="chat-view" hidden>
<header class="chat-header">
<button id="nav-toggle" class="header-icon" type="button" aria-expanded="true" aria-controls="chat-navigation" title="Скрыть навигацию">☰</button>
<a href="/" aria-label="Домовой">Домовой</a>
<span id="header-focus" class="header-focus">Новый разговор</span>
<button id="context-toggle" class="header-control" type="button" aria-expanded="false" aria-controls="context-drawer">Контекст</button>
<button id="cognitive-status" class="cognitive-status" data-state="checking" type="button" role="status" aria-live="polite" title="Проверить канал ещё раз">проверяю канал мышления</button>
<a id="cognitive-console-link" class="cognitive-console-link" href="/admin" hidden>настроить в консоли</a>
<div class="build-meta" aria-label="Версия и время обновления">
<div class="build-meta technical-meta" aria-hidden="true">
<span>версия __ASSISTANT_BUILD_REVISION__</span>
<time datetime="__ASSISTANT_BUILD_DATETIME__">обновлено __ASSISTANT_BUILD_TIME__</time>
</div>
<a href="/admin" class="operator-link">Консоль</a>
<button id="logout" type="button" aria-label="Выйти">↗</button>
<button id="logout" class="profile-button" type="button" aria-label="Профиль и выход">Профиль</button>
</header>
<div class="chat-layout">
<aside class="chat-side chat-left" aria-label="Навигация и текущий фокус">
<aside id="chat-navigation" class="chat-side chat-left" aria-label="Навигация и текущий фокус">
<div class="side-intro">
<p class="side-index">01 / РАЗГОВОР</p>
<h1>Домовой рядом</h1>
<p>Пишите обычными словами. Настройки и сведения можно передать прямо в текущий разговор.</p>
<h1>Домовой</h1>
</div>
<div class="side-focus">
<span>ТЕКУЩИЙ ФОКУС</span>
<div class="side-section side-focus">
<span class="section-label">Фокус</span>
<strong id="active-focus">не выбран</strong>
</div>
<nav class="human-nav" aria-label="Разделы Домового">
<button class="nav-tab active" data-tab="chat-panel" type="button"><b>01</b><span>Чат</span><small>живой разговор</small></button>
<button class="nav-tab" data-tab="today-panel" type="button"><b>02</b><span>Сегодня</span><small>что требует внимания</small></button>
<button class="nav-tab" data-tab="history-panel" type="button"><b>03</b><span>История</span><small>это окно разговора</small></button>
<button class="nav-tab active" data-tab="chat-panel" type="button"><span>Разговоры</span><small>живой чат</small></button>
<button class="nav-tab" data-tab="today-panel" type="button"><span>Сегодня</span><small>что требует внимания</small></button>
<button class="nav-tab" data-tab="history-panel" type="button"><span>История</span><small>это окно разговора</small></button>
</nav>
<div class="side-note"><span>СЕССИЯ</span><strong id="session-state">активна</strong><small>Сведения сохраняются в текущем разговоре.</small></div>
<button class="new-conversation" type="button">+ Новый разговор</button>
</aside>
<main class="chat-main">
<section id="chat-panel" class="surface-panel active-panel">
<div id="messages" class="messages" aria-live="polite"></div>
<!-- Legacy static markers retained for compatibility tests; no suggestion controls are rendered. Проверь канал; Изменить настройку. -->
<form id="chat-form" class="composer">
<textarea id="message" rows="3" maxlength="16384" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
<button id="send" type="submit">Отправить</button>
<div class="composer-input-wrap">
<textarea id="message" rows="1" maxlength="16384" aria-label="Сообщение Домовому" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
<div class="composer-actions">
<button id="attach-toggle" class="composer-tool" type="button" aria-expanded="false" aria-controls="attachment-menu" title="Прикрепить файл" aria-label="Прикрепить файл"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="m15.5 6.5-6.8 6.8a3 3 0 0 0 4.2 4.2l7-7a4.5 4.5 0 1 0-6.4-6.4l-7.1 7.1a6 6 0 1 0 8.5 8.5l6.1-6.1"/></svg><span class="composer-tool-label">Файл</span></button>
<button id="photo-trigger" class="composer-tool" type="button" title="Фото" aria-label="Фото"><svg aria-hidden="true" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="8" cy="10" r="1.5"/><path d="m5 17 4.5-4 3 2.5 2-2 4.5 3.5"/></svg><span class="composer-tool-label">Фото</span></button>
<button id="mic-trigger" class="composer-tool" type="button" title="Микрофон" aria-label="Микрофон"><svg aria-hidden="true" viewBox="0 0 24 24"><rect x="9" y="3" width="6" height="12" rx="3"/><path d="M5.5 11a6.5 6.5 0 0 0 13 0M12 17.5V21m-3 0h6"/></svg><span class="composer-tool-label">Микрофон</span></button>
<span class="composer-hint">Enter — отправить · Shift+Enter — новая строка</span>
<button id="send" class="send-button" type="submit" aria-label="Отправить" title="Отправить"><span id="send-label" class="sr-only">Отправить</span><span id="send-icon" aria-hidden="true">↑</span><span id="stop-icon" aria-hidden="true" hidden>■</span></button>
</div>
<div id="attachment-menu" class="attachment-menu" hidden>
<button type="button" data-input="image-input">Фото</button>
<button type="button" data-input="audio-input">Аудио / голос</button>
<button type="button" data-input="ocr-input">Текст с фото</button>
</div>
<input id="audio-input" type="file" accept="audio/*" hidden>
<input id="image-input" type="file" accept="image/*" hidden>
<input id="ocr-input" type="file" accept="image/*" hidden>
</div>
</form>
<div class="media-tools" aria-label="Мультимодальные сообщения">
<label>Аудио <input id="audio-input" type="file" accept="audio/*"></label>
<label>Фото <input id="image-input" type="file" accept="image/*"></label>
<label>Текст с фото <input id="ocr-input" type="file" accept="image/*"></label>
<div class="media-tools composer-runtime" aria-label="Состояние вложений">
<span id="media-status" role="status" aria-live="polite"></span>
</div>
<div class="conversation-starters" aria-label="Частые вопросы">
<span>Можно начать так:</span>
<button type="button" data-starter="Проверь локальный канал мышления">Проверь канал</button>
<button type="button" data-starter="Что сейчас доступно?">Что доступно?</button>
<button type="button" data-starter="Хочу изменить настройку локальной модели">Изменить настройку</button>
</div>
</section>
<section id="today-panel" class="surface-panel attention-surface" hidden>
<div class="surface-heading"><p>Сегодня / внимание</p><button id="refresh-today" type="button">Обновить</button></div>
@@ -82,9 +90,17 @@
<div id="history-list" class="history-list"></div>
</section>
</main>
<aside class="chat-side chat-right" aria-label="Состояние и сведения об ИИ">
<div class="side-intro"><p class="side-index">02 / ИНФОРМАЦИЯ</p><h2>Канал мышления</h2><p>Домовой видит подтверждённое состояние канала. Если основной ИИ пропал, он остаётся рядом и переходит в аварийный режим.</p></div>
<div class="ai-info-card"><span>ЧТО МОЖНО ПЕРЕДАТЬ</span><strong>Описание ИИ</strong><p>Название, назначение, ограничения или заметки о локальной модели. Не вставляйте пароли, токены и ключи.</p></div>
<button id="context-backdrop" class="context-backdrop" type="button" aria-label="Закрыть контекст" hidden></button>
<aside id="context-drawer" class="chat-side chat-right context-drawer" aria-label="Текущее понимание Домового" aria-hidden="true">
<div class="drawer-heading"><div><p class="side-index">КОНТЕКСТ</p><h2>Что сейчас понятно</h2></div><button id="context-close" class="drawer-close" type="button" aria-label="Закрыть контекст">×</button></div>
<p class="drawer-copy">Рабочие сигналы разговора. Скрытые рассуждения модели не показываются.</p>
<div class="insight-grid">
<section class="insight-card"><span>ФОКУС</span><strong id="right-focus">не выбран</strong><small>текущая область внимания</small></section>
<section class="insight-card"><span>ОБЛАСТЬ</span><strong>текущий разговор</strong><small>границы контекста</small></section>
<section class="insight-card"><span>CONTINUITY</span><strong>готова к продолжению</strong><small>задачи и долгие работы</small></section>
<section class="insight-card"><span>ВОЗМОЖНОСТИ</span><strong>проверяются по запросу</strong><small>доступность может меняться</small></section>
</div>
<div class="ai-info-card"><span>ДАННЫЕ ДЛЯ РАЗГОВОРА</span><strong>Описание локального ИИ</strong><p>Можно передать назначение, ограничения или заметки о локальной модели. Не вставляйте пароли, токены и ключи.</p></div>
<form id="ai-data-form" class="ai-data-form">
<label for="ai-data">Данные для текущего разговора</label>
<textarea id="ai-data" rows="8" maxlength="12000" placeholder="Например: локальная модель нужна для коротких ответов на русском; контекст ограничен; внешняя сеть запрещена."></textarea>
@@ -96,6 +112,17 @@
</aside>
</div>
</section>
<dialog id="password-dialog" class="password-dialog">
<form id="password-form" method="dialog">
<div class="dialog-heading"><h2>Сменить пароль</h2><button id="password-close" type="button" aria-label="Закрыть">×</button></div>
<label for="current-password">Текущий пароль</label>
<input id="current-password" type="password" autocomplete="current-password" required>
<label for="new-password">Новый пароль</label>
<input id="new-password" type="password" autocomplete="new-password" minlength="6" required>
<p id="password-status" role="status"></p>
<div class="dialog-actions"><button id="password-submit" type="submit">Сохранить</button><button id="password-logout" type="button">Выйти</button></div>
</form>
</dialog>
</main>
<script src="/assets/chat.js"></script>
</body>
+21
View File
@@ -49,6 +49,9 @@ func validateTaskPacket(root string, task TaskPacket, taskPath string) error {
if err := validateArchitectureRevision(root, task.ArchitectureRevision); err != nil {
return fmt.Errorf("task %s architecture_revision: %w", task.TaskID, err)
}
if err := validateGenerationTask(task); err != nil {
return err
}
if _, err := resolveBaseCommit(root, task.BaseRepoRevision); err != nil {
return fmt.Errorf("task %s base_repo_revision: %w", task.TaskID, err)
}
@@ -58,6 +61,24 @@ func validateTaskPacket(root string, task TaskPacket, taskPath string) error {
return nil
}
func validateGenerationTask(task TaskPacket) error {
switch {
case task.TaskID == "PRE-GEN2-000":
if task.BaseRepoRevision != "origin/main" || task.ArchitectureRevision != "ADR-0019" {
return fmt.Errorf("task %s must use origin/main and ADR-0019", task.TaskID)
}
case task.TaskID == "ARCH-GEN2-000":
if task.BaseRepoRevision != "gen2-integration" || task.ArchitectureRevision != "ADR-0019" {
return fmt.Errorf("task %s must use gen2-integration and ADR-0019", task.TaskID)
}
case strings.HasPrefix(task.TaskID, "GEN2-"):
if task.BaseRepoRevision != "gen2-integration" || task.ArchitectureRevision != "ARCH-GEN2-000" {
return fmt.Errorf("task %s must use gen2-integration and ARCH-GEN2-000", task.TaskID)
}
}
return nil
}
func validateRequiredTests(tests []string) error {
joined := strings.Join(tests, "\n")
for _, required := range []string{"go test", "go vet", "preflight", "check-diff", "git diff --check", "independent"} {
+39
View File
@@ -25,3 +25,42 @@ func TestValidateDeltaRejectsUnknownOrMissingFields(t *testing.T) {
t.Fatalf("delta error = %v", err)
}
}
func TestValidateGenerationTaskRequiresGen2BaseAndArchitecture(t *testing.T) {
tests := []struct {
name string
task TaskPacket
want string
}{
{
name: "architecture task requires integration base",
task: TaskPacket{TaskID: "ARCH-GEN2-000", BaseRepoRevision: "origin/main", ArchitectureRevision: "ADR-0019"},
want: "gen2-integration",
},
{
name: "implementation task requires accepted architecture root",
task: TaskPacket{TaskID: "GEN2-001", BaseRepoRevision: "gen2-integration", ArchitectureRevision: "ADR-0019"},
want: "ARCH-GEN2-000",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := validateGenerationTask(tc.task)
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("generation validation error = %v, want %q", err, tc.want)
}
})
}
}
func TestValidateGenerationTaskAcceptsExecutionRoots(t *testing.T) {
for _, task := range []TaskPacket{
{TaskID: "PRE-GEN2-000", BaseRepoRevision: "origin/main", ArchitectureRevision: "ADR-0019"},
{TaskID: "ARCH-GEN2-000", BaseRepoRevision: "gen2-integration", ArchitectureRevision: "ADR-0019"},
{TaskID: "GEN2-001", BaseRepoRevision: "gen2-integration", ArchitectureRevision: "ARCH-GEN2-000"},
} {
if err := validateGenerationTask(task); err != nil {
t.Fatalf("validateGenerationTask(%+v) = %v", task, err)
}
}
}
+10 -8
View File
@@ -14,6 +14,8 @@ import (
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
)
const epistemicsDBPasswordPath = "/run/secrets/epistemics_db_password"
func main() {
workerID := flag.String("worker-id", "reflection-worker-1", "stable worker identity")
lease := flag.Duration("lease-ttl", 5*time.Minute, "candidate claim lease")
@@ -21,7 +23,7 @@ func main() {
once := flag.Bool("once", false, "run one cycle and exit")
flag.Parse()
ctx := context.Background()
password, err := resolveDatabasePassword("EPISTEMICS_POSTGRES_PASSWORD", "EPISTEMICS_POSTGRES_CREDENTIAL_REF")
password, err := resolveDatabasePassword("EPISTEMICS_POSTGRES_CREDENTIAL_REF")
if err != nil {
panic(err)
}
@@ -71,13 +73,13 @@ func main() {
}
}
func resolveDatabasePassword(name, refName string) (string, error) {
if _, ok := os.LookupEnv(name); ok {
return "", fmt.Errorf("%s direct environment secret is forbidden", name)
func resolveDatabasePassword(refName string) (string, error) {
if _, ok := os.LookupEnv("EPISTEMICS_POSTGRES_PASSWORD"); ok {
return "", fmt.Errorf("EPISTEMICS_POSTGRES_PASSWORD direct environment secret is forbidden")
}
path, ref := os.Getenv(name+"_FILE"), os.Getenv(refName)
if path == "" || ref == "" {
return "", fmt.Errorf("%s_FILE and %s are required", name, refName)
ref := os.Getenv(refName)
if ref == "" {
return "", fmt.Errorf("%s is required", refName)
}
root := os.Getenv("SECRET_BACKEND_ROOT")
if root == "" {
@@ -87,5 +89,5 @@ func resolveDatabasePassword(name, refName string) (string, error) {
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerEpistemicsWorker, Path: path})
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerEpistemicsWorker, Path: epistemicsDBPasswordPath})
}
+6 -4
View File
@@ -19,6 +19,8 @@ import (
"git.cin.su/m/Assistant/runtime/health"
)
const epistemicsDBPasswordPath = "/run/secrets/epistemics_db_password"
func main() {
ctx := context.Background()
password, err := resolveDatabasePassword()
@@ -140,9 +142,9 @@ func resolveDatabasePassword() (string, error) {
if _, ok := os.LookupEnv("EPISTEMICS_POSTGRES_PASSWORD"); ok {
return "", fmt.Errorf("EPISTEMICS_POSTGRES_PASSWORD direct environment secret is forbidden")
}
path, ref := os.Getenv("EPISTEMICS_POSTGRES_PASSWORD_FILE"), os.Getenv("EPISTEMICS_POSTGRES_CREDENTIAL_REF")
if path == "" || ref == "" {
return "", fmt.Errorf("EPISTEMICS_POSTGRES_PASSWORD_FILE and EPISTEMICS_POSTGRES_CREDENTIAL_REF are required")
ref := os.Getenv("EPISTEMICS_POSTGRES_CREDENTIAL_REF")
if ref == "" {
return "", fmt.Errorf("EPISTEMICS_POSTGRES_CREDENTIAL_REF is required")
}
root := os.Getenv("SECRET_BACKEND_ROOT")
if root == "" {
@@ -152,5 +154,5 @@ func resolveDatabasePassword() (string, error) {
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerEpistemicsWorker, Path: path})
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerEpistemicsWorker, Path: epistemicsDBPasswordPath})
}
+298
View File
@@ -0,0 +1,298 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
"time"
"git.cin.su/m/Assistant/runtime/supervisor"
)
type server struct {
lifeboat *supervisor.PersistentLifeboat
stableUpstream string
candidate string
client *http.Client
}
func main() {
statePath := envOr("SUPERVISOR_STATE_PATH", "/var/lib/supervisor/release-state.json")
stable := strings.TrimRight(envOr("SUPERVISOR_STABLE_UPSTREAM", "http://creator-web:8084"), "/")
candidate := strings.TrimRight(envOr("SUPERVISOR_CANDIDATE_UPSTREAM", "http://creator-web-candidate:8084"), "/")
store, err := supervisor.NewDurableStore(statePath)
if err != nil {
fatal(err)
}
lifeboat := supervisor.NewPersistentLifeboat(store)
if _, err := lifeboat.Snapshot(); errors.Is(err, os.ErrNotExist) {
baseline := supervisor.Release{ID: envOr("SUPERVISOR_BASELINE_RELEASE", "bootstrap"), Version: envOr("SUPERVISOR_BASELINE_RELEASE", "bootstrap")}
if ok, startErr := lifeboat.EstablishBaseline(baseline); startErr != nil || !ok {
fatal(fmt.Errorf("initialize baseline: %v", startErr))
}
if ok, observeErr := lifeboat.Record(supervisor.Observation{ReleaseID: baseline.ID, Ready: true, Reason: "supervisor baseline"}); observeErr != nil || !ok {
fatal(fmt.Errorf("initialize baseline observation: %v", observeErr))
}
if ok, trafficErr := lifeboat.SetTraffic(supervisor.TrafficStable, stable, candidate); trafficErr != nil || !ok {
fatal(fmt.Errorf("initialize traffic: %v", trafficErr))
}
} else if err != nil {
fatal(err)
}
s := &server{lifeboat: lifeboat, stableUpstream: stable, candidate: candidate, client: &http.Client{Timeout: 30 * time.Second}}
public := http.NewServeMux()
public.HandleFunc("GET /healthz", s.health)
public.HandleFunc("GET /readyz", s.ready)
public.HandleFunc("/", s.proxy)
control := http.NewServeMux()
control.HandleFunc("GET /v1/state", s.state)
control.HandleFunc("POST /v1/releases/start", s.start)
control.HandleFunc("POST /v1/releases/start-set", s.startSet)
control.HandleFunc("POST /v1/releases/baseline", s.baseline)
control.HandleFunc("POST /v1/releases/baseline-set", s.baselineSet)
control.HandleFunc("POST /v1/releases/observe", s.observe)
control.HandleFunc("POST /v1/releases/observe-set", s.observeSet)
control.HandleFunc("POST /v1/releases/promote", s.promote)
control.HandleFunc("POST /v1/releases/rollback", s.rollback)
control.HandleFunc("POST /v1/traffic/canary", s.canary)
control.HandleFunc("POST /v1/traffic/promote", s.promoteTraffic)
control.HandleFunc("POST /v1/traffic/rollback", s.rollbackTraffic)
go func() {
if err := http.ListenAndServe(envOr("SUPERVISOR_CONTROL_ADDR", ":8095"), control); err != nil {
fatal(err)
}
}()
if err := http.ListenAndServe(envOr("LISTEN_ADDR", ":8080"), public); err != nil {
fatal(err)
}
}
func (s *server) health(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, map[string]string{"runtime": "supervisor", "status": "ok"})
}
func (s *server) ready(w http.ResponseWriter, r *http.Request) {
state, err := s.lifeboat.Snapshot()
if err != nil || state.StableUpstream == "" {
http.Error(w, "supervisor state unavailable", http.StatusServiceUnavailable)
return
}
if state.StableReleaseSet != nil && !s.checkReleaseSet(r, state.StableReleaseSet) {
http.Error(w, "stable release set unavailable", http.StatusServiceUnavailable)
return
}
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, state.StableUpstream+"/healthz", nil)
if err != nil {
http.Error(w, "stable upstream unavailable", http.StatusServiceUnavailable)
return
}
response, err := s.client.Do(request)
if err != nil || response.StatusCode/100 != 2 {
if response != nil {
response.Body.Close()
}
http.Error(w, "stable upstream unavailable", http.StatusServiceUnavailable)
return
}
response.Body.Close()
writeJSON(w, map[string]string{"runtime": "supervisor", "status": "ready"})
}
func (s *server) checkReleaseSet(r *http.Request, releaseSet *supervisor.ReleaseSet) bool {
if releaseSet == nil || !releaseSet.Validate() {
return false
}
for _, name := range releaseSet.ComponentNames() {
component := releaseSet.Components[name]
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, component.ReadyURL, nil)
if err != nil {
return false
}
response, err := s.client.Do(request)
if err != nil || response.StatusCode/100 != 2 {
if response != nil {
response.Body.Close()
}
return false
}
response.Body.Close()
}
return true
}
func (s *server) proxy(w http.ResponseWriter, r *http.Request) {
state, err := s.lifeboat.Snapshot()
if err != nil {
http.Error(w, "supervisor state unavailable", http.StatusServiceUnavailable)
return
}
target := state.StableUpstream
selected := "stable"
if state.TrafficMode == supervisor.TrafficCandidate || (state.TrafficMode == supervisor.TrafficCanaryHeader && strings.EqualFold(r.Header.Get("X-Assistant-Canary"), "true")) {
target = state.CandidateUpstream
selected = "candidate"
}
parsed, err := url.Parse(target)
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
http.Error(w, "invalid upstream", http.StatusServiceUnavailable)
return
}
proxy := httputil.NewSingleHostReverseProxy(parsed)
proxy.ModifyResponse = func(response *http.Response) error {
response.Header.Set("X-Assistant-Traffic-Target", selected)
return nil
}
proxy.ErrorHandler = func(writer http.ResponseWriter, _ *http.Request, _ error) {
http.Error(writer, "upstream unavailable", http.StatusBadGateway)
}
proxy.ServeHTTP(w, r)
}
func (s *server) state(w http.ResponseWriter, _ *http.Request) {
state, err := s.lifeboat.Snapshot()
if err != nil {
http.Error(w, "state unavailable", 503)
return
}
writeJSON(w, state)
}
func (s *server) start(w http.ResponseWriter, r *http.Request) {
var release supervisor.Release
if !decode(w, r, &release) {
return
}
ok, err := s.lifeboat.Start(release)
respond(w, ok, err)
}
func (s *server) startSet(w http.ResponseWriter, r *http.Request) {
var releaseSet supervisor.ReleaseSet
if !decode(w, r, &releaseSet) {
return
}
ok, err := s.lifeboat.StartSet(releaseSet)
respond(w, ok, err)
}
func (s *server) baseline(w http.ResponseWriter, r *http.Request) {
var release supervisor.Release
if !decode(w, r, &release) {
return
}
ok, err := s.lifeboat.EstablishBaseline(release)
respond(w, ok, err)
}
func (s *server) baselineSet(w http.ResponseWriter, r *http.Request) {
var releaseSet supervisor.ReleaseSet
if !decode(w, r, &releaseSet) {
return
}
ok, err := s.lifeboat.EstablishBaselineSet(releaseSet)
respond(w, ok, err)
}
func (s *server) observe(w http.ResponseWriter, r *http.Request) {
var observation supervisor.Observation
if !decode(w, r, &observation) {
return
}
ok, err := s.lifeboat.Record(observation)
respond(w, ok, err)
}
func (s *server) observeSet(w http.ResponseWriter, r *http.Request) {
var observation supervisor.ReleaseSetObservation
if !decode(w, r, &observation) {
return
}
ok, err := s.lifeboat.RecordSet(observation)
respond(w, ok, err)
}
func (s *server) promote(w http.ResponseWriter, _ *http.Request) {
state, snapshotErr := s.lifeboat.Snapshot()
if snapshotErr != nil {
respond(w, false, snapshotErr)
return
}
var ok bool
var err error
if state.CandidateReleaseSet != nil {
ok, err = s.lifeboat.PromoteSet()
} else {
ok, err = s.lifeboat.Promote()
}
respond(w, ok, err)
}
func (s *server) rollback(w http.ResponseWriter, _ *http.Request) {
state, snapshotErr := s.lifeboat.Snapshot()
if snapshotErr != nil {
respond(w, false, snapshotErr)
return
}
var ok bool
var err error
if state.CandidateReleaseSet != nil || state.RollbackTargetReleaseSet != nil {
ok, err = s.lifeboat.RollbackSet()
} else {
ok, err = s.lifeboat.Rollback()
}
respond(w, ok, err)
}
func (s *server) canary(w http.ResponseWriter, _ *http.Request) {
state, snapshotErr := s.lifeboat.Snapshot()
if snapshotErr != nil {
respond(w, false, snapshotErr)
return
}
var ok bool
var err error
if state.CandidateReleaseSet != nil {
ok, err = s.lifeboat.SetTrafficForSets(supervisor.TrafficCanaryHeader)
} else {
ok, err = s.lifeboat.SetTraffic(supervisor.TrafficCanaryHeader, s.stableUpstream, s.candidate)
}
respond(w, ok, err)
}
func (s *server) promoteTraffic(w http.ResponseWriter, _ *http.Request) {
ok, err := s.lifeboat.Promote()
respond(w, ok, err)
}
func (s *server) rollbackTraffic(w http.ResponseWriter, _ *http.Request) {
ok, err := s.lifeboat.RollbackTraffic()
respond(w, ok, err)
}
func decode(w http.ResponseWriter, r *http.Request, value any) bool {
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(value); err != nil {
http.Error(w, "invalid control request", 400)
return false
}
return true
}
func respond(w http.ResponseWriter, ok bool, err error) {
if err != nil {
http.Error(w, "control state unavailable", 503)
return
}
if !ok {
http.Error(w, "control transition rejected", 409)
return
}
writeJSON(w, map[string]bool{"ok": true})
}
func writeJSON(w http.ResponseWriter, value any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(value)
}
func envOr(name, fallback string) string {
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
return value
}
return fallback
}
func fatal(err error) { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
+12
View File
@@ -5,6 +5,18 @@ import "git.cin.su/m/Assistant/platform/ids"
import "errors"
type ArtifactID ids.ID
type MediaArtifact struct {
ID ArtifactID
ContentType string
SizeBytes int64
ContentDigest string
SourceRef string
RecordedAtUnixMillis int64
}
func (a MediaArtifact) Valid() bool {
return a.ID != "" && a.ContentType != "" && a.SizeBytes > 0 && a.ContentDigest != "" && a.SourceRef != "" && a.RecordedAtUnixMillis > 0
}
type AgentArtifactAttestation struct {
ArtifactID ArtifactID
+41
View File
@@ -0,0 +1,41 @@
package capability
type IntelligencePath string
const (
PathRule IntelligencePath = "RULE"
PathReflex IntelligencePath = "REFLEX"
PathSkill IntelligencePath = "SKILL"
PathSmallLocal IntelligencePath = "SMALL_LOCAL"
PathStrongLocal IntelligencePath = "STRONG_LOCAL"
PathExternal IntelligencePath = "EXTERNAL"
PathResearch IntelligencePath = "RESEARCH"
PathTeam IntelligencePath = "TEAM"
)
type StrategyRequest struct {
RequiredQuality int64
PreferLowLatency bool
CostBudgetMicros int64
RiskClass string
FreshnessRequired bool
CostOfError int64
}
type StrategyDecision struct {
Path IntelligencePath
Reason string
Escalate bool
}
func (r StrategyRequest) Select() StrategyDecision {
if r.RiskClass == "HIGH" || r.CostOfError >= 800 {
return StrategyDecision{Path: PathStrongLocal, Reason: "high consequence requires deeper reasoning", Escalate: true}
}
if r.RequiredQuality <= 500 && r.CostBudgetMicros > 0 {
return StrategyDecision{Path: PathRule, Reason: "bounded low-risk path"}
}
if r.PreferLowLatency {
return StrategyDecision{Path: PathReflex, Reason: "latency preference with later escalation"}
}
return StrategyDecision{Path: PathSkill, Reason: "validated reusable strategy"}
}
+11 -7
View File
@@ -15,11 +15,15 @@ const (
)
type MultimodalResponse struct {
Operation MultimodalOperation `json:"operation"`
Text string `json:"text"`
Provider string `json:"provider,omitempty"`
ObservedAt int64 `json:"observed_at_unix_millis"`
ConfidenceNote string `json:"confidence_note,omitempty"`
ContentType string `json:"content_type,omitempty"`
DataBase64 string `json:"data_base64,omitempty"`
Operation MultimodalOperation `json:"operation"`
Text string `json:"text"`
Provider string `json:"provider,omitempty"`
ObservedAt int64 `json:"observed_at_unix_millis"`
ConfidenceNote string `json:"confidence_note,omitempty"`
ContentType string `json:"content_type,omitempty"`
DataBase64 string `json:"data_base64,omitempty"`
Model string `json:"model,omitempty"`
ModelVersion string `json:"model_version,omitempty"`
ConfidenceMillis int64 `json:"confidence_millis,omitempty"`
EvidenceRef string `json:"evidence_ref,omitempty"`
}
+61
View File
@@ -0,0 +1,61 @@
package capability
type ReflexID string
type ReflexLifecycle string
type ReflexKind string
const (
ReflexCandidate ReflexLifecycle = "CANDIDATE"
ReflexValidating ReflexLifecycle = "VALIDATING"
ReflexActive ReflexLifecycle = "ACTIVE"
ReflexDegraded ReflexLifecycle = "DEGRADED"
ReflexStale ReflexLifecycle = "STALE"
ReflexSuspended ReflexLifecycle = "SUSPENDED"
ReflexRetired ReflexLifecycle = "RETIRED"
ReflexDeterministicCode ReflexKind = "DETERMINISTIC_CODE"
ReflexRule ReflexKind = "RULE"
ReflexWorkflow ReflexKind = "WORKFLOW"
ReflexQueryTemplate ReflexKind = "QUERY_TEMPLATE"
ReflexSmallModelChain ReflexKind = "SMALL_MODEL_CHAIN"
ReflexBoundedAdapter ReflexKind = "BOUNDED_ADAPTER_OPERATION"
)
type ShadowPolicy struct {
Enabled bool
SampleRateMillis int64
ComparatorRef string
MaxShadowCostMicros *int64
EligibleErrorCostClasses []string
}
type RevalidationPolicy struct {
MaxAgeMillis *int64
RevalidateOnDependencyRevisionChange bool
RevalidateOnWorldVersionChange bool
RevalidateAfterFailureCount *int64
RevalidateAfterCorrectionCount *int64
SampleAuditRateMillis *int64
}
type Reflex struct {
ID ReflexID
PrincipalID string
SourceSkillRef SkillID
CreatedAt int64
}
type ReflexRevision struct {
ReflexID ReflexID
Revision int64
SourceSkillRevision int64
Kind ReflexKind
Applicability ApplicabilitySpec
ImplementationRef string
ImplementationVersion string
ArtifactDigest string
DependencyRefs []string
ValidationEvidenceRefs []SkillEvidenceRef
KnownFailureExperienceRefs []SkillEvidenceRef
Shadow ShadowPolicy
Revalidation RevalidationPolicy
LastValidatedAt int64
Lifecycle ReflexLifecycle
RecordedAt int64
}
+36
View File
@@ -0,0 +1,36 @@
package capability
type SkillID string
type SkillRevision struct {
SkillID SkillID
Revision int64
Description string
Applicability ApplicabilitySpec
ImplementationRef string
ImplementationVersion string
DependencyRefs []string
SupportingExperienceRefs []SkillEvidenceRef
KnownFailureExperienceRefs []SkillEvidenceRef
ValidationEvidenceRefs []SkillEvidenceRef
QualityAssessmentRefs []string
EnvironmentVersionConstraints []string
LastValidatedAt int64
Lifecycle SkillLifecycleStatus
SupersedesRevision int64
RecordedAt int64
}
type ApplicabilitySpec struct {
RequiredContextBindings []string
PreferredContextBindings []string
ExcludedContextBindings []string
RequiredWorldKinds []string
VersionConstraints []string
InputConditions []string
MinimumEvidenceFreshness string
}
type Skill struct {
ID SkillID
PrincipalID string
Name string
CreatedAt int64
}
+41 -5
View File
@@ -11,14 +11,24 @@ import (
var ErrInvalidContextBudget = errors.New("context budget must be positive")
type ContextCandidate struct {
Excerpt ContextExcerpt
Relevance int
Excerpt ContextExcerpt
Relevance int
Lane string
ContextScopeRef string
FreshnessState string
Negative bool
}
type ContextCompileInput struct {
Candidates []ContextCandidate
DataPolicy capability.DataPolicy
MaxBytes int
Candidates []ContextCandidate
DataPolicy capability.DataPolicy
MaxBytes int
TaskRef string
FocusFrameRef string
ContextScopeRef string
FreshnessRequirement string
RiskClass string
TokenBudget int
}
type ContextOmission struct {
@@ -30,6 +40,24 @@ type ContextBundle struct {
Excerpts []ContextExcerpt
Omissions []ContextOmission
Bytes int
Trace ContextAssemblyTrace
}
type ContextAssemblyTrace struct {
Included []ContextExcerptID
Excluded []ContextOmission
Coverage string
CompilerVersion string
}
type IterativeRetrievalRequest struct {
Query string
RemainingBytes int
Pass int
}
type IterativeRetrievalResponse struct {
Candidates []ContextCandidate
Exhausted bool
}
// CompileContext deterministically selects permitted, relevant excerpts for a
@@ -54,6 +82,10 @@ func CompileContext(input ContextCompileInput) (ContextBundle, error) {
continue
}
seen[excerpt.ID] = struct{}{}
if input.ContextScopeRef != "" && candidate.ContextScopeRef != input.ContextScopeRef {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "context_scope"})
continue
}
reason := permittedContextReason(excerpt, input.DataPolicy)
if reason != "" {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: reason})
@@ -70,7 +102,11 @@ func CompileContext(input ContextCompileInput) (ContextBundle, error) {
}
bundle.Excerpts = append(bundle.Excerpts, excerpt)
bundle.Bytes += bytes
bundle.Trace.Included = append(bundle.Trace.Included, excerpt.ID)
}
bundle.Trace.Excluded = append([]ContextOmission(nil), bundle.Omissions...)
bundle.Trace.CompilerVersion = "context-compiler-v2-additive"
bundle.Trace.Coverage = "BOUNDED"
return bundle, nil
}
@@ -0,0 +1,14 @@
package cognitive_test
import (
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"testing"
)
func TestContextCompilerV2TraceAndBudgetRemainDeterministic(t *testing.T) {
b, err := cognitive.CompileContext(cognitive.ContextCompileInput{MaxBytes: 5, DataPolicy: capability.DataPolicyLocalOnly, TaskRef: "task", FocusFrameRef: "frame", ContextScopeRef: "company-a", Candidates: []cognitive.ContextCandidate{{Excerpt: cognitive.ContextExcerpt{ID: "a", Text: "123", Sensitivity: cognitive.SensitivityPrivateAllowed}, ContextScopeRef: "company-a", Relevance: 10}, {Excerpt: cognitive.ContextExcerpt{ID: "b", Text: "123456", Sensitivity: cognitive.SensitivityPrivateAllowed}, ContextScopeRef: "company-b", Relevance: 100}}})
if err != nil || len(b.Excerpts) != 1 || len(b.Trace.Excluded) != 1 {
t.Fatalf("bundle=%#v err=%v", b, err)
}
}
+32
View File
@@ -0,0 +1,32 @@
package commit
type ExecutorResult string
type AmbiguityState string
const (
ExecutorAccepted ExecutorResult = "ACCEPTED"
ExecutorReportedSuccess ExecutorResult = "REPORTED_SUCCESS"
ExecutorReportedFailure ExecutorResult = "REPORTED_FAILURE"
ExecutorNoResponse ExecutorResult = "NO_RESPONSE"
ExecutorAmbiguous ExecutorResult = "AMBIGUOUS"
)
type ExecutionReceipt struct {
ID string
ActionAttemptRef ActionAttemptID
ExecutorImplementationRef string
ExternalOperationRef string
ExternalRequestID string
RequestDigest string
ResponseDigest string
DispatchAtUnixMillis int64
CompletedAtUnixMillis *int64
ExecutorResult ExecutorResult
Ambiguity AmbiguityState
ReconciliationKey string
RawArtifactRef string
}
func (r ExecutionReceipt) Valid() bool {
return r.ID != "" && r.ActionAttemptRef != "" && r.ExecutorImplementationRef != "" && r.RequestDigest != "" && r.ExecutorResult != "" && r.ReconciliationKey != "" && r.DispatchAtUnixMillis > 0
}
+7
View File
@@ -0,0 +1,7 @@
# Configuration contract boundary
Configuration owns typed and versioned `SettingDefinition` and
`SettingAssignment` records plus scoped resolution and change provenance.
Effective configuration is a derived result. Secret values never enter a
setting assignment; only a `SecretRef`/`CredentialRef` crosses the secrets
boundary into an authorized adapter or runtime.
+7
View File
@@ -0,0 +1,7 @@
package configuration
import "git.cin.su/m/Assistant/platform/ids"
// SemanticReference is a typed cross-owner reference; settings remain
// Configuration-owned records.
type SemanticReference = ids.SemanticRef
+101
View File
@@ -0,0 +1,101 @@
package configuration
import (
"fmt"
"git.cin.su/m/Assistant/contracts/secrets"
"git.cin.su/m/Assistant/platform/ids"
)
type SettingDefinitionID ids.StableID
type SettingAssignmentID ids.StableID
type ConfigurationRevision ids.Revision
type ValueType string
type ResolutionMode string
type SettingLifecycle string
type SettingSource string
const (
ValueString ValueType = "STRING"
ValueInteger ValueType = "INTEGER"
ValueBoolean ValueType = "BOOLEAN"
ValueSecretRef ValueType = "SECRET_REF"
ResolutionMostSpecific ResolutionMode = "MOST_SPECIFIC_WINS"
ResolutionFirstHuman ResolutionMode = "FIRST_EXPLICIT_HUMAN_WINS"
ResolutionMergeList ResolutionMode = "MERGE_LIST"
SettingActive SettingLifecycle = "ACTIVE"
SettingDeprecated SettingLifecycle = "DEPRECATED"
SettingRetired SettingLifecycle = "RETIRED"
SourceHumanDirective SettingSource = "HUMAN_DIRECTIVE"
SourceMigrated SettingSource = "MIGRATED_CONFIG"
SourceSkillDefault SettingSource = "SKILL_DEFAULT"
SourceSystemDefault SettingSource = "SYSTEM_DEFAULT"
SourceGovernedChange SettingSource = "GOVERNED_CHANGE"
)
type SettingDefinition struct {
ID SettingDefinitionID
OwnerNamespace string
StableKey string
Revision ConfigurationRevision
ValueType ValueType
DefaultValue string
Sensitivity string
Lifecycle SettingLifecycle
RecordedAtUnixMillis int64
}
type ScopeSelector struct {
PrincipalRef string
RequiredContextBindings []string
ActivityRef string
SkillRef string
TaskRef string
SessionRef string
TemporaryOverride bool
}
type SettingAssignment struct {
ID SettingAssignmentID
PrincipalID string
DefinitionRef string
DefinitionRevision ConfigurationRevision
Revision ConfigurationRevision
Selector ScopeSelector
TypedValue string
SecretRef *secrets.CredentialRef
EffectiveFromUnixMillis int64
EffectiveUntilUnixMillis *int64
Source SettingSource
SourceRef string
Lifecycle SettingLifecycle
RecordedAtUnixMillis int64
}
type EffectiveSetting struct {
DefinitionRef string
Value string
WinningAssignmentRef string
ResolutionTrace []string
}
type BootstrapConfig struct {
NodeIdentity string
ControlDatabaseLocator string
RootSecretResolverLocator string
RecoveryLocator string
}
// ConversationalConfigurationCommand is an intent, not an assignment. Only
// Configuration may turn a SET command into durable state.
func (d SettingDefinition) Validate() error {
if d.ID == "" || d.OwnerNamespace == "" || d.StableKey == "" || !ids.Revision(d.Revision).Valid() || d.ValueType == "" || d.Lifecycle == "" || d.RecordedAtUnixMillis < 1 {
return fmt.Errorf("invalid setting definition")
}
return nil
}
func (a SettingAssignment) Validate() error {
if a.ID == "" || a.PrincipalID == "" || a.DefinitionRef == "" || !ids.Revision(a.Revision).Valid() || a.Source == "" || a.Lifecycle == "" || a.RecordedAtUnixMillis < 1 {
return fmt.Errorf("invalid setting assignment")
}
if a.SecretRef != nil && a.TypedValue != "" {
return fmt.Errorf("secret assignment must not contain typed value")
}
return nil
}
+90
View File
@@ -0,0 +1,90 @@
package epistemics
import (
"fmt"
"git.cin.su/m/Assistant/contracts/world"
)
type VolatilityMode string
const (
VolatilityImmutable VolatilityMode = "IMMUTABLE"
VolatilityVersionBound VolatilityMode = "VERSION_BOUND"
VolatilityVerySlow VolatilityMode = "VERY_SLOW"
VolatilitySlow VolatilityMode = "SLOW"
VolatilityMedium VolatilityMode = "MEDIUM"
VolatilityFast VolatilityMode = "FAST"
VolatilityEphemeral VolatilityMode = "EPHEMERAL"
VolatilityCustom VolatilityMode = "CUSTOM"
)
type TemporalApplicability struct {
ObservedAtUnixMillis int64
ValidFromUnixMillis *int64
ValidUntilUnixMillis *int64
LastVerifiedAtUnixMillis *int64
Volatility VolatilitySpec
VersionBindings []world.VersionBinding
}
type VolatilitySpec struct {
Mode VolatilityMode
ExpectedChangeIntervalMillis *int64
RevalidateAfterMillis *int64
HardStaleAfterMillis *int64
InvalidateOnVersionChange bool
RevalidationPolicyRef string
}
type FreshnessAssessmentState string
const (
FreshnessAssessmentCurrent FreshnessAssessmentState = "CURRENT"
FreshnessAssessmentAcceptable FreshnessAssessmentState = "ACCEPTABLE"
FreshnessAssessmentAging FreshnessAssessmentState = "AGING"
FreshnessAssessmentStale FreshnessAssessmentState = "STALE"
FreshnessAssessmentVersionMismatch FreshnessAssessmentState = "VERSION_MISMATCH"
FreshnessAssessmentHistoricalOnly FreshnessAssessmentState = "HISTORICAL_ONLY"
FreshnessAssessmentUnknown FreshnessAssessmentState = "UNKNOWN"
)
type FreshnessAssessment struct {
SourceKnowledgeRef string
TaskRef string
State FreshnessAssessmentState
Reason string
AgeMillis int64
VersionMatchState string
RevalidationAttemptRef string
AssessedAtUnixMillis int64
}
type RevalidationRequest struct {
ID string
SourceKnowledgeRef string
TaskRef string
RequiredVersionRefs []world.SemanticReference
RequestedAtUnixMillis int64
}
type RevalidationResponse struct {
RequestID string
Assessment FreshnessAssessment
NewEvidenceRefs []EvidenceRef
CompletedAtUnixMillis int64
}
func (v VolatilitySpec) Validate() error {
if v.Mode == "" {
return fmt.Errorf("volatility mode is required")
}
return nil
}
func (t TemporalApplicability) Validate() error {
if t.ObservedAtUnixMillis < 1 {
return fmt.Errorf("observed time is required")
}
return t.Volatility.Validate()
}
func (a FreshnessAssessment) Validate() error {
if a.SourceKnowledgeRef == "" || a.TaskRef == "" || a.State == "" || a.AgeMillis < 0 || a.AssessedAtUnixMillis < 1 {
return fmt.Errorf("freshness assessment is incomplete")
}
return nil
}
+30
View File
@@ -0,0 +1,30 @@
package epistemics
type WorldOutcomeState string
const (
OutcomeVerifiedSuccess WorldOutcomeState = "VERIFIED_SUCCESS"
OutcomeVerifiedPartial WorldOutcomeState = "VERIFIED_PARTIAL"
OutcomeVerifiedFailure WorldOutcomeState = "VERIFIED_FAILURE"
OutcomeUnverified WorldOutcomeState = "UNVERIFIED"
OutcomeUnknownState WorldOutcomeState = "UNKNOWN"
OutcomeConflicted WorldOutcomeState = "CONFLICTED"
)
type WorldOutcomeAssessment struct {
ID string
PrincipalID string
ActionAttemptRef string
ExecutionReceiptRefs []string
ExpectedEffectRef string
ObservationRefs []ObservationID
ClaimRefs []ClaimID
State WorldOutcomeState
ConfidenceMillis int64
EvidenceRefs []EvidenceRef
AssessedAt int64
}
func (a WorldOutcomeAssessment) Valid() bool {
return a.ID != "" && a.PrincipalID != "" && a.ActionAttemptRef != "" && a.State != "" && a.ConfidenceMillis >= 0 && a.ConfidenceMillis <= 1000 && a.AssessedAt > 0
}
+7
View File
@@ -0,0 +1,7 @@
package epistemics
import "git.cin.su/m/Assistant/platform/ids"
// SemanticReference identifies evidence-related records without transferring
// canonical ownership to Epistemics.
type SemanticReference = ids.SemanticRef
+34 -15
View File
@@ -17,23 +17,27 @@ type CoverageState string
type ConflictState string
type EvidenceKind string
type ResearchFindingState string
type ReliabilityState string
const (
FreshnessCurrent FreshnessState = "CURRENT"
FreshnessStale FreshnessState = "STALE"
FreshnessUnknown FreshnessState = "UNKNOWN"
CoverageComplete CoverageState = "COMPLETE"
CoveragePartial CoverageState = "PARTIAL"
CoverageUnknown CoverageState = "UNKNOWN"
ConflictNone ConflictState = "NONE"
ConflictPresent ConflictState = "PRESENT"
ConflictUnknown ConflictState = "UNKNOWN"
EvidenceObserved EvidenceKind = "OBSERVED"
EvidenceAssumed EvidenceKind = "ASSUMED"
FindingSupported ResearchFindingState = "SUPPORTED"
FindingStale ResearchFindingState = "STALE"
FindingConflicted ResearchFindingState = "CONFLICTED"
FindingUnknown ResearchFindingState = "UNKNOWN"
FreshnessCurrent FreshnessState = "CURRENT"
FreshnessStale FreshnessState = "STALE"
FreshnessUnknown FreshnessState = "UNKNOWN"
CoverageComplete CoverageState = "COMPLETE"
CoveragePartial CoverageState = "PARTIAL"
CoverageUnknown CoverageState = "UNKNOWN"
ConflictNone ConflictState = "NONE"
ConflictPresent ConflictState = "PRESENT"
ConflictUnknown ConflictState = "UNKNOWN"
EvidenceObserved EvidenceKind = "OBSERVED"
EvidenceAssumed EvidenceKind = "ASSUMED"
FindingSupported ResearchFindingState = "SUPPORTED"
FindingStale ResearchFindingState = "STALE"
FindingConflicted ResearchFindingState = "CONFLICTED"
FindingUnknown ResearchFindingState = "UNKNOWN"
ReliabilityCurrent ReliabilityState = "CURRENT"
ReliabilityAging ReliabilityState = "AGING"
ReliabilityUnknown ReliabilityState = "UNKNOWN"
)
type ResearchPlan struct {
@@ -52,6 +56,21 @@ type Source struct {
Publisher string
}
type SourceReliabilityAssessment struct {
SourceRef string
DomainRef string
Freshness FreshnessState
HistoryScoreMillis int64
ReliabilityScoreMillis int64
State ReliabilityState
EvidenceRefs []string
AssessedAtUnixMillis int64
}
func (a SourceReliabilityAssessment) Valid() bool {
return a.SourceRef != "" && a.DomainRef != "" && a.Freshness != "" && a.HistoryScoreMillis >= 0 && a.HistoryScoreMillis <= 1000 && a.ReliabilityScoreMillis >= 0 && a.ReliabilityScoreMillis <= 1000 && a.State != "" && a.AssessedAtUnixMillis > 0
}
type SourceSnapshot struct {
ID SourceSnapshotID
SourceID SourceID
+20 -10
View File
@@ -1,5 +1,7 @@
package epistemics
import "git.cin.su/m/Assistant/platform/ids"
type BeliefState string
const (
@@ -9,19 +11,27 @@ const (
)
type Observation struct {
ID ObservationID
SourceSnapshot SourceSnapshotID
Excerpt SourceExcerptID
Text string
ObservedAt int64
ID ObservationID
SourceSnapshot SourceSnapshotID
Excerpt SourceExcerptID
Text string
ObservedAt int64
PrincipalID string
ContextScopeRef string
ObservationKind string
Payload string
ExtractorRef string
}
type Claim struct {
ID ClaimID
Statement string
ObservationID ObservationID
CitationID CitationID
Verification VerificationState
ID ClaimID
Statement string
ObservationID ObservationID
CitationID CitationID
Verification VerificationState
PropositionID ids.ID
RevisedFrom *ClaimID
RecordedAtUnixMillis int64
}
type Belief struct {
+48
View File
@@ -24,6 +24,8 @@ type HelpMethodKind string
type OutcomeClass string
type ReflectionTriggerKind string
type ReflectionCandidateState string
type PropositionKind string
type BeliefAssessmentState string
const (
VerificationUnverified VerificationState = "UNVERIFIED"
@@ -73,6 +75,12 @@ const (
ReflectionCandidateEvaluating ReflectionCandidateState = "EVALUATING"
ReflectionCandidateSuperseded ReflectionCandidateState = "SUPERSEDED"
ReflectionCandidateResolved ReflectionCandidateState = "RESOLVED"
PropositionWorldFacet PropositionKind = "WORLD_FACET"
PropositionWorldRelation PropositionKind = "WORLD_RELATION"
PropositionOwnerDefined PropositionKind = "OWNER_DEFINED"
PropositionTextual PropositionKind = "TEXTUAL"
BeliefAssessmentCurrent BeliefAssessmentState = "CURRENT"
BeliefAssessmentSuperseded BeliefAssessmentState = "SUPERSEDED"
)
// SystemExpectation records what Domovoy expected a meaningful help path to
@@ -114,6 +122,15 @@ type ExperienceRecord struct {
VerificationEvidence []EvidenceRef
ConfidenceMillis int64
RecordedAtUnixMillis int64
ProblemStatement string
AttemptRef string
WhyAttempted string
ApplicabilityConditions []string
ContextScopeRef string
EpisodeRef string
GoalRef string
TaskRef string
WhySucceededOrFailed string
}
// ReflectionCandidate records that broader reflection may be warranted.
@@ -134,3 +151,34 @@ type ReflectionCandidate struct {
ClaimedBy string
ClaimExpiresAtUnixMillis int64
}
type Proposition struct {
ID ids.ID
Kind PropositionKind
SubjectRef string
PredicateRef string
ObjectValue string
ObjectRef string
Qualifiers []string
TextFallback string
}
type BeliefAssessment struct {
ID ids.ID
ClaimID ClaimID
Revision ids.Revision
State BeliefAssessmentState
ConfidenceMillis int64
EvidenceRefs []EvidenceRef
AssessedAtUnixMillis int64
}
type EpisodeID ids.ID
type Episode struct {
ID EpisodeID
PrincipalID string
ExperienceRefs []ExperienceRecordID
Summary string
Outcome OutcomeClass
RecordedAtUnixMillis int64
}
+8
View File
@@ -5,6 +5,14 @@ type CreatorCredentials struct {
Password string `json:"password"`
}
type CreatorAccount struct {
Username string
ActorID ActorID
PrincipalID PrincipalID
PasswordHash string
Revision int64
}
type CreatorSession struct {
ActorID ActorID
PrincipalID PrincipalID
+24
View File
@@ -6,6 +6,30 @@ type ActorID ids.ID
type PrincipalID ids.ID
type IdentityID ids.ID
type DataRealmKind string
const (
DataRealmPersonal DataRealmKind = "PERSONAL"
DataRealmShared DataRealmKind = "SHARED"
DataRealmPublic DataRealmKind = "PUBLIC"
)
// PrincipalDataRealm is an identity-owned access boundary. It does not make
// a speaker or World person referent an authenticated principal.
type PrincipalDataRealm struct {
ID string
OwnerPrincipalID PrincipalID
Kind DataRealmKind
MemberPrincipalIDs []PrincipalID
Shared bool
Revision int64
RecordedAtUnixMillis int64
}
func (r PrincipalDataRealm) Valid() bool {
return r.ID != "" && r.OwnerPrincipalID != "" && (r.Kind == DataRealmPersonal || r.Kind == DataRealmShared || r.Kind == DataRealmPublic) && r.Revision > 0 && r.RecordedAtUnixMillis > 0 && (r.Kind != DataRealmShared || r.Shared)
}
// PrincipalBinding keeps the operational actor separate from the interest on
// whose behalf it acts. A Creator is not exempt from this distinction.
type PrincipalBinding struct {
+52
View File
@@ -25,6 +25,44 @@ type SemanticApplicability string
type SemanticCandidateSource string
type SemanticCandidateSelectionState string
type InformationRequestKind string
type ConfigurationCommandKind string
type SpeakerAttribution struct {
SourceRef string
PrincipalCandidate string
ConfidenceMillis int64
Authenticated bool
ObservationRef string
}
type IngressModality string
const (
IngressText IngressModality = "TEXT"
IngressAudio IngressModality = "AUDIO"
IngressImage IngressModality = "IMAGE"
IngressVideo IngressModality = "VIDEO"
IngressDevice IngressModality = "DEVICE"
)
type IngressEnvelope struct {
ID string
Modality IngressModality
SourceRef string
DeviceRef string
LocationRef string
PrincipalID string
ArtifactRef string
ObservationRef string
ProviderRef string
ModelRef string
ModelVersion string
ConfidenceMillis int64
Degraded bool
RecordedAtUnixMillis int64
}
func (e IngressEnvelope) Valid() bool {
return e.ID != "" && e.Modality != "" && e.SourceRef != "" && e.ConfidenceMillis >= 0 && e.ConfidenceMillis <= 1000 && e.RecordedAtUnixMillis > 0 && (!e.Degraded || e.ObservationRef == "")
}
const (
ChannelWebChat Channel = "WEB_CHAT"
@@ -75,6 +113,8 @@ const (
InformationRequestMemoryInventory InformationRequestKind = "MEMORY_INVENTORY"
InformationRequestTimeDifference InformationRequestKind = "TIME_DIFFERENCE"
InformationRequestResearchFailureExplanation InformationRequestKind = "RESEARCH_FAILURE_EXPLANATION"
ConfigurationCommandSet ConfigurationCommandKind = "SET"
ConfigurationCommandQuery ConfigurationCommandKind = "QUERY"
)
type Conversation struct {
@@ -106,6 +146,18 @@ type OriginalHumanIntent struct {
RecordedAtUnixMillis int64
Text string
}
// ConfigurationCommand is Interaction-owned intent. Configuration remains
// the only owner allowed to materialise a SettingAssignment.
type ConfigurationCommand struct {
Kind ConfigurationCommandKind
PrincipalID string
DefinitionRef string
TypedValue string
TemporaryOverride bool
SessionRef string
SourceRef string
}
type InteractionRelation struct {
ID InteractionRelationID
FromUtteranceID HumanUtteranceID
+9
View File
@@ -0,0 +1,9 @@
# SELF contract boundary
SELF owns evidence-backed competence, weakness, dependency, learning, and
self-evaluation records. Raw evidence remains Epistemics-owned; capability
health remains Capability-owned; tasks and development remain Work-owned;
release switching remains Supervisor/runtime-owned.
SELF is observational/evaluative and is not a mandatory request-path
orchestrator or an authority source.
+25
View File
@@ -0,0 +1,25 @@
package self
type BenchmarkCaseRef string
type PersonalBenchmark struct {
ID string
PrincipalID string
ApprovedCaseRefs []BenchmarkCaseRef
GoalProfile []string
PreferenceEvidenceRefs []SemanticReference
RecordedAt int64
}
type BenchmarkEvaluation struct {
CaseRef BenchmarkCaseRef
Path string
QualityMillis int64
ReliabilityMillis int64
LatencyMillis int64
CostMicros int64
CorrectionCount int64
EvidenceRefs []SemanticReference
}
func (e BenchmarkEvaluation) Valid() bool {
return e.CaseRef != "" && len(e.EvidenceRefs) > 0 && e.QualityMillis >= 0 && e.ReliabilityMillis >= 0 && e.CorrectionCount >= 0
}
+7
View File
@@ -0,0 +1,7 @@
package self
import "git.cin.su/m/Assistant/platform/ids"
// SemanticReference lets SELF cite evidence and capabilities without owning
// their canonical state.
type SemanticReference = ids.SemanticRef
+92
View File
@@ -0,0 +1,92 @@
package self
import "fmt"
type AssessmentID string
type LearningLedgerEntryID string
type AssessmentState string
const (
AssessmentCandidate AssessmentState = "CANDIDATE"
AssessmentConfirmed AssessmentState = "CONFIRMED"
AssessmentMonitoring AssessmentState = "MONITORING"
AssessmentResolved AssessmentState = "RESOLVED"
)
type CompetenceAssessment struct {
ID AssessmentID
PrincipalID string
SubjectRef SemanticReference
Revision uint64
ContextMatcher []string
QualityMillis int64
ReliabilityMillis int64
ConfidenceMillis int64
EvidenceRefs []SemanticReference
LastValidatedAt int64
AssessedAt int64
SupersedesRef *SemanticReference
}
type WeaknessPattern struct {
ID AssessmentID
PrincipalID string
Revision uint64
PatternKey string
ContextMatcher []string
FailureMode string
DetectionEvidenceRefs []SemanticReference
State AssessmentState
RecordedAt int64
}
type DependencyAssessment struct {
ID AssessmentID
SubjectRef SemanticReference
DependencyRef SemanticReference
ImpactClass string
CurrentEffect string
EvidenceRefs []SemanticReference
AssessedAt int64
}
type ImprovementNeed struct {
ID AssessmentID
PrincipalID string
SubjectRef SemanticReference
ProblemSummary string
EvidenceRefs []SemanticReference
ExpectedBenefit string
Priority int
State string
CreatedAt int64
}
type LearningLedgerEntry struct {
ID LearningLedgerEntryID
PrincipalID string
ImprovementNeedRef *SemanticReference
ObservedProblem string
EvidenceRefs []SemanticReference
Diagnosis string
Hypothesis string
ProposedChangeRef SemanticReference
ExpectedImprovement string
ActualEvaluationRefs []SemanticReference
DevelopmentGateRef *SemanticReference
BeforeVersionRefs []SemanticReference
AfterVersionRefs []SemanticReference
Decision string
SideEffects []string
RollbackRef *SemanticReference
RecordedAt int64
}
func (a CompetenceAssessment) Validate() error {
if a.ID == "" || a.PrincipalID == "" || !a.SubjectRef.Valid() || a.Revision < 1 || a.ConfidenceMillis < 0 || a.ConfidenceMillis > 1000 || len(a.EvidenceRefs) == 0 || a.AssessedAt < 1 {
return fmt.Errorf("competence requires evidence")
}
return nil
}
func (l LearningLedgerEntry) Validate() error {
if l.ID == "" || l.PrincipalID == "" || l.ObservedProblem == "" || len(l.EvidenceRefs) == 0 || l.Decision == "" || l.RecordedAt < 1 {
return fmt.Errorf("learning ledger entry is incomplete")
}
return nil
}
+141
View File
@@ -0,0 +1,141 @@
package situation
import (
"fmt"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/platform/clock"
"git.cin.su/m/Assistant/platform/ids"
)
type ContextScopeID ids.StableID
type FocusFrameID ids.StableID
type FocusTransitionID ids.StableID
type FocusRevision ids.Revision
type RoleKey string
type MatchPolicy string
type FocusFrameState string
type FocusProvenance string
type FocusTransitionOperation string
const (
MatchRequired MatchPolicy = "REQUIRED"
MatchPreferred MatchPolicy = "PREFERRED"
MatchRelated MatchPolicy = "RELATED"
MatchExcluded MatchPolicy = "EXCLUDED"
RolePerson RoleKey = "PERSON"
RoleOrganization RoleKey = "ORGANIZATION"
RoleFamily RoleKey = "FAMILY"
RoleEnvironment RoleKey = "ENVIRONMENT"
RoleLocation RoleKey = "LOCATION"
RoleSystem RoleKey = "SYSTEM"
RoleDatabase RoleKey = "DATABASE"
RoleRepository RoleKey = "REPOSITORY"
RoleBranch RoleKey = "BRANCH"
RoleVersion RoleKey = "VERSION"
RoleActivity RoleKey = "ACTIVITY"
RoleProject RoleKey = "PROJECT"
RoleActiveEntity RoleKey = "ACTIVE_ENTITY"
FocusCandidate FocusFrameState = "CANDIDATE"
FocusActive FocusFrameState = "ACTIVE"
FocusSuspended FocusFrameState = "SUSPENDED"
FocusExpired FocusFrameState = "EXPIRED"
FocusSuperseded FocusFrameState = "SUPERSEDED"
FocusExplicitHuman FocusProvenance = "EXPLICIT_HUMAN"
FocusUserConfirmed FocusProvenance = "USER_CONFIRMED"
FocusInherited FocusProvenance = "INHERITED"
FocusEntityReference FocusProvenance = "ENTITY_REFERENCE"
FocusTaskReference FocusProvenance = "TASK_REFERENCE"
FocusRepositoryReference FocusProvenance = "REPOSITORY_REFERENCE"
FocusEnvironmentObservation FocusProvenance = "ENVIRONMENT_OBSERVATION"
FocusInferred FocusProvenance = "INFERRED"
TransitionActivate FocusTransitionOperation = "ACTIVATE"
TransitionSuspend FocusTransitionOperation = "SUSPEND"
TransitionResume FocusTransitionOperation = "RESUME"
TransitionSupersede FocusTransitionOperation = "SUPERSEDE"
TransitionExpire FocusTransitionOperation = "EXPIRE"
)
type ContextBinding struct {
RoleKey RoleKey
TargetRef SemanticReference
MatchPolicy MatchPolicy
Provenance string
ConfidenceMillis int64
SourceRef *SemanticReference
}
type ContextScope struct {
ID ContextScopeID
PrincipalID identity.PrincipalID
Revision FocusRevision
Bindings []ContextBinding
RecordedAt clock.Instant
}
type FocusFrame struct {
ID FocusFrameID
PrincipalID identity.PrincipalID
ActorID identity.ActorID
Revision FocusRevision
AuthorityScopeRef authority.ScopeID
ContextScopeRef SemanticReference
GoalRef *work.GoalID
TaskRef *work.TaskID
SourceUtteranceRef string
Provenance FocusProvenance
ConfidenceMillis int64
State FocusFrameState
ParentFrameRef *FocusFrameID
ActivatedAt clock.Instant
SuspendedAt *clock.Instant
ExpiredAt *clock.Instant
RecordedAt clock.Instant
}
type FocusTransition struct {
TransitionID FocusTransitionID
FrameID FocusFrameID
FromRevision FocusRevision
ToRevision FocusRevision
Operation FocusTransitionOperation
CauseRef string
RecordedAt clock.Instant
}
func (s ContextScope) Validate() error {
if s.ID == "" || s.PrincipalID == "" || !ids.Revision(s.Revision).Valid() || !s.RecordedAt.Valid() {
return fmt.Errorf("invalid context scope")
}
for _, b := range s.Bindings {
if b.RoleKey == "" || !b.TargetRef.Valid() || !validMatchPolicy(b.MatchPolicy) {
return fmt.Errorf("invalid context binding")
}
}
return nil
}
func (f FocusFrame) Validate() error {
if f.ID == "" || f.PrincipalID == "" || f.ActorID == "" || !ids.Revision(f.Revision).Valid() || !f.ContextScopeRef.Valid() || f.State == "" || f.Provenance == "" || !f.RecordedAt.Valid() {
return fmt.Errorf("invalid focus frame")
}
return nil
}
func (t FocusTransition) Validate() error {
if t.TransitionID == "" || t.FrameID == "" || !ids.Revision(t.FromRevision).Valid() || !ids.Revision(t.ToRevision).Valid() || t.Operation == "" || !t.RecordedAt.Valid() {
return fmt.Errorf("invalid focus transition")
}
return nil
}
func validMatchPolicy(p MatchPolicy) bool {
return p == MatchRequired || p == MatchPreferred || p == MatchRelated || p == MatchExcluded
}
+22
View File
@@ -0,0 +1,22 @@
package situation_test
import (
"testing"
"git.cin.su/m/Assistant/contracts/identity"
contract "git.cin.su/m/Assistant/contracts/situation"
"git.cin.su/m/Assistant/platform/clock"
"git.cin.su/m/Assistant/platform/ids"
)
func TestFocusV2ContractsValidateTypedScopeAndFrame(t *testing.T) {
rev := ids.Revision(1)
scope := contract.ContextScope{ID: "scope", PrincipalID: identity.PrincipalID("company-a"), Revision: contract.FocusRevision(rev), RecordedAt: clock.Instant(1), Bindings: []contract.ContextBinding{{RoleKey: contract.RoleOrganization, TargetRef: contract.SemanticReference{OwnerNamespace: "world", SemanticKind: "organization", StableID: "a"}, MatchPolicy: contract.MatchRequired}}}
if err := scope.Validate(); err != nil {
t.Fatal(err)
}
frame := contract.FocusFrame{ID: "frame", PrincipalID: scope.PrincipalID, ActorID: "actor", Revision: 1, ContextScopeRef: contract.SemanticReference{OwnerNamespace: "situation", SemanticKind: "context_scope", StableID: ids.StableID(scope.ID), Revision: &rev}, State: contract.FocusActive, Provenance: contract.FocusExplicitHuman, RecordedAt: 1}
if err := frame.Validate(); err != nil {
t.Fatal(err)
}
}
+7
View File
@@ -0,0 +1,7 @@
package situation
import "git.cin.su/m/Assistant/platform/ids"
// SemanticReference keeps Situation references transport-neutral while
// ContextScope semantics remain owned by Situation.
type SemanticReference = ids.SemanticRef
+24
View File
@@ -0,0 +1,24 @@
package work
type ConsequenceAssessment struct {
ID string
TargetRefs []string
Impact int64
Reversibility int64
Uncertainty int64
Scope int64
Recoverability int64
CostOfError int64
ExpectedConsequences []string
MitigationOptions []string
RequiresDryRun bool
RequiresBackup bool
RecommendedConfirmation string
OwnerAccepted bool
EvidenceRefs []EvidenceRef
RecordedAtUnixMillis int64
}
func (a ConsequenceAssessment) Valid() bool {
return a.ID != "" && len(a.TargetRefs) > 0 && a.Impact >= 0 && a.Impact <= 1000 && a.Reversibility >= 0 && a.Reversibility <= 1000 && a.Uncertainty >= 0 && a.Uncertainty <= 1000 && a.Recoverability >= 0 && a.Recoverability <= 1000 && a.RecordedAtUnixMillis > 0
}
+28
View File
@@ -0,0 +1,28 @@
package work
import "git.cin.su/m/Assistant/contracts/world"
type FreshnessRequirementMode string
type FreshnessFallbackPolicy string
const (
FreshnessAny FreshnessRequirementMode = "ANY"
FreshnessPreferCurrent FreshnessRequirementMode = "PREFER_CURRENT"
FreshnessRequireCurrent FreshnessRequirementMode = "REQUIRE_CURRENT"
FreshnessRequireWithinAge FreshnessRequirementMode = "REQUIRE_WITHIN_AGE"
FreshnessRequireVersionMatch FreshnessRequirementMode = "REQUIRE_VERSION_MATCH"
FreshnessLiveRequired FreshnessRequirementMode = "LIVE_REQUIRED"
FallbackAllowStaleDisclosure FreshnessFallbackPolicy = "ALLOW_STALE_WITH_DISCLOSURE"
FallbackHypothesisOnly FreshnessFallbackPolicy = "USE_AS_HYPOTHESIS_ONLY"
FallbackWaitForSource FreshnessFallbackPolicy = "WAIT_FOR_SOURCE"
FallbackAskOwner FreshnessFallbackPolicy = "ASK_OWNER"
FallbackStopUnknown FreshnessFallbackPolicy = "STOP_WITH_UNKNOWN"
)
type TaskFreshnessRequirement struct {
Mode FreshnessRequirementMode
MaximumAgeMillis *int64
RequiredVersionRefs []world.SemanticReference
RevalidationRequired bool
FallbackPolicy FreshnessFallbackPolicy
}
+39
View File
@@ -0,0 +1,39 @@
package work
type EvolutionRunStage string
const (
EvolutionNeedRecorded EvolutionRunStage = "IMPROVEMENT_NEED"
EvolutionCandidateBuilt EvolutionRunStage = "CANDIDATE"
EvolutionIndependentlyEvaluated EvolutionRunStage = "INDEPENDENT_EVALUATION"
EvolutionSandboxed EvolutionRunStage = "SANDBOX_BUILD"
EvolutionBenchmarked EvolutionRunStage = "BENCHMARK"
EvolutionCanaried EvolutionRunStage = "CANARY"
EvolutionPromoted EvolutionRunStage = "PROMOTION"
EvolutionRolledBack EvolutionRunStage = "ROLLBACK"
)
type GovernedEvolutionResult struct {
NeedRef string
CandidateRef string
EvaluationRef string
Stages []EvolutionRunStage
EvidenceRefs []EvidenceRef
LearningLedgerOutcomeRef string
Promoted bool
RollbackRef string
}
// GovernedEvolutionOps are explicit gates. The caller supplies existing
// development executor/supervisor operations; this interface grants no new
// authority and has no autonomous promotion implementation.
type GovernedEvolutionOps struct {
CreateCandidate func() (EvolutionCandidate, error)
Evaluate func(EvolutionCandidate) (EvolutionEvaluation, error)
SandboxBuild func(EvolutionCandidate) (EvidenceRef, error)
Benchmark func(EvolutionCandidate) (EvidenceRef, bool, error)
Canary func(EvolutionCandidate) (EvidenceRef, bool, error)
Promote func(EvolutionCandidate, EvolutionEvaluation) (string, error)
Rollback func(EvolutionCandidate) (string, error)
LearningLedgerOutcome func(GovernedEvolutionResult) (string, error)
}
+51
View File
@@ -0,0 +1,51 @@
package work
type LongRunningWorkID string
type LongRunningState string
const (
LongRunningProposed LongRunningState = "PROPOSED"
LongRunningRunning LongRunningState = "RUNNING"
LongRunningPaused LongRunningState = "PAUSED"
LongRunningBlocked LongRunningState = "BLOCKED"
LongRunningCompleted LongRunningState = "COMPLETED"
LongRunningExhausted LongRunningState = "EXHAUSTED"
LongRunningCancelled LongRunningState = "CANCELLED"
)
type Investigation struct {
ID LongRunningWorkID
Objective string
HypothesisRefs []EvidenceRef
AttemptRefs []AgentRunID
FindingRefs []EvidenceRef
Blockers []string
CheckpointRefs []AgentCheckpointID
NextStep string
State LongRunningState
Revision int64
TimeBudgetMillis int64
CostBudgetMicros int64
MaxAttempts int
RecordedAtUnixMillis int64
}
// InvestigationCheckpoint is the durable boundary between attempts. A
// checkpoint is written only after an attempt has been verified, so a worker
// restart can resume at NextStep without replaying that attempt.
type InvestigationCheckpoint struct {
ID AgentCheckpointID
InvestigationID LongRunningWorkID
CompletedAttemptRefs []AgentRunID
CompletedFindingRefs []EvidenceRef
NextStep string
RecordedAtUnixMillis int64
}
func (c InvestigationCheckpoint) Valid() bool {
return c.ID != "" && c.InvestigationID != "" && c.NextStep != "" && c.RecordedAtUnixMillis > 0
}
func (w Investigation) Valid() bool {
return w.ID != "" && w.Objective != "" && w.State != "" && w.Revision > 0 && w.TimeBudgetMillis >= 0 && w.CostBudgetMicros >= 0 && w.MaxAttempts >= 0 && w.RecordedAtUnixMillis > 0
}
+32
View File
@@ -0,0 +1,32 @@
package work
type PerspectiveID string
type PerspectiveSpec struct {
ID PerspectiveID
ObjectiveLens string
FocusRef string
RetrievalProfileRef string
SkillRefs []string
ToolRefs []string
EvaluationCriteria []string
CapabilityRequirements []string
ExpectedOutputShape string
Critic bool
}
type SpecialistAssignment struct {
Perspective PerspectiveSpec
SubtaskRef string
FocusRef string
SkillRefs []string
ToolRefs []string
EvidenceRefs []EvidenceRef
}
type TeamDecision struct {
UseTeam bool
Reason string
Assignments []SpecialistAssignment
}
func (p PerspectiveSpec) Valid() bool {
return p.ID != "" && p.ObjectiveLens != "" && p.FocusRef != "" && len(p.SkillRefs) > 0 && len(p.ToolRefs) > 0 && len(p.EvaluationCriteria) > 0 && p.ExpectedOutputShape != ""
}
+36
View File
@@ -0,0 +1,36 @@
package work
type EvolutionChangeLevel string
const (
EvolutionKnowledge EvolutionChangeLevel = "KNOWLEDGE"
EvolutionBehavior EvolutionChangeLevel = "BEHAVIOR"
EvolutionCapability EvolutionChangeLevel = "CAPABILITY"
EvolutionCore EvolutionChangeLevel = "CORE"
)
type EvolutionCandidate struct {
ID string
ProposalRef string
ChangeLevel EvolutionChangeLevel
CreatorIdentity string
BaseVersionRef string
CandidateVersionRef string
MustNotWorsen []string
SandboxEvidenceRefs []EvidenceRef
BenchmarkEvidenceRefs []EvidenceRef
RollbackRef string
State string
}
type EvolutionEvaluation struct {
CandidateRef string
EvaluatorIdentity string
Approved bool
EvidenceRefs []EvidenceRef
MustNotWorsenPassed bool
RecordedAtUnixMillis int64
}
func (e EvolutionEvaluation) ValidFor(c EvolutionCandidate) bool {
return e.CandidateRef == c.ID && e.EvaluatorIdentity != "" && e.EvaluatorIdentity != c.CreatorIdentity && e.Approved && e.MustNotWorsenPassed && len(e.EvidenceRefs) > 0 && e.RecordedAtUnixMillis > 0
}
+9
View File
@@ -0,0 +1,9 @@
# World contract boundary
World owns typed, versioned referents and vocabulary: `WorldObject`, kind and
facet/relation definitions, aliases, identity-resolution decisions, and
merge/split lineage. It does not own observations, claims, beliefs, or the
truth of values about a referent; those remain Epistemics-owned evidence.
Cross-owner references are typed semantic references. They do not create
cross-owner foreign keys, joins, writes, or a generic `Entity(any)` API.
+322
View File
@@ -0,0 +1,322 @@
package world
import (
"fmt"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/platform/clock"
"git.cin.su/m/Assistant/platform/ids"
)
// SemanticReference is the transport envelope used by World contracts. It is
// a reference only; it does not expose another owner's storage.
type SemanticReference = ids.SemanticRef
// VersionBinding is a typed applicability constraint. Absence of VersionRef
// means unknown, never an implicit "latest".
type VersionMatchMode string
const (
VersionExact VersionMatchMode = "EXACT"
VersionCompatibleFamily VersionMatchMode = "COMPATIBLE_FAMILY"
VersionAtOrBefore VersionMatchMode = "AT_OR_BEFORE"
VersionUnknown VersionMatchMode = "UNKNOWN"
)
type VersionBinding struct {
SubjectRef SemanticReference
VersionRef SemanticReference
MatchMode VersionMatchMode
}
type EnvironmentReference struct {
EnvironmentRef SemanticReference
VersionRef *SemanticReference
}
func (b VersionBinding) Validate() error {
if !b.SubjectRef.Valid() || !b.VersionRef.Valid() {
return fmt.Errorf("version binding references are invalid")
}
if b.MatchMode != VersionExact && b.MatchMode != VersionCompatibleFamily && b.MatchMode != VersionAtOrBefore && b.MatchMode != VersionUnknown {
return fmt.Errorf("version binding match mode is invalid")
}
return nil
}
func (r EnvironmentReference) Validate() error {
if !r.EnvironmentRef.Valid() {
return fmt.Errorf("environment reference is invalid")
}
if r.VersionRef != nil && !r.VersionRef.Valid() {
return fmt.Errorf("environment version reference is invalid")
}
return nil
}
type PrincipalRealm string
const (
RealmPersonal PrincipalRealm = "PERSONAL"
RealmShared PrincipalRealm = "SHARED"
RealmPublic PrincipalRealm = "PUBLIC"
RealmSystem PrincipalRealm = "SYSTEM"
)
type SensitivityClass string
const (
SensitivityPublic SensitivityClass = "PUBLIC"
SensitivityPrivate SensitivityClass = "PRIVATE"
SensitivityRestricted SensitivityClass = "RESTRICTED"
SensitivitySecretReference SensitivityClass = "SECRET_REFERENCE"
)
type ValueTypeKind string
const (
ValueString ValueTypeKind = "STRING"
ValueInteger ValueTypeKind = "INTEGER"
ValueDecimal ValueTypeKind = "DECIMAL"
ValueBoolean ValueTypeKind = "BOOLEAN"
ValueTime ValueTypeKind = "TIME"
ValueDuration ValueTypeKind = "DURATION"
ValueEnum ValueTypeKind = "ENUM"
ValueReference ValueTypeKind = "REFERENCE"
ValueBinaryRef ValueTypeKind = "BINARY_REF"
ValueList ValueTypeKind = "LIST"
ValueStruct ValueTypeKind = "STRUCT"
)
type ValueField struct {
Name string
Type ValueTypeSpec
}
type ValueTypeSpec struct {
Kind ValueTypeKind
Element *ValueTypeSpec
Fields []ValueField
EnumValues []string
ReferenceKind string
}
type WorldObjectID ids.StableID
type KindDefinitionID ids.StableID
type WorldAliasID ids.StableID
type PersonReferent struct {
WorldObjectRef SemanticReference
DisplayName string
ConfidenceMillis int64
}
func (p PersonReferent) Valid() bool {
return p.WorldObjectRef.Valid() && p.DisplayName != "" && p.ConfidenceMillis >= 0 && p.ConfidenceMillis <= 1000
}
type FacetDefinitionID ids.StableID
type RelationDefinitionID ids.StableID
type ResolutionDecisionID ids.StableID
type LineageID ids.StableID
type LifecycleState string
const (
LifecycleActive LifecycleState = "ACTIVE"
LifecycleMerged LifecycleState = "MERGED"
LifecycleSplit LifecycleState = "SPLIT"
LifecycleRetired LifecycleState = "RETIRED"
LifecycleUnknownIdentity LifecycleState = "UNKNOWN_IDENTITY"
)
type WorldObject struct {
ID WorldObjectID
PrincipalID identity.PrincipalID
KindDefinitionRef SemanticReference
KindRevision ids.Revision
Lifecycle LifecycleState
CreatedAt clock.Instant
}
type WorldObjectKindDefinition struct {
ID KindDefinitionID
KindKey string
Revision ids.Revision
DisplayName string
ParentKindRef *SemanticReference
Lifecycle LifecycleState
DefinitionDigest string
RecordedAt clock.Instant
}
type WorldAlias struct {
ID WorldAliasID
PrincipalID identity.PrincipalID
ObjectID WorldObjectID
Alias string
RecordedAt clock.Instant
}
type FacetDefinition struct {
ID FacetDefinitionID
KindKey string
Revision ids.Revision
FacetKey string
ValueType ValueTypeSpec
DefinitionDigest string
RecordedAt clock.Instant
}
type RelationDefinition struct {
ID RelationDefinitionID
KindKey string
Revision ids.Revision
RelationKey string
TargetKindKey string
DefinitionDigest string
RecordedAt clock.Instant
}
type ResolutionDecision string
const (
ResolutionSame ResolutionDecision = "SAME"
ResolutionDistinct ResolutionDecision = "DISTINCT"
ResolutionMerge ResolutionDecision = "MERGE"
ResolutionSplit ResolutionDecision = "SPLIT"
ResolutionUnresolved ResolutionDecision = "UNRESOLVED"
)
type IdentityResolutionCandidate struct {
ObjectRef SemanticReference
Evidence []SemanticReference
Confidence int64
}
type IdentityResolutionDecisionRecord struct {
ID ResolutionDecisionID
PrincipalID identity.PrincipalID
Candidates []IdentityResolutionCandidate
Decision ResolutionDecision
Evidence []SemanticReference
ConfidenceMillis int64
DecidedAt clock.Instant
Revision ids.Revision
}
type WorldLineage struct {
ID LineageID
Operation ResolutionDecision
SourceRefs []SemanticReference
SuccessorRefs []SemanticReference
RecordedAt clock.Instant
}
func (w WorldObject) Validate() error {
if w.ID == "" || !w.KindDefinitionRef.Valid() || !w.KindRevision.Valid() || !validLifecycle(w.Lifecycle) || !w.CreatedAt.Valid() {
return fmt.Errorf("world object is incomplete")
}
return nil
}
func (k WorldObjectKindDefinition) Validate() error {
if k.ID == "" || k.KindKey == "" || !k.Revision.Valid() || k.DisplayName == "" || !validLifecycle(k.Lifecycle) || k.DefinitionDigest == "" || !k.RecordedAt.Valid() {
return fmt.Errorf("world kind definition is incomplete")
}
if k.ParentKindRef != nil && !k.ParentKindRef.Valid() {
return fmt.Errorf("world kind parent reference is invalid")
}
return nil
}
func (a WorldAlias) Validate() error {
if a.ID == "" || a.ObjectID == "" || a.Alias == "" || !a.RecordedAt.Valid() {
return fmt.Errorf("world alias is incomplete")
}
return nil
}
func (f FacetDefinition) Validate() error {
if f.ID == "" || f.KindKey == "" || !f.Revision.Valid() || f.FacetKey == "" || f.DefinitionDigest == "" || !f.RecordedAt.Valid() {
return fmt.Errorf("facet definition is incomplete")
}
return f.ValueType.Validate()
}
func (r RelationDefinition) Validate() error {
if r.ID == "" || r.KindKey == "" || !r.Revision.Valid() || r.RelationKey == "" || r.TargetKindKey == "" || r.DefinitionDigest == "" || !r.RecordedAt.Valid() {
return fmt.Errorf("relation definition is incomplete")
}
return nil
}
func (d IdentityResolutionDecisionRecord) Validate() error {
if d.ID == "" || !validDecision(d.Decision) || len(d.Candidates) == 0 || d.ConfidenceMillis < 0 || !d.DecidedAt.Valid() || !d.Revision.Valid() {
return fmt.Errorf("identity resolution decision is incomplete")
}
for _, candidate := range d.Candidates {
if !candidate.ObjectRef.Valid() || candidate.Confidence < 0 || candidate.Confidence > 1000 {
return fmt.Errorf("identity resolution candidate is invalid")
}
for _, evidence := range candidate.Evidence {
if !evidence.Valid() {
return fmt.Errorf("identity resolution evidence reference is invalid")
}
}
}
return nil
}
func (l WorldLineage) Validate() error {
if l.ID == "" || (l.Operation != ResolutionMerge && l.Operation != ResolutionSplit) || len(l.SourceRefs) == 0 || len(l.SuccessorRefs) == 0 || !l.RecordedAt.Valid() {
return fmt.Errorf("world lineage is incomplete")
}
return nil
}
func validLifecycle(value LifecycleState) bool {
return value == LifecycleActive || value == LifecycleMerged || value == LifecycleSplit || value == LifecycleRetired || value == LifecycleUnknownIdentity
}
func validDecision(value ResolutionDecision) bool {
return value == ResolutionSame || value == ResolutionDistinct || value == ResolutionMerge || value == ResolutionSplit || value == ResolutionUnresolved
}
func (s ValueTypeSpec) Validate() error {
if s.Kind == "" {
return fmt.Errorf("value type kind is required")
}
switch s.Kind {
case ValueList:
if s.Element == nil {
return fmt.Errorf("list element type is required")
}
return s.Element.Validate()
case ValueStruct:
if len(s.Fields) == 0 {
return fmt.Errorf("struct fields are required")
}
seen := map[string]struct{}{}
for _, field := range s.Fields {
if field.Name == "" {
return fmt.Errorf("struct field name is required")
}
if _, ok := seen[field.Name]; ok {
return fmt.Errorf("duplicate struct field %q", field.Name)
}
seen[field.Name] = struct{}{}
if err := field.Type.Validate(); err != nil {
return fmt.Errorf("field %s: %w", field.Name, err)
}
}
case ValueEnum:
if len(s.EnumValues) == 0 {
return fmt.Errorf("enum values are required")
}
case ValueReference:
if s.ReferenceKind == "" {
return fmt.Errorf("reference kind is required")
}
}
return nil
}
+17
View File
@@ -0,0 +1,17 @@
package world
import "testing"
func TestValueTypeSpecValidatesTypedRecursiveShape(t *testing.T) {
spec := ValueTypeSpec{Kind: ValueStruct, Fields: []ValueField{{Name: "name", Type: ValueTypeSpec{Kind: ValueString}}, {Name: "refs", Type: ValueTypeSpec{Kind: ValueList, Element: &ValueTypeSpec{Kind: ValueReference, ReferenceKind: "world.object"}}}}}
if err := spec.Validate(); err != nil {
t.Fatalf("ValueTypeSpec.Validate() = %v", err)
}
}
func TestValueTypeSpecRejectsDuplicateFields(t *testing.T) {
spec := ValueTypeSpec{Kind: ValueStruct, Fields: []ValueField{{Name: "same", Type: ValueTypeSpec{Kind: ValueString}}, {Name: "same", Type: ValueTypeSpec{Kind: ValueString}}}}
if err := spec.Validate(); err == nil {
t.Fatal("duplicate fields were accepted")
}
}
+22
View File
@@ -0,0 +1,22 @@
package world
import (
"testing"
"git.cin.su/m/Assistant/platform/clock"
"git.cin.su/m/Assistant/platform/ids"
)
func TestWorldObjectValidationKeepsReferentSeparateFromClaims(t *testing.T) {
object := WorldObject{ID: "object-1", KindDefinitionRef: ids.SemanticRef{OwnerNamespace: "world", SemanticKind: "kind", StableID: "kind-1"}, KindRevision: 1, Lifecycle: LifecycleActive, CreatedAt: clock.Instant(1)}
if err := object.Validate(); err != nil {
t.Fatalf("WorldObject.Validate() = %v", err)
}
}
func TestIdentityResolutionRequiresEvidenceBackedCandidates(t *testing.T) {
decision := IdentityResolutionDecisionRecord{ID: "decision-1", Decision: ResolutionSame, Candidates: []IdentityResolutionCandidate{{ObjectRef: ids.SemanticRef{OwnerNamespace: "world", SemanticKind: "object", StableID: "object-1"}, Confidence: 900}}, DecidedAt: clock.Instant(1), Revision: 1}
if err := decision.Validate(); err != nil {
t.Fatalf("IdentityResolutionDecisionRecord.Validate() = %v", err)
}
}
@@ -0,0 +1,58 @@
-- Gen2 owner-role provisioning for the dedicated bootstrap/migration principal.
-- This is intentionally separate from feature migrations and may be run repeatedly.
BEGIN;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'world_role') THEN
CREATE ROLE world_role LOGIN;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'configuration_role') THEN
CREATE ROLE configuration_role LOGIN;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'self_role') THEN
CREATE ROLE self_role LOGIN;
END IF;
END
$$;
DO $$
DECLARE
role_name TEXT;
can_login BOOLEAN;
BEGIN
FOREACH role_name IN ARRAY ARRAY['world_role', 'configuration_role', 'self_role'] LOOP
SELECT rolcanlogin INTO can_login FROM pg_roles WHERE rolname = role_name;
IF can_login IS DISTINCT FROM TRUE THEN
RAISE EXCEPTION 'Gen2 owner role % exists but is not a V1-compatible LOGIN role', role_name;
END IF;
END LOOP;
END
$$;
CREATE SCHEMA IF NOT EXISTS world AUTHORIZATION world_role;
CREATE SCHEMA IF NOT EXISTS configuration AUTHORIZATION configuration_role;
CREATE SCHEMA IF NOT EXISTS self AUTHORIZATION self_role;
REVOKE ALL ON SCHEMA world, configuration, self FROM PUBLIC;
DO $$
DECLARE
schema_name TEXT;
expected_owner TEXT;
actual_owner TEXT;
BEGIN
FOREACH schema_name IN ARRAY ARRAY['world', 'configuration', 'self'] LOOP
expected_owner := schema_name || '_role';
SELECT pg_get_userbyid(nspowner)
INTO actual_owner
FROM pg_namespace
WHERE nspname = schema_name;
IF actual_owner IS DISTINCT FROM expected_owner THEN
RAISE EXCEPTION 'Gen2 schema % is owned by %, expected %', schema_name, actual_owner, expected_owner;
END IF;
END LOOP;
END
$$;
COMMIT;
@@ -0,0 +1,82 @@
BEGIN;
CREATE SCHEMA IF NOT EXISTS world;
SET ROLE world_role;
CREATE TABLE IF NOT EXISTS world.kind_definitions (
kind_definition_id TEXT PRIMARY KEY,
kind_key TEXT NOT NULL,
revision BIGINT NOT NULL CHECK (revision > 0),
display_name TEXT NOT NULL,
parent_owner_namespace TEXT NOT NULL DEFAULT '',
parent_semantic_kind TEXT NOT NULL DEFAULT '',
parent_stable_id TEXT NOT NULL DEFAULT '',
parent_revision BIGINT,
lifecycle TEXT NOT NULL,
definition_digest TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0),
UNIQUE (kind_key, revision)
);
CREATE TABLE IF NOT EXISTS world.objects (
world_object_id TEXT PRIMARY KEY,
principal_id TEXT,
kind_owner_namespace TEXT NOT NULL,
kind_semantic_kind TEXT NOT NULL,
kind_stable_id TEXT NOT NULL,
kind_revision BIGINT,
lifecycle TEXT NOT NULL,
created_at_unix_millis BIGINT NOT NULL CHECK (created_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS world.aliases (
world_alias_id TEXT PRIMARY KEY,
principal_id TEXT,
world_object_id TEXT NOT NULL,
alias TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS world.facet_definitions (
facet_definition_id TEXT PRIMARY KEY,
kind_key TEXT NOT NULL,
revision BIGINT NOT NULL CHECK (revision > 0),
facet_key TEXT NOT NULL,
value_type JSONB NOT NULL,
definition_digest TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0),
UNIQUE (kind_key, facet_key, revision)
);
CREATE TABLE IF NOT EXISTS world.relation_definitions (
relation_definition_id TEXT PRIMARY KEY,
kind_key TEXT NOT NULL,
revision BIGINT NOT NULL CHECK (revision > 0),
relation_key TEXT NOT NULL,
target_kind_key TEXT NOT NULL,
definition_digest TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0),
UNIQUE (kind_key, relation_key, revision)
);
CREATE TABLE IF NOT EXISTS world.identity_resolution_decisions (
resolution_decision_id TEXT PRIMARY KEY,
principal_id TEXT,
candidates JSONB NOT NULL,
decision TEXT NOT NULL,
evidence JSONB NOT NULL,
confidence_millis BIGINT NOT NULL CHECK (confidence_millis >= 0),
decided_at_unix_millis BIGINT NOT NULL CHECK (decided_at_unix_millis > 0),
revision BIGINT NOT NULL CHECK (revision > 0)
);
CREATE TABLE IF NOT EXISTS world.lineage (
lineage_id TEXT PRIMARY KEY,
operation TEXT NOT NULL,
source_refs JSONB NOT NULL,
successor_refs JSONB NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0)
);
RESET ROLE;
COMMIT;
@@ -0,0 +1,47 @@
BEGIN;
SET ROLE situation_role;
CREATE TABLE IF NOT EXISTS situation.context_scopes (
context_scope_id TEXT PRIMARY KEY,
principal_id TEXT NOT NULL,
revision BIGINT NOT NULL CHECK (revision > 0),
bindings JSONB NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS situation.focus_frames (
focus_frame_id TEXT PRIMARY KEY,
principal_id TEXT NOT NULL,
actor_id TEXT NOT NULL,
revision BIGINT NOT NULL CHECK (revision > 0),
authority_scope_ref TEXT NOT NULL,
context_scope_ref JSONB NOT NULL,
goal_ref TEXT,
task_ref TEXT,
source_utterance_ref TEXT,
provenance TEXT NOT NULL,
confidence_millis BIGINT NOT NULL CHECK (confidence_millis >= 0),
state TEXT NOT NULL,
parent_frame_ref TEXT,
activated_at_unix_millis BIGINT NOT NULL CHECK (activated_at_unix_millis > 0),
suspended_at_unix_millis BIGINT,
expired_at_unix_millis BIGINT,
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS situation.focus_transitions (
transition_id TEXT PRIMARY KEY,
focus_frame_id TEXT NOT NULL,
from_revision BIGINT NOT NULL CHECK (from_revision > 0),
to_revision BIGINT NOT NULL CHECK (to_revision > 0),
operation TEXT NOT NULL,
cause_ref TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0)
);
CREATE INDEX IF NOT EXISTS focus_transitions_frame_order ON situation.focus_transitions(focus_frame_id, recorded_at_unix_millis, transition_id);
GRANT SELECT, INSERT, UPDATE ON situation.context_scopes, situation.focus_frames, situation.focus_transitions TO situation_role;
RESET ROLE;
COMMIT;
@@ -0,0 +1,7 @@
BEGIN;
SET ROLE configuration_role;
CREATE TABLE IF NOT EXISTS configuration.setting_definitions (setting_definition_id TEXT PRIMARY KEY, owner_namespace TEXT NOT NULL, stable_key TEXT NOT NULL, revision BIGINT NOT NULL CHECK(revision>0), value_type TEXT NOT NULL, default_value TEXT, sensitivity TEXT NOT NULL, lifecycle TEXT NOT NULL, recorded_at_unix_millis BIGINT NOT NULL);
CREATE TABLE IF NOT EXISTS configuration.setting_assignments (setting_assignment_id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, definition_ref JSONB NOT NULL, revision BIGINT NOT NULL CHECK(revision>0), selector JSONB NOT NULL, typed_value TEXT, secret_ref TEXT, effective_from_unix_millis BIGINT NOT NULL, effective_until_unix_millis BIGINT, source TEXT NOT NULL, source_ref TEXT NOT NULL, lifecycle TEXT NOT NULL, recorded_at_unix_millis BIGINT NOT NULL);
GRANT SELECT, INSERT, UPDATE ON configuration.setting_definitions, configuration.setting_assignments TO configuration_role;
RESET ROLE;
COMMIT;
@@ -0,0 +1,7 @@
BEGIN;
SET ROLE self_role;
CREATE TABLE IF NOT EXISTS self.competence_assessments (assessment_id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, subject_ref JSONB NOT NULL, revision BIGINT NOT NULL CHECK(revision>0), context_matcher JSONB NOT NULL, confidence_millis BIGINT NOT NULL, evidence_refs JSONB NOT NULL, assessed_at_unix_millis BIGINT NOT NULL);
CREATE TABLE IF NOT EXISTS self.learning_ledger (entry_id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, observed_problem TEXT NOT NULL, evidence_refs JSONB NOT NULL, diagnosis TEXT NOT NULL, hypothesis TEXT NOT NULL, decision TEXT NOT NULL, recorded_at_unix_millis BIGINT NOT NULL);
GRANT SELECT, INSERT, UPDATE ON self.competence_assessments, self.learning_ledger TO self_role;
RESET ROLE;
COMMIT;
@@ -0,0 +1,22 @@
BEGIN;
SET ROLE work_role;
CREATE TABLE IF NOT EXISTS work.long_running_work (
investigation_id TEXT PRIMARY KEY, objective TEXT NOT NULL,
hypothesis_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], attempt_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
finding_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], blockers TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
checkpoint_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], next_step TEXT NOT NULL, state TEXT NOT NULL,
revision BIGINT NOT NULL, time_budget_millis BIGINT NOT NULL, cost_budget_micros BIGINT NOT NULL,
max_attempts INTEGER NOT NULL, recorded_at_unix_millis BIGINT NOT NULL,
CHECK (investigation_id <> ''), CHECK (objective <> ''), CHECK (next_step <> ''), CHECK (revision > 0), CHECK (time_budget_millis >= 0), CHECK (cost_budget_micros >= 0), CHECK (max_attempts >= 0), CHECK (recorded_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS work.investigation_checkpoints (
checkpoint_id TEXT PRIMARY KEY, investigation_id TEXT NOT NULL, completed_attempt_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
completed_finding_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], next_step TEXT NOT NULL, recorded_at_unix_millis BIGINT NOT NULL,
CHECK (checkpoint_id <> ''), CHECK (investigation_id <> ''), CHECK (next_step <> ''), CHECK (recorded_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS work.investigation_attempts (
investigation_id TEXT NOT NULL, attempt_ref TEXT NOT NULL, verified_at_unix_millis BIGINT NOT NULL,
PRIMARY KEY (investigation_id, attempt_ref), CHECK (investigation_id <> ''), CHECK (attempt_ref <> ''), CHECK (verified_at_unix_millis > 0)
);
RESET ROLE;
COMMIT;
@@ -0,0 +1,11 @@
BEGIN;
SET ROLE identity_role;
CREATE TABLE IF NOT EXISTS identity.principal_data_realms (
realm_id TEXT PRIMARY KEY, owner_principal_id TEXT NOT NULL, realm_kind TEXT NOT NULL,
member_principal_ids TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], shared BOOLEAN NOT NULL DEFAULT FALSE,
revision BIGINT NOT NULL, recorded_at_unix_millis BIGINT NOT NULL,
CHECK (realm_id <> ''), CHECK (owner_principal_id <> ''), CHECK (realm_kind IN ('PERSONAL','SHARED','PUBLIC')),
CHECK (realm_kind <> 'SHARED' OR shared), CHECK (revision > 0), CHECK (recorded_at_unix_millis > 0)
);
RESET ROLE;
COMMIT;
@@ -0,0 +1,20 @@
BEGIN;
SET ROLE identity_role;
CREATE TABLE IF NOT EXISTS identity.creator_accounts (
username TEXT PRIMARY KEY,
actor_id TEXT NOT NULL,
principal_id TEXT NOT NULL,
password_hash TEXT NOT NULL,
revision BIGINT NOT NULL DEFAULT 1,
password_changed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (username <> ''),
CHECK (actor_id <> ''),
CHECK (principal_id <> ''),
CHECK (password_hash <> ''),
CHECK (revision > 0),
UNIQUE (actor_id, principal_id)
);
CREATE INDEX IF NOT EXISTS creator_accounts_binding_idx ON identity.creator_accounts (actor_id, principal_id);
REVOKE ALL ON identity.creator_accounts FROM PUBLIC;
RESET ROLE;
COMMIT;
+10
View File
@@ -25,6 +25,16 @@ psql_exec() {
docker exec -i "$POSTGRES_CONTAINER" psql -v ON_ERROR_STOP=1 -U "$POSTGRES_SUPERUSER" -d "$DATABASE" "$@"
}
# Owner roles and schemas are provisioned by the dedicated bootstrap boundary,
# before feature migrations run. This keeps role creation out of migrations and
# makes existing PostgreSQL volumes converge idempotently as well.
GEN2_BOOTSTRAP="$REPO_DIR/database/bootstrap/002_gen2_roles_and_schemas.sql"
if [[ ! -f "$GEN2_BOOTSTRAP" ]]; then
echo "migration runner: Gen2 bootstrap is missing" >&2
exit 1
fi
psql_exec < "$GEN2_BOOTSTRAP"
psql_exec -c "CREATE TABLE IF NOT EXISTS public.assistant_migration_history (migration_id TEXT PRIMARY KEY, applied_at_unix_millis BIGINT NOT NULL CHECK (applied_at_unix_millis > 0));"
if [[ -n "${MIGRATION_BASELINE:-}" ]]; then
+104 -13
View File
@@ -65,20 +65,15 @@ services:
POSTGRES_PASSWORD: ${SITUATION_DB_PASSWORD}
IDENTITY_POSTGRES_USER: identity_role
IDENTITY_POSTGRES_PASSWORD: ${IDENTITY_DB_PASSWORD}
CREATOR_BOOTSTRAP_TOKEN_FILE: /run/secrets/creator_bootstrap_token
CREATOR_SESSION_SIGNING_KEY_FILE: /run/secrets/creator_session_signing_key
CREATOR_USERNAME: ${CREATOR_USERNAME}
CREATOR_PASSWORD_FILE: /run/secrets/creator_password
WORK_POSTGRES_USER: work_role
WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD}
CAPABILITY_POSTGRES_USER: capability_role
CAPABILITY_POSTGRES_PASSWORD: ${CAPABILITY_DB_PASSWORD}
EPISTEMICS_POSTGRES_USER: epistemics_role
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
EPISTEMICS_POSTGRES_PASSWORD_FILE: /run/secrets/epistemics_db_password
INTERACTION_POSTGRES_USER: interaction_role
INTERACTION_POSTGRES_CREDENTIAL_REF: credential://domovoy/interaction/db-password/v1
INTERACTION_POSTGRES_PASSWORD_FILE: /run/secrets/interaction_db_password
SECRET_BACKEND_ROOT: /run/secrets
COGNITIVE_ADMISSION_POLICY_ID: creator-advisory-local-v1
COGNITIVE_ADMISSION_MAX_CONCURRENT: ${COGNITIVE_ADMISSION_MAX_CONCURRENT:-1}
@@ -88,6 +83,22 @@ services:
depends_on: { postgres: { condition: service_healthy } }
volumes:
- assistant_core_secrets:/run/secrets:ro
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null && wget -qO- http://localhost:8080/readyz >/dev/null"]
interval: 10s
timeout: 20s
retries: 20
core-runtime-candidate:
profiles: ["assistant-canary"]
extends:
service: core-runtime
restart: "no"
environment:
RESEARCH_RUNTIME_URL: http://research-runtime-candidate:8087
depends_on:
postgres: { condition: service_healthy }
research-runtime-candidate: { condition: service_started }
agent-canary:
profiles: ["agent-canary"]
@@ -182,7 +193,6 @@ services:
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
EPISTEMICS_POSTGRES_USER: epistemics_role
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
EPISTEMICS_POSTGRES_PASSWORD_FILE: /run/secrets/epistemics_db_password
SECRET_BACKEND_ROOT: /run/secrets
depends_on: { postgres: { condition: service_healthy } }
volumes:
@@ -202,12 +212,24 @@ services:
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
EPISTEMICS_POSTGRES_USER: epistemics_role
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
EPISTEMICS_POSTGRES_PASSWORD_FILE: /run/secrets/epistemics_db_password
SECRET_BACKEND_ROOT: /run/secrets
depends_on: { postgres: { condition: service_healthy } }
volumes:
- assistant_core_secrets:/run/secrets:ro
- assistant_research_sources:/var/lib/research-sources:ro
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8087/healthz >/dev/null && wget -qO- http://localhost:8087/readyz >/dev/null"]
interval: 10s
timeout: 10s
retries: 20
research-runtime-candidate:
profiles: ["assistant-canary"]
extends:
service: research-runtime
restart: "no"
depends_on:
postgres: { condition: service_healthy }
continuity-runtime:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: continuity-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
@@ -253,8 +275,10 @@ services:
LOCAL_CPU_PROVIDER_URL: http://docker.cin.su:18082/v1
LOCAL_CPU_PROVIDER_API_KEY_FILE: ${LOCAL_CPU_PROVIDER_API_KEY_FILE:-}
LOCAL_CPU_PROVIDER_CREDENTIAL_REF: ${LOCAL_CPU_PROVIDER_CREDENTIAL_REF:-credential://domovoy/local-cpu/provider-api-key/v1}
LOCAL_MEDIUM_PROVIDER_URL: ${LOCAL_MEDIUM_PROVIDER_URL:-http://llm-local-api:8080/v1}
LOCAL_MEDIUM_PROVIDER_API_KEY_FILE: ${LOCAL_MEDIUM_PROVIDER_API_KEY_FILE:-}
# The medium chat route is served by the authenticated local gateway.
# llm-local-api is the embeddings service and must not be used for chat.
LOCAL_MEDIUM_PROVIDER_URL: ${LOCAL_MEDIUM_PROVIDER_URL:-http://llm-local-gateway:8080/v1}
LOCAL_MEDIUM_PROVIDER_API_KEY_FILE: ${LOCAL_MEDIUM_PROVIDER_API_KEY_FILE:-/run/secrets/llm_local_gateway_api_key}
LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF: ${LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF:-credential://domovoy/local-medium/provider-api-key/v1}
LOCAL_GPU_PROVIDER_TIMEOUT: ${LOCAL_GPU_PROVIDER_TIMEOUT:-60s}
LOCAL_LIFEBOAT_PROVIDER_URL: http://cognitive-lifeboat:8086/v1
@@ -263,10 +287,28 @@ services:
LOCAL_IMAGE_MODEL: ${LOCAL_IMAGE_MODEL:-z_image_turbo_bf16.safetensors}
COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal
COGNITIVE_ALLOW_CANARY: "true"
depends_on: [core-runtime, cognitive-lifeboat]
volumes:
- assistant_core_secrets:/run/secrets:ro
networks: [default, llm-local]
depends_on:
core-runtime: { condition: service_started }
cognitive-lifeboat: { condition: service_healthy }
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8083/healthz >/dev/null && wget -qO- http://localhost:8083/readyz >/dev/null"]
interval: 10s
timeout: 20s
retries: 20
cognitive-worker-candidate:
profiles: ["assistant-canary"]
extends:
service: cognitive-worker
restart: "no"
environment:
CORE_RUNTIME_URL: http://core-runtime-candidate:8080
depends_on:
core-runtime-candidate: { condition: service_started }
cognitive-lifeboat: { condition: service_healthy }
cognitive-lifeboat:
image: ghcr.io/ggml-org/llama.cpp:server
@@ -277,7 +319,7 @@ services:
- --port
- "8086"
- --model
- /models/Qwen3-8B-Q4_K_M.gguf
- /models/Qwen3.8-27B-UD-Q2_K_XL.gguf
- --alias
- domovoy-lifeboat
- --jinja
@@ -299,6 +341,29 @@ services:
timeout: 3s
retries: 10
supervisor:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: supervisor, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
environment:
LISTEN_ADDR: ":8080"
SUPERVISOR_CONTROL_ADDR: ":8095"
SUPERVISOR_STATE_PATH: /var/lib/supervisor/release-state.json
SUPERVISOR_STABLE_UPSTREAM: http://creator-web:8084
SUPERVISOR_CANDIDATE_UPSTREAM: http://creator-web-candidate:8084
SUPERVISOR_BASELINE_RELEASE: ${ASSISTANT_SOURCE_REVISION:-unknown}
ports:
- "${CREATOR_WEB_PORT:-18080}:8080"
- "127.0.0.1:${SUPERVISOR_CONTROL_PORT:-18085}:8095"
depends_on:
creator-web: { condition: service_healthy }
volumes:
- assistant_supervisor_state:/var/lib/supervisor
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null && wget -qO- http://localhost:8080/readyz >/dev/null"]
interval: 10s
timeout: 5s
retries: 20
creator-web:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: creator-web, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
@@ -310,14 +375,39 @@ services:
COGNITIVE_RUNTIME_URL: http://cognitive-worker:8083
AGENT_WORKER_URL: http://agent-worker:8090
ASSISTANT_BUILD_REVISION: ${ASSISTANT_SOURCE_REVISION:-unknown}
ports: ["${CREATOR_WEB_PORT:-18080}:8084"]
depends_on: [core-runtime, continuity-runtime, commit-runtime, cognitive-worker]
depends_on:
core-runtime: { condition: service_started }
continuity-runtime: { condition: service_started }
commit-runtime: { condition: service_started }
cognitive-worker: { condition: service_healthy }
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8084/healthz >/dev/null"]
interval: 10s
timeout: 3s
retries: 10
creator-web-candidate:
profiles: ["assistant-canary"]
extends:
service: creator-web
restart: "no"
environment:
CORE_RUNTIME_URL: http://core-runtime-candidate:8080
CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081
COMMIT_RUNTIME_URL: http://commit-runtime:8082
COGNITIVE_RUNTIME_URL: http://cognitive-worker-candidate:8083
AGENT_WORKER_URL: http://agent-worker:8090
depends_on:
core-runtime-candidate: { condition: service_started }
continuity-runtime: { condition: service_started }
commit-runtime: { condition: service_started }
cognitive-worker-candidate: { condition: service_healthy }
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8084/healthz >/dev/null"]
interval: 10s
timeout: 5s
retries: 20
volumes:
assistant_pg:
assistant_nats:
@@ -333,6 +423,7 @@ volumes:
external: true
assistant_research_sources:
name: assistant_research_sources
assistant_supervisor_state:
networks:
llm-local:
+1
View File
@@ -4,5 +4,6 @@ LABEL org.opencontainers.image.revision=$SOURCE_REVISION \
org.opencontainers.image.source="https://git.cin.su/m/Assistant.git"
COPY database/bootstrap/001_roles_and_schemas.sql /docker-entrypoint-initdb.d/001_roles_and_schemas.sql
COPY database/bootstrap/002_gen2_roles_and_schemas.sql /docker-entrypoint-initdb.d/002_gen2_roles_and_schemas.sql
COPY deploy/images/002_owner_passwords.sh /docker-entrypoint-initdb.d/002_owner_passwords.sh
RUN chmod 0555 /docker-entrypoint-initdb.d/002_owner_passwords.sh
+1
View File
@@ -11,6 +11,7 @@ ARG SOURCE_REVISION=unknown
LABEL org.opencontainers.image.revision=$SOURCE_REVISION \
org.opencontainers.image.source="https://git.cin.su/m/Assistant.git"
RUN addgroup -S assistant && adduser -S -G assistant assistant
RUN mkdir -p /var/lib/supervisor && chown assistant:assistant /var/lib/supervisor
USER assistant
COPY --from=build /out/runtime /usr/local/bin/assistant-runtime
ENTRYPOINT ["/usr/local/bin/assistant-runtime"]
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_DIR="${REPO_DIR:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}"
ENV_FILE="${ENV_FILE:-/home/m/assistant-deploy/production-compose.env}"
COMPOSE_FILE="$BASE_DIR/deploy/docker-compose.prod.yml"
SUPERVISOR_CONTROL_URL="${SUPERVISOR_CONTROL_URL:-http://127.0.0.1:${SUPERVISOR_CONTROL_PORT:-18085}}"
REVISION="${1:?candidate revision is required}"
HEALTH_ATTEMPTS="${HEALTH_ATTEMPTS:-360}"
set -a
. "$ENV_FILE"
set +a
compose() { docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }
control() { curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL$1" "${@:2}" >/dev/null; }
fail_rollback() {
control /v1/traffic/rollback || true
control /v1/releases/rollback || true
echo "production ReleaseSet canary failed; stable ReleaseSet rollback requested" >&2
exit 1
}
services=(creator-web-candidate core-runtime-candidate cognitive-worker-candidate research-runtime-candidate)
containers=(assistant-creator-web-candidate-1 assistant-core-runtime-candidate-1 assistant-cognitive-worker-candidate-1 assistant-research-runtime-candidate-1)
stable_services=(creator-web core-runtime cognitive-worker research-runtime)
stable_ids() {
local service container id
for service in "${stable_services[@]}"; do
container="assistant-${service}-1"
id=$(docker inspect -f '{{.Id}}' "$container" 2>/dev/null || true)
[[ -n "$id" ]] || { echo "stable container missing: $container" >&2; return 1; }
printf '%s=%s\n' "$service" "$id"
done
}
assert_stable_ids_unchanged() {
local after
after=$(stable_ids) || fail_rollback
[[ "$after" == "$stable_ids_before" ]] || {
echo "stable container IDs changed during candidate operation" >&2
diff -u <(printf '%s\n' "$stable_ids_before") <(printf '%s\n' "$after") >&2 || true
fail_rollback
}
}
stable_ids_before=$(stable_ids) || exit 1
compose --profile assistant-canary build --build-arg "SOURCE_REVISION=$REVISION" "${services[@]}"
wait_healthy() {
local container="$1" status="" ready=""
ready=""
for attempt in $(seq 1 "$HEALTH_ATTEMPTS"); do
status=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}}' "$container" 2>/dev/null || true)
if [[ "$status" == healthy ]]; then ready=true; break; fi
sleep 2
done
[[ "$ready" == true ]] || fail_rollback
}
compose --profile assistant-canary up -d --no-build --no-deps research-runtime-candidate core-runtime-candidate
wait_healthy assistant-research-runtime-candidate-1
wait_healthy assistant-core-runtime-candidate-1
compose --profile assistant-canary up -d --no-build --no-deps cognitive-worker-candidate
wait_healthy assistant-cognitive-worker-candidate-1
compose --profile assistant-canary up -d --no-build --no-deps creator-web-candidate
wait_healthy assistant-creator-web-candidate-1
assert_stable_ids_unchanged
for container in "${containers[@]}"; do
image_revision=$(docker inspect -f '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$container" 2>/dev/null || true)
[[ "$image_revision" == "$REVISION" ]] || fail_rollback
done
assert_stable_ids_unchanged
if [[ "${PREPARE_ONLY:-false}" == true ]]; then
echo "production ReleaseSet candidate prepared revision=$REVISION"
exit 0
fi
release_json=$(cat <<JSON
{
"id":"candidate-$REVISION",
"revision":"$REVISION",
"components":{
"creator-web":{"name":"creator-web","revision":"$REVISION","upstream":"http://creator-web-candidate:8084","health_url":"http://creator-web-candidate:8084/healthz","ready_url":"http://creator-web-candidate:8084/healthz"},
"core-runtime":{"name":"core-runtime","revision":"$REVISION","upstream":"http://core-runtime-candidate:8080","health_url":"http://core-runtime-candidate:8080/healthz","ready_url":"http://core-runtime-candidate:8080/readyz"},
"cognitive-worker":{"name":"cognitive-worker","revision":"$REVISION","upstream":"http://cognitive-worker-candidate:8083","health_url":"http://cognitive-worker-candidate:8083/healthz","ready_url":"http://cognitive-worker-candidate:8083/readyz"},
"research-runtime":{"name":"research-runtime","revision":"$REVISION","upstream":"http://research-runtime-candidate:8087","health_url":"http://research-runtime-candidate:8087/healthz","ready_url":"http://research-runtime-candidate:8087/readyz"}
}
}
JSON
)
stable_json=$(curl -fsS --max-time 10 "$SUPERVISOR_CONTROL_URL/v1/state")
stable_set=$(printf '%s' "$stable_json" | jq -c '.stable_release_set // empty')
if [[ -z "$stable_set" ]]; then
stable_revision=$(printf '%s' "$stable_json" | jq -r '.current.version // "unknown"')
stable_set=$(cat <<JSON
{"id":"stable-$stable_revision","revision":"$stable_revision","components":{
"creator-web":{"name":"creator-web","revision":"$stable_revision","upstream":"http://creator-web:8084","health_url":"http://creator-web:8084/healthz","ready_url":"http://creator-web:8084/readyz"},
"core-runtime":{"name":"core-runtime","revision":"$stable_revision","upstream":"http://core-runtime:8080","health_url":"http://core-runtime:8080/healthz","ready_url":"http://core-runtime:8080/readyz"},
"cognitive-worker":{"name":"cognitive-worker","revision":"$stable_revision","upstream":"http://cognitive-worker:8083","health_url":"http://cognitive-worker:8083/healthz","ready_url":"http://cognitive-worker:8083/readyz"},
"research-runtime":{"name":"research-runtime","revision":"$stable_revision","upstream":"http://research-runtime:8087","health_url":"http://research-runtime:8087/healthz","ready_url":"http://research-runtime:8087/readyz"}}}
JSON
)
curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL/v1/releases/baseline-set" -H 'Content-Type: application/json' --data "$stable_set" >/dev/null || fail_rollback
fi
curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL/v1/releases/start-set" -H 'Content-Type: application/json' --data "$release_json" >/dev/null || fail_rollback
assert_stable_ids_unchanged
observation=$(printf '{"release_id":"candidate-%s","ready":true,"reason":"all ReleaseSet healthchecks and image revisions passed","observed_at_unix_millis":%s,"components":{"creator-web":{"revision":"%s","health":true,"ready":true},"core-runtime":{"revision":"%s","health":true,"ready":true},"cognitive-worker":{"revision":"%s","health":true,"ready":true},"research-runtime":{"revision":"%s","health":true,"ready":true}}}\n' "$REVISION" "$(date +%s%3N)" "$REVISION" "$REVISION" "$REVISION" "$REVISION")
curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL/v1/releases/observe-set" -H 'Content-Type: application/json' --data "$observation" >/dev/null || fail_rollback
assert_stable_ids_unchanged
control /v1/traffic/canary || fail_rollback
assert_stable_ids_unchanged
headers=$(mktemp)
trap 'rm -f "$headers"' EXIT
if ! curl -fsS --max-time 10 -D "$headers" -H 'X-Assistant-Canary: true' "https://assistant.cin.su/login" -o /dev/null; then fail_rollback; fi
grep -qi '^X-Assistant-Traffic-Target: candidate' "$headers" || fail_rollback
control /v1/releases/promote || fail_rollback
assert_stable_ids_unchanged
echo "production ReleaseSet canary promoted revision=$REVISION"
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_DIR="${REPO_DIR:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}"
ENV_FILE="${ENV_FILE:-/home/m/assistant-deploy/production-compose.env}"
COMPOSE_FILE="$BASE_DIR/deploy/docker-compose.prod.yml"
set -a
. "$ENV_FILE"
set +a
compose() { docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }
stable_services=(creator-web core-runtime cognitive-worker research-runtime)
stable_ids() {
local service container id
for service in "${stable_services[@]}"; do
container="assistant-${service}-1"
id=$(docker inspect -f '{{.Id}}' "$container" 2>/dev/null || true)
[[ -n "$id" ]] || { echo "stable container missing: $container" >&2; return 1; }
printf '%s=%s\n' "$service" "$id"
done
}
before=$(stable_ids)
compose up -d --no-build --no-deps supervisor
after=$(stable_ids)
[[ "$before" == "$after" ]] || {
echo "stable container IDs changed while updating supervisor" >&2
diff -u <(printf '%s\n' "$before") <(printf '%s\n' "$after") >&2 || true
exit 1
}
printf '%s\n' "$after"
+1 -5
View File
@@ -1,5 +1 @@
{
"task_file": "dev/tasks/DEV-000295-household-scenario-audit.json",
"task_id": "DEV-000295",
"task_revision": 2
}
{"task_file":"dev/tasks/GEN2-COMPAT-OBS-001-production-compatibility-observation.json","task_id":"GEN2-COMPAT-OBS-001","task_revision":2}
@@ -0,0 +1,101 @@
{
"task_id": "ARCH-GEN2-000",
"task_revision": 1,
"architecture_revision": "ADR-0019",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Сделать Gen2 официальным архитектурным поколением, а не внешним пожеланием.",
"requirement_ids": [
"REQ-ARCH-GEN2-000-GOAL",
"REQ-ARCH-GEN2-000-BOUNDARY",
"REQ-ARCH-GEN2-000-GATE"
],
"allowed_change_scope": {
"modules": [],
"paths": [
"architecture/**",
"development/**",
"docs/**",
"AGENTS.md",
"cmd/devctl/**",
"ci/architecture/**",
".gitea/workflows/ci.yml",
"dev/**"
]
},
"forbidden_change_scope": {
"paths": [
"security/**",
"go.mod",
"go.sum",
"deploy/**",
"database/**",
"adapters/models/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_database_owner_roles": 0
},
"contracts": {
"consume": [],
"may_change": []
},
"semantic_owner": "Architecture/Governance. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
"canonical_state": [
"нет runtime writer change",
"No task may create a second canonical owner for an already-owned mutable semantic concept."
],
"state_transitions": [
"V1 governance -> Gen2 documents accepted -> architecture authority/registry updated -> later implementation tasks become legal"
],
"side_effects_and_privileges": [
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
"Do not weaken architecture/security/test guardrails to make the task pass."
],
"persistence_and_migrations": [
"No runtime schema change. This task adopts Gen2 canonical documents/governance only."
],
"failure_retry_restart": [
"Rollback rule: revert architecture adoption до начала schema migration; baseline tag остаётся неизменным",
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
],
"assumptions": [
"Program milestone: M0; test profile: FAST.",
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
"Executable task id is ARCH-GEN2-000 because current devctl accepts ARCH-* as an architecture_revision root; this corresponds exactly to GEN2-000 in the Master Plan.",
"Depends on: PRE-GEN2-000."
],
"acceptance_scenarios": [
"ARCH-GEN2-000-SCN-01: внести DOMOVOY_CONSTITUTION, GEN2_TARGET_ARCHITECTURE, GEN2_CANONICAL_DATA_MODEL, GEN2_MIGRATION_MAP и GEN2_MASTER_PLAN в repo",
"ARCH-GEN2-000-SCN-02: принять ADR/architecture-generation decision, который разрешает новые owners World/Configuration/SELF и Gen2 migrations",
"ARCH-GEN2-000-SCN-03: обновить authority order архитектурных документов",
"ARCH-GEN2-000-SCN-04: закрыть/заморозить DEV-000295 как V1 baseline/audit evidence или явно supersede его Gen2 program",
"ARCH-GEN2-000-SCN-05: добавить Gen2 task validation в devctl",
"ARCH-GEN2-000-SCN-06: добавить FAST/MILESTONE/FULL test profiles или эквивалентную profile metadata без тяжёлого переписывания CI",
"ARCH-GEN2-000-SCN-07: внести GEN2_EXECUTION_CONTRACT, GEN2_TASK_INDEX и все последующие GEN2 task packets; сделать их canonical execution program",
"ARCH-GEN2-000-EXIT: Codex перед каждым Gen2 task обязан читать canonical Gen2 docs; ordinary V1 task больше не имеет права менять Gen2 architecture."
],
"required_tests": [
"FAST: go test ./cmd/devctl/...",
"FAST: go vet ./cmd/devctl/...",
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check gen2-integration --",
"FAST architecture: go run ./cmd/devctl check-architecture"
],
"terminal_gap_states": [
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT",
"REQUIRES_MIGRATION_DECISION"
]
}
@@ -0,0 +1,102 @@
{
"task_id": "GEN2-001",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Формально закрепить owners, forbidden dependencies и migration law before tables.",
"requirement_ids": [
"REQ-GEN2-001-GOAL",
"REQ-GEN2-001-BOUNDARY",
"REQ-GEN2-001-GATE"
],
"allowed_change_scope": {
"modules": [],
"paths": [
"architecture/**",
"contracts/world/**",
"contracts/configuration/**",
"contracts/self/**",
"cmd/devctl/**",
"ci/architecture/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-001-accept-gen2-owner-registry-and-dependency-rules.json"
]
},
"forbidden_change_scope": {
"paths": [
"security/**",
"go.mod",
"go.sum",
".gitea/workflows/**",
"deploy/**",
"database/**",
"adapters/models/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 32,
"new_canonical_owners": 3,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_database_owner_roles": 3
},
"contracts": {
"consume": [],
"may_change": [
"Only contracts explicitly within GEN2-001 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
]
},
"semantic_owner": "Architecture Registry. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
"canonical_state": [
"нет data writer change",
"No task may create a second canonical owner for an already-owned mutable semantic concept."
],
"state_transitions": [
"V1 governance -> Gen2 documents accepted -> architecture authority/registry updated -> later implementation tasks become legal"
],
"side_effects_and_privileges": [
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
"Do not weaken architecture/security/test guardrails to make the task pass."
],
"persistence_and_migrations": [
"No database schema change is authorized by this packet unless a revised task explicitly grants database/migration scope."
],
"failure_retry_restart": [
"Rollback rule: architecture revert возможен только до первого зависимого Gen2 task; далее через новый ADR",
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
],
"assumptions": [
"Program milestone: M0; test profile: FAST.",
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
"Depends on: ARCH-GEN2-000."
],
"acceptance_scenarios": [
"GEN2-001-SCN-01: принять owner registry Gen2",
"GEN2-001-SCN-02: развести AuthorityScope и ContextScope",
"GEN2-001-SCN-03: зафиксировать World != truth owner, SELF != orchestrator, Memory != owner",
"GEN2-001-SCN-04: зафиксировать single canonical writer, no cross-owner SQL/FK/JOIN by default",
"GEN2-001-SCN-05: зафиксировать Supervisor как runtime layer, не semantic owner",
"GEN2-001-SCN-06: добавить architecture checks для новых границ там, где это возможно дешёво",
"GEN2-001-EXIT: все последующие task packets могут однозначно указать current_owner/target_owner."
],
"required_tests": [
"FAST: go test ./cmd/devctl/...",
"FAST: go vet ./cmd/devctl/...",
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check gen2-integration --",
"FAST architecture: go run ./cmd/devctl check-architecture"
],
"terminal_gap_states": [
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT",
"REQUIRES_MIGRATION_DECISION"
]
}
@@ -0,0 +1,113 @@
{
"task_id": "GEN2-002",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Дать Gen2 стабильные ID/revision/time/SemanticRef/PrincipalRealm/Sensitivity primitives без giant shared domain model.",
"requirement_ids": [
"REQ-GEN2-002-GOAL",
"REQ-GEN2-002-BOUNDARY",
"REQ-GEN2-002-GATE"
],
"allowed_change_scope": {
"modules": [
"world",
"situation",
"epistemics",
"configuration",
"self"
],
"paths": [
"platform/ids/**",
"platform/clock/**",
"platform/errors/**",
"contracts/world/**",
"contracts/situation/**",
"contracts/epistemics/**",
"contracts/configuration/**",
"contracts/self/**",
"tests/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-002-introduce-shared-gen2-data-primitives-without-shared-domain-semantics.json"
]
},
"forbidden_change_scope": {
"paths": [
"security/**",
"go.mod",
"go.sum",
"architecture/**",
".gitea/workflows/**",
"deploy/**",
"database/**",
"adapters/models/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_database_owner_roles": 0
},
"contracts": {
"consume": [
"contracts.identity@v1",
"contracts.authority@v1"
],
"may_change": [
"Only contracts explicitly within GEN2-002 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
]
},
"semantic_owner": "Platform/Contracts. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
"canonical_state": [
"существующие owners остаются writers",
"No task may create a second canonical owner for an already-owned mutable semantic concept."
],
"state_transitions": [
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: существующие owners остаются writers"
],
"side_effects_and_privileges": [
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
"Do not weaken architecture/security/test guardrails to make the task pass."
],
"persistence_and_migrations": [
"No database schema change is authorized by this packet unless a revised task explicitly grants database/migration scope."
],
"failure_retry_restart": [
"Rollback rule: additive primitives; old code не удалять",
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
],
"assumptions": [
"Program milestone: M1; test profile: FAST.",
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
"Depends on: GEN2-001."
],
"acceptance_scenarios": [
"GEN2-002-SCN-01: определить StableID/Revision primitives",
"GEN2-002-SCN-02: определить SemanticRef как cross-owner reference без прямого DB ownership",
"GEN2-002-SCN-03: определить time primitives и data realm/sensitivity primitives",
"GEN2-002-SCN-04: реализовать typed value algebra там, где он действительно общий технический контракт",
"GEN2-002-SCN-05: не переносить domain meaning в platform/common",
"GEN2-002-EXIT: World/Focus/Epistemics/Configuration могут ссылаться на одинаковые технические refs без shared semantic owner."
],
"required_tests": [
"FAST: go test ./platform/ids/... ./contracts/world/... ./contracts/situation/... ./contracts/epistemics/... ./contracts/configuration/... ./contracts/self/...",
"FAST: go vet ./platform/ids/... ./contracts/world/... ./contracts/situation/... ./contracts/epistemics/... ./contracts/configuration/... ./contracts/self/...",
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check gen2-integration --"
],
"terminal_gap_states": [
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT",
"REQUIRES_MIGRATION_DECISION"
]
}
@@ -0,0 +1,110 @@
{
"task_id": "GEN2-003",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Создать устойчивые referents/aliases/types/facet definitions/relation definitions, не присваивая World владение истиной значений.",
"requirement_ids": [
"REQ-GEN2-003-GOAL",
"REQ-GEN2-003-BOUNDARY",
"REQ-GEN2-003-GATE"
],
"allowed_change_scope": {
"modules": [
"world"
],
"paths": [
"modules/world/**",
"contracts/world/**",
"adapters/postgres/world_repository.go",
"adapters/postgres/world_*.go",
"database/migrations/*.sql",
"tests/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-003-add-narrow-world-owner-and-typed-vocabulary.json"
]
},
"forbidden_change_scope": {
"paths": [
"security/**",
"go.mod",
"go.sum",
"architecture/**",
".gitea/workflows/**",
"deploy/**",
"adapters/models/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_database_owner_roles": 1
},
"contracts": {
"consume": [
"contracts.identity@v1",
"contracts.authority@v1"
],
"may_change": [
"Only contracts explicitly within GEN2-003 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
]
},
"semantic_owner": "World (new). Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
"canonical_state": [
"before: project/situation/ad-hoc identifiers; after: World canonical only for referent vocabulary, not facts",
"No task may create a second canonical owner for an already-owned mutable semantic concept."
],
"state_transitions": [
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: before: project/situation/ad-hoc identifiers; after: World canonical only for referent vocabulary, not facts"
],
"side_effects_and_privileges": [
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
"Do not weaken architecture/security/test guardrails to make the task pass."
],
"persistence_and_migrations": [
"Database changes must be additive next-numbered migrations only until an explicitly approved retirement task; never edit historical V1 migrations 001-045.",
"At most one canonical writer is active for each semantic state; backfill/shadow reads do not create a second canonical writer.",
"The positive new_database_owner_roles delta in this packet is a devctl-v1 database-surface compatibility gate; it does not authorize cross-owner SQL/FK/JOIN or an extra owner unless explicitly stated by this task."
],
"failure_retry_restart": [
"Rollback rule: V1 identifiers остаются compatibility refs; World tables additive; switch readers back без удаления World evidence",
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth.",
"Cutover must keep the previous read/compatibility path available until the task or milestone explicitly proves retirement."
],
"assumptions": [
"Program milestone: M1; test profile: FAST.",
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
"Depends on: GEN2-002."
],
"acceptance_scenarios": [
"GEN2-003-SCN-01: создать WorldObject stable identity",
"GEN2-003-SCN-02: WorldObjectKindDefinition",
"GEN2-003-SCN-03: WorldAlias",
"GEN2-003-SCN-04: FacetDefinition и RelationDefinition с versioned typed schemas",
"GEN2-003-SCN-05: identity-resolution candidate/decision structures",
"GEN2-003-SCN-06: обеспечить split/merge lineage",
"GEN2-003-SCN-07: начать с минимального набора объектов, необходимых для Focus/Scope migration",
"GEN2-003-EXIT: одинаковые имена разных компаний/баз могут иметь разные stable referents; World не хранит claim «какое значение истинно»."
],
"required_tests": [
"FAST: go test ./modules/world/... ./contracts/world/... ./adapters/postgres",
"FAST: go vet ./modules/world/... ./contracts/world/... ./adapters/postgres",
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check gen2-integration --"
],
"terminal_gap_states": [
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT",
"REQUIRES_MIGRATION_DECISION"
]
}
@@ -0,0 +1,116 @@
{
"task_id": "GEN2-004",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Сделать Focus многомерным: person/org/system/database/repository/version/activity/goal/task/entities, а Project оставить лишь возможной гранью.",
"requirement_ids": [
"REQ-GEN2-004-GOAL",
"REQ-GEN2-004-BOUNDARY",
"REQ-GEN2-004-GATE"
],
"allowed_change_scope": {
"modules": [
"situation",
"interaction"
],
"paths": [
"modules/situation/**",
"contracts/situation/**",
"adapters/postgres/situation_repository.go",
"database/migrations/*.sql",
"cmd/core-runtime/**",
"modules/interaction/**",
"contracts/interaction/**",
"tests/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-004-replace-project-centric-focus-with-contextscope-focusframe-focusstack.json"
]
},
"forbidden_change_scope": {
"paths": [
"security/**",
"go.mod",
"go.sum",
"architecture/**",
".gitea/workflows/**",
"deploy/**",
"adapters/models/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_database_owner_roles": 1
},
"contracts": {
"consume": [
"contracts.identity@v1",
"contracts.authority@v1",
"contracts.world@gen2",
"contracts.work@v1",
"contracts.capability@v1"
],
"may_change": [
"Only contracts explicitly within GEN2-004 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
]
},
"semantic_owner": "Situation. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
"canonical_state": [
"before: V1 situation/project focus; after: Situation FocusFrame/Transition; ActiveProjectFocus becomes projection/compatibility",
"No task may create a second canonical owner for an already-owned mutable semantic concept."
],
"state_transitions": [
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: before: V1 situation/project focus; after: Situation FocusFrame/Transition; ActiveProjectFocus becomes projection/compatibility"
],
"side_effects_and_privileges": [
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
"Do not weaken architecture/security/test guardrails to make the task pass."
],
"persistence_and_migrations": [
"Database changes must be additive next-numbered migrations only until an explicitly approved retirement task; never edit historical V1 migrations 001-045.",
"At most one canonical writer is active for each semantic state; backfill/shadow reads do not create a second canonical writer.",
"The positive new_database_owner_roles delta in this packet is a devctl-v1 database-surface compatibility gate; it does not authorize cross-owner SQL/FK/JOIN or an extra owner unless explicitly stated by this task."
],
"failure_retry_restart": [
"Rollback rule: feature switch возвращает V1 read path; Gen2 transitions сохраняются",
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth.",
"Cutover must keep the previous read/compatibility path available until the task or milestone explicitly proves retirement."
],
"assumptions": [
"Program milestone: M1; test profile: FAST.",
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
"Depends on: GEN2-003."
],
"acceptance_scenarios": [
"GEN2-004-SCN-01: добавить ContextScope RoleKey/ScopeValue/MatchPolicy",
"GEN2-004-SCN-02: добавить FocusFrame stable ID + revision",
"GEN2-004-SCN-03: добавить FocusTransition append-only ledger",
"GEN2-004-SCN-04: добавить FocusStack/parent/suspended/resume semantics",
"GEN2-004-SCN-05: реализовать explicit focus command и inferred focus как разные evidence paths",
"GEN2-004-SCN-06: построить compatibility projection ActiveProjectFocus из FocusFrame",
"GEN2-004-SCN-07: переключить core/context readers на Focus V2 постепенно",
"GEN2-004-EXIT: короткое отвлечение может push/pop focus; Company A/UNF и Company B/UNF не смешиваются по текущему scope."
],
"required_tests": [
"FAST: go test ./modules/situation/... ./contracts/situation/... ./modules/interaction/... ./cmd/core-runtime/... ./adapters/postgres",
"FAST: go vet ./modules/situation/... ./contracts/situation/... ./modules/interaction/... ./cmd/core-runtime/... ./adapters/postgres",
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check gen2-integration --"
],
"terminal_gap_states": [
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT",
"REQUIRES_MIGRATION_DECISION"
]
}
@@ -0,0 +1,107 @@
{
"task_id": "GEN2-005",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Заложить безопасное same_as?/merge/split и привязку Focus к конкретной версии/окружению.",
"requirement_ids": [
"REQ-GEN2-005-GOAL",
"REQ-GEN2-005-BOUNDARY",
"REQ-GEN2-005-GATE"
],
"allowed_change_scope": {
"modules": [
"world",
"situation"
],
"paths": [
"modules/world/**",
"contracts/world/**",
"modules/situation/**",
"contracts/situation/**",
"tests/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-005-world-identity-resolution-and-version-environment-bindings.json"
]
},
"forbidden_change_scope": {
"paths": [
"security/**",
"go.mod",
"go.sum",
"architecture/**",
".gitea/workflows/**",
"deploy/**",
"database/**",
"adapters/models/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_database_owner_roles": 0
},
"contracts": {
"consume": [
"contracts.world@gen2",
"contracts.situation@gen2"
],
"may_change": [
"Only contracts explicitly within GEN2-005 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
]
},
"semantic_owner": "World + Situation contracts, no truth ownership. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
"canonical_state": [
"World writer только identity/referent resolution",
"No task may create a second canonical owner for an already-owned mutable semantic concept."
],
"state_transitions": [
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: World writer только identity/referent resolution"
],
"side_effects_and_privileges": [
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
"Do not weaken architecture/security/test guardrails to make the task pass."
],
"persistence_and_migrations": [
"No database schema change is authorized by this packet unless a revised task explicitly grants database/migration scope."
],
"failure_retry_restart": [
"Rollback rule: resolution decisions versioned/reversible; no destructive identity merge",
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
],
"assumptions": [
"Program milestone: M1; test profile: MILESTONE.",
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
"Depends on: GEN2-004."
],
"acceptance_scenarios": [
"GEN2-005-SCN-01: реализовать identity resolution candidate с confidence/evidence refs",
"GEN2-005-SCN-02: не объединять автоматически неоднозначных людей/системы",
"GEN2-005-SCN-03: добавить reversible merge/split lineage",
"GEN2-005-SCN-04: добавить version/environment reference primitives, используемые позже Epistemics freshness",
"GEN2-005-EXIT: «Иван Telegram» и «Иван CRM» могут быть связаны с уверенностью без необратимого склеивания; две УНФ не путаются."
],
"required_tests": [
"FAST: go test ./modules/world/... ./modules/situation/...",
"FAST: go vet ./modules/world/... ./modules/situation/...",
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check gen2-integration --",
"MILESTONE M1: go test ./modules/world/... ./modules/situation/... ./modules/interaction/... ./cmd/core-runtime/...",
"MILESTONE M1: run focused scope/focus company-A-vs-company-B and focus-stack regressions; do not run full repository suite."
],
"terminal_gap_states": [
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT",
"REQUIRES_MIGRATION_DECISION"
]
}
@@ -0,0 +1,111 @@
{
"task_id": "GEN2-006",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Убрать зависимость от denormalized truth_bundles как конечной модели истины и создать переоцениваемую канонику evidence/claim/belief.",
"requirement_ids": [
"REQ-GEN2-006-GOAL",
"REQ-GEN2-006-BOUNDARY",
"REQ-GEN2-006-GATE"
],
"allowed_change_scope": {
"modules": [
"epistemics"
],
"paths": [
"modules/epistemics/**",
"contracts/epistemics/**",
"adapters/postgres/epistemics_repository.go",
"adapters/research/**",
"database/migrations/*.sql",
"tests/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-006-normalize-epistemics-evidence-propositions-claims-and-belief-assessments.json"
]
},
"forbidden_change_scope": {
"paths": [
"security/**",
"go.mod",
"go.sum",
"architecture/**",
".gitea/workflows/**",
"deploy/**",
"adapters/models/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_database_owner_roles": 1
},
"contracts": {
"consume": [
"contracts.identity@v1",
"contracts.artifact@v1",
"contracts.world@gen2"
],
"may_change": [
"Only contracts explicitly within GEN2-006 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
]
},
"semantic_owner": "Epistemics. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
"canonical_state": [
"before: V1 truth bundle paths; after: normalized Epistemics for new Gen2 knowledge; old bundle writer disabled at cutover or made projection",
"No task may create a second canonical owner for an already-owned mutable semantic concept."
],
"state_transitions": [
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: before: V1 truth bundle paths; after: normalized Epistemics for new Gen2 knowledge; old bundle writer disabled at cutover or made projection"
],
"side_effects_and_privileges": [
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
"Do not weaken architecture/security/test guardrails to make the task pass."
],
"persistence_and_migrations": [
"Database changes must be additive next-numbered migrations only until an explicitly approved retirement task; never edit historical V1 migrations 001-045.",
"At most one canonical writer is active for each semantic state; backfill/shadow reads do not create a second canonical writer.",
"The positive new_database_owner_roles delta in this packet is a devctl-v1 database-surface compatibility gate; it does not authorize cross-owner SQL/FK/JOIN or an extra owner unless explicitly stated by this task."
],
"failure_retry_restart": [
"Rollback rule: read switch назад на V1 bundle; normalized evidence не удалять",
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth.",
"Cutover must keep the previous read/compatibility path available until the task or milestone explicitly proves retirement."
],
"assumptions": [
"Program milestone: M2; test profile: FAST.",
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
"Depends on: GEN2-005."
],
"acceptance_scenarios": [
"GEN2-006-SCN-01: добавить Source/SourceSnapshot/SourceExcerpt",
"GEN2-006-SCN-02: добавить immutable Observation",
"GEN2-006-SCN-03: добавить Proposition/Claim",
"GEN2-006-SCN-04: добавить versioned BeliefAssessment",
"GEN2-006-SCN-05: backfill существующих truth/research данных с lineage",
"GEN2-006-SCN-06: оставить truth_bundles read compatibility до позднего retirement",
"GEN2-006-SCN-07: обеспечить возможность новой модели переоценить старый Claim без переписывания Observation",
"GEN2-006-EXIT: источник, наблюдение, утверждение и текущая оценка уверенности раздельны и traceable."
],
"required_tests": [
"FAST: go test ./modules/epistemics/... ./contracts/epistemics/... ./adapters/research/... ./adapters/postgres",
"FAST: go vet ./modules/epistemics/... ./contracts/epistemics/... ./adapters/research/... ./adapters/postgres",
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check gen2-integration --"
],
"terminal_gap_states": [
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT",
"REQUIRES_MIGRATION_DECISION"
]
}
@@ -0,0 +1,111 @@
{
"task_id": "GEN2-007",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Сделать старение знания контекстным: прошлогодняя УНФ может быть stale для live-debug и полезной аналогией без live-доступа.",
"requirement_ids": [
"REQ-GEN2-007-GOAL",
"REQ-GEN2-007-BOUNDARY",
"REQ-GEN2-007-GATE"
],
"allowed_change_scope": {
"modules": [
"epistemics",
"work"
],
"paths": [
"modules/epistemics/**",
"contracts/epistemics/**",
"contracts/work/**",
"database/migrations/*.sql",
"tests/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-007-general-freshness-volatility-temporal-applicability-and-revalidation.json"
]
},
"forbidden_change_scope": {
"paths": [
"security/**",
"go.mod",
"go.sum",
"architecture/**",
".gitea/workflows/**",
"deploy/**",
"adapters/models/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_database_owner_roles": 1
},
"contracts": {
"consume": [
"contracts.epistemics@gen2",
"contracts.work@v1",
"contracts.world@gen2"
],
"may_change": [
"Only contracts explicitly within GEN2-007 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
]
},
"semantic_owner": "Epistemics. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
"canonical_state": [
"Epistemics writes observation/verification metadata; freshness status computed",
"No task may create a second canonical owner for an already-owned mutable semantic concept."
],
"state_transitions": [
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: Epistemics writes observation/verification metadata; freshness status computed"
],
"side_effects_and_privileges": [
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
"Do not weaken architecture/security/test guardrails to make the task pass."
],
"persistence_and_migrations": [
"Database changes must be additive next-numbered migrations only until an explicitly approved retirement task; never edit historical V1 migrations 001-045.",
"At most one canonical writer is active for each semantic state; backfill/shadow reads do not create a second canonical writer.",
"The positive new_database_owner_roles delta in this packet is a devctl-v1 database-surface compatibility gate; it does not authorize cross-owner SQL/FK/JOIN or an extra owner unless explicitly stated by this task."
],
"failure_retry_restart": [
"Rollback rule: metadata additive; old readers ignore it",
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
],
"assumptions": [
"Program milestone: M2; test profile: FAST.",
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
"Depends on: GEN2-006."
],
"acceptance_scenarios": [
"GEN2-007-SCN-01: добавить TemporalApplicability",
"GEN2-007-SCN-02: VolatilitySpec/decay class",
"GEN2-007-SCN-03: last_verified/observed metadata",
"GEN2-007-SCN-04: VersionBinding",
"GEN2-007-SCN-05: TaskFreshnessRequirement",
"GEN2-007-SCN-06: вычисляемый FreshnessAssessment вместо permanent `stale` flag",
"GEN2-007-SCN-07: active revalidation request/response contract",
"GEN2-007-SCN-08: fallback semantics: memory-only solution должен быть явно маркирован как непроверенный текущими данными",
"GEN2-007-EXIT: один и тот же Claim получает разную пригодность в зависимости от task freshness/risk."
],
"required_tests": [
"FAST: go test ./modules/epistemics/... ./contracts/epistemics/... ./contracts/work/...",
"FAST: go vet ./modules/epistemics/... ./contracts/epistemics/... ./contracts/work/...",
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check gen2-integration --"
],
"terminal_gap_states": [
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT",
"REQUIRES_MIGRATION_DECISION"
]
}
@@ -0,0 +1,110 @@
{
"task_id": "GEN2-008",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Сохранять не только «что сработало», но problem/attempt/why/result/why failed/conditions/evidence.",
"requirement_ids": [
"REQ-GEN2-008-GOAL",
"REQ-GEN2-008-BOUNDARY",
"REQ-GEN2-008-GATE"
],
"allowed_change_scope": {
"modules": [
"epistemics",
"artifact"
],
"paths": [
"modules/epistemics/**",
"contracts/epistemics/**",
"adapters/postgres/epistemics_repository.go",
"contracts/artifact/**",
"database/migrations/*.sql",
"tests/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-008-promote-episode-and-experience-v2-including-negative-experience.json"
]
},
"forbidden_change_scope": {
"paths": [
"security/**",
"go.mod",
"go.sum",
"architecture/**",
".gitea/workflows/**",
"deploy/**",
"adapters/models/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_database_owner_roles": 1
},
"contracts": {
"consume": [
"contracts.epistemics@gen2",
"contracts.artifact@v1",
"contracts.world@gen2"
],
"may_change": [
"Only contracts explicitly within GEN2-008 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
]
},
"semantic_owner": "Epistemics. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
"canonical_state": [
"Epistemics remains canonical Experience writer; Episode projection derived",
"No task may create a second canonical owner for an already-owned mutable semantic concept."
],
"state_transitions": [
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: Epistemics remains canonical Experience writer; Episode projection derived"
],
"side_effects_and_privileges": [
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
"Do not weaken architecture/security/test guardrails to make the task pass."
],
"persistence_and_migrations": [
"Database changes must be additive next-numbered migrations only until an explicitly approved retirement task; never edit historical V1 migrations 001-045.",
"At most one canonical writer is active for each semantic state; backfill/shadow reads do not create a second canonical writer.",
"The positive new_database_owner_roles delta in this packet is a devctl-v1 database-surface compatibility gate; it does not authorize cross-owner SQL/FK/JOIN or an extra owner unless explicitly stated by this task."
],
"failure_retry_restart": [
"Rollback rule: старые Experience records остаются валидны; Episode projection rebuildable",
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
],
"assumptions": [
"Program milestone: M2; test profile: FAST.",
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
"Depends on: GEN2-007."
],
"acceptance_scenarios": [
"GEN2-008-SCN-01: расширить ExperienceRecord V1 без потери старых IDs/evidence",
"GEN2-008-SCN-02: сделать success/failure first-class",
"GEN2-008-SCN-03: добавить conditions/applicability/version bindings",
"GEN2-008-SCN-04: создать Episode как derived/rebuildable episodic projection",
"GEN2-008-SCN-05: связать user correction с ReflectionCandidate/Experience",
"GEN2-008-SCN-06: добавить anti-pattern: failure under conditions != forever wrong",
"GEN2-008-EXIT: retrieval позже может отдельно запросить «что уже пробовали и почему не сработало»."
],
"required_tests": [
"FAST: go test ./modules/epistemics/... ./contracts/epistemics/... ./adapters/postgres",
"FAST: go vet ./modules/epistemics/... ./contracts/epistemics/... ./adapters/postgres",
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check gen2-integration --"
],
"terminal_gap_states": [
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT",
"REQUIRES_MIGRATION_DECISION"
]
}
@@ -0,0 +1,114 @@
{
"task_id": "GEN2-009",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Дать многоканальный retrieval по canonical owners без единой memory_items каноники.",
"requirement_ids": [
"REQ-GEN2-009-GOAL",
"REQ-GEN2-009-BOUNDARY",
"REQ-GEN2-009-GATE"
],
"allowed_change_scope": {
"modules": [],
"paths": [
"runtime/retrieval/**",
"contracts/cognitive/**",
"adapters/postgres/*retrieval*.go",
"adapters/postgres/*search*.go",
"database/migrations/*.sql",
"tests/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-009-build-memory-retrieval-fabric-as-rebuildable-projections.json"
]
},
"forbidden_change_scope": {
"paths": [
"security/**",
"go.mod",
"go.sum",
"architecture/**",
".gitea/workflows/**",
"deploy/**",
"adapters/models/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_database_owner_roles": 1
},
"contracts": {
"consume": [
"contracts.interaction@v1",
"contracts.world@gen2",
"contracts.epistemics@gen2",
"contracts.work@v1",
"contracts.continuity@v1",
"contracts.capability@v1",
"contracts.configuration@gen2",
"contracts.self@gen2"
],
"may_change": [
"Only contracts explicitly within GEN2-009 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
]
},
"semantic_owner": "No canonical Memory owner; projection layer. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
"canonical_state": [
"canonical writers unchanged; retrieval index only projection writer",
"No task may create a second canonical owner for an already-owned mutable semantic concept."
],
"state_transitions": [
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: canonical writers unchanged; retrieval index only projection writer"
],
"side_effects_and_privileges": [
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
"Do not weaken architecture/security/test guardrails to make the task pass."
],
"persistence_and_migrations": [
"Database changes must be additive next-numbered migrations only until an explicitly approved retirement task; never edit historical V1 migrations 001-045.",
"At most one canonical writer is active for each semantic state; backfill/shadow reads do not create a second canonical writer.",
"The positive new_database_owner_roles delta in this packet is a devctl-v1 database-surface compatibility gate; it does not authorize cross-owner SQL/FK/JOIN or an extra owner unless explicitly stated by this task."
],
"failure_retry_restart": [
"Rollback rule: drop/rebuild projection and use V1 memory retrieval",
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth.",
"Cutover must keep the previous read/compatibility path available until the task or milestone explicitly proves retirement."
],
"assumptions": [
"Program milestone: M2; test profile: FAST.",
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
"Depends on: GEN2-008."
],
"acceptance_scenarios": [
"GEN2-009-SCN-01: создать RetrievalDocument projection с source SemanticRef",
"GEN2-009-SCN-02: projection builders из Interaction/World/Epistemics/Work/Continuity/Capability/Configuration/SELF",
"GEN2-009-SCN-03: жёсткий ContextScope/realm/version filter before similarity",
"GEN2-009-SCN-04: lanes: semantic, entity/graph-like refs, temporal, goal/commitment, positive experience, negative experience, contradiction, skill",
"GEN2-009-SCN-05: ranking signals: relevance, freshness, confidence, salience, scope distance, cost",
"GEN2-009-SCN-06: hot/warm/cold/archive retrieval policy как operational storage concern, не truth",
"GEN2-009-SCN-07: projection versioning/rebuild command",
"GEN2-009-EXIT: vector index можно полностью перестроить без изменения canonical memory; cross-company leak blocked before embedding rank."
],
"required_tests": [
"FAST: go test ./runtime/retrieval/... ./contracts/cognitive/... ./adapters/postgres",
"FAST: go vet ./runtime/retrieval/... ./contracts/cognitive/... ./adapters/postgres",
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check gen2-integration --"
],
"terminal_gap_states": [
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT",
"REQUIRES_MIGRATION_DECISION"
]
}
@@ -0,0 +1,110 @@
{
"task_id": "GEN2-010",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Формировать минимально достаточный model context под текущий Task/Focus/Model/Risk/Freshness и уметь дозапрашивать память во время reasoning.",
"requirement_ids": [
"REQ-GEN2-010-GOAL",
"REQ-GEN2-010-BOUNDARY",
"REQ-GEN2-010-GATE"
],
"allowed_change_scope": {
"modules": [],
"paths": [
"contracts/cognitive/**",
"cmd/cognitive-worker/**",
"cmd/core-runtime/**",
"runtime/retrieval/**",
"tests/context_compiler/**",
"tests/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-010-context-compiler-v2-with-budgets-coverage-and-iterative-retrieval.json"
]
},
"forbidden_change_scope": {
"paths": [
"security/**",
"go.mod",
"go.sum",
"architecture/**",
".gitea/workflows/**",
"deploy/**",
"database/**",
"adapters/models/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_database_owner_roles": 0
},
"contracts": {
"consume": [
"contracts.cognitive@v1",
"contracts.situation@gen2",
"contracts.epistemics@gen2",
"contracts.capability@v1"
],
"may_change": [
"Only contracts explicitly within GEN2-010 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
]
},
"semantic_owner": "Cognitive contract/read path, no canonical owner. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
"canonical_state": [
"none; read/compile only",
"No task may create a second canonical owner for an already-owned mutable semantic concept."
],
"state_transitions": [
"task input -> bounded Gen2 change -> FAST verification -> merge to gen2-integration; canonical writer rule: none; read/compile only"
],
"side_effects_and_privileges": [
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
"Do not weaken architecture/security/test guardrails to make the task pass."
],
"persistence_and_migrations": [
"No database schema change is authorized by this packet unless a revised task explicitly grants database/migration scope."
],
"failure_retry_restart": [
"Rollback rule: feature switch to Context Compiler V1; V2 trace/projections retained",
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
],
"assumptions": [
"Program milestone: M2; test profile: FAST.",
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
"Depends on: GEN2-009."
],
"acceptance_scenarios": [
"GEN2-010-SCN-01: ContextCompileRequest: task/focus/scope/model/token budget/freshness/risk",
"GEN2-010-SCN-02: candidate lanes + deterministic scope filtering",
"GEN2-010-SCN-03: positive+negative experience balancing",
"GEN2-010-SCN-04: contradiction pass",
"GEN2-010-SCN-05: coverage check: не упущены ли critical commitments/known failures",
"GEN2-010-SCN-06: ContextBundle с explicit token allocation",
"GEN2-010-SCN-07: ContextAssemblyTrace: почему элемент включён/исключён",
"GEN2-010-SCN-08: iterative memory search tool для model reasoning",
"GEN2-010-SCN-09: маленькой локальной модели давать меньше и точнее, а не просто урезать хвост",
"GEN2-010-EXIT: локальная модель получает релевантный пакет без любимого цвета в 1C-debug; нужные прошлые грабли попадают в context."
],
"required_tests": [
"FAST: go test ./contracts/cognitive/... ./cmd/cognitive-worker/... ./cmd/core-runtime/... ./runtime/retrieval/... ./tests/context_compiler/...",
"FAST: go vet ./contracts/cognitive/... ./cmd/cognitive-worker/... ./cmd/core-runtime/... ./runtime/retrieval/... ./tests/context_compiler/...",
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check gen2-integration --"
],
"terminal_gap_states": [
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT",
"REQUIRES_MIGRATION_DECISION"
]
}
@@ -0,0 +1,112 @@
{
"task_id": "GEN2-011",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Перестать использовать ad-hoc PersonalMemory ContextText как долгосрочную основу памяти, не теряя utterance history.",
"requirement_ids": [
"REQ-GEN2-011-GOAL",
"REQ-GEN2-011-BOUNDARY",
"REQ-GEN2-011-GATE"
],
"allowed_change_scope": {
"modules": [
"interaction"
],
"paths": [
"modules/interaction/**",
"contracts/interaction/**",
"adapters/postgres/interaction_repository.go",
"cmd/core-runtime/**",
"cmd/cognitive-worker/**",
"runtime/retrieval/**",
"tests/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-011-cut-personalmemory-v1-to-compatibility-fallback.json"
]
},
"forbidden_change_scope": {
"paths": [
"security/**",
"go.mod",
"go.sum",
"architecture/**",
".gitea/workflows/**",
"deploy/**",
"database/**",
"adapters/models/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_database_owner_roles": 0
},
"contracts": {
"consume": [
"contracts.interaction@v1",
"contracts.cognitive@gen2",
"contracts.situation@gen2"
],
"may_change": [
"Only contracts explicitly within GEN2-011 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
]
},
"semantic_owner": "Interaction -> retrieval fabric reads multiple owners. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
"canonical_state": [
"utterance writer unchanged; PersonalMemory derived writer frozen; retrieval projection primary",
"No task may create a second canonical owner for an already-owned mutable semantic concept."
],
"state_transitions": [
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: utterance writer unchanged; PersonalMemory derived writer frozen; retrieval projection primary"
],
"side_effects_and_privileges": [
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
"Do not weaken architecture/security/test guardrails to make the task pass."
],
"persistence_and_migrations": [
"No database schema change is authorized by this packet unless a revised task explicitly grants database/migration scope."
],
"failure_retry_restart": [
"Rollback rule: switch reads back to PersonalMemory; no canonical loss",
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth.",
"Cutover must keep the previous read/compatibility path available until the task or milestone explicitly proves retirement."
],
"assumptions": [
"Program milestone: M2; test profile: MILESTONE.",
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
"Depends on: GEN2-010."
],
"acceptance_scenarios": [
"GEN2-011-SCN-01: оставить HumanUtterance append-only canonical history",
"GEN2-011-SCN-02: построить Gen2 retrieval из этой истории",
"GEN2-011-SCN-03: сравнить V1 PersonalMemory vs Gen2 context on representative cases",
"GEN2-011-SCN-04: переключить primary read на Gen2",
"GEN2-011-SCN-05: оставить PersonalMemory read fallback до M6",
"GEN2-011-SCN-06: запретить новый semantic growth V1 PersonalMemory",
"GEN2-011-EXIT: PersonalMemory больше не определяет архитектуру новых features."
],
"required_tests": [
"FAST: go test ./modules/interaction/... ./cmd/core-runtime/... ./cmd/cognitive-worker/... ./runtime/retrieval/...",
"FAST: go vet ./modules/interaction/... ./cmd/core-runtime/... ./cmd/cognitive-worker/... ./runtime/retrieval/...",
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check gen2-integration --",
"MILESTONE M2: go test ./modules/epistemics/... ./modules/interaction/... ./cmd/cognitive-worker/... ./cmd/core-runtime/... ./runtime/retrieval/... ./tests/context_compiler/...",
"MILESTONE M2: run freshness, negative-experience, contradiction, scope-isolation and PersonalMemory-fallback regressions only."
],
"terminal_gap_states": [
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT",
"REQUIRES_MIGRATION_DECISION"
]
}
@@ -0,0 +1,119 @@
{
"task_id": "GEN2-012",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Убрать ordinary runtime settings из ENV и дать Skills/capabilities возможность объявлять новые typed scoped settings без глобальной schema rewrite.",
"requirement_ids": [
"REQ-GEN2-012-GOAL",
"REQ-GEN2-012-BOUNDARY",
"REQ-GEN2-012-GATE"
],
"allowed_change_scope": {
"modules": [
"configuration"
],
"paths": [
"modules/configuration/**",
"contracts/configuration/**",
"adapters/postgres/configuration_repository.go",
"platform/secrets/**",
"database/migrations/*.sql",
"cmd/core-runtime/**",
"cmd/cognitive-worker/**",
"adapters/models/**",
"adapters/research/**",
"adapters/multimodal/**",
"deploy/docker-compose.prod.yml",
"tests/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-012-add-typed-scoped-versioned-configuration-owner-and-bootstrap-boundary.json"
]
},
"forbidden_change_scope": {
"paths": [
"security/**",
"go.mod",
"go.sum",
"architecture/**",
".gitea/workflows/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 1,
"new_third_party_dependencies": 0,
"new_database_owner_roles": 1
},
"contracts": {
"consume": [
"contracts.identity@v1",
"contracts.world@gen2",
"contracts.secrets@v1"
],
"may_change": [
"Only contracts explicitly within GEN2-012 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
]
},
"semantic_owner": "Configuration (new). Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
"canonical_state": [
"before: ENV/files/ad-hoc DB; after: Configuration for migrated non-bootstrap settings",
"No task may create a second canonical owner for an already-owned mutable semantic concept."
],
"state_transitions": [
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: before: ENV/files/ad-hoc DB; after: Configuration for migrated non-bootstrap settings"
],
"side_effects_and_privileges": [
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
"Do not weaken architecture/security/test guardrails to make the task pass.",
"Deploy/release surface changes are allowed only for the bounded purpose of this packet; production promotion requires the packet's stated evidence and rollback path."
],
"persistence_and_migrations": [
"Database changes must be additive next-numbered migrations only until an explicitly approved retirement task; never edit historical V1 migrations 001-045.",
"At most one canonical writer is active for each semantic state; backfill/shadow reads do not create a second canonical writer.",
"The positive new_database_owner_roles delta in this packet is a devctl-v1 database-surface compatibility gate; it does not authorize cross-owner SQL/FK/JOIN or an extra owner unless explicitly stated by this task."
],
"failure_retry_restart": [
"Rollback rule: per-consumer fallback to old read path; Configuration values retained",
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth.",
"Cutover must keep the previous read/compatibility path available until the task or milestone explicitly proves retirement."
],
"assumptions": [
"Program milestone: M3; test profile: FAST.",
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
"Depends on: GEN2-011."
],
"acceptance_scenarios": [
"GEN2-012-SCN-01: inventory всех ENV/config consumers",
"GEN2-012-SCN-02: создать SettingDefinition stable identity + schema/version/owner/type/default/validation",
"GEN2-012-SCN-03: SettingAssignment + ScopeSelector",
"GEN2-012-SCN-04: deterministic inheritance/resolution",
"GEN2-012-SCN-05: EffectiveConfiguration trace",
"GEN2-012-SCN-06: SecretRef only for credentials",
"GEN2-012-SCN-07: определить минимальный bootstrap, который может остаться вне Configuration",
"GEN2-012-SCN-08: импортировать non-secret текущие значения",
"GEN2-012-SCN-09: shadow compare old vs resolved configuration",
"GEN2-012-SCN-10: cutover consumers по одному, Commit runtime последним из критических",
"GEN2-012-EXIT: можно сказать «для Skill X в Company A значение Y», а Company B получает другое значение без новых ENV vars/columns."
],
"required_tests": [
"FAST: go test ./modules/configuration/... ./contracts/configuration/... ./platform/secrets/... ./cmd/core-runtime/... ./adapters/models/... ./adapters/research/...",
"FAST: go vet ./modules/configuration/... ./contracts/configuration/... ./platform/secrets/... ./cmd/core-runtime/... ./adapters/models/... ./adapters/research/...",
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check gen2-integration --"
],
"terminal_gap_states": [
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT",
"REQUIRES_MIGRATION_DECISION"
]
}

Some files were not shown because too many files have changed in this diff Show More