From 9b136621ef17982ae118705f9d0663dc014e5fa6 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sun, 16 Aug 2026 15:22:49 +0300 Subject: [PATCH] feat: implement Foundation V1 walking skeleton --- .editorconfig | 14 + .env.example | 19 + .gitea/workflows/ci.yml | 21 + .gitignore | 32 + AGENTS.md | 73 ++ Makefile | 15 + PROJECT_MANIFEST.json | 12 + PROJECT_TREE.txt | 202 +++++ README.md | 52 ++ START_HERE_CODEX.md | 29 + adapters/models/deterministic.go | 33 + adapters/models/doc.go | 3 + adapters/nats/doc.go | 3 + adapters/portainer/doc.go | 3 + adapters/postgres/authority_repository.go | 29 + adapters/postgres/capability_repository.go | 58 ++ adapters/postgres/commit_repository.go | 27 + adapters/postgres/continuity_repository.go | 37 + adapters/postgres/doc.go | 3 + adapters/postgres/identity_repository.go | 28 + adapters/postgres/situation_repository.go | 41 + adapters/postgres/work_repository.go | 41 + adapters/temporal/doc.go | 3 + adapters/testexecutor/executor.go | 32 + architecture/ARCHITECTURE_CONSTITUTION.md | 76 ++ architecture/CORE_SEMANTIC_MODEL_V1.md | 164 ++++ architecture/DECISION_CONSOLIDATION_V1.md | 87 ++ architecture/README.md | 7 + architecture/UNRESOLVED_QUESTIONS.md | 33 + architecture/WALKING_SKELETON_SPEC.md | 132 +++ ...R-0001-extractable-modular-architecture.md | 14 + ...0002-codex-is-implementation-capability.md | 7 + architecture/adrs/README.md | 5 + architecture/architecture-registry.json | 283 ++++++ architecture/architecture-registry.md | 169 ++++ architecture/architecture-registry.yaml | 150 ++++ .../reference/FOUNDATION_FREEZE_PACK_V1.docx | Bin 0 -> 57298 bytes .../reference/FOUNDATION_FREEZE_PACK_V1.md | 802 ++++++++++++++++++ ci/architecture/README.md | 3 + ci/architecture/protected-paths.txt | 6 + cmd/cognitive-worker/main.go | 34 + cmd/commit-runtime/main.go | 131 +++ cmd/continuity-runtime/main.go | 51 ++ cmd/core-runtime/main.go | 222 +++++ cmd/devctl/architecture_test.go | 70 ++ cmd/devctl/main.go | 244 ++++++ contracts/artifact/doc.go | 3 + contracts/artifact/types.go | 5 + contracts/authority/doc.go | 3 + contracts/authority/types.go | 40 + contracts/capability/doc.go | 3 + contracts/capability/types.go | 39 + contracts/commit/doc.go | 3 + contracts/commit/types.go | 51 ++ contracts/continuity/doc.go | 3 + contracts/continuity/types.go | 38 + contracts/epistemics/doc.go | 3 + contracts/epistemics/types.go | 7 + contracts/identity/doc.go | 3 + contracts/identity/session.go | 7 + contracts/identity/types.go | 15 + contracts/situation/doc.go | 3 + contracts/situation/types.go | 32 + contracts/work/doc.go | 3 + contracts/work/types.go | 19 + database/README.md | 5 + database/bootstrap/001_roles_and_schemas.sql | 146 ++++ .../001_bootstrap_creator_authority.sql | 16 + .../002_continuity_prospective_items.sql | 16 + .../003_situation_focus_bindings.sql | 15 + database/migrations/004_work_tasks.sql | 5 + database/migrations/005_commit_actions.sql | 6 + database/migrations/006_capability_health.sql | 12 + .../migrations/007_capability_health_v2.sql | 18 + database/migrations/artifact/README.md | 3 + database/migrations/authority/README.md | 3 + database/migrations/capability/README.md | 3 + database/migrations/commit/README.md | 3 + database/migrations/continuity/README.md | 3 + database/migrations/epistemics/README.md | 3 + database/migrations/identity/README.md | 3 + database/migrations/situation/README.md | 3 + database/migrations/work/README.md | 3 + deploy/PORTAINER.md | 14 + deploy/docker-compose.dev.yml | 95 +++ deploy/docker-compose.prod.yml | 86 ++ deploy/images/002_owner_passwords.sh | 23 + deploy/images/postgres.Dockerfile | 5 + deploy/images/runtime.Dockerfile | 13 + dev/README.md | 5 + dev/task-packet.schema.json | 79 ++ .../DEV-000001-bootstrap-guardrails.json | 32 + ...0002-walking-skeleton-focus-isolation.json | 17 + .../DEV-000003-production-docker-stack.json | 23 + ...EV-000004-bootstrap-creator-authority.json | 23 + dev/tasks/DEV-000005-safe-action-commit.json | 23 + ...DEV-000006-durable-prospective-memory.json | 17 + ...V-000007-deterministic-cognitive-fake.json | 15 + .../DEV-000008-continuity-postgres-api.json | 15 + ...V-000009-situation-focus-postgres-api.json | 15 + .../DEV-000010-work-task-postgres-api.json | 1 + .../DEV-000011-focus-guarded-task-commit.json | 1 + dev/tasks/DEV-000012-commit-postgres-api.json | 1 + .../DEV-000013-topology-equivalence.json | 1 + ...DEV-000014-no-llm-degraded-capability.json | 36 + ...-000015-capability-semantic-evolution.json | 32 + .../DEV-000016-owner-acl-negative-test.json | 15 + ...-runtime-authorized-action-validation.json | 15 + .../DEV-000018-foundation-exit-audit.json | 1 + .../DEV-000019-creator-authentication.json | 1 + ...V-000020-test-executor-reconciliation.json | 15 + ...-architecture-sentinel-negative-tests.json | 15 + ...V-000022-remove-commit-identity-drift.json | 15 + .../DEV-000023-remote-topology-harness.json | 15 + development/CODEX_DEVELOPMENT_CONTRACT.md | 109 +++ development/CREDENTIAL_POLICY.md | 17 + development/README.md | 7 + docs/CREDENTIALS_AND_SECRETS.md | 22 + docs/DEPLOYMENT.md | 17 + docs/FOUNDATION_EXIT_AUDIT.md | 28 + docs/LOCAL_PATH_AND_RESOURCES.md | 11 + docs/MIGRATIONS.md | 9 + go.mod | 12 + go.sum | 19 + modules/artifact/README.arch.md | 7 + modules/artifact/application/doc.go | 3 + modules/artifact/domain/doc.go | 3 + modules/artifact/module.go | 4 + modules/artifact/ports/doc.go | 3 + modules/authority/README.arch.md | 7 + modules/authority/application/doc.go | 3 + modules/authority/domain/creator.go | 22 + modules/authority/domain/doc.go | 3 + modules/authority/domain/grant.go | 19 + modules/authority/module.go | 4 + modules/authority/ports/doc.go | 3 + modules/capability/README.arch.md | 7 + modules/capability/application/doc.go | 3 + modules/capability/domain/doc.go | 3 + modules/capability/domain/health.go | 41 + modules/capability/module.go | 4 + modules/capability/ports/doc.go | 3 + modules/capability/ports/repository.go | 13 + modules/commit/README.arch.md | 7 + modules/commit/application/doc.go | 3 + modules/commit/domain/commit.go | 78 ++ modules/commit/domain/doc.go | 3 + modules/commit/module.go | 4 + modules/commit/ports/doc.go | 3 + modules/commit/ports/executor.go | 27 + modules/continuity/README.arch.md | 7 + modules/continuity/application/doc.go | 3 + modules/continuity/domain/doc.go | 3 + modules/continuity/domain/prospective.go | 90 ++ modules/continuity/module.go | 4 + modules/continuity/ports/doc.go | 3 + modules/continuity/ports/repository.go | 12 + modules/epistemics/README.arch.md | 7 + modules/epistemics/application/doc.go | 3 + modules/epistemics/domain/doc.go | 3 + modules/epistemics/module.go | 4 + modules/epistemics/ports/doc.go | 3 + modules/identity/README.arch.md | 7 + modules/identity/application/doc.go | 3 + modules/identity/domain/creator.go | 17 + modules/identity/domain/doc.go | 3 + modules/identity/domain/session.go | 54 ++ modules/identity/module.go | 4 + modules/identity/ports/doc.go | 3 + modules/situation/README.arch.md | 7 + modules/situation/application/doc.go | 3 + modules/situation/domain/doc.go | 3 + modules/situation/domain/focus.go | 60 ++ modules/situation/module.go | 4 + modules/situation/ports/doc.go | 3 + modules/work/README.arch.md | 7 + modules/work/application/doc.go | 3 + modules/work/domain/doc.go | 3 + modules/work/domain/task.go | 51 ++ modules/work/module.go | 4 + modules/work/ports/doc.go | 3 + ops/RUNBOOK.md | 8 + platform/README.md | 3 + platform/clock/clock.go | 9 + platform/errors/errors.go | 22 + platform/ids/ids.go | 19 + runtime/health/server.go | 22 + scripts/bootstrap.ps1 | 23 + scripts/bootstrap.sh | 6 + security/README.md | 5 + tests/action_commit/safe_commit_test.go | 72 ++ .../test_executor_reconciliation_test.go | 45 + tests/architecture/registry_test.go | 21 + tests/cognitive_fake/deterministic_test.go | 24 + tests/creator_auth/session_test.go | 44 + .../creator_authority_test.go | 33 + .../capability_health_test.go | 39 + tests/owner_acl/verify.ps1 | 25 + tests/prospective_memory/recovery_test.go | 39 + .../capability_health_v2_test.go | 32 + tests/topology_equivalence/commit_test.go | 23 + tests/topology_equivalence/focus_test.go | 36 + tests/topology_equivalence/verify-remote.ps1 | 81 ++ tests/walking_skeleton/README.md | 5 + .../walking_skeleton/focus_isolation_test.go | 84 ++ 205 files changed, 6345 insertions(+) create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 Makefile create mode 100644 PROJECT_MANIFEST.json create mode 100644 PROJECT_TREE.txt create mode 100644 README.md create mode 100644 START_HERE_CODEX.md create mode 100644 adapters/models/deterministic.go create mode 100644 adapters/models/doc.go create mode 100644 adapters/nats/doc.go create mode 100644 adapters/portainer/doc.go create mode 100644 adapters/postgres/authority_repository.go create mode 100644 adapters/postgres/capability_repository.go create mode 100644 adapters/postgres/commit_repository.go create mode 100644 adapters/postgres/continuity_repository.go create mode 100644 adapters/postgres/doc.go create mode 100644 adapters/postgres/identity_repository.go create mode 100644 adapters/postgres/situation_repository.go create mode 100644 adapters/postgres/work_repository.go create mode 100644 adapters/temporal/doc.go create mode 100644 adapters/testexecutor/executor.go create mode 100644 architecture/ARCHITECTURE_CONSTITUTION.md create mode 100644 architecture/CORE_SEMANTIC_MODEL_V1.md create mode 100644 architecture/DECISION_CONSOLIDATION_V1.md create mode 100644 architecture/README.md create mode 100644 architecture/UNRESOLVED_QUESTIONS.md create mode 100644 architecture/WALKING_SKELETON_SPEC.md create mode 100644 architecture/adrs/ADR-0001-extractable-modular-architecture.md create mode 100644 architecture/adrs/ADR-0002-codex-is-implementation-capability.md create mode 100644 architecture/adrs/README.md create mode 100644 architecture/architecture-registry.json create mode 100644 architecture/architecture-registry.md create mode 100644 architecture/architecture-registry.yaml create mode 100644 architecture/reference/FOUNDATION_FREEZE_PACK_V1.docx create mode 100644 architecture/reference/FOUNDATION_FREEZE_PACK_V1.md create mode 100644 ci/architecture/README.md create mode 100644 ci/architecture/protected-paths.txt create mode 100644 cmd/cognitive-worker/main.go create mode 100644 cmd/commit-runtime/main.go create mode 100644 cmd/continuity-runtime/main.go create mode 100644 cmd/core-runtime/main.go create mode 100644 cmd/devctl/architecture_test.go create mode 100644 cmd/devctl/main.go create mode 100644 contracts/artifact/doc.go create mode 100644 contracts/artifact/types.go create mode 100644 contracts/authority/doc.go create mode 100644 contracts/authority/types.go create mode 100644 contracts/capability/doc.go create mode 100644 contracts/capability/types.go create mode 100644 contracts/commit/doc.go create mode 100644 contracts/commit/types.go create mode 100644 contracts/continuity/doc.go create mode 100644 contracts/continuity/types.go create mode 100644 contracts/epistemics/doc.go create mode 100644 contracts/epistemics/types.go create mode 100644 contracts/identity/doc.go create mode 100644 contracts/identity/session.go create mode 100644 contracts/identity/types.go create mode 100644 contracts/situation/doc.go create mode 100644 contracts/situation/types.go create mode 100644 contracts/work/doc.go create mode 100644 contracts/work/types.go create mode 100644 database/README.md create mode 100644 database/bootstrap/001_roles_and_schemas.sql create mode 100644 database/migrations/001_bootstrap_creator_authority.sql create mode 100644 database/migrations/002_continuity_prospective_items.sql create mode 100644 database/migrations/003_situation_focus_bindings.sql create mode 100644 database/migrations/004_work_tasks.sql create mode 100644 database/migrations/005_commit_actions.sql create mode 100644 database/migrations/006_capability_health.sql create mode 100644 database/migrations/007_capability_health_v2.sql create mode 100644 database/migrations/artifact/README.md create mode 100644 database/migrations/authority/README.md create mode 100644 database/migrations/capability/README.md create mode 100644 database/migrations/commit/README.md create mode 100644 database/migrations/continuity/README.md create mode 100644 database/migrations/epistemics/README.md create mode 100644 database/migrations/identity/README.md create mode 100644 database/migrations/situation/README.md create mode 100644 database/migrations/work/README.md create mode 100644 deploy/PORTAINER.md create mode 100644 deploy/docker-compose.dev.yml create mode 100644 deploy/docker-compose.prod.yml create mode 100644 deploy/images/002_owner_passwords.sh create mode 100644 deploy/images/postgres.Dockerfile create mode 100644 deploy/images/runtime.Dockerfile create mode 100644 dev/README.md create mode 100644 dev/task-packet.schema.json create mode 100644 dev/tasks/DEV-000001-bootstrap-guardrails.json create mode 100644 dev/tasks/DEV-000002-walking-skeleton-focus-isolation.json create mode 100644 dev/tasks/DEV-000003-production-docker-stack.json create mode 100644 dev/tasks/DEV-000004-bootstrap-creator-authority.json create mode 100644 dev/tasks/DEV-000005-safe-action-commit.json create mode 100644 dev/tasks/DEV-000006-durable-prospective-memory.json create mode 100644 dev/tasks/DEV-000007-deterministic-cognitive-fake.json create mode 100644 dev/tasks/DEV-000008-continuity-postgres-api.json create mode 100644 dev/tasks/DEV-000009-situation-focus-postgres-api.json create mode 100644 dev/tasks/DEV-000010-work-task-postgres-api.json create mode 100644 dev/tasks/DEV-000011-focus-guarded-task-commit.json create mode 100644 dev/tasks/DEV-000012-commit-postgres-api.json create mode 100644 dev/tasks/DEV-000013-topology-equivalence.json create mode 100644 dev/tasks/DEV-000014-no-llm-degraded-capability.json create mode 100644 dev/tasks/DEV-000015-capability-semantic-evolution.json create mode 100644 dev/tasks/DEV-000016-owner-acl-negative-test.json create mode 100644 dev/tasks/DEV-000017-runtime-authorized-action-validation.json create mode 100644 dev/tasks/DEV-000018-foundation-exit-audit.json create mode 100644 dev/tasks/DEV-000019-creator-authentication.json create mode 100644 dev/tasks/DEV-000020-test-executor-reconciliation.json create mode 100644 dev/tasks/DEV-000021-architecture-sentinel-negative-tests.json create mode 100644 dev/tasks/DEV-000022-remove-commit-identity-drift.json create mode 100644 dev/tasks/DEV-000023-remote-topology-harness.json create mode 100644 development/CODEX_DEVELOPMENT_CONTRACT.md create mode 100644 development/CREDENTIAL_POLICY.md create mode 100644 development/README.md create mode 100644 docs/CREDENTIALS_AND_SECRETS.md create mode 100644 docs/DEPLOYMENT.md create mode 100644 docs/FOUNDATION_EXIT_AUDIT.md create mode 100644 docs/LOCAL_PATH_AND_RESOURCES.md create mode 100644 docs/MIGRATIONS.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 modules/artifact/README.arch.md create mode 100644 modules/artifact/application/doc.go create mode 100644 modules/artifact/domain/doc.go create mode 100644 modules/artifact/module.go create mode 100644 modules/artifact/ports/doc.go create mode 100644 modules/authority/README.arch.md create mode 100644 modules/authority/application/doc.go create mode 100644 modules/authority/domain/creator.go create mode 100644 modules/authority/domain/doc.go create mode 100644 modules/authority/domain/grant.go create mode 100644 modules/authority/module.go create mode 100644 modules/authority/ports/doc.go create mode 100644 modules/capability/README.arch.md create mode 100644 modules/capability/application/doc.go create mode 100644 modules/capability/domain/doc.go create mode 100644 modules/capability/domain/health.go create mode 100644 modules/capability/module.go create mode 100644 modules/capability/ports/doc.go create mode 100644 modules/capability/ports/repository.go create mode 100644 modules/commit/README.arch.md create mode 100644 modules/commit/application/doc.go create mode 100644 modules/commit/domain/commit.go create mode 100644 modules/commit/domain/doc.go create mode 100644 modules/commit/module.go create mode 100644 modules/commit/ports/doc.go create mode 100644 modules/commit/ports/executor.go create mode 100644 modules/continuity/README.arch.md create mode 100644 modules/continuity/application/doc.go create mode 100644 modules/continuity/domain/doc.go create mode 100644 modules/continuity/domain/prospective.go create mode 100644 modules/continuity/module.go create mode 100644 modules/continuity/ports/doc.go create mode 100644 modules/continuity/ports/repository.go create mode 100644 modules/epistemics/README.arch.md create mode 100644 modules/epistemics/application/doc.go create mode 100644 modules/epistemics/domain/doc.go create mode 100644 modules/epistemics/module.go create mode 100644 modules/epistemics/ports/doc.go create mode 100644 modules/identity/README.arch.md create mode 100644 modules/identity/application/doc.go create mode 100644 modules/identity/domain/creator.go create mode 100644 modules/identity/domain/doc.go create mode 100644 modules/identity/domain/session.go create mode 100644 modules/identity/module.go create mode 100644 modules/identity/ports/doc.go create mode 100644 modules/situation/README.arch.md create mode 100644 modules/situation/application/doc.go create mode 100644 modules/situation/domain/doc.go create mode 100644 modules/situation/domain/focus.go create mode 100644 modules/situation/module.go create mode 100644 modules/situation/ports/doc.go create mode 100644 modules/work/README.arch.md create mode 100644 modules/work/application/doc.go create mode 100644 modules/work/domain/doc.go create mode 100644 modules/work/domain/task.go create mode 100644 modules/work/module.go create mode 100644 modules/work/ports/doc.go create mode 100644 ops/RUNBOOK.md create mode 100644 platform/README.md create mode 100644 platform/clock/clock.go create mode 100644 platform/errors/errors.go create mode 100644 platform/ids/ids.go create mode 100644 runtime/health/server.go create mode 100644 scripts/bootstrap.ps1 create mode 100644 scripts/bootstrap.sh create mode 100644 security/README.md create mode 100644 tests/action_commit/safe_commit_test.go create mode 100644 tests/action_commit/test_executor_reconciliation_test.go create mode 100644 tests/architecture/registry_test.go create mode 100644 tests/cognitive_fake/deterministic_test.go create mode 100644 tests/creator_auth/session_test.go create mode 100644 tests/creator_authority/creator_authority_test.go create mode 100644 tests/degraded_continuity/capability_health_test.go create mode 100644 tests/owner_acl/verify.ps1 create mode 100644 tests/prospective_memory/recovery_test.go create mode 100644 tests/semantic_evolution/capability_health_v2_test.go create mode 100644 tests/topology_equivalence/commit_test.go create mode 100644 tests/topology_equivalence/focus_test.go create mode 100644 tests/topology_equivalence/verify-remote.ps1 create mode 100644 tests/walking_skeleton/README.md create mode 100644 tests/walking_skeleton/focus_isolation_test.go diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c0acfb0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.go] +indent_style = tab + +[*.{md,yml,yaml,json,sql}] +indent_style = space +indent_size = 2 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ad6c890 --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +# Local development only. Copy to .env.local; never commit real secrets. +ASSISTANT_ENV=development +ASSISTANT_LOG_LEVEL=info + +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_DB=assistant +POSTGRES_SUPERUSER=assistant_bootstrap +POSTGRES_SUPERUSER_PASSWORD=CHANGE_ME_LOCAL_ONLY + +NATS_URL=nats://localhost:4222 + +# Git / deployment targets (no passwords here) +GIT_REMOTE=https://git.cin.su/m/Assistant.git +PORTAINER_URL=https://docker.cin.su +# Prefer API key stored in OS/env secret store, never Git. +PORTAINER_API_KEY= +PORTAINER_ENDPOINT_ID= +PORTAINER_STACK_NAME=assistant diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..b9f3b82 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,21 @@ +name: CI +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.26.6' + - name: Format check + run: test -z "$(gofmt -l .)" + - name: Architecture Sentinel + run: go run ./cmd/devctl check-architecture + - name: Test + run: go test ./... + - name: Vet + run: go vet ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48574f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# secrets/local overrides +.env +.env.* +!.env.example +!*.env.example +secrets/ +*.pem +*.key +*.p12 +*.pfx + +# Go +/bin/ +/dist/ +*.test +*.out +coverage.* + +# IDE/OS +.idea/ +.vscode/ +.DS_Store +Thumbs.db + +# runtime data +.data/ +data/ +logs/ +tmp/ + +# dev task generated artifacts +.devctl/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c11e527 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,73 @@ +# AGENTS.md — Codex operating contract + +You are implementing this repository under a frozen architecture generation. **Capability to edit files is not authority to change architecture.** + +## Mandatory startup sequence + +Before any non-trivial change: + +1. Read `architecture/ARCHITECTURE_CONSTITUTION.md`. +2. Read `architecture/CORE_SEMANTIC_MODEL_V1.md`. +3. Read `architecture/architecture-registry.yaml` and `architecture/architecture-registry.json`. +4. Read `development/CODEX_DEVELOPMENT_CONTRACT.md`. +5. Read the active file under `dev/tasks/`. +6. Run `go run ./cmd/devctl preflight -task ` for Tier B+ tasks when available. + +If documents conflict, authority order is: + +`Architecture Constitution > accepted ADR > Architecture Registry > Core Semantic Model > module architecture file > task packet > examples/comments`. + +Do not silently resolve a conflict. Return `BLOCKED_BY_SPEC_GAP` or `BLOCKED_BY_ARCHITECTURE_GAP`. + +## Hard prohibitions + +- NO cross-module direct DB access. +- NO cross-owner SQL JOIN/FK/view/trigger by default. +- NO direct external mutation outside Commit/Executor boundary. +- NO model/provider call outside cognitive adapter/gateway boundaries. +- NO semantic durable timer implemented as `time.After`, unmanaged goroutine or in-memory queue. +- NO cross-module correctness depending on shared mutex, pointer, filesystem or process-local transaction. +- NO giant `common`, `shared`, generic `Entity`, generic `Status`, generic `Manager`, `ServiceLocator`, or `map[string]any` as a substitute for Core semantics. +- NO provider/runtime types in domain packages. +- NO weakening tests, architecture lints, DB ACLs, or guardrails to make a task pass. +- NO new third-party dependency in an ordinary feature task. +- NO protected-path modification unless the task explicitly grants Tier E authority. +- NO reading/copying literal passwords from other repositories into this repository. +- NO production secret in source, task packet, commit message, logs or tests. + +## Codex freedom + +Inside the declared owner/module and allowed paths, choose ordinary local implementation details freely. Prefer explicit, typed, boring Go over reflection, metaprogramming or framework tricks. + +Semantic duplication is preferable to false abstraction. Shared technical mechanisms may live under `platform/`; shared domain meaning may not. + +## When blocked + +Valid terminal states include: + +- `BLOCKED_BY_SPEC_GAP` +- `BLOCKED_BY_ARCHITECTURE_GAP` +- `BLOCKED_BY_MISSING_CAPABILITY` +- `BLOCKED_BY_CONFLICT` +- `REQUIRES_MIGRATION_DECISION` + +Never “make it work somehow” by violating a boundary. + +## Git and deployment + +- Canonical remote: `https://git.cin.su/m/Assistant.git`. +- Target deployment control plane: `https://docker.cin.su` (Portainer). +- Use configured credential helpers first; never echo secrets. +- If push/deploy credentials are unavailable, ask the user for the minimum needed access. +- Ordinary Codex work happens on a branch/worktree, never directly on protected `main`. + +## Required completion report + +State: +- task ID/revision; +- changed paths/modules; +- contracts/schemas/events/dependencies/privileges touched; +- tests actually executed; +- assumptions; +- findings not changed; +- gaps or limitations. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5dabb28 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +.PHONY: check test vet dev-up dev-down +check: + go run ./cmd/devctl check-architecture + +test: + go test ./... + +vet: + go vet ./... + +dev-up: + docker compose -f deploy/docker-compose.dev.yml up -d --build + +dev-down: + docker compose -f deploy/docker-compose.dev.yml down diff --git a/PROJECT_MANIFEST.json b/PROJECT_MANIFEST.json new file mode 100644 index 0000000..e6c72ed --- /dev/null +++ b/PROJECT_MANIFEST.json @@ -0,0 +1,12 @@ +{ + "project": "Assistant", + "local_expected_path": "Z:\\mst\\codex\\assistent", + "git_remote": "https://git.cin.su/m/Assistant.git", + "deployment_control_plane": "https://docker.cin.su", + "deployment_kind": "Portainer", + "semantic_generation": 1, + "architecture_generation": 1, + "go_baseline": "1.26.x", + "postgres_baseline": "18.6", + "first_task": "dev/tasks/DEV-000001-bootstrap-guardrails.json" +} diff --git a/PROJECT_TREE.txt b/PROJECT_TREE.txt new file mode 100644 index 0000000..4b5bc9c --- /dev/null +++ b/PROJECT_TREE.txt @@ -0,0 +1,202 @@ +.env.example +adapters/models/deterministic.go +adapters/models/doc.go +adapters/nats/doc.go +adapters/portainer/doc.go +adapters/postgres/authority_repository.go +adapters/postgres/capability_repository.go +adapters/postgres/commit_repository.go +adapters/postgres/continuity_repository.go +adapters/postgres/doc.go +adapters/postgres/identity_repository.go +adapters/postgres/situation_repository.go +adapters/postgres/work_repository.go +adapters/temporal/doc.go +adapters/testexecutor/executor.go +AGENTS.md +architecture/adrs/ADR-0001-extractable-modular-architecture.md +architecture/adrs/ADR-0002-codex-is-implementation-capability.md +architecture/adrs/README.md +architecture/ARCHITECTURE_CONSTITUTION.md +architecture/architecture-registry.json +architecture/architecture-registry.md +architecture/architecture-registry.yaml +architecture/CORE_SEMANTIC_MODEL_V1.md +architecture/DECISION_CONSOLIDATION_V1.md +architecture/README.md +architecture/reference/FOUNDATION_FREEZE_PACK_V1.docx +architecture/reference/FOUNDATION_FREEZE_PACK_V1.md +architecture/UNRESOLVED_QUESTIONS.md +architecture/WALKING_SKELETON_SPEC.md +ci/architecture/protected-paths.txt +ci/architecture/README.md +cmd/cognitive-worker/main.go +cmd/commit-runtime/main.go +cmd/continuity-runtime/main.go +cmd/core-runtime/main.go +cmd/devctl/architecture_test.go +cmd/devctl/main.go +contracts/artifact/doc.go +contracts/artifact/types.go +contracts/authority/doc.go +contracts/authority/types.go +contracts/capability/doc.go +contracts/capability/types.go +contracts/commit/doc.go +contracts/commit/types.go +contracts/continuity/doc.go +contracts/continuity/types.go +contracts/epistemics/doc.go +contracts/epistemics/types.go +contracts/identity/doc.go +contracts/identity/session.go +contracts/identity/types.go +contracts/situation/doc.go +contracts/situation/types.go +contracts/work/doc.go +contracts/work/types.go +database/bootstrap/001_roles_and_schemas.sql +database/migrations/001_bootstrap_creator_authority.sql +database/migrations/002_continuity_prospective_items.sql +database/migrations/003_situation_focus_bindings.sql +database/migrations/004_work_tasks.sql +database/migrations/005_commit_actions.sql +database/migrations/006_capability_health.sql +database/migrations/007_capability_health_v2.sql +database/migrations/artifact/README.md +database/migrations/authority/README.md +database/migrations/capability/README.md +database/migrations/commit/README.md +database/migrations/continuity/README.md +database/migrations/epistemics/README.md +database/migrations/identity/README.md +database/migrations/situation/README.md +database/migrations/work/README.md +database/README.md +deploy/docker-compose.dev.yml +deploy/docker-compose.prod.yml +deploy/images/002_owner_passwords.sh +deploy/images/postgres.Dockerfile +deploy/images/runtime.Dockerfile +deploy/PORTAINER.md +dev/README.md +dev/task-packet.schema.json +dev/tasks/DEV-000001-bootstrap-guardrails.json +dev/tasks/DEV-000002-walking-skeleton-focus-isolation.json +dev/tasks/DEV-000003-production-docker-stack.json +dev/tasks/DEV-000004-bootstrap-creator-authority.json +dev/tasks/DEV-000005-safe-action-commit.json +dev/tasks/DEV-000006-durable-prospective-memory.json +dev/tasks/DEV-000007-deterministic-cognitive-fake.json +dev/tasks/DEV-000008-continuity-postgres-api.json +dev/tasks/DEV-000009-situation-focus-postgres-api.json +dev/tasks/DEV-000010-work-task-postgres-api.json +dev/tasks/DEV-000011-focus-guarded-task-commit.json +dev/tasks/DEV-000012-commit-postgres-api.json +dev/tasks/DEV-000013-topology-equivalence.json +dev/tasks/DEV-000014-no-llm-degraded-capability.json +dev/tasks/DEV-000015-capability-semantic-evolution.json +dev/tasks/DEV-000016-owner-acl-negative-test.json +dev/tasks/DEV-000017-runtime-authorized-action-validation.json +dev/tasks/DEV-000018-foundation-exit-audit.json +dev/tasks/DEV-000019-creator-authentication.json +dev/tasks/DEV-000020-test-executor-reconciliation.json +dev/tasks/DEV-000021-architecture-sentinel-negative-tests.json +dev/tasks/DEV-000022-remove-commit-identity-drift.json +dev/tasks/DEV-000023-remote-topology-harness.json +development/CODEX_DEVELOPMENT_CONTRACT.md +development/CREDENTIAL_POLICY.md +development/README.md +docs/CREDENTIALS_AND_SECRETS.md +docs/DEPLOYMENT.md +docs/FOUNDATION_EXIT_AUDIT.md +docs/LOCAL_PATH_AND_RESOURCES.md +docs/MIGRATIONS.md +go.mod +go.sum +Makefile +modules/artifact/application/doc.go +modules/artifact/domain/doc.go +modules/artifact/module.go +modules/artifact/ports/doc.go +modules/artifact/README.arch.md +modules/authority/application/doc.go +modules/authority/domain/creator.go +modules/authority/domain/doc.go +modules/authority/domain/grant.go +modules/authority/module.go +modules/authority/ports/doc.go +modules/authority/README.arch.md +modules/capability/application/doc.go +modules/capability/domain/doc.go +modules/capability/domain/health.go +modules/capability/module.go +modules/capability/ports/doc.go +modules/capability/ports/repository.go +modules/capability/README.arch.md +modules/commit/application/doc.go +modules/commit/domain/commit.go +modules/commit/domain/doc.go +modules/commit/module.go +modules/commit/ports/doc.go +modules/commit/ports/executor.go +modules/commit/README.arch.md +modules/continuity/application/doc.go +modules/continuity/domain/doc.go +modules/continuity/domain/prospective.go +modules/continuity/module.go +modules/continuity/ports/doc.go +modules/continuity/ports/repository.go +modules/continuity/README.arch.md +modules/epistemics/application/doc.go +modules/epistemics/domain/doc.go +modules/epistemics/module.go +modules/epistemics/ports/doc.go +modules/epistemics/README.arch.md +modules/identity/application/doc.go +modules/identity/domain/creator.go +modules/identity/domain/doc.go +modules/identity/domain/session.go +modules/identity/module.go +modules/identity/ports/doc.go +modules/identity/README.arch.md +modules/situation/application/doc.go +modules/situation/domain/doc.go +modules/situation/domain/focus.go +modules/situation/module.go +modules/situation/ports/doc.go +modules/situation/README.arch.md +modules/work/application/doc.go +modules/work/domain/doc.go +modules/work/domain/task.go +modules/work/module.go +modules/work/ports/doc.go +modules/work/README.arch.md +ops/RUNBOOK.md +platform/clock/clock.go +platform/errors/errors.go +platform/ids/ids.go +platform/README.md +PROJECT_MANIFEST.json +PROJECT_TREE.txt +README.md +runtime/health/server.go +scripts/bootstrap.ps1 +scripts/bootstrap.sh +security/README.md +START_HERE_CODEX.md +tests/action_commit/safe_commit_test.go +tests/action_commit/test_executor_reconciliation_test.go +tests/architecture/registry_test.go +tests/cognitive_fake/deterministic_test.go +tests/creator_auth/session_test.go +tests/creator_authority/creator_authority_test.go +tests/degraded_continuity/capability_health_test.go +tests/owner_acl/verify.ps1 +tests/prospective_memory/recovery_test.go +tests/semantic_evolution/capability_health_v2_test.go +tests/topology_equivalence/commit_test.go +tests/topology_equivalence/focus_test.go +tests/topology_equivalence/verify-remote.ps1 +tests/walking_skeleton/focus_isolation_test.go +tests/walking_skeleton/README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..8998611 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# Assistant + +Long-lived, evolvable personal/work assistant. This repository is intentionally designed for implementation primarily through Codex while preserving strict semantic, authority, storage and deployment boundaries. + +## Fixed project coordinates + +- **Expected local project folder:** `Z:\mst\codex\assistent` +- **Git remote:** `https://git.cin.su/m/Assistant.git` +- **Deployment control plane / Portainer:** `https://docker.cin.su` +- **Go baseline:** 1.26.x +- **PostgreSQL baseline:** 18.6 +- **Semantic generation:** 1 +- **Architecture generation:** 1 + +The repository at `git.cin.su/m/Assistant` was externally reachable and empty when this bootstrap pack was created. `docker.cin.su` was externally reachable as Portainer. + +## First rule + +Read `AGENTS.md`, then `architecture/ARCHITECTURE_CONSTITUTION.md`, then the current task packet under `dev/tasks/` **before editing code**. + +## Bootstrap on Windows + +```powershell +Set-Location Z:\mst\codex\assistent +powershell -ExecutionPolicy Bypass -File .\scripts\bootstrap.ps1 +``` + +Then: + +```powershell +go run .\cmd\devctl doctor +go run .\cmd\devctl check-architecture +go test ./... +docker compose -f deploy/docker-compose.dev.yml up -d --build +``` + +## Credentials + +Do **not** commit passwords, API keys, tokens or copied credentials from sibling projects. + +Codex should try, in this order: + +1. existing OS/Git/Docker credential helpers without printing the secret; +2. existing environment variables / secret stores already configured for this machine; +3. inspect sibling projects only for non-secret deployment conventions, endpoint names and environment-variable names; +4. if actual credentials are still unavailable, ask the user. + +See `docs/CREDENTIALS_AND_SECRETS.md`. + +## V1 non-goals + +V1 must not implement autonomous self-modification, global collective learning, a universal ontology engine, Kubernetes as a requirement, custom consensus, autonomous plugin installation, full robotics/control-loop infrastructure, or a multi-user federation platform. Preserve extension paths; do not materialize speculative systems. diff --git a/START_HERE_CODEX.md b/START_HERE_CODEX.md new file mode 100644 index 0000000..9540d70 --- /dev/null +++ b/START_HERE_CODEX.md @@ -0,0 +1,29 @@ +# START HERE — CODEX + +This folder is intended to be copied/extracted to: + +`Z:\mst\codex\assistent` + +Do not begin feature implementation immediately. + +## Step 1 +Read `AGENTS.md` and the architecture authority files it references. + +## Step 2 +Run Windows bootstrap if applicable: + +```powershell +powershell -ExecutionPolicy Bypass -File .\scripts\bootstrap.ps1 +``` + +## Step 3 +Execute task packet `dev/tasks/DEV-000001-bootstrap-guardrails.json` only. + +The first task validates the guardrail factory itself. Do not start the Assistant semantic features before this passes. + +## Access + +Git remote is already known: `https://git.cin.su/m/Assistant.git`. +Deployment target is Portainer: `https://docker.cin.su`. + +Use existing Git/Docker/OS credential helpers if already configured. Never reveal them. If write/deploy access is absent, request the minimum credentials from the user rather than mining plaintext secrets from other project trees. diff --git a/adapters/models/deterministic.go b/adapters/models/deterministic.go new file mode 100644 index 0000000..b38d1f0 --- /dev/null +++ b/adapters/models/deterministic.go @@ -0,0 +1,33 @@ +package models + +import ( + "crypto/sha256" + "encoding/hex" + + "git.cin.su/m/Assistant/contracts/capability" + commitcontract "git.cin.su/m/Assistant/contracts/commit" + "git.cin.su/m/Assistant/contracts/identity" + "git.cin.su/m/Assistant/contracts/work" + "git.cin.su/m/Assistant/platform/ids" +) + +// DeterministicRequest is already target-bound. The adapter performs no text +// routing and emits a candidate only; Commit remains the execution boundary. +type DeterministicRequest struct { + ActorID identity.ActorID + CapabilityID capability.CapabilityID + Target work.TaskTarget + Operation commitcontract.ExternalOperation +} + +type Candidate struct { + Proposal commitcontract.ActionProposal + Seed string +} + +func Propose(request DeterministicRequest) Candidate { + seed := string(request.ActorID) + "|" + string(request.CapabilityID) + "|" + string(request.Target.TaskID) + "|" + string(request.Target.GoalID) + "|" + string(request.Target.ScopeID) + "|" + string(request.Operation) + digest := sha256.Sum256([]byte(seed)) + intent := commitcontract.ActionIntentID(ids.ID(hex.EncodeToString(digest[:16]))) + return Candidate{Seed: hex.EncodeToString(digest[:]), Proposal: commitcontract.ActionProposal{IntentID: intent, ActorID: request.ActorID, CapabilityID: request.CapabilityID, Target: request.Target, Operation: request.Operation, State: commitcontract.ActionProposed}} +} diff --git a/adapters/models/doc.go b/adapters/models/doc.go new file mode 100644 index 0000000..31ace75 --- /dev/null +++ b/adapters/models/doc.go @@ -0,0 +1,3 @@ +package models + +// Package models: Cognitive provider adapters. Provider output is candidate/proposal, never direct canonical truth/action. diff --git a/adapters/nats/doc.go b/adapters/nats/doc.go new file mode 100644 index 0000000..818b88e --- /dev/null +++ b/adapters/nats/doc.go @@ -0,0 +1,3 @@ +package nats + +// Package nats: NATS integration transport adapter. NATS is not canonical event history. diff --git a/adapters/portainer/doc.go b/adapters/portainer/doc.go new file mode 100644 index 0000000..ec957f7 --- /dev/null +++ b/adapters/portainer/doc.go @@ -0,0 +1,3 @@ +package portainer + +// Package portainer: Deployment adapter/tooling only; never imported by semantic domain modules. diff --git a/adapters/postgres/authority_repository.go b/adapters/postgres/authority_repository.go new file mode 100644 index 0000000..5ca1022 --- /dev/null +++ b/adapters/postgres/authority_repository.go @@ -0,0 +1,29 @@ +package postgres + +import ( + "context" + "fmt" + + "git.cin.su/m/Assistant/contracts/authority" + "github.com/jackc/pgx/v5/pgxpool" +) + +type AuthorityRepository struct{ pool *pgxpool.Pool } + +func OpenAuthority(ctx context.Context, c ContinuityConfig) (*AuthorityRepository, error) { + p, e := pgxpool.New(ctx, fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", c.Host, c.Port, c.Database, c.User, c.Password)) + if e != nil { + return nil, e + } + return &AuthorityRepository{p}, nil +} +func (r *AuthorityRepository) Close() { r.pool.Close() } +func (r *AuthorityRepository) LoadGrant(ctx context.Context, id authority.GrantID) (authority.AuthorityGrant, authority.Scope, error) { + var grant authority.AuthorityGrant + var scope authority.Scope + err := r.pool.QueryRow(ctx, `SELECT g.grant_id, g.actor_id, g.principal_id, g.scope_id, g.grant_class, g.revision, g.active, s.scope_id, s.global_scope, s.label FROM authority.authority_grants g JOIN authority.scopes s ON s.scope_id=g.scope_id WHERE g.grant_id=$1`, id).Scan(&grant.ID, &grant.ActorID, &grant.PrincipalID, &grant.ScopeID, &grant.Class, &grant.Revision, &grant.Active, &scope.ID, &scope.Global, &scope.Label) + if err != nil { + return authority.AuthorityGrant{}, authority.Scope{}, fmt.Errorf("load authority grant: %w", err) + } + return grant, scope, nil +} diff --git a/adapters/postgres/capability_repository.go b/adapters/postgres/capability_repository.go new file mode 100644 index 0000000..763d4a0 --- /dev/null +++ b/adapters/postgres/capability_repository.go @@ -0,0 +1,58 @@ +package postgres + +import ( + "context" + "errors" + "fmt" + + "git.cin.su/m/Assistant/contracts/capability" + "github.com/jackc/pgx/v5/pgxpool" +) + +var ErrCapabilityHealthNotFound = errors.New("capability health not found") + +type CapabilityRepository struct{ pool *pgxpool.Pool } + +func OpenCapability(ctx context.Context, config ContinuityConfig) (*CapabilityRepository, error) { + dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", config.Host, config.Port, config.Database, config.User, config.Password) + pool, err := pgxpool.New(ctx, dsn) + if err != nil { + return nil, err + } + return &CapabilityRepository{pool: pool}, nil +} + +func (r *CapabilityRepository) Close() { r.pool.Close() } + +func (r *CapabilityRepository) SaveHealth(ctx context.Context, health capability.Health) error { + _, err := r.pool.Exec(ctx, `INSERT INTO capability.health (capability_id, state, reason, revision, semantic_version, observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6) ON CONFLICT (capability_id) DO UPDATE SET state=EXCLUDED.state, reason=EXCLUDED.reason, revision=EXCLUDED.revision, semantic_version=EXCLUDED.semantic_version, observed_at_unix_millis=EXCLUDED.observed_at_unix_millis WHERE capability.health.revision < EXCLUDED.revision`, health.CapabilityID, health.State, health.Reason, health.Revision, health.SemanticVersion, health.ObservedAtUnixMillis) + return err +} + +func (r *CapabilityRepository) LoadHealth(ctx context.Context, id capability.CapabilityID) (capability.Health, error) { + var health capability.Health + err := r.pool.QueryRow(ctx, `SELECT capability_id, state, reason, revision, semantic_version, observed_at_unix_millis FROM capability.health WHERE capability_id=$1`, id).Scan(&health.CapabilityID, &health.State, &health.Reason, &health.Revision, &health.SemanticVersion, &health.ObservedAtUnixMillis) + if err != nil { + return capability.Health{}, fmt.Errorf("load capability health: %w", err) + } + return health, nil +} + +func (r *CapabilityRepository) RebuildHealthProjection(ctx context.Context) (int64, error) { + tx, err := r.pool.Begin(ctx) + if err != nil { + return 0, err + } + defer tx.Rollback(ctx) + if _, err = tx.Exec(ctx, `DELETE FROM capability.health_projection`); err != nil { + return 0, err + } + result, err := tx.Exec(ctx, `INSERT INTO capability.health_projection (capability_id, state, reason, revision, semantic_version, observed_at_unix_millis) SELECT capability_id, state, reason, revision, semantic_version, observed_at_unix_millis FROM capability.health`) + if err != nil { + return 0, err + } + if err = tx.Commit(ctx); err != nil { + return 0, err + } + return result.RowsAffected(), nil +} diff --git a/adapters/postgres/commit_repository.go b/adapters/postgres/commit_repository.go new file mode 100644 index 0000000..bde2a33 --- /dev/null +++ b/adapters/postgres/commit_repository.go @@ -0,0 +1,27 @@ +package postgres + +import ( + "context" + "fmt" + commitcontract "git.cin.su/m/Assistant/contracts/commit" + "github.com/jackc/pgx/v5/pgxpool" +) + +type CommitRepository struct{ pool *pgxpool.Pool } + +func OpenCommit(ctx context.Context, c ContinuityConfig) (*CommitRepository, error) { + p, e := pgxpool.New(ctx, fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", c.Host, c.Port, c.Database, c.User, c.Password)) + if e != nil { + return nil, e + } + return &CommitRepository{p}, nil +} +func (r *CommitRepository) Close() { r.pool.Close() } +func (r *CommitRepository) SaveAuthorized(ctx context.Context, a commitcontract.AuthorizedAction) error { + _, e := r.pool.Exec(ctx, `INSERT INTO commit.authorized_actions (authorized_action_id,action_intent_id,state,revision) VALUES ($1,$2,$3,1) ON CONFLICT (authorized_action_id) DO NOTHING`, a.ID, a.Proposal.IntentID, a.State) + return e +} +func (r *CommitRepository) SaveAttempt(ctx context.Context, a commitcontract.ActionAttempt) error { + _, e := r.pool.Exec(ctx, `INSERT INTO commit.action_attempts (action_attempt_id,authorized_action_id,action_intent_id,state) VALUES ($1,$2,$3,$4) ON CONFLICT (action_attempt_id) DO NOTHING`, a.ID, a.AuthorizedID, a.IntentID, a.State) + return e +} diff --git a/adapters/postgres/continuity_repository.go b/adapters/postgres/continuity_repository.go new file mode 100644 index 0000000..17d3d7b --- /dev/null +++ b/adapters/postgres/continuity_repository.go @@ -0,0 +1,37 @@ +package postgres + +import ( + "context" + "errors" + "fmt" + + "git.cin.su/m/Assistant/contracts/continuity" + "github.com/jackc/pgx/v5/pgxpool" +) + +var ErrProspectiveNotFound = errors.New("prospective item not found") + +type ContinuityConfig struct{ Host, Port, Database, User, Password string } +type ContinuityRepository struct{ pool *pgxpool.Pool } + +func OpenContinuity(ctx context.Context, config ContinuityConfig) (*ContinuityRepository, error) { + dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", config.Host, config.Port, config.Database, config.User, config.Password) + pool, err := pgxpool.New(ctx, dsn) + if err != nil { + return nil, err + } + return &ContinuityRepository{pool: pool}, nil +} +func (r *ContinuityRepository) Close() { r.pool.Close() } +func (r *ContinuityRepository) Save(ctx context.Context, item continuity.ProspectiveItem) error { + _, err := r.pool.Exec(ctx, `INSERT INTO continuity.prospective_items (prospective_item_id, commitment_id, owner_principal_id, scope_id, goal_id, trigger_at_unix_millis, state, revision) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT (prospective_item_id) DO UPDATE SET state=EXCLUDED.state, revision=EXCLUDED.revision`, item.ID, item.CommitmentID, item.OwnerPrincipalID, item.ScopeID, item.GoalID, item.TriggerAtUnixMillis, item.State, item.Revision) + return err +} +func (r *ContinuityRepository) Load(ctx context.Context, id continuity.ProspectiveItemID) (continuity.ProspectiveItem, error) { + var item continuity.ProspectiveItem + err := r.pool.QueryRow(ctx, `SELECT prospective_item_id, commitment_id, owner_principal_id, scope_id, goal_id, trigger_at_unix_millis, state, revision FROM continuity.prospective_items WHERE prospective_item_id=$1`, id).Scan(&item.ID, &item.CommitmentID, &item.OwnerPrincipalID, &item.ScopeID, &item.GoalID, &item.TriggerAtUnixMillis, &item.State, &item.Revision) + if err != nil { + return continuity.ProspectiveItem{}, fmt.Errorf("load prospective item: %w", err) + } + return item, nil +} diff --git a/adapters/postgres/doc.go b/adapters/postgres/doc.go new file mode 100644 index 0000000..4d81c0e --- /dev/null +++ b/adapters/postgres/doc.go @@ -0,0 +1,3 @@ +package postgres + +// Package postgres: PostgreSQL implementations of owner persistence ports. No domain meaning belongs here. diff --git a/adapters/postgres/identity_repository.go b/adapters/postgres/identity_repository.go new file mode 100644 index 0000000..5b6e689 --- /dev/null +++ b/adapters/postgres/identity_repository.go @@ -0,0 +1,28 @@ +package postgres + +import ( + "context" + "fmt" + + "git.cin.su/m/Assistant/contracts/identity" + "github.com/jackc/pgx/v5/pgxpool" +) + +type IdentityRepository struct{ pool *pgxpool.Pool } + +func OpenIdentity(ctx context.Context, config ContinuityConfig) (*IdentityRepository, error) { + pool, err := pgxpool.New(ctx, fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", config.Host, config.Port, config.Database, config.User, config.Password)) + if err != nil { + return nil, err + } + return &IdentityRepository{pool: pool}, nil +} +func (r *IdentityRepository) Close() { r.pool.Close() } +func (r *IdentityRepository) LoadBinding(ctx context.Context, actor identity.ActorID, principal identity.PrincipalID) (identity.PrincipalBinding, error) { + var binding identity.PrincipalBinding + err := r.pool.QueryRow(ctx, `SELECT actor_id, principal_id, active FROM identity.principal_bindings WHERE actor_id=$1 AND principal_id=$2`, actor, principal).Scan(&binding.ActorID, &binding.PrincipalID, &binding.Active) + if err != nil { + return identity.PrincipalBinding{}, fmt.Errorf("load identity binding: %w", err) + } + return binding, nil +} diff --git a/adapters/postgres/situation_repository.go b/adapters/postgres/situation_repository.go new file mode 100644 index 0000000..39e039f --- /dev/null +++ b/adapters/postgres/situation_repository.go @@ -0,0 +1,41 @@ +package postgres + +import ( + "context" + "fmt" + + "git.cin.su/m/Assistant/contracts/situation" + "github.com/jackc/pgx/v5/pgxpool" +) + +type SituationRepository struct{ pool *pgxpool.Pool } + +func OpenSituation(ctx context.Context, config ContinuityConfig) (*SituationRepository, error) { + dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", config.Host, config.Port, config.Database, config.User, config.Password) + pool, err := pgxpool.New(ctx, dsn) + if err != nil { + return nil, err + } + return &SituationRepository{pool: pool}, nil +} +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) + return err +} +func (r *SituationRepository) ByRuntime(ctx context.Context, runtime situation.RuntimeBinding) ([]situation.FocusBinding, error) { + rows, err := r.pool.Query(ctx, `SELECT focus_binding_id, actor_id, session_id, workspace_id, project_id, goal_id, scope_id, active FROM situation.focus_bindings WHERE session_id=$1 AND workspace_id=$2`, runtime.SessionID, runtime.Workspace) + if err != nil { + return nil, err + } + defer rows.Close() + var bindings []situation.FocusBinding + for rows.Next() { + var binding situation.FocusBinding + if err := rows.Scan(&binding.ID, &binding.ActorID, &binding.SessionID, &binding.Workspace, &binding.ProjectID, &binding.GoalID, &binding.ScopeID, &binding.Active); err != nil { + return nil, err + } + bindings = append(bindings, binding) + } + return bindings, rows.Err() +} diff --git a/adapters/postgres/work_repository.go b/adapters/postgres/work_repository.go new file mode 100644 index 0000000..800f9ac --- /dev/null +++ b/adapters/postgres/work_repository.go @@ -0,0 +1,41 @@ +package postgres + +import ( + "context" + "fmt" + + "git.cin.su/m/Assistant/contracts/work" + "github.com/jackc/pgx/v5/pgxpool" +) + +type WorkRepository struct{ pool *pgxpool.Pool } + +func OpenWork(ctx context.Context, config ContinuityConfig) (*WorkRepository, error) { + dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", config.Host, config.Port, config.Database, config.User, config.Password) + pool, err := pgxpool.New(ctx, dsn) + if err != nil { + return nil, err + } + return &WorkRepository{pool}, nil +} +func (r *WorkRepository) Close() { r.pool.Close() } +func (r *WorkRepository) Create(ctx context.Context, target work.TaskTarget) error { + _, err := r.pool.Exec(ctx, `INSERT INTO work.tasks (task_id,goal_id,scope_id,revision) VALUES ($1,$2,$3,$4) ON CONFLICT (task_id) DO NOTHING`, target.TaskID, target.GoalID, target.ScopeID, target.Revision) + return err +} +func (r *WorkRepository) Commit(ctx context.Context, target work.TaskTarget) (work.TaskRevision, error) { + var revision work.TaskRevision + err := r.pool.QueryRow(ctx, `UPDATE work.tasks SET revision=revision+1 WHERE task_id=$1 AND goal_id=$2 AND scope_id=$3 AND revision=$4 RETURNING revision`, target.TaskID, target.GoalID, target.ScopeID, target.Revision).Scan(&revision) + if err != nil { + return 0, fmt.Errorf("commit task: %w", err) + } + return revision, nil +} +func (r *WorkRepository) Load(ctx context.Context, id work.TaskID) (work.TaskTarget, error) { + var target work.TaskTarget + err := r.pool.QueryRow(ctx, `SELECT task_id, goal_id, scope_id, revision FROM work.tasks WHERE task_id=$1`, id).Scan(&target.TaskID, &target.GoalID, &target.ScopeID, &target.Revision) + if err != nil { + return work.TaskTarget{}, fmt.Errorf("load task: %w", err) + } + return target, nil +} diff --git a/adapters/temporal/doc.go b/adapters/temporal/doc.go new file mode 100644 index 0000000..ba81211 --- /dev/null +++ b/adapters/temporal/doc.go @@ -0,0 +1,3 @@ +package temporal + +// Package temporal: Optional Temporal workflow adapter. Temporal never owns the only copy of a future obligation. diff --git a/adapters/testexecutor/executor.go b/adapters/testexecutor/executor.go new file mode 100644 index 0000000..01654fc --- /dev/null +++ b/adapters/testexecutor/executor.go @@ -0,0 +1,32 @@ +package testexecutor + +import ( + commitcontract "git.cin.su/m/Assistant/contracts/commit" + "git.cin.su/m/Assistant/modules/commit/ports" +) + +// Executor is a deterministic external-system simulator. ActionIntentID is +// the idempotency identity, so replay can never create a second effect. +type Executor struct { + LoseAcknowledgement bool + effects map[commitcontract.ActionIntentID]bool +} + +func New(loseAcknowledgement bool) *Executor { + return &Executor{LoseAcknowledgement: loseAcknowledgement, effects: make(map[commitcontract.ActionIntentID]bool)} +} + +func (e *Executor) Execute(action commitcontract.AuthorizedAction) ports.ExecutionResult { + e.effects[action.Proposal.IntentID] = true + if e.LoseAcknowledgement { + return ports.ExecutionResult{Known: false} + } + return ports.ExecutionResult{Known: true, Succeeded: true} +} + +func (e *Executor) Reconcile(action commitcontract.AuthorizedAction) ports.ExecutionResult { + applied, known := e.effects[action.Proposal.IntentID] + return ports.ExecutionResult{Known: known, Succeeded: applied} +} + +func (e *Executor) EffectCount() int { return len(e.effects) } diff --git a/architecture/ARCHITECTURE_CONSTITUTION.md b/architecture/ARCHITECTURE_CONSTITUTION.md new file mode 100644 index 0000000..738842a --- /dev/null +++ b/architecture/ARCHITECTURE_CONSTITUTION.md @@ -0,0 +1,76 @@ +# 1. ARCHITECTURE_CONSTITUTION.md + +## 1.1 Назначение + +Этот документ — краткий набор законов репозитория и production-архитектуры. Он выше обычных task packets, примеров и локальных README. Codex не является архитектурным authority: он реализует только то, на что выдана task authority. + +## 1.2 Иерархия архитектурной власти + +1. Architecture Constitution. +2. Accepted ADR / Foundation Decision. +3. Machine-readable Architecture Registry. +4. Versioned semantic contracts and state machines. +5. Module architecture specification. +6. Codex Task Packet. +7. Implementation and examples. + +Если нижний уровень противоречит верхнему, нижний считается ошибочным. Существующий код является evidence of implementation, но не authority to redefine architecture. + +## 1.3 Неприкосновенные инварианты V1 + +1. LLM/model/provider не является Assistant. +2. Capability не означает Authority. +3. Observation, Claim, Belief, Prediction, Action и Outcome — разные семантические классы. +4. Model output никогда напрямую не мутирует canonical semantic state и не выполняет external mutation. +5. Любая реальная мутация проходит Proposal → semantic owner validation → Authority/Integrity checks → Commit → Executor → Outcome/Verification. +6. Любая executable activity имеет live justification path к действующей Goal, Commitment, Responsibility или Policy. +7. Отмена/суперсессия justification должна распространяться до зависимых работ перед следующим commit. +8. Canonical owner — один на конкретный mutable semantic state в конкретной semantic generation. +9. Другие модули не пишут canonical storage владельца напрямую. +10. Cross-owner SQL writes, cross-owner FK и скрытая cross-owner trigger logic запрещены по умолчанию. +11. Derived projections/caches/indexes не являются source of truth и должны быть rebuildable либо явно classified otherwise. +12. Temporal/NATS/Redis/vector index/model memory не являются единственным местом хранения commitments, authority, semantic history или future obligations. +13. Durable future obligations живут в canonical prospective state независимо от workflow/scheduler engine. +14. External side effect должен быть idempotent/reconcilable; timeout не равен failure; UNKNOWN_OUTCOME — first-class state. +15. No correctness property may depend on a shared in-process lock across bounded modules. +16. Semantic topology, process topology, storage topology и model topology независимы и меняются отдельно. +17. Co-location — deployment optimization, не архитектурная привилегия. +18. Major bounded contracts должны быть serializable и не зависеть от Go pointers, shared memory, global variables или ambient process state. +19. Core/domain не импортирует PostgreSQL/NATS/Temporal/MCP/model-provider packages. +20. Никакого giant `common/shared` domain package; technical shared mechanisms допускаются только в `platform` и не владеют domain semantics. +21. Semantic duplication предпочтительнее ложной абстракции. +22. Event-driven взаимодействие используется ради temporal decoupling/history, а не потому, что два модуля разные. +23. Public query не должен скрыто изменять canonical state. +24. Durable background responsibility не реализуется unmanaged goroutine/timer. +25. `context.Context` не переносит скрытый user/scope/authority/task state; это явные typed fields. +26. Unknown/Conflict/NotApplicable/NotObserved не схлопываются в nil/false/default. +27. Type system, package visibility, DB ACL и CI должны делать критические ошибки невозможными там, где это возможно. +28. Architecture guardrails themselves are protected and cannot validate their own weakening alone. +29. Codex ordinary feature task не меняет Constitution, Architecture Registry, public semantic topology, guardrails или canonical ownership без отдельного approved change. +30. Codex может завершить задачу typed gap (`BLOCKED_BY_SPEC_GAP`, `BLOCKED_BY_ARCHITECTURE_GAP`) вместо архитектурного обхода. +31. Every persistent thing has lifecycle, owner/justification, stop/retirement path and recovery semantics appropriate to criticality. +32. Historical meaning is not rewritten by current interpretation; corrections/supersessions are appended/versioned. +33. Newer/stronger model never automatically receives broader authority. +34. More intelligence, more code, more services or more automations are not goals themselves. +35. KEEP/WAIT/DO_NOTHING are valid decisions. +36. The system must remain recoverable, exportable and replaceable. + +## 1.4 Codex-specific hard prohibitions + +- No direct cross-module DB access “temporarily”. +- No cross-schema JOIN as application dependency between canonical owners. +- No direct provider/model call outside Cognitive Gateway/adapters. +- No external mutation outside Commit/Executor path. +- No generic `SaveEntity(any)`, `Invoke(any)`, global Service Locator or God Context. +- No semantic side effects in Go package `init()`. +- No semantic durable timers using `time.After`/sleep in request process. +- No production semantic behavior hidden only in middleware, SQL trigger or YAML DSL. +- No silent retry of ambiguous external mutation. +- No weakening of architecture/security tests inside unrelated feature task. +- No new third-party dependency, canonical table, integration contract or privileged secret access without declared change classification. + +## 1.5 Core change rule + +Change the lowest layer that eliminates the causal class of the problem. Escalate only when necessary: + +`data/config → local implementation → skill/strategy → module contract → ownership/schema → architecture → constitutional invariant`. diff --git a/architecture/CORE_SEMANTIC_MODEL_V1.md b/architecture/CORE_SEMANTIC_MODEL_V1.md new file mode 100644 index 0000000..5bee19c --- /dev/null +++ b/architecture/CORE_SEMANTIC_MODEL_V1.md @@ -0,0 +1,164 @@ +# 3. CORE_SEMANTIC_MODEL_V1.md + +## 3.1 Scope + +This is a V1 semantic freeze, not an eternal ontology. It defines the minimum durable concepts required to build the walking skeleton and preserve future migration freedom. + +## 3.2 Canonical Core V1 concepts + +### Identity +Stable semantic identity independent of current name, locator, account, process or key. Supports identifier history and future merge/split corrections. + +### Actor +Entity capable of requests/actions/representation. Actor is not automatically a Principal. + +### Principal +The person/organization/accounting/legal/operational interest on whose behalf an action is performed. Same human may act for multiple principals. + +### Scope +Boundary for data, authority, project/context and permissible propagation. + +### AuthorityGrant / AuthorityContext +Evidence that an Actor may perform a class of actions for a Principal within Scope and validity conditions. Capability never implies grant. + +### Situation +Time-bounded interpretation of “what is happening now”: actor, interface/session, focus, project/workspace/environment bindings, active task, relevant resources, confidence/provenance. Situation is derived and expires/revalidates. + +### Observation +Something obtained from a source/sensor/tool/user report. Carries provenance, observed time, coverage and source semantics. + +### Claim +A proposition asserted by a source or derived system. It is not automatically fact. + +### Belief/Assessment +Current system interpretation supported by evidence; versioned/revisable and scoped. + +### Intent +Current request/desired change inferred from interaction; can be revised/superseded. Intent is not automatically long-term preference. + +### Goal +Desired world/state transition or outcome with lifecycle and lineage. Goal may be refined/superseded; method is not identical to goal. + +### Task +Durable unit of work that must survive session/process, coordinate actors, or carry lifecycle significance. Ephemeral reasoning steps are not Tasks. + +### Decision +Selection/commitment among alternatives with rationale/evidence refs where consequence warrants. Decision can have validity/revisit conditions. + +### ActionProposal +Structured candidate to change canonical state, communicate, allocate resources or mutate external world. Proposal has no execution authority by itself. + +### AuthorizedAction +Proposal that passed owner validation, current authority, revision/target/integrity checks and received commit identity. + +### ActionAttempt +Concrete execution attempt of an AuthorizedAction. Multiple attempts may serve one ActionIntent; attempt is not outcome. + +### Outcome +Observed/verifiable result attributable with stated confidence. Supports UNKNOWN/partial/provisional maturity. + +### Commitment +A future obligation/expectation created by promise/agreement/explicit user request/policy. Must have owner/beneficiary, fulfilment or renegotiation path and prospective continuity. + +### ProspectiveItem +Canonical future-relevance object used by Continuity for reminder/wake/review/expected transition. V1 may implement only reminder/wake subset; it remains independent of Temporal. + +### Event +Meaningful semantic transition or occurrence. Event is not a log line and not every low-level tool callback becomes Event. + +### Artifact +Durable content/resource with identity, content/schema metadata, provenance and scope. Payload may live outside PostgreSQL. + +### Capability +Declared/tested ability to observe, reason, communicate or act, with contracts, limits, provenance, health and allowed-use metadata. Capability availability ≠ authority. + +### Revision +Optimistic semantic version/precondition for mutable canonical objects and contracts. + +### Provenance +Lineage linking observations, claims, derived states, actions and artifacts to sources/derivations without treating copies as independent evidence. + +## 3.3 Concepts intentionally NOT Core primitives in V1 + +These remain extension/derived concepts until evidence requires promotion: `Thread`, `Journey`, `Program`, `Trajectory`, `Portfolio`, `Responsibility`, `Risk`, `Opportunity`, `Monitor`, `ControlLoop`, `Routine`, `Theory`, `Frontier`. Foundation must permit them later but Codex must not materialize separate Core tables/services merely because they were discussed. + +## 3.4 Required semantic distinctions + +`Actor ≠ Principal` +`Identity ≠ Identifier ≠ Locator` +`Situation ≠ RequestContext ≠ ModelContext` +`Observation ≠ Claim ≠ Belief` +`Intent ≠ Goal ≠ Task` +`Task ≠ ActionProposal ≠ AuthorizedAction ≠ ActionAttempt ≠ Outcome` +`Commitment ≠ Reminder delivery` +`Capability ≠ Authority` +`Event ≠ Integration message ≠ Log` +`Canonical state ≠ Projection ≠ Cache` +`Prediction ≠ Expected-by-process ≠ Required-by-commitment`. + +## 3.5 Required state semantics + +### Goal lifecycle (minimum) +`ACTIVE | WAITING | DORMANT | SATISFIED | SUPERSEDED | ABANDONED` + +Transitions must preserve reason and lineage. `ABANDONED` does not mean failure; `SUPERSEDED` must point to successor when known. + +### Task lifecycle (minimum) +`PENDING | ACTIVE | WAITING | COMPLETED | CANCELLED | FAILED` + +Task has `revision`, `justification_ref`, optional `wake_condition_ref`, owner and completion evidence. `WAITING` must say what it waits for or be explicitly dormant at higher level. + +### Action lifecycle +`PROPOSED → VALIDATED → AUTHORIZED/REJECTED → EXECUTING → SUCCEEDED | FAILED | UNKNOWN_OUTCOME | COMPENSATED`. + +Retry preserves ActionIntent identity and creates a new attempt identity. `UNKNOWN_OUTCOME` blocks blind retry when duplicate effect is possible. + +### Commitment lifecycle +`ACTIVE | WAITING | AT_RISK | FULFILLED | RENEGOTIATED | REVOKED | EXPIRED`. + +Future promise is not fulfilled by notification delivery alone unless the promise itself was only to notify. + +### Prospective item lifecycle +`REGISTERED | ARMED | TRIGGERED | REVALIDATING | DELIVERED/STARTED | SATISFIED | SUPPRESSED | EXPIRED`. + +Trigger firing does not bypass relevance/authority revalidation. + +## 3.6 Temporal model + +Foundation must support distinguishing, where applicable: + +- event/occurrence time; +- observed time; +- recorded time; +- validity interval; +- local-time intention vs absolute instant; +- relative schedule relation (`after Event X + duration`) vs prematurely flattened timestamp. + +Do not put four timestamps on every row blindly. Use common temporal value objects/contracts only where semantics require them. + +## 3.7 Strong identifiers + +Go Core should use distinct types for at least: `IdentityID`, `ActorID`, `PrincipalID`, `ScopeID`, `SituationID`, `GoalID`, `TaskID`, `CommitmentID`, `ActionIntentID`, `ActionAttemptID`, `ArtifactID`, `CapabilityID`, plus revision types. They serialize to language-neutral primitives. + +## 3.8 Ownership baseline + +| Semantic area | Canonical owner V1 | Notes | +|---|---|---| +| Identity/Actor/Principal bindings | Identity module | Other modules reference IDs/projections only. | +| Scope/Authority grants | Authority module | Commit queries current validation; no hidden default authority. | +| Situation/focus bindings | Situation module | Derived/ephemeral portions may use cache, but durable bindings/provenance explicitly classified. | +| Goal/Task/Decision | Work module | Can later split; V1 owner chosen to preserve local consistency. | +| Commitment/Prospective items | Continuity module | Temporal is execution engine adapter, not owner. | +| Observations/Claims/Beliefs/Provenance | Epistemics module | Search/vector indexes derived. | +| Action commit state | Commit module | External executor isolated. | +| Artifact metadata/lineage | Artifact module or platform-owned artifact service | Payload storage replaceable. | +| Capability registry/bindings | Capability module | Provider-specific adapters outside semantic owner. | + +## 3.9 Consistency baseline + +- Authority validation and Action authorization: strong owner-local consistency + revision preconditions. +- Task state transitions: owner-local optimistic revision/CAS. +- Cross-owner projections: eventual/rebuildable unless an explicit use case requires stronger semantics. +- UI aggregate views: derived projections; no cross-owner canonical joins. +- Integration reactions: owner-local commit + transactional outbox; consumers idempotent. +- No global total event order requirement; causal/aggregate ordering only where needed. diff --git a/architecture/DECISION_CONSOLIDATION_V1.md b/architecture/DECISION_CONSOLIDATION_V1.md new file mode 100644 index 0000000..93f14bd --- /dev/null +++ b/architecture/DECISION_CONSOLIDATION_V1.md @@ -0,0 +1,87 @@ +# 2. DECISION_CONSOLIDATION_V1.md + +## 2.1 Purpose + +This inventory separates accepted decisions from explored ideas. Statuses are normative: + +- **ACCEPTED_V1** — implementation may rely on it now. +- **EXTENSION_POINT_V1** — foundation must not block it; no full implementation required. +- **LATER** — explicitly excluded from current implementation scope. +- **RESEARCH** — unresolved; Codex must not choose a permanent answer silently. +- **REJECTED_V1** — do not implement this approach in V1. + +## 2.2 Accepted foundation decisions + +| ID | Decision | Status | Implementation consequence | +|---|---|---|---| +| FD-001 | Assistant ≠ LLM/model/provider/process | ACCEPTED_V1 | Cognitive providers are replaceable capabilities. | +| FD-002 | Extractable Modular Architecture | ACCEPTED_V1 | Deploy together when useful, design bounded components separately. | +| FD-003 | One PostgreSQL cluster may host V1, but canonical owners use separate schemas/roles | ACCEPTED_V1 | DB ACL physically prevents cross-owner writes. | +| FD-004 | Cross-owner FK/trigger business logic forbidden by default | ACCEPTED_V1 | Relations use semantic IDs/contracts/projections. | +| FD-005 | NATS/Temporal/Redis are implementation mechanisms, not semantic sources of truth | ACCEPTED_V1 | Canonical event/prospective state survives their loss. | +| FD-006 | Major cross-boundary contracts are remote-capable | ACCEPTED_V1 | In-process and remote adapters satisfy same contract tests. | +| FD-007 | No mandatory microservice-per-noun | ACCEPTED_V1 | Semantic modules and deployment runtimes are independent mappings. | +| FD-008 | Commit/Executor is an early physical isolation boundary | ACCEPTED_V1 | External credentials and mutation authority are not globally available. | +| FD-009 | Model output produces candidates/proposals, never direct canonical mutation | ACCEPTED_V1 | Typed semantic validation and commit gates. | +| FD-010 | Canonical event/outbox pattern for cross-owner durable reactions | ACCEPTED_V1 | Owner-local ACID + outbox; no distributed ACID assumption. | +| FD-011 | Prospective Memory canonicalized outside Temporal | ACCEPTED_V1 | Reminder/wake/commitment state rebuilds after workflow-engine loss. | +| FD-012 | Identity, Principal, Scope and Authority are separate concepts | ACCEPTED_V1 | Strong typed IDs and explicit command envelopes. | +| FD-013 | Goal/Task/Action/Outcome are distinct | ACCEPTED_V1 | Task completion does not imply goal/outcome success. | +| FD-014 | Situation binds request to project/workspace/environment via runtime context, not keywords | ACCEPTED_V1 | Focus Isolation Suite is foundational acceptance test. | +| FD-015 | Unknown and conflict states preserved | ACCEPTED_V1 | No premature boolean/default collapse. | +| FD-016 | Bitemporal-capable history model | ACCEPTED_V1 | Separate event/observed/valid/recorded semantics where relevant. | +| FD-017 | Canonical history append/supersede rather than retroactive semantic rewrite | ACCEPTED_V1 | Historical replay can reconstruct what was known then. | +| FD-018 | Development through Codex requires Development Commit Gate | ACCEPTED_V1 | Task authority + CI/ACL/architecture checks precede merge. | +| FD-019 | Architecture is executable | ACCEPTED_V1 | Critical rules need compiler/linter/ACL/test/observability enforcement. | +| FD-020 | V1 uses explicit boring typed mechanisms over meta-frameworks/DSLs | ACCEPTED_V1 | No generic entity engine/universal workflow DSL. | +| FD-021 | Contract/state ownership precedes DDL/protobuf | ACCEPTED_V1 | Schema is derived from semantic contracts. | +| FD-022 | Walking Skeleton before broad subsystem implementation | ACCEPTED_V1 | Foundation validated vertically, not through stub zoo. | +| FD-023 | Stronger future model is candidate, not automatic replacement | ACCEPTED_V1 | Eval/shadow/canary/promotion lifecycle. | +| FD-024 | Architecture boundaries are hypotheses and may split/merge | ACCEPTED_V1 | Stable IDs/lineage; topology migration supported. | +| FD-025 | Critical deterministic invariants are not delegated to free-form LLM judgment | ACCEPTED_V1 | Fuzzy cognition proposes; deterministic gates enforce. | + +## 2.3 Extension points that must be preserved now + +| ID | Topic | Status | V1 requirement | +|---|---|---|---| +| EP-001 | Portfolio/Trajectory engines | EXTENSION_POINT_V1 | Core goal/project/event model must permit later orchestration. | +| EP-002 | Responsibilities/Risks/Opportunities/ControlLoops | EXTENSION_POINT_V1 | Semantic namespaces/lifecycles extensible; no full engines required. | +| EP-003 | Self-development/self-migration | EXTENSION_POINT_V1 | Architecture/change provenance and replacement paths exist; autonomous self-editing absent. | +| EP-004 | Collective/global learning | EXTENSION_POINT_V1 | Knowledge provenance/scope prevents unsafe future promotion. | +| EP-005 | Multi-user/organization federation | EXTENSION_POINT_V1 | Actor/Principal/Scope not hardcoded to single user. | +| EP-006 | Robotics/edge realtime control | EXTENSION_POINT_V1 | Capability/authority model supports out-of-process bounded controllers. | +| EP-007 | Alternate coding agents | EXTENSION_POINT_V1 | Codex Task Packet provider-neutral; Codex adapter first. | +| EP-008 | Semantic generation migration | EXTENSION_POINT_V1 | Version/schema IDs and migration framework exist; no universal ontology engine. | + +## 2.4 Explicit V1 non-goals + +The following are **LATER or RESEARCH**, not implementation requirements for Foundation V1: + +- autonomous production self-modification; +- autonomous installation of arbitrary plugins/components; +- global collective learning between unrelated users; +- Kubernetes requirement; +- custom consensus protocol; +- custom database or event-bus implementation; +- full robotics stack; +- universal ontology/DSL engine; +- one service per semantic noun; +- complete multi-tenant enterprise federation; +- training custom foundation models as part of V1; +- perfect global causal graph; +- general autonomous portfolio optimization of human life; +- fully autonomous Codex development swarm. + +## 2.5 Rejected V1 approaches + +- REJECTED: “Start as free modular monolith and split later.” +- REJECTED: giant shared domain model/package. +- REJECTED: cross-module joins because “same DB for now”. +- REJECTED: event-sourcing everything by default. +- REJECTED: microservices for every primitive. +- REJECTED: direct LLM→tool→world mutation path. +- REJECTED: Temporal workflow as canonical future obligation. +- REJECTED: vector DB/Redis/model memory as canonical knowledge. +- REJECTED: generic `entity(type,payload jsonb)` as primary Core model. +- REJECTED: implicit global utility score for all goals. +- REJECTED: relying on Codex remembering architecture from previous sessions. diff --git a/architecture/README.md b/architecture/README.md new file mode 100644 index 0000000..84efb9c --- /dev/null +++ b/architecture/README.md @@ -0,0 +1,7 @@ +# Architecture authority + +This directory contains architecture authority artifacts. `ARCHITECTURE_CONSTITUTION.md` outranks all lower-level files. `architecture-registry.yaml` is the human-authoritative machine-readable registry; `architecture-registry.json` is the generated/bootstrapping mirror used by the initial stdlib-only `devctl`. + +Do not casually edit either registry copy. A Tier E task must update both atomically until registry generation is automated. + +`reference/FOUNDATION_FREEZE_PACK_V1.*` preserves the freeze package used to bootstrap this repository. It is reference/history, not higher authority than current accepted files in this directory. diff --git a/architecture/UNRESOLVED_QUESTIONS.md b/architecture/UNRESOLVED_QUESTIONS.md new file mode 100644 index 0000000..64aa1a9 --- /dev/null +++ b/architecture/UNRESOLVED_QUESTIONS.md @@ -0,0 +1,33 @@ +# 7. UNRESOLVED_QUESTIONS.md + +These questions are intentionally unresolved. Codex must not silently turn temporary assumptions into permanent architecture. + +| ID | Question | Status | Temporary V1 position | Blocking? | Revisit trigger | +|---|---|---|---|---|---| +| UQ-001 | Exact split of `Work` into Goal/Task/Decision modules | RESEARCH | One canonical Work owner for V1 | No | Contract churn/co-change/consistency pressure | +| UQ-002 | Artifact metadata as separate module vs platform service | PROPOSED | Keep separate semantic owner if persistence begins | No | First large artifact integration | +| UQ-003 | Exact NATS subject taxonomy | DEFERRED | Define only subjects required by skeleton | No | First durable cross-owner event | +| UQ-004 | Exact Temporal workflow topology | DEFERRED | Temporal adapter behind Continuity; canonical prospective state outside | No | WS-002 implementation | +| UQ-005 | gRPC vs another remote transport for internal calls | DEFERRED | Contracts language-neutral; choose simplest baseline during remote drill | No | WS-004 | +| UQ-006 | Extent of canonical event archive vs per-owner transition history | RESEARCH | Do not event-source everything; retain meaningful events/outbox | No | Replay/recovery requirements emerge | +| UQ-007 | Formal policy engine technology | LATER | Deterministic typed Authority/Commit rules in Go V1 | No | Policy complexity outgrows code safely | +| UQ-008 | Vector store implementation | LATER | Search indexes derived; no foundation dependency | No | Epistemic retrieval slice | +| UQ-009 | Redis requirement | LATER | Optional cache only; correctness must not depend on it | No | Performance evidence | +| UQ-010 | Full Identity merge/split production workflow | EXTENSION_POINT | IDs/lineage schema must not preclude it; test simplified correction later | No | Multi-source identity pressure | +| UQ-011 | Portfolio/Trajectory/Responsibility primitives promotion | RESEARCH | Not Core V1 | No | Repeated distinct lifecycle/authority need | +| UQ-012 | Self-development automation | LATER | Development Control Plane external/manual-governed | No | Foundation + safe change/recovery mature | +| UQ-013 | Multi-user organization governance | LATER | Actor/Principal/Scope designed generally; expose single-user V1 if desired | No | Actual shared workspace requirement | +| UQ-014 | Canonical wire codec longevity | RESEARCH | Versioned schema/envelope; initial protobuf acceptable, not eternal identity | No | First non-Go module or major semantic migration | +| UQ-015 | Exact data retention/privacy policy engine | RESEARCH | Store scope/provenance/sensitivity hooks; implement concrete policies per product deployment | No | Product/legal requirements | + +## 7.1 Spec-gap protocol + +If implementation encounters a semantically material unknown not listed here, Codex emits a `SpecificationGap` with: + +- exact scenario; +- current contract insufficiency; +- smallest temporary safe behavior if one exists; +- whether work can continue independently; +- proposed alternatives without choosing permanent architecture. + +No architecture gap is solved by creating an unowned table, generic JSON object, cross-module direct read/write, or hidden provider-specific field. diff --git a/architecture/WALKING_SKELETON_SPEC.md b/architecture/WALKING_SKELETON_SPEC.md new file mode 100644 index 0000000..f16af9c --- /dev/null +++ b/architecture/WALKING_SKELETON_SPEC.md @@ -0,0 +1,132 @@ +# 6. WALKING_SKELETON_SPEC.md + +## 6.1 Goal + +Prove the Foundation through a small set of end-to-end semantic properties before broad implementation. Do not build all future modules/stubs first. + +## 6.2 Skeleton topology + +Initial process topology: + +```text +Client/Test Harness + | +Core Runtime + - Identity + - Situation + - Work + - Epistemics (minimal) + - Capability registry (minimal) + | + +---- Cognitive Worker Adapter (deterministic fake first, real model later) + | + +---- Continuity Runtime ---- Temporal adapter (optional/replaceable) + | + +---- Commit Runtime (separate process, isolated credentials) + | + Test Executor +``` + +One PostgreSQL cluster, separate owner schemas/roles. NATS may be used for integration events once owner-local outbox exists; skeleton must also prove semantics do not live only in NATS. + +## 6.3 Scenario WS-001 — Focus Isolation / wrong-project prevention + +**Given** one Actor has two simultaneously active Codex/workspace situations bound to Project/Goal contexts A and B. Both domains contain an identically named concept such as `temperature`. + +**When** the identical user phrase is sent through each session without naming the project. + +**Then** Situation resolution binds each request to its own session/workspace/project context using runtime bindings/provenance; no keyword shortcut is allowed; canonical writes carry target/revision/scope and cannot affect the other context. + +Required failure cases: +- stale focus binding; +- two worktrees of same repository; +- ambiguous target → typed ambiguity, no mutation; +- Task revision changed between reasoning and commit → commit rejected/re-evaluated. + +Acceptance property: **wrong-project mutation is mechanically prevented even if cognition proposes the wrong target**. + +## 6.4 Scenario WS-002 — Durable prospective memory independent of Temporal + +**Given** user creates a future reminder/commitment with semantic context and fulfilment condition. + +**When** canonical Continuity transaction commits, process restarts, Temporal state is deleted/unavailable, then workflow infrastructure is rebuilt. + +**Then** obligation remains in PostgreSQL canonical prospective state, is reconstructed, does not duplicate occurrence, revalidates relevance before delivery, and can distinguish `DELIVERED` from underlying `FULFILLED`. + +Failure cases: +- system offline past trigger; +- 100 duplicate trigger messages; +- goal superseded before trigger; +- delivery channel unavailable; +- recurrence missed during outage. + +Acceptance property: **loss of workflow engine does not lose or blindly replay future obligations**. + +## 6.5 Scenario WS-003 — Safe Action Commit and unknown outcome + +**Given** cognition produces a structured external mutation proposal. + +**When** Commit Runtime receives it. + +**Then** it validates current Authority, Scope, Target identity, task/goal justification and relevant revisions before producing AuthorizedAction. Only isolated Executor has external mutation credentials. + +Injected case: executor applies external effect but connection times out before acknowledgement. + +Expected result: `UNKNOWN_OUTCOME`; no blind retry; reconciliation query/verification required. A retry uses same ActionIntentID and a new AttemptID only after semantics permit. + +Acceptance property: **no external side effect can bypass authority/commit and timeout cannot cause duplicate mutation by default**. + +## 6.6 Scenario WS-004 — Topology equivalence + +Run WS-001/002/003 twice: + +1. bounded modules co-located where allowed; +2. Commit and Continuity accessed through remote adapters/processes. + +Canonical semantic outcomes/invariants must be equivalent. Timing and expected temporary unavailability may differ; domain meaning must not. + +Acceptance property: **process extraction does not require domain rewrite**. + +## 6.7 Scenario WS-005 — No-LLM degraded continuity + +Remove all cognitive providers. + +System must still: +- identify existing actors/sessions where deterministically available; +- preserve events/state; +- keep canonical commitments/prospective schedules; +- perform deterministic authority/integrity checks; +- expose degraded capability state; +- avoid inventing new AI decisions/actions. + +Acceptance property: **loss of cognition degrades intelligence, not continuity/integrity**. + +## 6.8 Scenario WS-006 — Semantic evolution drill + +Create a deliberately small v1→v2 contract/schema change in a non-critical skeleton concept. Prove: + +- old historical object remains interpretable; +- new writer/readers use new version; +- unsupported semantics are explicit, not silently defaulted; +- active live objects migrate/validate; +- derived projections rebuild; +- rollback/forward-fix mode is documented. + +Acceptance property: **architecture evolution is tested as a transformation, not merely promised**. + +## 6.9 Foundation Exit Criteria + +Foundation V1 is eligible for freeze when all are true: + +1. Fresh Codex session can implement a bounded feature from repository artifacts alone. +2. Cross-owner DB write is physically denied. +3. Wrong-project mutation fails even under adversarial cognition proposal. +4. Stale/revoked authority cannot create AuthorizedAction. +5. Duplicate/replay does not duplicate external side effects in covered semantics. +6. Future obligation survives workflow-engine loss/rebuild. +7. Core remains continuity-capable without any LLM. +8. Same semantic scenarios pass in at least two process topologies. +9. Provider can be swapped/faked without domain contract change. +10. Declared and observed architecture graphs match or all drift has registered findings. +11. One semantic contract migration drill succeeds. +12. Architecture guardrail suite proves its own critical enforcement with negative tests. diff --git a/architecture/adrs/ADR-0001-extractable-modular-architecture.md b/architecture/adrs/ADR-0001-extractable-modular-architecture.md new file mode 100644 index 0000000..bb8f8be --- /dev/null +++ b/architecture/adrs/ADR-0001-extractable-modular-architecture.md @@ -0,0 +1,14 @@ +# ADR-0001 — Extractable Modular Architecture + +**Status:** ACCEPTED_V1 + +V1 may co-deploy modules, but major bounded components are designed separately and must remain extractable. Co-location is a deployment optimization, never an architectural privilege. + +Semantic topology, process topology, storage topology and cognitive/provider topology are independent mappings. + +Consequences: +- remote-capable contracts are serializable and do not depend on shared mutable state; +- cross-owner ACID transactions are forbidden; +- separate PostgreSQL schemas/roles from day one; +- Commit Runtime is physically isolated earlier because it holds mutation authority/secrets; +- topology equivalence tests are Foundation criteria. diff --git a/architecture/adrs/ADR-0002-codex-is-implementation-capability.md b/architecture/adrs/ADR-0002-codex-is-implementation-capability.md new file mode 100644 index 0000000..56519e5 --- /dev/null +++ b/architecture/adrs/ADR-0002-codex-is-implementation-capability.md @@ -0,0 +1,7 @@ +# ADR-0002 — Codex is an implementation capability + +**Status:** ACCEPTED_V1 + +Codex is expected to implement most code but is not architectural authority. Repository structure, type system, DB ACLs, protected paths, CI and task packets must mechanically enforce what can be enforced. + +A fresh Codex session must be able to work correctly from repository artifacts without prior chat history. diff --git a/architecture/adrs/README.md b/architecture/adrs/README.md new file mode 100644 index 0000000..574265d --- /dev/null +++ b/architecture/adrs/README.md @@ -0,0 +1,5 @@ +# ADRs + +Accepted architecture decisions live here. Normal implementation tasks may read but not create/change accepted ADRs unless explicitly authorized. + +Use `ADR-XXXX-title.md` with: problem, evidence, alternatives, decision, rationale, consequences, validity conditions, revisit triggers, migration/rollback, status. diff --git a/architecture/architecture-registry.json b/architecture/architecture-registry.json new file mode 100644 index 0000000..27dd863 --- /dev/null +++ b/architecture/architecture-registry.json @@ -0,0 +1,283 @@ +{ + "registry_version": 1, + "semantic_generation": 1, + "architecture_generation": 1, + "modules": { + "identity": { + "id": "mod.identity", + "owns": [ + "Identity", + "Actor", + "PrincipalBinding" + ], + "database_schema": "identity", + "boundary_class": "remote_capable", + "may_depend_on": [ + "platform.ids", + "platform.events" + ], + "forbidden": [ + "adapters.models", + "adapters.temporal" + ] + }, + "authority": { + "id": "mod.authority", + "owns": [ + "Scope", + "AuthorityGrant", + "AuthorityValidation" + ], + "database_schema": "authority", + "boundary_class": "remote_capable", + "may_depend_on": [ + "contracts.identity", + "platform.ids", + "platform.events" + ] + }, + "situation": { + "id": "mod.situation", + "owns": [ + "Situation", + "FocusBinding" + ], + "database_schema": "situation", + "boundary_class": "remote_capable", + "may_depend_on": [ + "contracts.identity", + "contracts.authority", + "contracts.work", + "contracts.capability" + ] + }, + "work": { + "id": "mod.work", + "owns": [ + "Goal", + "Task", + "Decision" + ], + "database_schema": "work", + "boundary_class": "remote_capable", + "may_depend_on": [ + "contracts.identity", + "contracts.authority", + "platform.events" + ] + }, + "epistemics": { + "id": "mod.epistemics", + "owns": [ + "Observation", + "Claim", + "Belief", + "Provenance" + ], + "database_schema": "epistemics", + "boundary_class": "remote_capable", + "may_depend_on": [ + "contracts.identity", + "contracts.artifact", + "platform.events" + ] + }, + "continuity": { + "id": "mod.continuity", + "owns": [ + "Commitment", + "ProspectiveItem" + ], + "database_schema": "continuity", + "boundary_class": "remote_capable", + "may_depend_on": [ + "contracts.work", + "contracts.identity", + "contracts.authority", + "platform.events" + ] + }, + "capability": { + "id": "mod.capability", + "owns": [ + "Capability", + "CapabilityBinding", + "CapabilityHealth" + ], + "database_schema": "capability", + "boundary_class": "remote_capable", + "may_depend_on": [ + "contracts.identity", + "contracts.authority", + "contracts.artifact" + ] + }, + "commit": { + "id": "mod.commit", + "owns": [ + "ActionProposalRecord", + "AuthorizedAction", + "ActionAttemptRecord" + ], + "database_schema": "commit", + "boundary_class": "remote_capable", + "may_depend_on": [ + "contracts.authority", + "contracts.capability", + "contracts.work", + "platform.events" + ], + "privileged": true + }, + "artifact": { + "id": "mod.artifact", + "owns": [ + "ArtifactMetadata", + "ArtifactLineage" + ], + "database_schema": "artifact", + "boundary_class": "remote_capable", + "may_depend_on": [ + "contracts.identity" + ] + } + }, + "platform": { + "allowed_packages": [ + "platform.ids", + "platform.clock", + "platform.outbox", + "platform.inbox", + "platform.telemetry", + "platform.errors" + ], + "rule": "platform owns mechanisms, never domain meaning" + }, + "runtime_topology_v1": { + "core_runtime": { + "modules": [ + "identity", + "situation", + "work", + "epistemics", + "capability", + "artifact" + ] + }, + "continuity_runtime": { + "modules": [ + "continuity" + ] + }, + "commit_runtime": { + "modules": [ + "authority", + "commit" + ], + "isolation_reason": [ + "secrets", + "external_mutation_authority", + "failure_isolation" + ] + }, + "cognitive_workers": { + "modules": [], + "role": "replaceable cognition/context compilation workers; no canonical ownership" + } + }, + "storage_topology_v1": { + "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" + }, + "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", + "class": "HARD", + "enforcement": [ + "commit_runtime", + "property_tests", + "secret_acl" + ] + }, + { + "id": "INV-OWN-001", + "property": "Only semantic owner writes canonical state", + "class": "HARD", + "enforcement": [ + "db_acl", + "architecture_linter" + ] + }, + { + "id": "INV-CONT-001", + "property": "Future obligations survive workflow engine loss", + "class": "HARD", + "enforcement": [ + "canonical_prospective_store", + "recovery_test" + ] + }, + { + "id": "INV-TOPO-001", + "property": "Major bounded contracts preserve semantics across inproc and remote topology", + "class": "HARD", + "enforcement": [ + "topology_equivalence_tests" + ] + }, + { + "id": "INV-EPI-001", + "property": "Model output cannot directly become canonical fact/action", + "class": "HARD", + "enforcement": [ + "package_boundaries", + "commit_apis", + "semantic_validation_tests" + ] + }, + { + "id": "INV-PROJ-001", + "property": "Projection eventually converges from canonical state", + "class": "CONVERGENCE", + "enforcement": [ + "projection_rebuild_tests", + "lag_metrics" + ] + } + ], + "protected_paths": [ + "architecture/ARCHITECTURE_CONSTITUTION.md", + "architecture/architecture-registry.yaml", + "architecture/adrs/**", + "ci/architecture/**", + "security/**" + ], + "default_feature_task_authority": { + "may_write": [ + "declared module paths", + "module-owned tests" + ], + "may_not_change": [ + "canonical_ownership", + "public_cross_module_contracts", + "protected_paths", + "third_party_dependencies", + "database_owner_roles", + "privileged_capabilities" + ] + } +} diff --git a/architecture/architecture-registry.md b/architecture/architecture-registry.md new file mode 100644 index 0000000..2960d11 --- /dev/null +++ b/architecture/architecture-registry.md @@ -0,0 +1,169 @@ +# 4. architecture-registry.yaml + +Below is the normative starter registry. It is intentionally small. Tooling may extend the schema, but new relation types must have an enforcement/use case. + +```yaml +registry_version: 1 +semantic_generation: 1 +architecture_generation: 1 + +modules: + identity: + id: mod.identity + owns: [Identity, Actor, PrincipalBinding] + database_schema: identity + boundary_class: remote_capable + may_depend_on: [platform.ids, platform.events] + forbidden: [adapters.models, adapters.temporal] + + authority: + id: mod.authority + owns: [Scope, AuthorityGrant, AuthorityValidation] + database_schema: authority + boundary_class: remote_capable + may_depend_on: [contracts.identity, platform.ids, platform.events] + + situation: + id: mod.situation + owns: [Situation, FocusBinding] + database_schema: situation + boundary_class: remote_capable + may_depend_on: [contracts.identity, contracts.work, contracts.capability] + + work: + id: mod.work + owns: [Goal, Task, Decision] + database_schema: work + boundary_class: remote_capable + may_depend_on: [contracts.identity, contracts.authority, platform.events] + + epistemics: + id: mod.epistemics + owns: [Observation, Claim, Belief, Provenance] + database_schema: epistemics + boundary_class: remote_capable + may_depend_on: [contracts.identity, contracts.artifact, platform.events] + + continuity: + id: mod.continuity + owns: [Commitment, ProspectiveItem] + database_schema: continuity + boundary_class: remote_capable + may_depend_on: [contracts.work, contracts.identity, contracts.authority, platform.events] + + capability: + id: mod.capability + owns: [Capability, CapabilityBinding, CapabilityHealth] + database_schema: capability + boundary_class: remote_capable + may_depend_on: [contracts.identity, contracts.authority, contracts.artifact] + + commit: + id: mod.commit + owns: [ActionProposalRecord, AuthorizedAction, ActionAttemptRecord] + database_schema: commit + boundary_class: remote_capable + may_depend_on: [contracts.authority, contracts.capability, contracts.work, platform.events] + privileged: true + + artifact: + id: mod.artifact + owns: [ArtifactMetadata, ArtifactLineage] + database_schema: artifact + boundary_class: remote_capable + may_depend_on: [contracts.identity] + +platform: + allowed_packages: + - platform.ids + - platform.clock + - platform.outbox + - platform.inbox + - platform.telemetry + - platform.errors + rule: "platform owns mechanisms, never domain meaning" + +runtime_topology_v1: + core_runtime: + modules: [identity, situation, work, epistemics, capability, artifact] + continuity_runtime: + modules: [continuity] + commit_runtime: + modules: [authority, commit] + isolation_reason: [secrets, external_mutation_authority, failure_isolation] + cognitive_workers: + modules: [] + role: "replaceable cognition/context compilation workers; no canonical ownership" + +storage_topology_v1: + 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 + 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" + class: HARD + enforcement: [commit_runtime, property_tests, secret_acl] + - id: INV-OWN-001 + property: "Only semantic owner writes canonical state" + class: HARD + enforcement: [db_acl, architecture_linter] + - id: INV-CONT-001 + property: "Future obligations survive workflow engine loss" + class: HARD + enforcement: [canonical_prospective_store, recovery_test] + - id: INV-TOPO-001 + property: "Major bounded contracts preserve semantics across inproc and remote topology" + class: HARD + enforcement: [topology_equivalence_tests] + - id: INV-EPI-001 + property: "Model output cannot directly become canonical fact/action" + class: HARD + enforcement: [package_boundaries, commit_apis, semantic_validation_tests] + - id: INV-PROJ-001 + property: "Projection eventually converges from canonical state" + class: CONVERGENCE + enforcement: [projection_rebuild_tests, lag_metrics] + +protected_paths: + - architecture/ARCHITECTURE_CONSTITUTION.md + - architecture/architecture-registry.yaml + - architecture/adrs/** + - ci/architecture/** + - security/** + +default_feature_task_authority: + may_write: ["declared module paths", "module-owned tests"] + may_not_change: + - canonical_ownership + - public_cross_module_contracts + - protected_paths + - third_party_dependencies + - database_owner_roles + - privileged_capabilities +``` + +## 4.1 Registry enforcement required before large Codex implementation + +Minimum deterministic checks: + +1. Go import graph against `may_depend_on`/forbidden edges. +2. No `/modules/*/domain` imports from adapters/runtime/provider packages. +3. Migration scanner rejects cross-owner writes/FKs/triggers/views unless explicit approved exception. +4. PostgreSQL role tests prove owner isolation. +5. Diff-scope check against Codex Task Packet. +6. Protected-path rule. +7. Public contract change detection. +8. Required CI guardrail health: missing checker result is failure, not pass. diff --git a/architecture/architecture-registry.yaml b/architecture/architecture-registry.yaml new file mode 100644 index 0000000..348356e --- /dev/null +++ b/architecture/architecture-registry.yaml @@ -0,0 +1,150 @@ +registry_version: 1 +semantic_generation: 1 +architecture_generation: 1 + +modules: + identity: + id: mod.identity + owns: [Identity, Actor, PrincipalBinding] + database_schema: identity + boundary_class: remote_capable + may_depend_on: [platform.ids, platform.events] + forbidden: [adapters.models, adapters.temporal] + + authority: + id: mod.authority + owns: [Scope, AuthorityGrant, AuthorityValidation] + database_schema: authority + boundary_class: remote_capable + may_depend_on: [contracts.identity, platform.ids, platform.events] + + situation: + id: mod.situation + owns: [Situation, FocusBinding] + database_schema: situation + boundary_class: remote_capable + may_depend_on: [contracts.identity, contracts.authority, contracts.work, contracts.capability] + + work: + id: mod.work + owns: [Goal, Task, Decision] + database_schema: work + boundary_class: remote_capable + may_depend_on: [contracts.identity, contracts.authority, platform.events] + + epistemics: + id: mod.epistemics + owns: [Observation, Claim, Belief, Provenance] + database_schema: epistemics + boundary_class: remote_capable + may_depend_on: [contracts.identity, contracts.artifact, platform.events] + + continuity: + id: mod.continuity + owns: [Commitment, ProspectiveItem] + database_schema: continuity + boundary_class: remote_capable + may_depend_on: [contracts.work, contracts.identity, contracts.authority, platform.events] + + capability: + id: mod.capability + owns: [Capability, CapabilityBinding, CapabilityHealth] + database_schema: capability + boundary_class: remote_capable + may_depend_on: [contracts.identity, contracts.authority, contracts.artifact] + + commit: + id: mod.commit + owns: [ActionProposalRecord, AuthorizedAction, ActionAttemptRecord] + database_schema: commit + boundary_class: remote_capable + may_depend_on: [contracts.authority, contracts.capability, contracts.work, platform.events] + privileged: true + + artifact: + id: mod.artifact + owns: [ArtifactMetadata, ArtifactLineage] + database_schema: artifact + boundary_class: remote_capable + may_depend_on: [contracts.identity] + +platform: + allowed_packages: + - platform.ids + - platform.clock + - platform.outbox + - platform.inbox + - platform.telemetry + - platform.errors + rule: "platform owns mechanisms, never domain meaning" + +runtime_topology_v1: + core_runtime: + modules: [identity, situation, work, epistemics, capability, artifact] + continuity_runtime: + modules: [continuity] + commit_runtime: + modules: [authority, commit] + isolation_reason: [secrets, external_mutation_authority, failure_isolation] + cognitive_workers: + modules: [] + role: "replaceable cognition/context compilation workers; no canonical ownership" + +storage_topology_v1: + 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 + 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" + class: HARD + enforcement: [commit_runtime, property_tests, secret_acl] + - id: INV-OWN-001 + property: "Only semantic owner writes canonical state" + class: HARD + enforcement: [db_acl, architecture_linter] + - id: INV-CONT-001 + property: "Future obligations survive workflow engine loss" + class: HARD + enforcement: [canonical_prospective_store, recovery_test] + - id: INV-TOPO-001 + property: "Major bounded contracts preserve semantics across inproc and remote topology" + class: HARD + enforcement: [topology_equivalence_tests] + - id: INV-EPI-001 + property: "Model output cannot directly become canonical fact/action" + class: HARD + enforcement: [package_boundaries, commit_apis, semantic_validation_tests] + - id: INV-PROJ-001 + property: "Projection eventually converges from canonical state" + class: CONVERGENCE + enforcement: [projection_rebuild_tests, lag_metrics] + +protected_paths: + - architecture/ARCHITECTURE_CONSTITUTION.md + - architecture/architecture-registry.yaml + - architecture/adrs/** + - ci/architecture/** + - security/** + +default_feature_task_authority: + may_write: ["declared module paths", "module-owned tests"] + may_not_change: + - canonical_ownership + - public_cross_module_contracts + - protected_paths + - third_party_dependencies + - database_owner_roles + - privileged_capabilities diff --git a/architecture/reference/FOUNDATION_FREEZE_PACK_V1.docx b/architecture/reference/FOUNDATION_FREEZE_PACK_V1.docx new file mode 100644 index 0000000000000000000000000000000000000000..fbe983cfe1cf4fd1481f901eb0cacd9a93c78dd5 GIT binary patch literal 57298 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 literal 0 HcmV?d00001 diff --git a/architecture/reference/FOUNDATION_FREEZE_PACK_V1.md b/architecture/reference/FOUNDATION_FREEZE_PACK_V1.md new file mode 100644 index 0000000..a0dc785 --- /dev/null +++ b/architecture/reference/FOUNDATION_FREEZE_PACK_V1.md @@ -0,0 +1,802 @@ +# Universal Assistant — Foundation Freeze Pack V1 + +Версия: 1.0 +Дата фиксации: 16 августа 2026 +Основание: Architecture Specification V2 + adversarial review + Codex-oriented development review +Статус: **IMPLEMENTATION BASELINE CANDIDATE** + +# 1. ARCHITECTURE_CONSTITUTION.md + +## 1.1 Назначение + +Этот документ — краткий набор законов репозитория и production-архитектуры. Он выше обычных task packets, примеров и локальных README. Codex не является архитектурным authority: он реализует только то, на что выдана task authority. + +## 1.2 Иерархия архитектурной власти + +1. Architecture Constitution. +2. Accepted ADR / Foundation Decision. +3. Machine-readable Architecture Registry. +4. Versioned semantic contracts and state machines. +5. Module architecture specification. +6. Codex Task Packet. +7. Implementation and examples. + +Если нижний уровень противоречит верхнему, нижний считается ошибочным. Существующий код является evidence of implementation, но не authority to redefine architecture. + +## 1.3 Неприкосновенные инварианты V1 + +1. LLM/model/provider не является Assistant. +2. Capability не означает Authority. +3. Observation, Claim, Belief, Prediction, Action и Outcome — разные семантические классы. +4. Model output никогда напрямую не мутирует canonical semantic state и не выполняет external mutation. +5. Любая реальная мутация проходит Proposal → semantic owner validation → Authority/Integrity checks → Commit → Executor → Outcome/Verification. +6. Любая executable activity имеет live justification path к действующей Goal, Commitment, Responsibility или Policy. +7. Отмена/суперсессия justification должна распространяться до зависимых работ перед следующим commit. +8. Canonical owner — один на конкретный mutable semantic state в конкретной semantic generation. +9. Другие модули не пишут canonical storage владельца напрямую. +10. Cross-owner SQL writes, cross-owner FK и скрытая cross-owner trigger logic запрещены по умолчанию. +11. Derived projections/caches/indexes не являются source of truth и должны быть rebuildable либо явно classified otherwise. +12. Temporal/NATS/Redis/vector index/model memory не являются единственным местом хранения commitments, authority, semantic history или future obligations. +13. Durable future obligations живут в canonical prospective state независимо от workflow/scheduler engine. +14. External side effect должен быть idempotent/reconcilable; timeout не равен failure; UNKNOWN_OUTCOME — first-class state. +15. No correctness property may depend on a shared in-process lock across bounded modules. +16. Semantic topology, process topology, storage topology и model topology независимы и меняются отдельно. +17. Co-location — deployment optimization, не архитектурная привилегия. +18. Major bounded contracts должны быть serializable и не зависеть от Go pointers, shared memory, global variables или ambient process state. +19. Core/domain не импортирует PostgreSQL/NATS/Temporal/MCP/model-provider packages. +20. Никакого giant `common/shared` domain package; technical shared mechanisms допускаются только в `platform` и не владеют domain semantics. +21. Semantic duplication предпочтительнее ложной абстракции. +22. Event-driven взаимодействие используется ради temporal decoupling/history, а не потому, что два модуля разные. +23. Public query не должен скрыто изменять canonical state. +24. Durable background responsibility не реализуется unmanaged goroutine/timer. +25. `context.Context` не переносит скрытый user/scope/authority/task state; это явные typed fields. +26. Unknown/Conflict/NotApplicable/NotObserved не схлопываются в nil/false/default. +27. Type system, package visibility, DB ACL и CI должны делать критические ошибки невозможными там, где это возможно. +28. Architecture guardrails themselves are protected and cannot validate their own weakening alone. +29. Codex ordinary feature task не меняет Constitution, Architecture Registry, public semantic topology, guardrails или canonical ownership без отдельного approved change. +30. Codex может завершить задачу typed gap (`BLOCKED_BY_SPEC_GAP`, `BLOCKED_BY_ARCHITECTURE_GAP`) вместо архитектурного обхода. +31. Every persistent thing has lifecycle, owner/justification, stop/retirement path and recovery semantics appropriate to criticality. +32. Historical meaning is not rewritten by current interpretation; corrections/supersessions are appended/versioned. +33. Newer/stronger model never automatically receives broader authority. +34. More intelligence, more code, more services or more automations are not goals themselves. +35. KEEP/WAIT/DO_NOTHING are valid decisions. +36. The system must remain recoverable, exportable and replaceable. + +## 1.4 Codex-specific hard prohibitions + +- No direct cross-module DB access “temporarily”. +- No cross-schema JOIN as application dependency between canonical owners. +- No direct provider/model call outside Cognitive Gateway/adapters. +- No external mutation outside Commit/Executor path. +- No generic `SaveEntity(any)`, `Invoke(any)`, global Service Locator or God Context. +- No semantic side effects in Go package `init()`. +- No semantic durable timers using `time.After`/sleep in request process. +- No production semantic behavior hidden only in middleware, SQL trigger or YAML DSL. +- No silent retry of ambiguous external mutation. +- No weakening of architecture/security tests inside unrelated feature task. +- No new third-party dependency, canonical table, integration contract or privileged secret access without declared change classification. + +## 1.5 Core change rule + +Change the lowest layer that eliminates the causal class of the problem. Escalate only when necessary: + +`data/config → local implementation → skill/strategy → module contract → ownership/schema → architecture → constitutional invariant`. + + +--- + +# 2. DECISION_CONSOLIDATION_V1.md + +## 2.1 Purpose + +This inventory separates accepted decisions from explored ideas. Statuses are normative: + +- **ACCEPTED_V1** — implementation may rely on it now. +- **EXTENSION_POINT_V1** — foundation must not block it; no full implementation required. +- **LATER** — explicitly excluded from current implementation scope. +- **RESEARCH** — unresolved; Codex must not choose a permanent answer silently. +- **REJECTED_V1** — do not implement this approach in V1. + +## 2.2 Accepted foundation decisions + +| ID | Decision | Status | Implementation consequence | +|---|---|---|---| +| FD-001 | Assistant ≠ LLM/model/provider/process | ACCEPTED_V1 | Cognitive providers are replaceable capabilities. | +| FD-002 | Extractable Modular Architecture | ACCEPTED_V1 | Deploy together when useful, design bounded components separately. | +| FD-003 | One PostgreSQL cluster may host V1, but canonical owners use separate schemas/roles | ACCEPTED_V1 | DB ACL physically prevents cross-owner writes. | +| FD-004 | Cross-owner FK/trigger business logic forbidden by default | ACCEPTED_V1 | Relations use semantic IDs/contracts/projections. | +| FD-005 | NATS/Temporal/Redis are implementation mechanisms, not semantic sources of truth | ACCEPTED_V1 | Canonical event/prospective state survives their loss. | +| FD-006 | Major cross-boundary contracts are remote-capable | ACCEPTED_V1 | In-process and remote adapters satisfy same contract tests. | +| FD-007 | No mandatory microservice-per-noun | ACCEPTED_V1 | Semantic modules and deployment runtimes are independent mappings. | +| FD-008 | Commit/Executor is an early physical isolation boundary | ACCEPTED_V1 | External credentials and mutation authority are not globally available. | +| FD-009 | Model output produces candidates/proposals, never direct canonical mutation | ACCEPTED_V1 | Typed semantic validation and commit gates. | +| FD-010 | Canonical event/outbox pattern for cross-owner durable reactions | ACCEPTED_V1 | Owner-local ACID + outbox; no distributed ACID assumption. | +| FD-011 | Prospective Memory canonicalized outside Temporal | ACCEPTED_V1 | Reminder/wake/commitment state rebuilds after workflow-engine loss. | +| FD-012 | Identity, Principal, Scope and Authority are separate concepts | ACCEPTED_V1 | Strong typed IDs and explicit command envelopes. | +| FD-013 | Goal/Task/Action/Outcome are distinct | ACCEPTED_V1 | Task completion does not imply goal/outcome success. | +| FD-014 | Situation binds request to project/workspace/environment via runtime context, not keywords | ACCEPTED_V1 | Focus Isolation Suite is foundational acceptance test. | +| FD-015 | Unknown and conflict states preserved | ACCEPTED_V1 | No premature boolean/default collapse. | +| FD-016 | Bitemporal-capable history model | ACCEPTED_V1 | Separate event/observed/valid/recorded semantics where relevant. | +| FD-017 | Canonical history append/supersede rather than retroactive semantic rewrite | ACCEPTED_V1 | Historical replay can reconstruct what was known then. | +| FD-018 | Development through Codex requires Development Commit Gate | ACCEPTED_V1 | Task authority + CI/ACL/architecture checks precede merge. | +| FD-019 | Architecture is executable | ACCEPTED_V1 | Critical rules need compiler/linter/ACL/test/observability enforcement. | +| FD-020 | V1 uses explicit boring typed mechanisms over meta-frameworks/DSLs | ACCEPTED_V1 | No generic entity engine/universal workflow DSL. | +| FD-021 | Contract/state ownership precedes DDL/protobuf | ACCEPTED_V1 | Schema is derived from semantic contracts. | +| FD-022 | Walking Skeleton before broad subsystem implementation | ACCEPTED_V1 | Foundation validated vertically, not through stub zoo. | +| FD-023 | Stronger future model is candidate, not automatic replacement | ACCEPTED_V1 | Eval/shadow/canary/promotion lifecycle. | +| FD-024 | Architecture boundaries are hypotheses and may split/merge | ACCEPTED_V1 | Stable IDs/lineage; topology migration supported. | +| FD-025 | Critical deterministic invariants are not delegated to free-form LLM judgment | ACCEPTED_V1 | Fuzzy cognition proposes; deterministic gates enforce. | + +## 2.3 Extension points that must be preserved now + +| ID | Topic | Status | V1 requirement | +|---|---|---|---| +| EP-001 | Portfolio/Trajectory engines | EXTENSION_POINT_V1 | Core goal/project/event model must permit later orchestration. | +| EP-002 | Responsibilities/Risks/Opportunities/ControlLoops | EXTENSION_POINT_V1 | Semantic namespaces/lifecycles extensible; no full engines required. | +| EP-003 | Self-development/self-migration | EXTENSION_POINT_V1 | Architecture/change provenance and replacement paths exist; autonomous self-editing absent. | +| EP-004 | Collective/global learning | EXTENSION_POINT_V1 | Knowledge provenance/scope prevents unsafe future promotion. | +| EP-005 | Multi-user/organization federation | EXTENSION_POINT_V1 | Actor/Principal/Scope not hardcoded to single user. | +| EP-006 | Robotics/edge realtime control | EXTENSION_POINT_V1 | Capability/authority model supports out-of-process bounded controllers. | +| EP-007 | Alternate coding agents | EXTENSION_POINT_V1 | Codex Task Packet provider-neutral; Codex adapter first. | +| EP-008 | Semantic generation migration | EXTENSION_POINT_V1 | Version/schema IDs and migration framework exist; no universal ontology engine. | + +## 2.4 Explicit V1 non-goals + +The following are **LATER or RESEARCH**, not implementation requirements for Foundation V1: + +- autonomous production self-modification; +- autonomous installation of arbitrary plugins/components; +- global collective learning between unrelated users; +- Kubernetes requirement; +- custom consensus protocol; +- custom database or event-bus implementation; +- full robotics stack; +- universal ontology/DSL engine; +- one service per semantic noun; +- complete multi-tenant enterprise federation; +- training custom foundation models as part of V1; +- perfect global causal graph; +- general autonomous portfolio optimization of human life; +- fully autonomous Codex development swarm. + +## 2.5 Rejected V1 approaches + +- REJECTED: “Start as free modular monolith and split later.” +- REJECTED: giant shared domain model/package. +- REJECTED: cross-module joins because “same DB for now”. +- REJECTED: event-sourcing everything by default. +- REJECTED: microservices for every primitive. +- REJECTED: direct LLM→tool→world mutation path. +- REJECTED: Temporal workflow as canonical future obligation. +- REJECTED: vector DB/Redis/model memory as canonical knowledge. +- REJECTED: generic `entity(type,payload jsonb)` as primary Core model. +- REJECTED: implicit global utility score for all goals. +- REJECTED: relying on Codex remembering architecture from previous sessions. + + +--- + +# 3. CORE_SEMANTIC_MODEL_V1.md + +## 3.1 Scope + +This is a V1 semantic freeze, not an eternal ontology. It defines the minimum durable concepts required to build the walking skeleton and preserve future migration freedom. + +## 3.2 Canonical Core V1 concepts + +### Identity +Stable semantic identity independent of current name, locator, account, process or key. Supports identifier history and future merge/split corrections. + +### Actor +Entity capable of requests/actions/representation. Actor is not automatically a Principal. + +### Principal +The person/organization/accounting/legal/operational interest on whose behalf an action is performed. Same human may act for multiple principals. + +### Scope +Boundary for data, authority, project/context and permissible propagation. + +### AuthorityGrant / AuthorityContext +Evidence that an Actor may perform a class of actions for a Principal within Scope and validity conditions. Capability never implies grant. + +### Situation +Time-bounded interpretation of “what is happening now”: actor, interface/session, focus, project/workspace/environment bindings, active task, relevant resources, confidence/provenance. Situation is derived and expires/revalidates. + +### Observation +Something obtained from a source/sensor/tool/user report. Carries provenance, observed time, coverage and source semantics. + +### Claim +A proposition asserted by a source or derived system. It is not automatically fact. + +### Belief/Assessment +Current system interpretation supported by evidence; versioned/revisable and scoped. + +### Intent +Current request/desired change inferred from interaction; can be revised/superseded. Intent is not automatically long-term preference. + +### Goal +Desired world/state transition or outcome with lifecycle and lineage. Goal may be refined/superseded; method is not identical to goal. + +### Task +Durable unit of work that must survive session/process, coordinate actors, or carry lifecycle significance. Ephemeral reasoning steps are not Tasks. + +### Decision +Selection/commitment among alternatives with rationale/evidence refs where consequence warrants. Decision can have validity/revisit conditions. + +### ActionProposal +Structured candidate to change canonical state, communicate, allocate resources or mutate external world. Proposal has no execution authority by itself. + +### AuthorizedAction +Proposal that passed owner validation, current authority, revision/target/integrity checks and received commit identity. + +### ActionAttempt +Concrete execution attempt of an AuthorizedAction. Multiple attempts may serve one ActionIntent; attempt is not outcome. + +### Outcome +Observed/verifiable result attributable with stated confidence. Supports UNKNOWN/partial/provisional maturity. + +### Commitment +A future obligation/expectation created by promise/agreement/explicit user request/policy. Must have owner/beneficiary, fulfilment or renegotiation path and prospective continuity. + +### ProspectiveItem +Canonical future-relevance object used by Continuity for reminder/wake/review/expected transition. V1 may implement only reminder/wake subset; it remains independent of Temporal. + +### Event +Meaningful semantic transition or occurrence. Event is not a log line and not every low-level tool callback becomes Event. + +### Artifact +Durable content/resource with identity, content/schema metadata, provenance and scope. Payload may live outside PostgreSQL. + +### Capability +Declared/tested ability to observe, reason, communicate or act, with contracts, limits, provenance, health and allowed-use metadata. Capability availability ≠ authority. + +### Revision +Optimistic semantic version/precondition for mutable canonical objects and contracts. + +### Provenance +Lineage linking observations, claims, derived states, actions and artifacts to sources/derivations without treating copies as independent evidence. + +## 3.3 Concepts intentionally NOT Core primitives in V1 + +These remain extension/derived concepts until evidence requires promotion: `Thread`, `Journey`, `Program`, `Trajectory`, `Portfolio`, `Responsibility`, `Risk`, `Opportunity`, `Monitor`, `ControlLoop`, `Routine`, `Theory`, `Frontier`. Foundation must permit them later but Codex must not materialize separate Core tables/services merely because they were discussed. + +## 3.4 Required semantic distinctions + +`Actor ≠ Principal` +`Identity ≠ Identifier ≠ Locator` +`Situation ≠ RequestContext ≠ ModelContext` +`Observation ≠ Claim ≠ Belief` +`Intent ≠ Goal ≠ Task` +`Task ≠ ActionProposal ≠ AuthorizedAction ≠ ActionAttempt ≠ Outcome` +`Commitment ≠ Reminder delivery` +`Capability ≠ Authority` +`Event ≠ Integration message ≠ Log` +`Canonical state ≠ Projection ≠ Cache` +`Prediction ≠ Expected-by-process ≠ Required-by-commitment`. + +## 3.5 Required state semantics + +### Goal lifecycle (minimum) +`ACTIVE | WAITING | DORMANT | SATISFIED | SUPERSEDED | ABANDONED` + +Transitions must preserve reason and lineage. `ABANDONED` does not mean failure; `SUPERSEDED` must point to successor when known. + +### Task lifecycle (minimum) +`PENDING | ACTIVE | WAITING | COMPLETED | CANCELLED | FAILED` + +Task has `revision`, `justification_ref`, optional `wake_condition_ref`, owner and completion evidence. `WAITING` must say what it waits for or be explicitly dormant at higher level. + +### Action lifecycle +`PROPOSED → VALIDATED → AUTHORIZED/REJECTED → EXECUTING → SUCCEEDED | FAILED | UNKNOWN_OUTCOME | COMPENSATED`. + +Retry preserves ActionIntent identity and creates a new attempt identity. `UNKNOWN_OUTCOME` blocks blind retry when duplicate effect is possible. + +### Commitment lifecycle +`ACTIVE | WAITING | AT_RISK | FULFILLED | RENEGOTIATED | REVOKED | EXPIRED`. + +Future promise is not fulfilled by notification delivery alone unless the promise itself was only to notify. + +### Prospective item lifecycle +`REGISTERED | ARMED | TRIGGERED | REVALIDATING | DELIVERED/STARTED | SATISFIED | SUPPRESSED | EXPIRED`. + +Trigger firing does not bypass relevance/authority revalidation. + +## 3.6 Temporal model + +Foundation must support distinguishing, where applicable: + +- event/occurrence time; +- observed time; +- recorded time; +- validity interval; +- local-time intention vs absolute instant; +- relative schedule relation (`after Event X + duration`) vs prematurely flattened timestamp. + +Do not put four timestamps on every row blindly. Use common temporal value objects/contracts only where semantics require them. + +## 3.7 Strong identifiers + +Go Core should use distinct types for at least: `IdentityID`, `ActorID`, `PrincipalID`, `ScopeID`, `SituationID`, `GoalID`, `TaskID`, `CommitmentID`, `ActionIntentID`, `ActionAttemptID`, `ArtifactID`, `CapabilityID`, plus revision types. They serialize to language-neutral primitives. + +## 3.8 Ownership baseline + +| Semantic area | Canonical owner V1 | Notes | +|---|---|---| +| Identity/Actor/Principal bindings | Identity module | Other modules reference IDs/projections only. | +| Scope/Authority grants | Authority module | Commit queries current validation; no hidden default authority. | +| Situation/focus bindings | Situation module | Derived/ephemeral portions may use cache, but durable bindings/provenance explicitly classified. | +| Goal/Task/Decision | Work module | Can later split; V1 owner chosen to preserve local consistency. | +| Commitment/Prospective items | Continuity module | Temporal is execution engine adapter, not owner. | +| Observations/Claims/Beliefs/Provenance | Epistemics module | Search/vector indexes derived. | +| Action commit state | Commit module | External executor isolated. | +| Artifact metadata/lineage | Artifact module or platform-owned artifact service | Payload storage replaceable. | +| Capability registry/bindings | Capability module | Provider-specific adapters outside semantic owner. | + +## 3.9 Consistency baseline + +- Authority validation and Action authorization: strong owner-local consistency + revision preconditions. +- Task state transitions: owner-local optimistic revision/CAS. +- Cross-owner projections: eventual/rebuildable unless an explicit use case requires stronger semantics. +- UI aggregate views: derived projections; no cross-owner canonical joins. +- Integration reactions: owner-local commit + transactional outbox; consumers idempotent. +- No global total event order requirement; causal/aggregate ordering only where needed. + + +--- + +# 4. architecture-registry.yaml + +Below is the normative starter registry. It is intentionally small. Tooling may extend the schema, but new relation types must have an enforcement/use case. + +```yaml +registry_version: 1 +semantic_generation: 1 +architecture_generation: 1 + +modules: + identity: + id: mod.identity + owns: [Identity, Actor, PrincipalBinding] + database_schema: identity + boundary_class: remote_capable + may_depend_on: [platform.ids, platform.events] + forbidden: [adapters.models, adapters.temporal] + + authority: + id: mod.authority + owns: [Scope, AuthorityGrant, AuthorityValidation] + database_schema: authority + boundary_class: remote_capable + may_depend_on: [contracts.identity, platform.ids, platform.events] + + situation: + id: mod.situation + owns: [Situation, FocusBinding] + database_schema: situation + boundary_class: remote_capable + may_depend_on: [contracts.identity, contracts.work, contracts.capability] + + work: + id: mod.work + owns: [Goal, Task, Decision] + database_schema: work + boundary_class: remote_capable + may_depend_on: [contracts.identity, contracts.authority, platform.events] + + epistemics: + id: mod.epistemics + owns: [Observation, Claim, Belief, Provenance] + database_schema: epistemics + boundary_class: remote_capable + may_depend_on: [contracts.identity, contracts.artifact, platform.events] + + continuity: + id: mod.continuity + owns: [Commitment, ProspectiveItem] + database_schema: continuity + boundary_class: remote_capable + may_depend_on: [contracts.work, contracts.identity, contracts.authority, platform.events] + + capability: + id: mod.capability + owns: [Capability, CapabilityBinding, CapabilityHealth] + database_schema: capability + boundary_class: remote_capable + may_depend_on: [contracts.identity, contracts.authority, contracts.artifact] + + commit: + id: mod.commit + owns: [ActionProposalRecord, AuthorizedAction, ActionAttemptRecord] + database_schema: commit + boundary_class: remote_capable + may_depend_on: [contracts.authority, contracts.capability, contracts.work, platform.events] + privileged: true + + artifact: + id: mod.artifact + owns: [ArtifactMetadata, ArtifactLineage] + database_schema: artifact + boundary_class: remote_capable + may_depend_on: [contracts.identity] + +platform: + allowed_packages: + - platform.ids + - platform.clock + - platform.outbox + - platform.inbox + - platform.telemetry + - platform.errors + rule: "platform owns mechanisms, never domain meaning" + +runtime_topology_v1: + core_runtime: + modules: [identity, situation, work, epistemics, capability, artifact] + continuity_runtime: + modules: [continuity] + commit_runtime: + modules: [authority, commit] + isolation_reason: [secrets, external_mutation_authority, failure_isolation] + cognitive_workers: + modules: [] + role: "replaceable cognition/context compilation workers; no canonical ownership" + +storage_topology_v1: + 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 + 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" + class: HARD + enforcement: [commit_runtime, property_tests, secret_acl] + - id: INV-OWN-001 + property: "Only semantic owner writes canonical state" + class: HARD + enforcement: [db_acl, architecture_linter] + - id: INV-CONT-001 + property: "Future obligations survive workflow engine loss" + class: HARD + enforcement: [canonical_prospective_store, recovery_test] + - id: INV-TOPO-001 + property: "Major bounded contracts preserve semantics across inproc and remote topology" + class: HARD + enforcement: [topology_equivalence_tests] + - id: INV-EPI-001 + property: "Model output cannot directly become canonical fact/action" + class: HARD + enforcement: [package_boundaries, commit_apis, semantic_validation_tests] + - id: INV-PROJ-001 + property: "Projection eventually converges from canonical state" + class: CONVERGENCE + enforcement: [projection_rebuild_tests, lag_metrics] + +protected_paths: + - architecture/ARCHITECTURE_CONSTITUTION.md + - architecture/architecture-registry.yaml + - architecture/adrs/** + - ci/architecture/** + - security/** + +default_feature_task_authority: + may_write: ["declared module paths", "module-owned tests"] + may_not_change: + - canonical_ownership + - public_cross_module_contracts + - protected_paths + - third_party_dependencies + - database_owner_roles + - privileged_capabilities +``` + +## 4.1 Registry enforcement required before large Codex implementation + +Minimum deterministic checks: + +1. Go import graph against `may_depend_on`/forbidden edges. +2. No `/modules/*/domain` imports from adapters/runtime/provider packages. +3. Migration scanner rejects cross-owner writes/FKs/triggers/views unless explicit approved exception. +4. PostgreSQL role tests prove owner isolation. +5. Diff-scope check against Codex Task Packet. +6. Protected-path rule. +7. Public contract change detection. +8. Required CI guardrail health: missing checker result is failure, not pass. + + +--- + +# 5. CODEX_DEVELOPMENT_CONTRACT.md + +## 5.1 Role + +Codex is an implementation capability. It can inspect, design locally, code, test and propose findings. It is **not** architectural authority and does not receive production authority merely because it can edit source. + +## 5.2 Task Packet schema + +Every non-trivial Codex implementation task must resolve to a packet similar to: + +```yaml +task_id: DEV-000123 +task_revision: 3 +architecture_revision: +base_repo_revision: +risk_tier: B +intent: "..." +requirement_ids: [REQ-...] +allowed_change_scope: + modules: [work] + paths: [modules/work/**, contracts/work/**] +forbidden_change_scope: + paths: [architecture/**, security/**, modules/identity/**] +allowed_architecture_delta: + new_cross_module_dependencies: 0 + new_canonical_owners: 0 + new_privileges: 0 +contracts: + consume: [contracts.identity@v1, contracts.authority@v1] + may_change: [] +semantic_owner: work +assumptions: [] +acceptance_scenarios: [SCN-...] +required_tests: [unit, contract, architecture] +terminal_gap_states: + - BLOCKED_BY_SPEC_GAP + - BLOCKED_BY_ARCHITECTURE_GAP + - BLOCKED_BY_MISSING_CAPABILITY +``` + +## 5.3 Risk tiers + +- **Tier A — module-local:** no public contract/schema ownership/privilege changes. Fast path. +- **Tier B — module contract/persistence local:** owner-local schema or stable port changes. Migration/contract tests. +- **Tier C — cross-module:** integration contracts/events/topology interactions. Independent architecture review. +- **Tier D — authority/canonical migration/external side effects:** adversarial/property/recovery tests and stricter promotion. +- **Tier E — Core/guardrails/constitutional:** proposal first; changed verifier cannot self-certify. + +## 5.4 Codex preflight for Tier B+ + +Before write, produce structured implementation plan containing: + +- modules/files expected to change; +- semantic owner(s); +- contracts consumed/changed; +- canonical state and state-machine transitions affected; +- side effects/privileges; +- persistence/migrations/events; +- failure/retry/restart semantics; +- tests to prove invariants. + +Plan is machine-checked against Task Packet. Unexpected architecture delta becomes finding/gap. + +## 5.5 Allowed local freedom + +Inside an owned module Codex may choose ordinary data structures, algorithms, file decomposition and local refactoring, provided public contracts/invariants/task scope remain intact. Architecture defines constraints, not every function. + +## 5.6 Mandatory Codex completion report + +- changed files/modules; +- contracts/schemas/events/dependencies/privileges touched; +- tests actually executed (CI is authoritative evidence); +- assumptions made; +- known limitations; +- unrelated findings not changed; +- any specification/architecture gaps. + +## 5.7 Development Commit Gate + +Candidate patch → deterministic build/tests → architecture sentinel → DB/migration checks → contract checks → risk-specific review → exact-artifact promotion/merge. + +Ordinary Codex tasks never write protected main directly. Promotion moves the evaluated artifact/commit, not a rebuilt unknown variant. + +## 5.8 Architecture Sentinel responsibilities + +Deterministically compare declared vs observed architecture: + +- import/dependency graph; +- DB writes/FKs/views/triggers; +- event publishers/subscribers; +- protected path and diff scope; +- third-party dependency diff; +- public contract diff; +- privileged secret/capability access where enforceable; +- guardrail execution health. + +AI reviewer handles semantic smells that cannot be mechanically proven: God Ports, ownership leakage, false abstraction, orchestration absorbing domain semantics, hidden co-location assumptions, unsafe retry reasoning. + +## 5.9 Architecture exceptions + +No `nolint`/architecture ignore without registered exception ID containing reason, scope, expiry/removal condition. New debt cannot be introduced silently. Existing baseline debt can be ratcheted down. + +## 5.10 Codex failure is valid + +Codex is explicitly allowed to return a typed gap rather than “make it work somehow”. A gap becomes architecture/spec/research task and may revise the original task packet. This is preferred to an unapproved shortcut. + +## 5.11 Cold Agent requirement + +At major milestones, a fresh Codex session with no conversational history must be able to explain module ownership/boundaries and implement a bounded task using canonical repo artifacts only. Critical architecture cannot live only in chat history. + + +--- + +# 6. WALKING_SKELETON_SPEC.md + +## 6.1 Goal + +Prove the Foundation through a small set of end-to-end semantic properties before broad implementation. Do not build all future modules/stubs first. + +## 6.2 Skeleton topology + +Initial process topology: + +```text +Client/Test Harness + | +Core Runtime + - Identity + - Situation + - Work + - Epistemics (minimal) + - Capability registry (minimal) + | + +---- Cognitive Worker Adapter (deterministic fake first, real model later) + | + +---- Continuity Runtime ---- Temporal adapter (optional/replaceable) + | + +---- Commit Runtime (separate process, isolated credentials) + | + Test Executor +``` + +One PostgreSQL cluster, separate owner schemas/roles. NATS may be used for integration events once owner-local outbox exists; skeleton must also prove semantics do not live only in NATS. + +## 6.3 Scenario WS-001 — Focus Isolation / wrong-project prevention + +**Given** one Actor has two simultaneously active Codex/workspace situations bound to Project/Goal contexts A and B. Both domains contain an identically named concept such as `temperature`. + +**When** the identical user phrase is sent through each session without naming the project. + +**Then** Situation resolution binds each request to its own session/workspace/project context using runtime bindings/provenance; no keyword shortcut is allowed; canonical writes carry target/revision/scope and cannot affect the other context. + +Required failure cases: +- stale focus binding; +- two worktrees of same repository; +- ambiguous target → typed ambiguity, no mutation; +- Task revision changed between reasoning and commit → commit rejected/re-evaluated. + +Acceptance property: **wrong-project mutation is mechanically prevented even if cognition proposes the wrong target**. + +## 6.4 Scenario WS-002 — Durable prospective memory independent of Temporal + +**Given** user creates a future reminder/commitment with semantic context and fulfilment condition. + +**When** canonical Continuity transaction commits, process restarts, Temporal state is deleted/unavailable, then workflow infrastructure is rebuilt. + +**Then** obligation remains in PostgreSQL canonical prospective state, is reconstructed, does not duplicate occurrence, revalidates relevance before delivery, and can distinguish `DELIVERED` from underlying `FULFILLED`. + +Failure cases: +- system offline past trigger; +- 100 duplicate trigger messages; +- goal superseded before trigger; +- delivery channel unavailable; +- recurrence missed during outage. + +Acceptance property: **loss of workflow engine does not lose or blindly replay future obligations**. + +## 6.5 Scenario WS-003 — Safe Action Commit and unknown outcome + +**Given** cognition produces a structured external mutation proposal. + +**When** Commit Runtime receives it. + +**Then** it validates current Authority, Scope, Target identity, task/goal justification and relevant revisions before producing AuthorizedAction. Only isolated Executor has external mutation credentials. + +Injected case: executor applies external effect but connection times out before acknowledgement. + +Expected result: `UNKNOWN_OUTCOME`; no blind retry; reconciliation query/verification required. A retry uses same ActionIntentID and a new AttemptID only after semantics permit. + +Acceptance property: **no external side effect can bypass authority/commit and timeout cannot cause duplicate mutation by default**. + +## 6.6 Scenario WS-004 — Topology equivalence + +Run WS-001/002/003 twice: + +1. bounded modules co-located where allowed; +2. Commit and Continuity accessed through remote adapters/processes. + +Canonical semantic outcomes/invariants must be equivalent. Timing and expected temporary unavailability may differ; domain meaning must not. + +Acceptance property: **process extraction does not require domain rewrite**. + +## 6.7 Scenario WS-005 — No-LLM degraded continuity + +Remove all cognitive providers. + +System must still: +- identify existing actors/sessions where deterministically available; +- preserve events/state; +- keep canonical commitments/prospective schedules; +- perform deterministic authority/integrity checks; +- expose degraded capability state; +- avoid inventing new AI decisions/actions. + +Acceptance property: **loss of cognition degrades intelligence, not continuity/integrity**. + +## 6.8 Scenario WS-006 — Semantic evolution drill + +Create a deliberately small v1→v2 contract/schema change in a non-critical skeleton concept. Prove: + +- old historical object remains interpretable; +- new writer/readers use new version; +- unsupported semantics are explicit, not silently defaulted; +- active live objects migrate/validate; +- derived projections rebuild; +- rollback/forward-fix mode is documented. + +Acceptance property: **architecture evolution is tested as a transformation, not merely promised**. + +## 6.9 Foundation Exit Criteria + +Foundation V1 is eligible for freeze when all are true: + +1. Fresh Codex session can implement a bounded feature from repository artifacts alone. +2. Cross-owner DB write is physically denied. +3. Wrong-project mutation fails even under adversarial cognition proposal. +4. Stale/revoked authority cannot create AuthorizedAction. +5. Duplicate/replay does not duplicate external side effects in covered semantics. +6. Future obligation survives workflow-engine loss/rebuild. +7. Core remains continuity-capable without any LLM. +8. Same semantic scenarios pass in at least two process topologies. +9. Provider can be swapped/faked without domain contract change. +10. Declared and observed architecture graphs match or all drift has registered findings. +11. One semantic contract migration drill succeeds. +12. Architecture guardrail suite proves its own critical enforcement with negative tests. + + +--- + +# 7. UNRESOLVED_QUESTIONS.md + +These questions are intentionally unresolved. Codex must not silently turn temporary assumptions into permanent architecture. + +| ID | Question | Status | Temporary V1 position | Blocking? | Revisit trigger | +|---|---|---|---|---|---| +| UQ-001 | Exact split of `Work` into Goal/Task/Decision modules | RESEARCH | One canonical Work owner for V1 | No | Contract churn/co-change/consistency pressure | +| UQ-002 | Artifact metadata as separate module vs platform service | PROPOSED | Keep separate semantic owner if persistence begins | No | First large artifact integration | +| UQ-003 | Exact NATS subject taxonomy | DEFERRED | Define only subjects required by skeleton | No | First durable cross-owner event | +| UQ-004 | Exact Temporal workflow topology | DEFERRED | Temporal adapter behind Continuity; canonical prospective state outside | No | WS-002 implementation | +| UQ-005 | gRPC vs another remote transport for internal calls | DEFERRED | Contracts language-neutral; choose simplest baseline during remote drill | No | WS-004 | +| UQ-006 | Extent of canonical event archive vs per-owner transition history | RESEARCH | Do not event-source everything; retain meaningful events/outbox | No | Replay/recovery requirements emerge | +| UQ-007 | Formal policy engine technology | LATER | Deterministic typed Authority/Commit rules in Go V1 | No | Policy complexity outgrows code safely | +| UQ-008 | Vector store implementation | LATER | Search indexes derived; no foundation dependency | No | Epistemic retrieval slice | +| UQ-009 | Redis requirement | LATER | Optional cache only; correctness must not depend on it | No | Performance evidence | +| UQ-010 | Full Identity merge/split production workflow | EXTENSION_POINT | IDs/lineage schema must not preclude it; test simplified correction later | No | Multi-source identity pressure | +| UQ-011 | Portfolio/Trajectory/Responsibility primitives promotion | RESEARCH | Not Core V1 | No | Repeated distinct lifecycle/authority need | +| UQ-012 | Self-development automation | LATER | Development Control Plane external/manual-governed | No | Foundation + safe change/recovery mature | +| UQ-013 | Multi-user organization governance | LATER | Actor/Principal/Scope designed generally; expose single-user V1 if desired | No | Actual shared workspace requirement | +| UQ-014 | Canonical wire codec longevity | RESEARCH | Versioned schema/envelope; initial protobuf acceptable, not eternal identity | No | First non-Go module or major semantic migration | +| UQ-015 | Exact data retention/privacy policy engine | RESEARCH | Store scope/provenance/sensitivity hooks; implement concrete policies per product deployment | No | Product/legal requirements | + +## 7.1 Spec-gap protocol + +If implementation encounters a semantically material unknown not listed here, Codex emits a `SpecificationGap` with: + +- exact scenario; +- current contract insufficiency; +- smallest temporary safe behavior if one exists; +- whether work can continue independently; +- proposed alternatives without choosing permanent architecture. + +No architecture gap is solved by creating an unowned table, generic JSON object, cross-module direct read/write, or hidden provider-specific field. +\n\n---\n\n# 8. IMPLEMENTATION ORDER / NEXT TASKS\n\nThe first implementation sequence after this freeze pack is deliberately narrow:\n\n1. Create repository skeleton and protected architecture paths.\n2. Implement Architecture Registry validator and Go import boundary linter.\n3. Create PostgreSQL schemas/roles and negative ACL tests.\n4. Define strong IDs, revision/envelope primitives and platform outbox/inbox foundations.\n5. Define V1 semantic state machines and owner-local repository contracts for WS-001..003 only.\n6. Implement deterministic/fake cognition adapter and typed proposals.\n7. Build WS-001 Focus Isolation end-to-end.\n8. Build isolated Commit/Test-Executor path and WS-003.\n9. Build canonical Continuity/Prospective subset and WS-002.\n10. Add in-process/remote adapter contract suites and execute WS-004.\n11. Execute no-LLM and semantic migration drills WS-005/006.\n12. Only after Foundation Exit Criteria pass, expand into broader Epistemics/Context Compiler/Codex integration.\n\n**Codex must not be given a task like “implement the whole assistant”.** Each step above becomes a versioned Task Packet with a bounded architecture delta.\n diff --git a/ci/architecture/README.md b/ci/architecture/README.md new file mode 100644 index 0000000..213e5f3 --- /dev/null +++ b/ci/architecture/README.md @@ -0,0 +1,3 @@ +# Protected architecture CI + +Changes here are Tier E. A feature task may not weaken or skip architecture checks. Missing guardrail execution is a failure, not a pass. diff --git a/ci/architecture/protected-paths.txt b/ci/architecture/protected-paths.txt new file mode 100644 index 0000000..581128a --- /dev/null +++ b/ci/architecture/protected-paths.txt @@ -0,0 +1,6 @@ +architecture/ARCHITECTURE_CONSTITUTION.md +architecture/architecture-registry.yaml +architecture/architecture-registry.json +architecture/adrs/ +ci/architecture/ +security/ diff --git a/cmd/cognitive-worker/main.go b/cmd/cognitive-worker/main.go new file mode 100644 index 0000000..8614bff --- /dev/null +++ b/cmd/cognitive-worker/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "encoding/json" + "git.cin.su/m/Assistant/adapters/models" + "git.cin.su/m/Assistant/runtime/health" + "log" + "net/http" + "os" +) + +func main() { + addr := os.Getenv("LISTEN_ADDR") + if addr == "" { + addr = ":8083" + } + mux := http.NewServeMux() + mux.Handle("/healthz", health.Handler{Runtime: "cognitive-worker", SemanticGeneration: 1, ArchitectureGeneration: 1}) + mux.HandleFunc("POST /v1/proposals/deterministic", deterministicProposal) + log.Printf("cognitive-worker listening on %s", addr) + if err := http.ListenAndServe(addr, mux); err != nil { + log.Fatal(err) + } +} + +func deterministicProposal(w http.ResponseWriter, r *http.Request) { + var request models.DeterministicRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + http.Error(w, "invalid deterministic proposal request", http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(models.Propose(request)) +} diff --git a/cmd/commit-runtime/main.go b/cmd/commit-runtime/main.go new file mode 100644 index 0000000..5658097 --- /dev/null +++ b/cmd/commit-runtime/main.go @@ -0,0 +1,131 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "net/url" + "os" + + "git.cin.su/m/Assistant/adapters/postgres" + "git.cin.su/m/Assistant/contracts/authority" + commitcontract "git.cin.su/m/Assistant/contracts/commit" + "git.cin.su/m/Assistant/contracts/identity" + authoritydomain "git.cin.su/m/Assistant/modules/authority/domain" + commitdomain "git.cin.su/m/Assistant/modules/commit/domain" + "git.cin.su/m/Assistant/runtime/health" +) + +type authorizationRequest struct { + Proposal commitcontract.ActionProposal + AuthorizedID commitcontract.AuthorizedActionID + GrantID authority.GrantID + PrincipalID identity.PrincipalID +} +type authorityValidator struct{} + +func (authorityValidator) Validate(context authority.AuthorizationContext) error { + return authoritydomain.ValidateGrant(context.Binding, context.Scope, context.Grant) +} + +func main() { + addr := os.Getenv("LISTEN_ADDR") + if addr == "" { + addr = ":8082" + } + config := postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE")} + config.User, config.Password = os.Getenv("POSTGRES_USER"), os.Getenv("POSTGRES_PASSWORD") + repository, err := postgres.OpenCommit(context.Background(), config) + if err != nil { + log.Fatal(err) + } + defer repository.Close() + config.User, config.Password = os.Getenv("AUTHORITY_POSTGRES_USER"), os.Getenv("AUTHORITY_POSTGRES_PASSWORD") + authorityRepository, err := postgres.OpenAuthority(context.Background(), config) + if err != nil { + log.Fatal(err) + } + defer authorityRepository.Close() + coreURL := os.Getenv("CORE_RUNTIME_URL") + if coreURL == "" { + log.Fatal("CORE_RUNTIME_URL is required") + } + mux := http.NewServeMux() + mux.Handle("/healthz", health.Handler{Runtime: "commit-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1}) + mux.HandleFunc("POST /v1/authorized-actions", func(w http.ResponseWriter, r *http.Request) { + var request authorizationRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + http.Error(w, "invalid authorization request", 400) + return + } + grant, scope, err := authorityRepository.LoadGrant(r.Context(), request.GrantID) + if err != nil { + http.Error(w, "authority grant not found", http.StatusConflict) + return + } + binding, err := loadJSON[identity.PrincipalBinding](r.Context(), coreURL+"/v1/principal-bindings/"+url.PathEscape(string(request.Proposal.ActorID))+"/"+url.PathEscape(string(request.PrincipalID))) + if err != nil { + http.Error(w, "identity binding unavailable", http.StatusConflict) + return + } + target, err := loadJSON[struct { + TaskID string + GoalID string + ScopeID string + Revision uint64 + }](r.Context(), coreURL+"/v1/tasks/"+url.PathEscape(string(request.Proposal.Target.TaskID))) + if err != nil || target.TaskID != string(request.Proposal.Target.TaskID) || target.GoalID != string(request.Proposal.Target.GoalID) || target.ScopeID != string(request.Proposal.Target.ScopeID) || target.Revision != uint64(request.Proposal.Target.Revision) { + http.Error(w, "work target is stale or unavailable", http.StatusConflict) + return + } + action, err := commitdomain.Authorize(request.Proposal, commitdomain.AuthorizationContext{Binding: binding, Scope: scope, Grant: grant}, authorityValidator{}, request.AuthorizedID) + if err != nil { + http.Error(w, "authorization denied", http.StatusConflict) + return + } + if err := repository.SaveAuthorized(r.Context(), action); err != nil { + http.Error(w, "save authorized action", 500) + return + } + w.WriteHeader(http.StatusCreated) + }) + mux.HandleFunc("POST /v1/action-attempts", func(w http.ResponseWriter, r *http.Request) { + var attempt commitcontract.ActionAttempt + if err := json.NewDecoder(r.Body).Decode(&attempt); err != nil { + http.Error(w, "invalid action attempt", 400) + return + } + if attempt.State != commitcontract.ActionUnknownOutcome && attempt.State != commitcontract.ActionSucceeded && attempt.State != commitcontract.ActionFailed { + http.Error(w, "attempt must have terminal outcome", http.StatusConflict) + return + } + if err := repository.SaveAttempt(r.Context(), attempt); err != nil { + http.Error(w, "save action attempt", 500) + return + } + w.WriteHeader(http.StatusCreated) + }) + log.Printf("commit-runtime listening on %s", addr) + if err := http.ListenAndServe(addr, mux); err != nil { + log.Fatal(err) + } +} + +func loadJSON[T any](ctx context.Context, endpoint string) (T, error) { + var value T + request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return value, err + } + response, err := http.DefaultClient.Do(request) + if err != nil { + return value, err + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + return value, fmt.Errorf("remote query status %d", response.StatusCode) + } + return value, json.NewDecoder(response.Body).Decode(&value) +} diff --git a/cmd/continuity-runtime/main.go b/cmd/continuity-runtime/main.go new file mode 100644 index 0000000..d8dd216 --- /dev/null +++ b/cmd/continuity-runtime/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "context" + "encoding/json" + "git.cin.su/m/Assistant/adapters/postgres" + "git.cin.su/m/Assistant/contracts/continuity" + "git.cin.su/m/Assistant/runtime/health" + "log" + "net/http" + "os" +) + +func main() { + addr := os.Getenv("LISTEN_ADDR") + if addr == "" { + addr = ":8081" + } + repository, err := postgres.OpenContinuity(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) + } + defer repository.Close() + mux := http.NewServeMux() + mux.Handle("/healthz", health.Handler{Runtime: "continuity-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1}) + mux.HandleFunc("POST /v1/prospective-items", func(w http.ResponseWriter, r *http.Request) { + var item continuity.ProspectiveItem + if err := json.NewDecoder(r.Body).Decode(&item); err != nil { + http.Error(w, "invalid prospective item", http.StatusBadRequest) + return + } + if err := repository.Save(r.Context(), item); err != nil { + http.Error(w, "save prospective item", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusCreated) + }) + mux.HandleFunc("GET /v1/prospective-items/{id}", func(w http.ResponseWriter, r *http.Request) { + item, err := repository.Load(r.Context(), continuity.ProspectiveItemID(r.PathValue("id"))) + if err != nil { + http.Error(w, "prospective item not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(item) + }) + log.Printf("continuity-runtime listening on %s", addr) + if err := http.ListenAndServe(addr, mux); err != nil { + log.Fatal(err) + } +} diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go new file mode 100644 index 0000000..dc5e400 --- /dev/null +++ b/cmd/core-runtime/main.go @@ -0,0 +1,222 @@ +package main + +import ( + "context" + "encoding/json" + "git.cin.su/m/Assistant/adapters/postgres" + "git.cin.su/m/Assistant/contracts/capability" + "git.cin.su/m/Assistant/contracts/identity" + "git.cin.su/m/Assistant/contracts/situation" + "git.cin.su/m/Assistant/contracts/work" + capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain" + identitydomain "git.cin.su/m/Assistant/modules/identity/domain" + situationdomain "git.cin.su/m/Assistant/modules/situation/domain" + "git.cin.su/m/Assistant/runtime/health" + "log" + "net/http" + "os" + "strings" + "time" +) + +func main() { + addr := os.Getenv("LISTEN_ADDR") + if addr == "" { + addr = ":8080" + } + bootstrapToken, sessionKey := os.Getenv("CREATOR_BOOTSTRAP_TOKEN"), []byte(os.Getenv("CREATOR_SESSION_SIGNING_KEY")) + 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) + } + defer repository.Close() + workRepository, err := postgres.OpenWork(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("WORK_POSTGRES_USER"), Password: os.Getenv("WORK_POSTGRES_PASSWORD")}) + if err != nil { + log.Fatal(err) + } + defer workRepository.Close() + capabilityRepository, err := postgres.OpenCapability(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("CAPABILITY_POSTGRES_USER"), Password: os.Getenv("CAPABILITY_POSTGRES_PASSWORD")}) + if err != nil { + log.Fatal(err) + } + defer capabilityRepository.Close() + identityRepository, err := postgres.OpenIdentity(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("IDENTITY_POSTGRES_USER"), Password: os.Getenv("IDENTITY_POSTGRES_PASSWORD")}) + if err != nil { + log.Fatal(err) + } + defer identityRepository.Close() + mux := http.NewServeMux() + mux.Handle("/healthz", health.Handler{Runtime: "core-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1}) + mux.HandleFunc("POST /v1/creator/sessions", func(w http.ResponseWriter, r *http.Request) { + if !identitydomain.BootstrapMatches(bootstrapToken, bearerToken(r)) { + http.Error(w, "creator authentication denied", http.StatusUnauthorized) + return + } + token, session, err := identitydomain.IssueCreatorSession(sessionKey, time.Now(), 8*time.Hour) + if err != nil { + http.Error(w, "creator session unavailable", http.StatusServiceUnavailable) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(struct { + Token string + Session identity.CreatorSession + }{token, session}) + }) + mux.HandleFunc("GET /v1/creator/sessions/current", func(w http.ResponseWriter, r *http.Request) { + session, err := identitydomain.ValidateCreatorSession(sessionKey, bearerToken(r), time.Now()) + if err != nil { + http.Error(w, "creator session denied", http.StatusUnauthorized) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(session) + }) + mux.HandleFunc("GET /v1/principal-bindings/{actor}/{principal}", func(w http.ResponseWriter, r *http.Request) { + binding, err := identityRepository.LoadBinding(r.Context(), identity.ActorID(r.PathValue("actor")), identity.PrincipalID(r.PathValue("principal"))) + if err != nil { + http.Error(w, "principal binding not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(binding) + }) + mux.HandleFunc("GET /v1/tasks/{id}", func(w http.ResponseWriter, r *http.Request) { + target, err := workRepository.Load(r.Context(), work.TaskID(r.PathValue("id"))) + if err != nil { + http.Error(w, "task not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(target) + }) + mux.HandleFunc("POST /v1/capabilities/health", func(w http.ResponseWriter, r *http.Request) { + var status capability.Health + if err := json.NewDecoder(r.Body).Decode(&status); err != nil { + http.Error(w, "invalid capability health", http.StatusBadRequest) + return + } + if err := capabilitydomain.ValidateHealth(status); err != nil { + http.Error(w, "invalid capability health", http.StatusBadRequest) + return + } + if err := capabilityRepository.SaveHealth(r.Context(), status); err != nil { + http.Error(w, "save capability health", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusCreated) + }) + mux.HandleFunc("GET /v1/capabilities/{id}/health", func(w http.ResponseWriter, r *http.Request) { + status, err := capabilityRepository.LoadHealth(r.Context(), capability.CapabilityID(r.PathValue("id"))) + if err != nil { + http.Error(w, "capability health not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(status) + }) + mux.HandleFunc("POST /v1/capabilities/health-projection/rebuild", func(w http.ResponseWriter, r *http.Request) { + rebuilt, err := capabilityRepository.RebuildHealthProjection(r.Context()) + if err != nil { + http.Error(w, "rebuild capability health projection", http.StatusInternalServerError) + return + } + _ = json.NewEncoder(w).Encode(struct{ Rebuilt int64 }{rebuilt}) + }) + mux.HandleFunc("POST /v1/focus-bindings", func(w http.ResponseWriter, r *http.Request) { + var binding situation.FocusBinding + if err := json.NewDecoder(r.Body).Decode(&binding); err != nil { + http.Error(w, "invalid focus binding", http.StatusBadRequest) + return + } + if err := repository.Save(r.Context(), binding); err != nil { + http.Error(w, "save focus binding", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusCreated) + }) + mux.HandleFunc("GET /v1/focus", func(w http.ResponseWriter, r *http.Request) { + runtime := situation.RuntimeBinding{SessionID: situation.SessionID(r.URL.Query().Get("session_id")), Workspace: situation.WorkspaceID(r.URL.Query().Get("workspace_id"))} + bindings, err := repository.ByRuntime(r.Context(), runtime) + if err != nil { + http.Error(w, "load focus", http.StatusInternalServerError) + return + } + focus, err := situationdomain.NewResolver(bindings).Resolve(runtime) + if err != nil { + http.Error(w, err.Error(), http.StatusConflict) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(focus) + }) + mux.HandleFunc("POST /v1/tasks", func(w http.ResponseWriter, r *http.Request) { + var target work.TaskTarget + if err := json.NewDecoder(r.Body).Decode(&target); err != nil { + http.Error(w, "invalid task", 400) + return + } + if err := workRepository.Create(r.Context(), target); err != nil { + http.Error(w, "create task", 500) + return + } + w.WriteHeader(http.StatusCreated) + }) + mux.HandleFunc("POST /v1/tasks/{id}/commit", func(w http.ResponseWriter, r *http.Request) { + var target work.TaskTarget + if err := json.NewDecoder(r.Body).Decode(&target); err != nil { + http.Error(w, "invalid task commit", 400) + return + } + target.TaskID = work.TaskID(r.PathValue("id")) + revision, err := workRepository.Commit(r.Context(), target) + if err != nil { + http.Error(w, "task revision conflict", http.StatusConflict) + return + } + _ = json.NewEncoder(w).Encode(struct{ Revision work.TaskRevision }{revision}) + }) + mux.HandleFunc("POST /v1/focus-task-commits", func(w http.ResponseWriter, r *http.Request) { + var request struct { + SessionID situation.SessionID + Workspace situation.WorkspaceID + Target work.TaskTarget + } + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + http.Error(w, "invalid guarded commit", http.StatusBadRequest) + return + } + bindings, err := repository.ByRuntime(r.Context(), situation.RuntimeBinding{SessionID: request.SessionID, Workspace: request.Workspace}) + if err != nil { + http.Error(w, "load focus", http.StatusInternalServerError) + return + } + focus, err := situationdomain.NewResolver(bindings).Resolve(situation.RuntimeBinding{SessionID: request.SessionID, Workspace: request.Workspace}) + if err != nil { + http.Error(w, "focus unavailable", http.StatusConflict) + return + } + if err := situationdomain.ValidateTaskTarget(focus, request.Target); err != nil { + http.Error(w, "target outside resolved focus", http.StatusConflict) + return + } + revision, err := workRepository.Commit(r.Context(), request.Target) + if err != nil { + http.Error(w, "task revision conflict", http.StatusConflict) + return + } + _ = json.NewEncoder(w).Encode(struct{ Revision work.TaskRevision }{revision}) + }) + log.Printf("core-runtime listening on %s", addr) + if err := http.ListenAndServe(addr, mux); err != nil { + log.Fatal(err) + } +} + +func bearerToken(r *http.Request) string { + value := r.Header.Get("Authorization") + if !strings.HasPrefix(value, "Bearer ") { + return "" + } + return strings.TrimPrefix(value, "Bearer ") +} diff --git a/cmd/devctl/architecture_test.go b/cmd/devctl/architecture_test.go new file mode 100644 index 0000000..f4d03ce --- /dev/null +++ b/cmd/devctl/architecture_test.go @@ -0,0 +1,70 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestArchitectureSentinelNegativeCases(t *testing.T) { + tests := []struct { + name string + importPath string + want string + }{ + {"domain infrastructure import", "git.cin.su/m/Assistant/adapters/postgres", "domain imports forbidden /adapters/"}, + {"cross-module implementation import", "git.cin.su/m/Assistant/modules/beta/domain", "cross-module implementation import beta"}, + {"undeclared contract dependency", "git.cin.su/m/Assistant/contracts/beta", "undeclared contract dependency contracts.beta"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + root := architectureFixture(t, test.importPath) + err := checkArchitectureAt(root) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("sentinel error = %v", err) + } + }) + } +} + +func TestArchitectureSentinelAcceptsDeclaredContractDependency(t *testing.T) { + root := architectureFixture(t, "git.cin.su/m/Assistant/contracts/beta") + registry := `{"registry_version":1,"semantic_generation":1,"architecture_generation":1,"modules":{"alpha":{"id":"mod.alpha","owns":[],"database_schema":"alpha","boundary_class":"remote_capable","may_depend_on":["contracts.beta"]},"beta":{"id":"mod.beta","owns":[],"database_schema":"beta","boundary_class":"remote_capable","may_depend_on":[]}}}` + writeFixture(t, filepath.Join(root, "architecture", "architecture-registry.json"), registry) + if err := checkArchitectureAt(root); err != nil { + t.Fatalf("declared dependency rejected: %v", err) + } +} + +func TestPreflightRejectsProtectedPathBelowTierE(t *testing.T) { + registry := Registry{ProtectedPaths: []string{"architecture/**", "security/**"}} + taskPacket := TaskPacket{TaskID: "DEV-bad", RiskTier: "C", Intent: "weaken guardrail"} + taskPacket.AllowedChangeScope.Paths = []string{"architecture/architecture-registry.json"} + if err := validateTaskScope(taskPacket, registry); err == nil || !strings.Contains(err.Error(), "protected path") { + t.Fatalf("protected path task error = %v", err) + } + taskPacket.RiskTier = "E" + if err := validateTaskScope(taskPacket, registry); err != nil { + t.Fatalf("Tier E protected path rejected: %v", err) + } +} + +func architectureFixture(t *testing.T, importPath string) string { + t.Helper() + root := t.TempDir() + registry := `{"registry_version":1,"semantic_generation":1,"architecture_generation":1,"modules":{"alpha":{"id":"mod.alpha","owns":[],"database_schema":"alpha","boundary_class":"remote_capable","may_depend_on":[]},"beta":{"id":"mod.beta","owns":[],"database_schema":"beta","boundary_class":"remote_capable","may_depend_on":[]}}}` + writeFixture(t, filepath.Join(root, "architecture", "architecture-registry.json"), registry) + writeFixture(t, filepath.Join(root, "modules", "alpha", "domain", "fixture.go"), "package domain\nimport _ \""+importPath+"\"\n") + return root +} + +func writeFixture(t *testing.T, path, content string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatal(err) + } +} diff --git a/cmd/devctl/main.go b/cmd/devctl/main.go new file mode 100644 index 0000000..5fac406 --- /dev/null +++ b/cmd/devctl/main.go @@ -0,0 +1,244 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "go/parser" + "go/token" + "io/fs" + "os" + "os/exec" + "path/filepath" + "strings" +) + +type Registry struct { + RegistryVersion int `json:"registry_version"` + SemanticGeneration int `json:"semantic_generation"` + ArchitectureGeneration int `json:"architecture_generation"` + Modules map[string]struct { + ID string `json:"id"` + Owns []string `json:"owns"` + DatabaseSchema string `json:"database_schema"` + BoundaryClass string `json:"boundary_class"` + MayDependOn []string `json:"may_depend_on"` + Forbidden []string `json:"forbidden"` + } `json:"modules"` + ProtectedPaths []string `json:"protected_paths"` +} + +type TaskPacket struct { + TaskID string `json:"task_id"` + RiskTier string `json:"risk_tier"` + Intent string `json:"intent"` + AllowedChangeScope struct { + Paths []string `json:"paths"` + } `json:"allowed_change_scope"` +} + +func main() { + if len(os.Args) < 2 { + usage() + os.Exit(2) + } + var err error + switch os.Args[1] { + case "doctor": + err = doctor() + case "check-architecture": + err = checkArchitecture() + case "show-registry": + err = showRegistry() + case "preflight": + err = preflight(os.Args[2:]) + default: + usage() + os.Exit(2) + } + if err != nil { + fmt.Fprintln(os.Stderr, "devctl:", err) + os.Exit(1) + } +} + +func usage() { + fmt.Println("devctl doctor | check-architecture | show-registry | preflight -task ") +} + +func loadRegistry() (Registry, error) { + b, err := os.ReadFile("architecture/architecture-registry.json") + if err != nil { + return Registry{}, err + } + var r Registry + if err = json.Unmarshal(b, &r); err != nil { + return Registry{}, err + } + return r, nil +} + +func doctor() error { + r, err := loadRegistry() + if err != nil { + return err + } + fmt.Printf("architecture generation=%d semantic generation=%d modules=%d\n", r.ArchitectureGeneration, r.SemanticGeneration, len(r.Modules)) + for _, tool := range []string{"git", "go", "docker"} { + p, e := exec.LookPath(tool) + if e != nil { + fmt.Printf("WARN: %s not found\n", tool) + } else { + fmt.Printf("OK: %s -> %s\n", tool, p) + } + } + cmd := exec.Command("git", "remote", "get-url", "origin") + out, e := cmd.CombinedOutput() + if e == nil { + fmt.Printf("git origin: %s", out) + } else { + fmt.Println("WARN: git origin not configured; run scripts/bootstrap.ps1") + } + return nil +} + +func showRegistry() error { + r, e := loadRegistry() + if e != nil { + return e + } + b, _ := json.MarshalIndent(r, "", " ") + fmt.Println(string(b)) + return nil +} + +func checkArchitecture() error { + return checkArchitectureAt(".") +} + +func checkArchitectureAt(root string) error { + b, err := os.ReadFile(filepath.Join(root, "architecture", "architecture-registry.json")) + if err != nil { + return err + } + var r Registry + if err := json.Unmarshal(b, &r); err != nil { + return err + } + var violations []string + err = filepath.WalkDir(filepath.Join(root, "modules"), func(path string, d fs.DirEntry, e error) error { + if e != nil { + return e + } + if d.IsDir() || !strings.HasSuffix(path, ".go") { + return nil + } + relative, err := filepath.Rel(root, path) + if err != nil { + return err + } + parts := strings.Split(filepath.ToSlash(relative), "/") + if len(parts) < 3 { + return nil + } + owner := parts[1] + module, ok := r.Modules[owner] + if !ok { + violations = append(violations, relative+": unknown module owner "+owner) + return nil + } + parsed, parseErr := parser.ParseFile(token.NewFileSet(), path, nil, parser.ImportsOnly) + if parseErr != nil { + return parseErr + } + allowed := map[string]bool{} + for _, dependency := range module.MayDependOn { + allowed[dependency] = true + } + for _, imported := range parsed.Imports { + importPath := strings.Trim(imported.Path.Value, "\"") + if strings.Contains(filepath.ToSlash(relative), "/domain/") { + for _, bad := range []string{"/adapters/", "/runtime/", "/cmd/"} { + if strings.Contains(importPath, "git.cin.su/m/Assistant"+bad) { + violations = append(violations, relative+": domain imports forbidden "+bad) + } + } + } + modulePrefix := "git.cin.su/m/Assistant/modules/" + if strings.HasPrefix(importPath, modulePrefix) { + other := strings.Split(strings.TrimPrefix(importPath, modulePrefix), "/")[0] + if other != owner { + violations = append(violations, relative+": cross-module implementation import "+other) + } + } + contractPrefix := "git.cin.su/m/Assistant/contracts/" + if strings.HasPrefix(importPath, contractPrefix) { + contractOwner := strings.Split(strings.TrimPrefix(importPath, contractPrefix), "/")[0] + if contractOwner != owner && !allowed["contracts."+contractOwner] { + violations = append(violations, relative+": undeclared contract dependency contracts."+contractOwner) + } + } + } + return nil + }) + if err != nil { + return err + } + if len(violations) > 0 { + for _, v := range violations { + fmt.Println("VIOLATION:", v) + } + return fmt.Errorf("%d architecture violation(s): %s", len(violations), strings.Join(violations, "; ")) + } + fmt.Println("architecture checks: OK") + return nil +} + +func preflight(args []string) error { + var task string + for i := 0; i < len(args); i++ { + if args[i] == "-task" && i+1 < len(args) { + task = args[i+1] + i++ + } + } + if task == "" { + return errors.New("missing -task ") + } + b, err := os.ReadFile(task) + if err != nil { + return err + } + var taskPacket TaskPacket + if err = json.Unmarshal(b, &taskPacket); err != nil { + return fmt.Errorf("task packet must be valid JSON in V1: %w", err) + } + if taskPacket.TaskID == "" || taskPacket.Intent == "" || taskPacket.RiskTier == "" { + return errors.New("task packet missing task_id, intent, or risk_tier") + } + registry, err := loadRegistry() + if err != nil { + return err + } + if err := validateTaskScope(taskPacket, registry); err != nil { + return err + } + fmt.Printf("task preflight loaded: %s tier=%s\n", taskPacket.TaskID, taskPacket.RiskTier) + return checkArchitecture() +} + +func validateTaskScope(taskPacket TaskPacket, registry Registry) error { + if taskPacket.RiskTier == "E" { + return nil + } + for _, allowed := range taskPacket.AllowedChangeScope.Paths { + allowed = filepath.ToSlash(allowed) + for _, protected := range registry.ProtectedPaths { + prefix := strings.TrimSuffix(filepath.ToSlash(protected), "/**") + if allowed == prefix || strings.HasPrefix(allowed, prefix+"/") { + return fmt.Errorf("task %s tier %s may not write protected path %s", taskPacket.TaskID, taskPacket.RiskTier, allowed) + } + } + } + return nil +} diff --git a/contracts/artifact/doc.go b/contracts/artifact/doc.go new file mode 100644 index 0000000..a07d9f1 --- /dev/null +++ b/contracts/artifact/doc.go @@ -0,0 +1,3 @@ +package artifact + +// Package artifact contains stable boundary contract types for the artifact semantic owner. diff --git a/contracts/artifact/types.go b/contracts/artifact/types.go new file mode 100644 index 0000000..a70b06c --- /dev/null +++ b/contracts/artifact/types.go @@ -0,0 +1,5 @@ +package artifact + +import "git.cin.su/m/Assistant/platform/ids" + +type ArtifactID ids.ID diff --git a/contracts/authority/doc.go b/contracts/authority/doc.go new file mode 100644 index 0000000..f1e0e16 --- /dev/null +++ b/contracts/authority/doc.go @@ -0,0 +1,3 @@ +package authority + +// Package authority contains stable boundary contract types for the authority semantic owner. diff --git a/contracts/authority/types.go b/contracts/authority/types.go new file mode 100644 index 0000000..2be4ee9 --- /dev/null +++ b/contracts/authority/types.go @@ -0,0 +1,40 @@ +package authority + +import ( + "git.cin.su/m/Assistant/contracts/identity" + "git.cin.su/m/Assistant/platform/ids" +) + +type ScopeID ids.ID +type GrantID ids.ID +type AuthorityRevision uint64 + +type GrantClass string + +const GrantClassCreator GrantClass = "CREATOR" + +type Scope struct { + ID ScopeID + Global bool + Label string +} + +// AuthorityGrant is an explicit, revisable authority record. It does not +// derive authority from a capability, provider, or product display name. +type AuthorityGrant struct { + ID GrantID + ActorID identity.ActorID + PrincipalID identity.PrincipalID + ScopeID ScopeID + Class GrantClass + Revision AuthorityRevision + Active bool +} + +// AuthorizationContext is the Authority owner's serializable validation +// input. Consumers do not depend directly on Identity's contract. +type AuthorizationContext struct { + Binding identity.PrincipalBinding + Scope Scope + Grant AuthorityGrant +} diff --git a/contracts/capability/doc.go b/contracts/capability/doc.go new file mode 100644 index 0000000..a8f596f --- /dev/null +++ b/contracts/capability/doc.go @@ -0,0 +1,3 @@ +package capability + +// Package capability contains stable boundary contract types for the capability semantic owner. diff --git a/contracts/capability/types.go b/contracts/capability/types.go new file mode 100644 index 0000000..46d5c32 --- /dev/null +++ b/contracts/capability/types.go @@ -0,0 +1,39 @@ +package capability + +import "git.cin.su/m/Assistant/platform/ids" + +type CapabilityID ids.ID +type CapabilityBindingID ids.ID + +type HealthState string + +const ( + HealthAvailable HealthState = "AVAILABLE" + HealthDegraded HealthState = "DEGRADED" + HealthUnavailable HealthState = "UNAVAILABLE" +) + +type HealthReason string + +const ( + ReasonCognitiveProvidersUnavailable HealthReason = "COGNITIVE_PROVIDERS_UNAVAILABLE" + ReasonNone HealthReason = "NONE" +) + +type HealthSemanticVersion int + +const ( + HealthV1 HealthSemanticVersion = 1 + HealthV2 HealthSemanticVersion = 2 +) + +// Health is the Capability owner's current, explicit statement about whether +// a capability can be used. It is not an authority grant. +type Health struct { + CapabilityID CapabilityID + State HealthState + Reason HealthReason + Revision int64 + SemanticVersion HealthSemanticVersion + ObservedAtUnixMillis *int64 +} diff --git a/contracts/commit/doc.go b/contracts/commit/doc.go new file mode 100644 index 0000000..3e91535 --- /dev/null +++ b/contracts/commit/doc.go @@ -0,0 +1,3 @@ +package commit + +// Package commit contains stable boundary contract types for the commit semantic owner. diff --git a/contracts/commit/types.go b/contracts/commit/types.go new file mode 100644 index 0000000..519b22f --- /dev/null +++ b/contracts/commit/types.go @@ -0,0 +1,51 @@ +package commit + +import ( + "git.cin.su/m/Assistant/contracts/authority" + "git.cin.su/m/Assistant/contracts/capability" + "git.cin.su/m/Assistant/contracts/identity" + "git.cin.su/m/Assistant/contracts/work" + "git.cin.su/m/Assistant/platform/ids" +) + +type ActionIntentID ids.ID +type ActionAttemptID ids.ID +type AttemptID = ActionAttemptID +type AuthorizedActionID ids.ID + +type ActionState string + +const ( + ActionProposed ActionState = "PROPOSED" + ActionAuthorized ActionState = "AUTHORIZED" + ActionExecuting ActionState = "EXECUTING" + ActionSucceeded ActionState = "SUCCEEDED" + ActionFailed ActionState = "FAILED" + ActionUnknownOutcome ActionState = "UNKNOWN_OUTCOME" +) + +type ExternalOperation string + +type ActionProposal struct { + IntentID ActionIntentID + ActorID identity.ActorID + CapabilityID capability.CapabilityID + Target work.TaskTarget + Operation ExternalOperation + State ActionState +} + +type AuthorizedAction struct { + ID AuthorizedActionID + Proposal ActionProposal + GrantID authority.GrantID + ScopeID authority.ScopeID + State ActionState +} + +type ActionAttempt struct { + ID ActionAttemptID + AuthorizedID AuthorizedActionID + IntentID ActionIntentID + State ActionState +} diff --git a/contracts/continuity/doc.go b/contracts/continuity/doc.go new file mode 100644 index 0000000..18f4914 --- /dev/null +++ b/contracts/continuity/doc.go @@ -0,0 +1,3 @@ +package continuity + +// Package continuity contains stable boundary contract types for the continuity semantic owner. diff --git a/contracts/continuity/types.go b/contracts/continuity/types.go new file mode 100644 index 0000000..84bd7fe --- /dev/null +++ b/contracts/continuity/types.go @@ -0,0 +1,38 @@ +package continuity + +import ( + "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/ids" +) + +type CommitmentID ids.ID +type ProspectiveItemID ids.ID +type ProspectiveRevision uint64 + +type ProspectiveState string + +const ( + ProspectiveRegistered ProspectiveState = "REGISTERED" + ProspectiveArmed ProspectiveState = "ARMED" + ProspectiveTriggered ProspectiveState = "TRIGGERED" + ProspectiveRevalidating ProspectiveState = "REVALIDATING" + ProspectiveDelivered ProspectiveState = "DELIVERED" + ProspectiveSatisfied ProspectiveState = "SATISFIED" + ProspectiveSuppressed ProspectiveState = "SUPPRESSED" + ProspectiveExpired ProspectiveState = "EXPIRED" +) + +// ProspectiveItem is canonical future relevance. TriggerAtUnixMillis is an +// absolute instant; scheduling engines are derived delivery mechanisms only. +type ProspectiveItem struct { + ID ProspectiveItemID + CommitmentID CommitmentID + OwnerPrincipalID identity.PrincipalID + ScopeID authority.ScopeID + GoalID work.GoalID + TriggerAtUnixMillis int64 + State ProspectiveState + Revision ProspectiveRevision +} diff --git a/contracts/epistemics/doc.go b/contracts/epistemics/doc.go new file mode 100644 index 0000000..ad16a8a --- /dev/null +++ b/contracts/epistemics/doc.go @@ -0,0 +1,3 @@ +package epistemics + +// Package epistemics contains stable boundary contract types for the epistemics semantic owner. diff --git a/contracts/epistemics/types.go b/contracts/epistemics/types.go new file mode 100644 index 0000000..94e6d6b --- /dev/null +++ b/contracts/epistemics/types.go @@ -0,0 +1,7 @@ +package epistemics + +import "git.cin.su/m/Assistant/platform/ids" + +type ObservationID ids.ID +type ClaimID ids.ID +type BeliefID ids.ID diff --git a/contracts/identity/doc.go b/contracts/identity/doc.go new file mode 100644 index 0000000..be5fc98 --- /dev/null +++ b/contracts/identity/doc.go @@ -0,0 +1,3 @@ +package identity + +// Package identity contains stable boundary contract types for the identity semantic owner. diff --git a/contracts/identity/session.go b/contracts/identity/session.go new file mode 100644 index 0000000..8b07c7d --- /dev/null +++ b/contracts/identity/session.go @@ -0,0 +1,7 @@ +package identity + +type CreatorSession struct { + ActorID ActorID + PrincipalID PrincipalID + ExpiresAtUnix int64 +} diff --git a/contracts/identity/types.go b/contracts/identity/types.go new file mode 100644 index 0000000..82da84e --- /dev/null +++ b/contracts/identity/types.go @@ -0,0 +1,15 @@ +package identity + +import "git.cin.su/m/Assistant/platform/ids" + +type ActorID ids.ID +type PrincipalID ids.ID +type IdentityID ids.ID + +// 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 { + ActorID ActorID + PrincipalID PrincipalID + Active bool +} diff --git a/contracts/situation/doc.go b/contracts/situation/doc.go new file mode 100644 index 0000000..139c235 --- /dev/null +++ b/contracts/situation/doc.go @@ -0,0 +1,3 @@ +package situation + +// Package situation contains stable boundary contract types for the situation semantic owner. diff --git a/contracts/situation/types.go b/contracts/situation/types.go new file mode 100644 index 0000000..a2a6aa2 --- /dev/null +++ b/contracts/situation/types.go @@ -0,0 +1,32 @@ +package situation + +import ( + "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/ids" +) + +type SituationID ids.ID +type FocusBindingID ids.ID +type SessionID ids.ID +type WorkspaceID ids.ID +type ProjectID ids.ID + +// FocusBinding is the situation-owned runtime context used to resolve a request. +// User text is deliberately absent: textual similarity is not a routing signal. +type FocusBinding struct { + ID FocusBindingID + ActorID identity.ActorID + SessionID SessionID + Workspace WorkspaceID + ProjectID ProjectID + GoalID work.GoalID + ScopeID authority.ScopeID + Active bool +} + +type RuntimeBinding struct { + SessionID SessionID + Workspace WorkspaceID +} diff --git a/contracts/work/doc.go b/contracts/work/doc.go new file mode 100644 index 0000000..36e65f4 --- /dev/null +++ b/contracts/work/doc.go @@ -0,0 +1,3 @@ +package work + +// Package work contains stable boundary contract types for the work semantic owner. diff --git a/contracts/work/types.go b/contracts/work/types.go new file mode 100644 index 0000000..233f056 --- /dev/null +++ b/contracts/work/types.go @@ -0,0 +1,19 @@ +package work + +import ( + "git.cin.su/m/Assistant/contracts/authority" + "git.cin.su/m/Assistant/platform/ids" +) + +type GoalID ids.ID +type TaskID ids.ID +type DecisionID ids.ID +type TaskRevision uint64 + +// TaskTarget carries the integrity context that a work-owned mutation must match. +type TaskTarget struct { + TaskID TaskID + GoalID GoalID + ScopeID authority.ScopeID + Revision TaskRevision +} diff --git a/database/README.md b/database/README.md new file mode 100644 index 0000000..c04e6c7 --- /dev/null +++ b/database/README.md @@ -0,0 +1,5 @@ +# Database ownership + +One PostgreSQL cluster is allowed in V1, but each semantic owner has its own schema and login role. Cross-owner writes and application JOINs are forbidden. Cross-owner foreign keys are forbidden by default. + +`bootstrap/001_roles_and_schemas.sql` is run by a dedicated bootstrap/migration principal, never automatically by every runtime replica. diff --git a/database/bootstrap/001_roles_and_schemas.sql b/database/bootstrap/001_roles_and_schemas.sql new file mode 100644 index 0000000..afd428c --- /dev/null +++ b/database/bootstrap/001_roles_and_schemas.sql @@ -0,0 +1,146 @@ +-- Bootstrap roles/schemas. Replace passwords via ALTER ROLE from secret-aware deployment; do not hardcode real passwords. +BEGIN; +DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'identity_role') THEN CREATE ROLE identity_role LOGIN; END IF; END $$; +CREATE SCHEMA IF NOT EXISTS identity AUTHORIZATION identity_role; +REVOKE ALL ON SCHEMA identity FROM PUBLIC; +DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authority_role') THEN CREATE ROLE authority_role LOGIN; END IF; END $$; +CREATE SCHEMA IF NOT EXISTS authority AUTHORIZATION authority_role; +REVOKE ALL ON SCHEMA authority FROM PUBLIC; +DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'situation_role') THEN CREATE ROLE situation_role LOGIN; END IF; END $$; +CREATE SCHEMA IF NOT EXISTS situation AUTHORIZATION situation_role; +REVOKE ALL ON SCHEMA situation FROM PUBLIC; +DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'work_role') THEN CREATE ROLE work_role LOGIN; END IF; END $$; +CREATE SCHEMA IF NOT EXISTS work AUTHORIZATION work_role; +REVOKE ALL ON SCHEMA work FROM PUBLIC; +DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'epistemics_role') THEN CREATE ROLE epistemics_role LOGIN; END IF; END $$; +CREATE SCHEMA IF NOT EXISTS epistemics AUTHORIZATION epistemics_role; +REVOKE ALL ON SCHEMA epistemics FROM PUBLIC; +DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'continuity_role') THEN CREATE ROLE continuity_role LOGIN; END IF; END $$; +CREATE SCHEMA IF NOT EXISTS continuity AUTHORIZATION continuity_role; +REVOKE ALL ON SCHEMA continuity FROM PUBLIC; +DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'capability_role') THEN CREATE ROLE capability_role LOGIN; END IF; END $$; +CREATE SCHEMA IF NOT EXISTS capability AUTHORIZATION capability_role; +REVOKE ALL ON SCHEMA capability FROM PUBLIC; +DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'commit_role') THEN CREATE ROLE commit_role LOGIN; END IF; END $$; +CREATE SCHEMA IF NOT EXISTS commit AUTHORIZATION commit_role; +REVOKE ALL ON SCHEMA commit FROM PUBLIC; +DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'artifact_role') THEN CREATE ROLE artifact_role LOGIN; END IF; END $$; +CREATE SCHEMA IF NOT EXISTS artifact AUTHORIZATION artifact_role; +REVOKE ALL ON SCHEMA artifact FROM PUBLIC; + +SET ROLE identity_role; +CREATE TABLE IF NOT EXISTS identity.actors ( + actor_id TEXT PRIMARY KEY, + display_name TEXT NOT NULL, + recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE TABLE IF NOT EXISTS identity.principals ( + principal_id TEXT PRIMARY KEY, + display_name TEXT NOT NULL, + recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE TABLE IF NOT EXISTS identity.principal_bindings ( + actor_id TEXT NOT NULL, + principal_id TEXT NOT NULL, + active BOOLEAN NOT NULL, + recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (actor_id, principal_id) +); +INSERT INTO identity.actors (actor_id, display_name) VALUES ('domovoy-creator-actor', 'Domovoy Creator') ON CONFLICT (actor_id) DO NOTHING; +INSERT INTO identity.principals (principal_id, display_name) VALUES ('domovoy-creator-principal', 'Domovoy Creator Principal') ON CONFLICT (principal_id) DO NOTHING; +INSERT INTO identity.principal_bindings (actor_id, principal_id, active) VALUES ('domovoy-creator-actor', 'domovoy-creator-principal', TRUE) ON CONFLICT (actor_id, principal_id) DO NOTHING; +RESET ROLE; + +SET ROLE authority_role; +CREATE TABLE IF NOT EXISTS authority.scopes ( + scope_id TEXT PRIMARY KEY, + label TEXT NOT NULL, + global_scope BOOLEAN NOT NULL, + recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE TABLE IF NOT EXISTS authority.authority_grants ( + grant_id TEXT PRIMARY KEY, + actor_id TEXT NOT NULL, + principal_id TEXT NOT NULL, + scope_id TEXT NOT NULL, + grant_class TEXT NOT NULL, + revision BIGINT NOT NULL, + active BOOLEAN NOT NULL, + valid_from TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + valid_until TIMESTAMPTZ, + CHECK (revision > 0) +); +INSERT INTO authority.scopes (scope_id, label, global_scope) VALUES ('domovoy-global-scope', 'All Domovoy scopes', TRUE) ON CONFLICT (scope_id) DO NOTHING; +INSERT INTO authority.authority_grants (grant_id, actor_id, principal_id, scope_id, grant_class, revision, active) VALUES ('domovoy-creator-grant', 'domovoy-creator-actor', 'domovoy-creator-principal', 'domovoy-global-scope', 'CREATOR', 1, TRUE) ON CONFLICT (grant_id) DO NOTHING; +RESET ROLE; + +SET ROLE commit_role; +CREATE TABLE IF NOT EXISTS commit.authorized_actions ( + authorized_action_id TEXT PRIMARY KEY, action_intent_id TEXT NOT NULL, + state TEXT NOT NULL, revision BIGINT NOT NULL, CHECK (revision > 0) +); +CREATE TABLE IF NOT EXISTS commit.action_attempts ( + action_attempt_id TEXT PRIMARY KEY, authorized_action_id TEXT NOT NULL, + action_intent_id TEXT NOT NULL, state TEXT NOT NULL, + recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); +RESET ROLE; + +SET ROLE work_role; +CREATE TABLE IF NOT EXISTS work.tasks ( + task_id TEXT PRIMARY KEY, goal_id TEXT NOT NULL, scope_id TEXT NOT NULL, + revision BIGINT NOT NULL, CHECK (revision > 0) +); +RESET ROLE; + +SET ROLE situation_role; +CREATE TABLE IF NOT EXISTS situation.focus_bindings ( + focus_binding_id TEXT PRIMARY KEY, + actor_id TEXT NOT NULL, + session_id TEXT NOT NULL, + workspace_id TEXT NOT NULL, + project_id TEXT NOT NULL, + goal_id TEXT NOT NULL, + scope_id TEXT NOT NULL, + active BOOLEAN NOT NULL, + recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); +RESET ROLE; + +SET ROLE capability_role; +CREATE TABLE IF NOT EXISTS capability.health ( + capability_id TEXT PRIMARY KEY, + state TEXT NOT NULL, + reason TEXT NOT NULL, + revision BIGINT NOT NULL, + semantic_version INTEGER NOT NULL, + observed_at_unix_millis BIGINT, + recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + CHECK (revision > 0), + CHECK ((semantic_version = 1 AND observed_at_unix_millis IS NULL) OR (semantic_version = 2 AND observed_at_unix_millis > 0)) +); +CREATE TABLE IF NOT EXISTS capability.health_projection ( + capability_id TEXT PRIMARY KEY, + state TEXT NOT NULL, + reason TEXT NOT NULL, + revision BIGINT NOT NULL, + semantic_version INTEGER NOT NULL, + observed_at_unix_millis BIGINT +); +RESET ROLE; + +SET ROLE continuity_role; +CREATE TABLE IF NOT EXISTS continuity.prospective_items ( + prospective_item_id TEXT PRIMARY KEY, + commitment_id TEXT NOT NULL, + owner_principal_id TEXT NOT NULL, + scope_id TEXT NOT NULL, + goal_id TEXT NOT NULL, + trigger_at_unix_millis BIGINT NOT NULL, + state TEXT NOT NULL, + revision BIGINT NOT NULL, + recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + CHECK (revision > 0) +); +RESET ROLE; +COMMIT; diff --git a/database/migrations/001_bootstrap_creator_authority.sql b/database/migrations/001_bootstrap_creator_authority.sql new file mode 100644 index 0000000..21ae386 --- /dev/null +++ b/database/migrations/001_bootstrap_creator_authority.sql @@ -0,0 +1,16 @@ +BEGIN; +SET ROLE identity_role; +CREATE TABLE IF NOT EXISTS identity.actors (actor_id TEXT PRIMARY KEY, display_name TEXT NOT NULL, recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP); +CREATE TABLE IF NOT EXISTS identity.principals (principal_id TEXT PRIMARY KEY, display_name TEXT NOT NULL, recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP); +CREATE TABLE IF NOT EXISTS identity.principal_bindings (actor_id TEXT NOT NULL, principal_id TEXT NOT NULL, active BOOLEAN NOT NULL, recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (actor_id, principal_id)); +INSERT INTO identity.actors (actor_id, display_name) VALUES ('domovoy-creator-actor', 'Domovoy Creator') ON CONFLICT (actor_id) DO NOTHING; +INSERT INTO identity.principals (principal_id, display_name) VALUES ('domovoy-creator-principal', 'Domovoy Creator Principal') ON CONFLICT (principal_id) DO NOTHING; +INSERT INTO identity.principal_bindings (actor_id, principal_id, active) VALUES ('domovoy-creator-actor', 'domovoy-creator-principal', TRUE) ON CONFLICT (actor_id, principal_id) DO NOTHING; +RESET ROLE; +SET ROLE authority_role; +CREATE TABLE IF NOT EXISTS authority.scopes (scope_id TEXT PRIMARY KEY, label TEXT NOT NULL, global_scope BOOLEAN NOT NULL, recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP); +CREATE TABLE IF NOT EXISTS authority.authority_grants (grant_id TEXT PRIMARY KEY, actor_id TEXT NOT NULL, principal_id TEXT NOT NULL, scope_id TEXT NOT NULL, grant_class TEXT NOT NULL, revision BIGINT NOT NULL, active BOOLEAN NOT NULL, valid_from TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, valid_until TIMESTAMPTZ, CHECK (revision > 0)); +INSERT INTO authority.scopes (scope_id, label, global_scope) VALUES ('domovoy-global-scope', 'All Domovoy scopes', TRUE) ON CONFLICT (scope_id) DO NOTHING; +INSERT INTO authority.authority_grants (grant_id, actor_id, principal_id, scope_id, grant_class, revision, active) VALUES ('domovoy-creator-grant', 'domovoy-creator-actor', 'domovoy-creator-principal', 'domovoy-global-scope', 'CREATOR', 1, TRUE) ON CONFLICT (grant_id) DO NOTHING; +RESET ROLE; +COMMIT; diff --git a/database/migrations/002_continuity_prospective_items.sql b/database/migrations/002_continuity_prospective_items.sql new file mode 100644 index 0000000..b3186ad --- /dev/null +++ b/database/migrations/002_continuity_prospective_items.sql @@ -0,0 +1,16 @@ +BEGIN; +SET ROLE continuity_role; +CREATE TABLE IF NOT EXISTS continuity.prospective_items ( + prospective_item_id TEXT PRIMARY KEY, + commitment_id TEXT NOT NULL, + owner_principal_id TEXT NOT NULL, + scope_id TEXT NOT NULL, + goal_id TEXT NOT NULL, + trigger_at_unix_millis BIGINT NOT NULL, + state TEXT NOT NULL, + revision BIGINT NOT NULL, + recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + CHECK (revision > 0) +); +RESET ROLE; +COMMIT; diff --git a/database/migrations/003_situation_focus_bindings.sql b/database/migrations/003_situation_focus_bindings.sql new file mode 100644 index 0000000..4527255 --- /dev/null +++ b/database/migrations/003_situation_focus_bindings.sql @@ -0,0 +1,15 @@ +BEGIN; +SET ROLE situation_role; +CREATE TABLE IF NOT EXISTS situation.focus_bindings ( + focus_binding_id TEXT PRIMARY KEY, + actor_id TEXT NOT NULL, + session_id TEXT NOT NULL, + workspace_id TEXT NOT NULL, + project_id TEXT NOT NULL, + goal_id TEXT NOT NULL, + scope_id TEXT NOT NULL, + active BOOLEAN NOT NULL, + recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); +RESET ROLE; +COMMIT; diff --git a/database/migrations/004_work_tasks.sql b/database/migrations/004_work_tasks.sql new file mode 100644 index 0000000..bf86618 --- /dev/null +++ b/database/migrations/004_work_tasks.sql @@ -0,0 +1,5 @@ +BEGIN; +SET ROLE work_role; +CREATE TABLE IF NOT EXISTS work.tasks (task_id TEXT PRIMARY KEY, goal_id TEXT NOT NULL, scope_id TEXT NOT NULL, revision BIGINT NOT NULL, CHECK (revision > 0)); +RESET ROLE; +COMMIT; diff --git a/database/migrations/005_commit_actions.sql b/database/migrations/005_commit_actions.sql new file mode 100644 index 0000000..698c606 --- /dev/null +++ b/database/migrations/005_commit_actions.sql @@ -0,0 +1,6 @@ +BEGIN; +SET ROLE commit_role; +CREATE TABLE IF NOT EXISTS commit.authorized_actions (authorized_action_id TEXT PRIMARY KEY, action_intent_id TEXT NOT NULL, state TEXT NOT NULL, revision BIGINT NOT NULL, CHECK (revision > 0)); +CREATE TABLE IF NOT EXISTS commit.action_attempts (action_attempt_id TEXT PRIMARY KEY, authorized_action_id TEXT NOT NULL, action_intent_id TEXT NOT NULL, state TEXT NOT NULL, recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP); +RESET ROLE; +COMMIT; diff --git a/database/migrations/006_capability_health.sql b/database/migrations/006_capability_health.sql new file mode 100644 index 0000000..8624dcf --- /dev/null +++ b/database/migrations/006_capability_health.sql @@ -0,0 +1,12 @@ +BEGIN; +SET ROLE capability_role; +CREATE TABLE IF NOT EXISTS capability.health ( + capability_id TEXT PRIMARY KEY, + state TEXT NOT NULL, + reason TEXT NOT NULL, + revision BIGINT NOT NULL, + recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + CHECK (revision > 0) +); +RESET ROLE; +COMMIT; diff --git a/database/migrations/007_capability_health_v2.sql b/database/migrations/007_capability_health_v2.sql new file mode 100644 index 0000000..833f7c6 --- /dev/null +++ b/database/migrations/007_capability_health_v2.sql @@ -0,0 +1,18 @@ +BEGIN; +SET ROLE capability_role; +ALTER TABLE capability.health ADD COLUMN IF NOT EXISTS semantic_version INTEGER; +ALTER TABLE capability.health ADD COLUMN IF NOT EXISTS observed_at_unix_millis BIGINT; +UPDATE capability.health SET semantic_version = 1 WHERE semantic_version IS NULL; +ALTER TABLE capability.health ALTER COLUMN semantic_version SET NOT NULL; +ALTER TABLE capability.health DROP CONSTRAINT IF EXISTS capability_health_semantic_shape; +ALTER TABLE capability.health ADD CONSTRAINT capability_health_semantic_shape CHECK ((semantic_version = 1 AND observed_at_unix_millis IS NULL) OR (semantic_version = 2 AND observed_at_unix_millis > 0)); +CREATE TABLE IF NOT EXISTS capability.health_projection ( + capability_id TEXT PRIMARY KEY, + state TEXT NOT NULL, + reason TEXT NOT NULL, + revision BIGINT NOT NULL, + semantic_version INTEGER NOT NULL, + observed_at_unix_millis BIGINT +); +RESET ROLE; +COMMIT; diff --git a/database/migrations/artifact/README.md b/database/migrations/artifact/README.md new file mode 100644 index 0000000..d71a533 --- /dev/null +++ b/database/migrations/artifact/README.md @@ -0,0 +1,3 @@ +# artifact migrations + +Only the `artifact` semantic owner may introduce canonical writes in this schema. Migrations must be explicit, versioned and tested. diff --git a/database/migrations/authority/README.md b/database/migrations/authority/README.md new file mode 100644 index 0000000..bf6e445 --- /dev/null +++ b/database/migrations/authority/README.md @@ -0,0 +1,3 @@ +# authority migrations + +Only the `authority` semantic owner may introduce canonical writes in this schema. Migrations must be explicit, versioned and tested. diff --git a/database/migrations/capability/README.md b/database/migrations/capability/README.md new file mode 100644 index 0000000..df043a7 --- /dev/null +++ b/database/migrations/capability/README.md @@ -0,0 +1,3 @@ +# capability migrations + +Only the `capability` semantic owner may introduce canonical writes in this schema. Migrations must be explicit, versioned and tested. diff --git a/database/migrations/commit/README.md b/database/migrations/commit/README.md new file mode 100644 index 0000000..f38d44a --- /dev/null +++ b/database/migrations/commit/README.md @@ -0,0 +1,3 @@ +# commit migrations + +Only the `commit` semantic owner may introduce canonical writes in this schema. Migrations must be explicit, versioned and tested. diff --git a/database/migrations/continuity/README.md b/database/migrations/continuity/README.md new file mode 100644 index 0000000..3978cbb --- /dev/null +++ b/database/migrations/continuity/README.md @@ -0,0 +1,3 @@ +# continuity migrations + +Only the `continuity` semantic owner may introduce canonical writes in this schema. Migrations must be explicit, versioned and tested. diff --git a/database/migrations/epistemics/README.md b/database/migrations/epistemics/README.md new file mode 100644 index 0000000..61ff2ec --- /dev/null +++ b/database/migrations/epistemics/README.md @@ -0,0 +1,3 @@ +# epistemics migrations + +Only the `epistemics` semantic owner may introduce canonical writes in this schema. Migrations must be explicit, versioned and tested. diff --git a/database/migrations/identity/README.md b/database/migrations/identity/README.md new file mode 100644 index 0000000..d38ffd0 --- /dev/null +++ b/database/migrations/identity/README.md @@ -0,0 +1,3 @@ +# identity migrations + +Only the `identity` semantic owner may introduce canonical writes in this schema. Migrations must be explicit, versioned and tested. diff --git a/database/migrations/situation/README.md b/database/migrations/situation/README.md new file mode 100644 index 0000000..b0f506a --- /dev/null +++ b/database/migrations/situation/README.md @@ -0,0 +1,3 @@ +# situation migrations + +Only the `situation` semantic owner may introduce canonical writes in this schema. Migrations must be explicit, versioned and tested. diff --git a/database/migrations/work/README.md b/database/migrations/work/README.md new file mode 100644 index 0000000..f0d060e --- /dev/null +++ b/database/migrations/work/README.md @@ -0,0 +1,3 @@ +# work migrations + +Only the `work` semantic owner may introduce canonical writes in this schema. Migrations must be explicit, versioned and tested. diff --git a/deploy/PORTAINER.md b/deploy/PORTAINER.md new file mode 100644 index 0000000..4cd8dde --- /dev/null +++ b/deploy/PORTAINER.md @@ -0,0 +1,14 @@ +# Portainer deployment target + +Target: `https://docker.cin.su`. + +The initial recommended stack file is `deploy/docker-compose.prod.yml`. + +## Credential behavior for Codex + +1. Try existing `PORTAINER_API_KEY` / endpoint metadata from the environment or an already configured secret store. +2. Do not print the key. +3. Do not scan unrelated repositories for plaintext passwords. +4. If no usable credential exists, ask the user for a Portainer API key (preferred) and endpoint ID if needed. + +The exact Portainer API call used for automated stack creation must be verified against the Portainer version actually running before Codex implements automation. Until then deployment through Portainer's Git/stack UI is valid. diff --git a/deploy/docker-compose.dev.yml b/deploy/docker-compose.dev.yml new file mode 100644 index 0000000..ef71506 --- /dev/null +++ b/deploy/docker-compose.dev.yml @@ -0,0 +1,95 @@ +name: assistant-dev +services: + postgres: + image: postgres:18.6-alpine + environment: + POSTGRES_DB: assistant + POSTGRES_USER: assistant_bootstrap + POSTGRES_PASSWORD: ${POSTGRES_SUPERUSER_PASSWORD:-assistant_dev_only} + CONTINUITY_DB_PASSWORD: ${CONTINUITY_DB_PASSWORD:-assistant_continuity_dev_only} + IDENTITY_DB_PASSWORD: ${IDENTITY_DB_PASSWORD:-assistant_identity_dev_only} + AUTHORITY_DB_PASSWORD: ${AUTHORITY_DB_PASSWORD:-assistant_authority_dev_only} + SITUATION_DB_PASSWORD: ${SITUATION_DB_PASSWORD:-assistant_situation_dev_only} + WORK_DB_PASSWORD: ${WORK_DB_PASSWORD:-assistant_work_dev_only} + CAPABILITY_DB_PASSWORD: ${CAPABILITY_DB_PASSWORD:-assistant_capability_dev_only} + COMMIT_DB_PASSWORD: ${COMMIT_DB_PASSWORD:-assistant_commit_dev_only} + ports: ["15432:5432"] + volumes: + - assistant_pg_dev:/var/lib/postgresql + - ../database/bootstrap/001_roles_and_schemas.sql:/docker-entrypoint-initdb.d/001_roles_and_schemas.sql:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U assistant_bootstrap -d assistant"] + interval: 5s + timeout: 3s + retries: 20 + + nats: + image: nats:2-alpine + command: ["-js", "-m", "8222"] + ports: ["14222:4222", "19222:8222"] + + core-runtime: + build: + context: .. + dockerfile: deploy/images/runtime.Dockerfile + args: { TARGET: core-runtime } + environment: + LISTEN_ADDR: ":8080" + POSTGRES_HOST: postgres + POSTGRES_PORT: "5432" + POSTGRES_DATABASE: assistant + POSTGRES_USER: situation_role + POSTGRES_PASSWORD: ${SITUATION_DB_PASSWORD:-assistant_situation_dev_only} + IDENTITY_POSTGRES_USER: identity_role + IDENTITY_POSTGRES_PASSWORD: ${IDENTITY_DB_PASSWORD:-assistant_identity_dev_only} + CREATOR_BOOTSTRAP_TOKEN: ${CREATOR_BOOTSTRAP_TOKEN:-creator_dev_only_replace_me} + CREATOR_SESSION_SIGNING_KEY: ${CREATOR_SESSION_SIGNING_KEY:-creator_dev_only_signing_key_32_bytes} + WORK_POSTGRES_USER: work_role + WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD:-assistant_work_dev_only} + CAPABILITY_POSTGRES_USER: capability_role + CAPABILITY_POSTGRES_PASSWORD: ${CAPABILITY_DB_PASSWORD:-assistant_capability_dev_only} + ports: ["19080:8080"] + depends_on: + postgres: { condition: service_healthy } + + continuity-runtime: + build: + context: .. + dockerfile: deploy/images/runtime.Dockerfile + args: { TARGET: continuity-runtime } + environment: + LISTEN_ADDR: ":8081" + POSTGRES_HOST: postgres + POSTGRES_PORT: "5432" + POSTGRES_DATABASE: assistant + POSTGRES_USER: continuity_role + POSTGRES_PASSWORD: ${CONTINUITY_DB_PASSWORD:-assistant_continuity_dev_only} + ports: ["19081:8081"] + + commit-runtime: + build: + context: .. + dockerfile: deploy/images/runtime.Dockerfile + args: { TARGET: commit-runtime } + environment: + LISTEN_ADDR: ":8082" + POSTGRES_HOST: postgres + POSTGRES_PORT: "5432" + POSTGRES_DATABASE: assistant + POSTGRES_USER: commit_role + POSTGRES_PASSWORD: ${COMMIT_DB_PASSWORD:-assistant_commit_dev_only} + AUTHORITY_POSTGRES_USER: authority_role + AUTHORITY_POSTGRES_PASSWORD: ${AUTHORITY_DB_PASSWORD:-assistant_authority_dev_only} + CORE_RUNTIME_URL: http://core-runtime:8080 + ports: ["19082:8082"] + + cognitive-worker: + build: + context: .. + dockerfile: deploy/images/runtime.Dockerfile + args: { TARGET: cognitive-worker } + environment: { LISTEN_ADDR: ":8083" } + ports: ["19083:8083"] + +volumes: + assistant_pg_dev: diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml new file mode 100644 index 0000000..42b53bf --- /dev/null +++ b/deploy/docker-compose.prod.yml @@ -0,0 +1,86 @@ +name: assistant +services: + postgres: + build: + context: .. + dockerfile: deploy/images/postgres.Dockerfile + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-assistant} + POSTGRES_USER: ${POSTGRES_SUPERUSER} + POSTGRES_PASSWORD: ${POSTGRES_SUPERUSER_PASSWORD} + CONTINUITY_DB_PASSWORD: ${CONTINUITY_DB_PASSWORD} + IDENTITY_DB_PASSWORD: ${IDENTITY_DB_PASSWORD} + AUTHORITY_DB_PASSWORD: ${AUTHORITY_DB_PASSWORD} + SITUATION_DB_PASSWORD: ${SITUATION_DB_PASSWORD} + WORK_DB_PASSWORD: ${WORK_DB_PASSWORD} + CAPABILITY_DB_PASSWORD: ${CAPABILITY_DB_PASSWORD} + COMMIT_DB_PASSWORD: ${COMMIT_DB_PASSWORD} + volumes: + - assistant_pg:/var/lib/postgresql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_SUPERUSER} -d ${POSTGRES_DB:-assistant}"] + interval: 10s + timeout: 5s + retries: 20 + + nats: + image: nats:2-alpine + restart: unless-stopped + command: ["-js", "-m", "8222"] + volumes: ["assistant_nats:/data"] + + core-runtime: + build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: core-runtime } } + restart: unless-stopped + environment: + LISTEN_ADDR: ":8080" + POSTGRES_HOST: postgres + POSTGRES_PORT: "5432" + POSTGRES_DATABASE: ${POSTGRES_DB:-assistant} + POSTGRES_USER: situation_role + POSTGRES_PASSWORD: ${SITUATION_DB_PASSWORD} + IDENTITY_POSTGRES_USER: identity_role + IDENTITY_POSTGRES_PASSWORD: ${IDENTITY_DB_PASSWORD} + CREATOR_BOOTSTRAP_TOKEN: ${CREATOR_BOOTSTRAP_TOKEN} + CREATOR_SESSION_SIGNING_KEY: ${CREATOR_SESSION_SIGNING_KEY} + WORK_POSTGRES_USER: work_role + WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD} + CAPABILITY_POSTGRES_USER: capability_role + CAPABILITY_POSTGRES_PASSWORD: ${CAPABILITY_DB_PASSWORD} + depends_on: { postgres: { condition: service_healthy } } + + continuity-runtime: + build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: continuity-runtime } } + restart: unless-stopped + environment: + LISTEN_ADDR: ":8081" + POSTGRES_HOST: postgres + POSTGRES_PORT: "5432" + POSTGRES_DATABASE: ${POSTGRES_DB:-assistant} + POSTGRES_USER: continuity_role + POSTGRES_PASSWORD: ${CONTINUITY_DB_PASSWORD} + + commit-runtime: + build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: commit-runtime } } + restart: unless-stopped + environment: + LISTEN_ADDR: ":8082" + POSTGRES_HOST: postgres + POSTGRES_PORT: "5432" + POSTGRES_DATABASE: ${POSTGRES_DB:-assistant} + POSTGRES_USER: commit_role + POSTGRES_PASSWORD: ${COMMIT_DB_PASSWORD} + AUTHORITY_POSTGRES_USER: authority_role + AUTHORITY_POSTGRES_PASSWORD: ${AUTHORITY_DB_PASSWORD} + CORE_RUNTIME_URL: http://core-runtime:8080 + # External mutation credentials belong here only via Portainer/Docker secrets/env injection. + + cognitive-worker: + build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: cognitive-worker } } + restart: unless-stopped + environment: { LISTEN_ADDR: ":8083" } + +volumes: + assistant_pg: + assistant_nats: diff --git a/deploy/images/002_owner_passwords.sh b/deploy/images/002_owner_passwords.sh new file mode 100644 index 0000000..1576988 --- /dev/null +++ b/deploy/images/002_owner_passwords.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env sh +set -eu +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -v ON_ERROR_STOP=1 -v continuity_password="$CONTINUITY_DB_PASSWORD" <<'SQL' +ALTER ROLE continuity_role PASSWORD :'continuity_password'; +SQL +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -v ON_ERROR_STOP=1 -v identity_password="$IDENTITY_DB_PASSWORD" <<'SQL' +ALTER ROLE identity_role PASSWORD :'identity_password'; +SQL +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -v ON_ERROR_STOP=1 -v authority_password="$AUTHORITY_DB_PASSWORD" <<'SQL' +ALTER ROLE authority_role PASSWORD :'authority_password'; +SQL +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -v ON_ERROR_STOP=1 -v situation_password="$SITUATION_DB_PASSWORD" <<'SQL' +ALTER ROLE situation_role PASSWORD :'situation_password'; +SQL +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -v ON_ERROR_STOP=1 -v work_password="$WORK_DB_PASSWORD" <<'SQL' +ALTER ROLE work_role PASSWORD :'work_password'; +SQL +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -v ON_ERROR_STOP=1 -v capability_password="$CAPABILITY_DB_PASSWORD" <<'SQL' +ALTER ROLE capability_role PASSWORD :'capability_password'; +SQL +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -v ON_ERROR_STOP=1 -v commit_password="$COMMIT_DB_PASSWORD" <<'SQL' +ALTER ROLE commit_role PASSWORD :'commit_password'; +SQL diff --git a/deploy/images/postgres.Dockerfile b/deploy/images/postgres.Dockerfile new file mode 100644 index 0000000..c27e2d6 --- /dev/null +++ b/deploy/images/postgres.Dockerfile @@ -0,0 +1,5 @@ +FROM postgres:18.6-alpine + +COPY database/bootstrap/001_roles_and_schemas.sql /docker-entrypoint-initdb.d/001_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 diff --git a/deploy/images/runtime.Dockerfile b/deploy/images/runtime.Dockerfile new file mode 100644 index 0000000..f98e085 --- /dev/null +++ b/deploy/images/runtime.Dockerfile @@ -0,0 +1,13 @@ +# syntax=docker/dockerfile:1 +FROM golang:1.26.6-alpine AS build +WORKDIR /src +COPY go.mod ./ +COPY . . +ARG TARGET +RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -o /out/runtime ./cmd/${TARGET} + +FROM alpine:3.23 +RUN addgroup -S assistant && adduser -S -G assistant assistant +USER assistant +COPY --from=build /out/runtime /usr/local/bin/assistant-runtime +ENTRYPOINT ["/usr/local/bin/assistant-runtime"] diff --git a/dev/README.md b/dev/README.md new file mode 100644 index 0000000..b7ab894 --- /dev/null +++ b/dev/README.md @@ -0,0 +1,5 @@ +# Codex tasks + +`DEV-000001` is the first task after placing this folder at `Z:\mst\codex\assistent`. + +Do not start `DEV-000002` until the guardrails, Git remote, CI and local stack in DEV-000001 are healthy. diff --git a/dev/task-packet.schema.json b/dev/task-packet.schema.json new file mode 100644 index 0000000..b1c3255 --- /dev/null +++ b/dev/task-packet.schema.json @@ -0,0 +1,79 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Codex Task Packet V1", + "type": "object", + "required": [ + "task_id", + "task_revision", + "architecture_revision", + "risk_tier", + "intent", + "allowed_change_scope", + "acceptance_scenarios", + "required_tests" + ], + "properties": { + "task_id": { + "type": "string" + }, + "task_revision": { + "type": "integer", + "minimum": 1 + }, + "architecture_revision": { + "type": "string" + }, + "base_repo_revision": { + "type": "string" + }, + "risk_tier": { + "enum": [ + "A", + "B", + "C", + "D", + "E" + ] + }, + "intent": { + "type": "string" + }, + "requirement_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "allowed_change_scope": { + "type": "object" + }, + "forbidden_change_scope": { + "type": "object" + }, + "allowed_architecture_delta": { + "type": "object" + }, + "semantic_owner": { + "type": "string" + }, + "acceptance_scenarios": { + "type": "array", + "items": { + "type": "string" + } + }, + "required_tests": { + "type": "array", + "items": { + "type": "string" + } + }, + "terminal_gap_states": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": true +} diff --git a/dev/tasks/DEV-000001-bootstrap-guardrails.json b/dev/tasks/DEV-000001-bootstrap-guardrails.json new file mode 100644 index 0000000..e990572 --- /dev/null +++ b/dev/tasks/DEV-000001-bootstrap-guardrails.json @@ -0,0 +1,32 @@ +{ + "task_id": "DEV-000001", + "task_revision": 1, + "architecture_revision": "bootstrap-1", + "base_repo_revision": "INITIAL", + "risk_tier": "E", + "intent": "Validate and complete the repository guardrail skeleton without changing accepted semantic architecture. Make devctl checks, Go build/test, DB ownership bootstrap, CI and local Docker development stack operational.", + "requirement_ids": ["INV-OWN-001", "INV-TOPO-001"], + "allowed_change_scope": { + "paths": ["cmd/devctl/**", "scripts/**", "ci/**", ".gitea/**", "deploy/**", "database/bootstrap/**", "tests/architecture/**"] + }, + "forbidden_change_scope": { + "paths": ["architecture/ARCHITECTURE_CONSTITUTION.md", "architecture/CORE_SEMANTIC_MODEL_V1.md"], + "semantic_changes": true + }, + "allowed_architecture_delta": { + "new_cross_module_dependencies": 0, + "new_canonical_owners": 0, + "new_privileges": 0, + "semantic_topology_changes": 0 + }, + "semantic_owner": "development-control-plane", + "acceptance_scenarios": [ + "Fresh Codex session can identify module owners and protected paths", + "Forbidden cross-module internal import makes architecture check fail", + "Project builds and tests with Go 1.26.x", + "Development docker compose starts PostgreSQL/NATS/runtime health endpoints", + "Git origin is https://git.cin.su/m/Assistant.git without embedded credentials" + ], + "required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl check-architecture", "docker compose config"], + "terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY"] +} diff --git a/dev/tasks/DEV-000002-walking-skeleton-focus-isolation.json b/dev/tasks/DEV-000002-walking-skeleton-focus-isolation.json new file mode 100644 index 0000000..04b587a --- /dev/null +++ b/dev/tasks/DEV-000002-walking-skeleton-focus-isolation.json @@ -0,0 +1,17 @@ +{ + "task_id": "DEV-000002", + "task_revision": 2, + "architecture_revision": "bootstrap-1", + "base_repo_revision": "AFTER_DEV_000001", + "risk_tier": "C", + "intent": "Implement the first vertical walking-skeleton slice for situation/focus isolation across two simultaneous project contexts, without keyword-based project selection.", + "allowed_change_scope": { + "paths": ["architecture/architecture-registry.yaml", "architecture/architecture-registry.json", "modules/situation/**", "modules/work/**", "contracts/situation/**", "contracts/work/**", "tests/walking_skeleton/**"] + }, + "forbidden_change_scope": {"paths": ["security/**", "modules/commit/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 1, "new_canonical_owners": 0, "new_privileges": 0}, + "semantic_owner": "situation", + "acceptance_scenarios": ["WS-001"], + "required_tests": ["unit", "contract", "focus-isolation black-box", "architecture"], + "terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP"] +} diff --git a/dev/tasks/DEV-000003-production-docker-stack.json b/dev/tasks/DEV-000003-production-docker-stack.json new file mode 100644 index 0000000..0eed6d7 --- /dev/null +++ b/dev/tasks/DEV-000003-production-docker-stack.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000003", + "task_revision": 1, + "architecture_revision": "bootstrap-1", + "base_repo_revision": "AFTER_DEV_000002", + "risk_tier": "D", + "intent": "Build and deploy the production Docker walking-skeleton stack to docker.cin.su without modifying unrelated workloads.", + "allowed_change_scope": { + "paths": ["deploy/**", "dev/tasks/DEV-000003-production-docker-stack.json"] + }, + "forbidden_change_scope": { + "paths": ["architecture/**", "security/**", "modules/**", "contracts/**"] + }, + "allowed_architecture_delta": { + "new_cross_module_dependencies": 0, + "new_canonical_owners": 0, + "new_privileges": 0 + }, + "semantic_owner": "deployment-control-plane", + "acceptance_scenarios": ["production Docker stack builds on docker.cin.su", "PostgreSQL is healthy", "all runtime health endpoints respond inside the stack"], + "required_tests": ["docker compose config", "remote Docker build", "runtime health checks"], + "terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"] +} diff --git a/dev/tasks/DEV-000004-bootstrap-creator-authority.json b/dev/tasks/DEV-000004-bootstrap-creator-authority.json new file mode 100644 index 0000000..3cee7e6 --- /dev/null +++ b/dev/tasks/DEV-000004-bootstrap-creator-authority.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000004", + "task_revision": 1, + "architecture_revision": "bootstrap-1", + "base_repo_revision": "AFTER_DEV_000003", + "risk_tier": "D", + "intent": "Bootstrap Domovoy's Creator as a distinct Actor and Principal with an explicit active global AuthorityGrant, without conflating identity, principal, scope, or authority.", + "allowed_change_scope": { + "paths": ["contracts/identity/**", "contracts/authority/**", "modules/identity/**", "modules/authority/**", "database/bootstrap/**", "database/migrations/**", "tests/creator_authority/**", "dev/tasks/DEV-000004-bootstrap-creator-authority.json"] + }, + "forbidden_change_scope": { + "paths": ["architecture/**", "security/**", "modules/commit/**"] + }, + "allowed_architecture_delta": { + "new_cross_module_dependencies": 0, + "new_canonical_owners": 0, + "new_privileges": 0 + }, + "semantic_owner": "authority", + "acceptance_scenarios": ["Creator Actor and Principal remain distinct", "Creator grant has an explicit global scope and revision", "revoked or malformed Creator grant is denied", "production database migration is idempotent"], + "required_tests": ["unit", "canonical schema bootstrap", "migration idempotency", "architecture"], + "terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "REQUIRES_MIGRATION_DECISION"] +} diff --git a/dev/tasks/DEV-000005-safe-action-commit.json b/dev/tasks/DEV-000005-safe-action-commit.json new file mode 100644 index 0000000..f462ef3 --- /dev/null +++ b/dev/tasks/DEV-000005-safe-action-commit.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000005", + "task_revision": 1, + "architecture_revision": "bootstrap-1", + "base_repo_revision": "AFTER_DEV_000004", + "risk_tier": "D", + "intent": "Implement the deterministic WS-003 Commit authorization state machine with explicit Authority, Scope, Capability, Task revision, executor isolation, and UNKNOWN_OUTCOME handling.", + "allowed_change_scope": { + "paths": ["contracts/commit/**", "contracts/authority/**", "contracts/capability/**", "modules/commit/**", "modules/authority/**", "tests/action_commit/**", "dev/tasks/DEV-000005-safe-action-commit.json"] + }, + "forbidden_change_scope": { + "paths": ["architecture/**", "security/**", "adapters/**", "modules/situation/**", "modules/work/**"] + }, + "allowed_architecture_delta": { + "new_cross_module_dependencies": 0, + "new_canonical_owners": 0, + "new_privileges": 0 + }, + "semantic_owner": "commit", + "acceptance_scenarios": ["WS-003"], + "required_tests": ["unit", "authority rejection", "unknown outcome", "no-blind-retry", "architecture"], + "terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP"] +} diff --git a/dev/tasks/DEV-000006-durable-prospective-memory.json b/dev/tasks/DEV-000006-durable-prospective-memory.json new file mode 100644 index 0000000..79e7d7b --- /dev/null +++ b/dev/tasks/DEV-000006-durable-prospective-memory.json @@ -0,0 +1,17 @@ +{ + "task_id": "DEV-000006", + "task_revision": 1, + "architecture_revision": "bootstrap-1", + "base_repo_revision": "AFTER_DEV_000005", + "risk_tier": "D", + "intent": "Implement WS-002 canonical ProspectiveItem state that survives scheduler loss and revalidates before delivery.", + "allowed_change_scope": { + "paths": ["contracts/continuity/**", "modules/continuity/**", "database/bootstrap/**", "database/migrations/**", "tests/prospective_memory/**", "dev/tasks/DEV-000006-durable-prospective-memory.json"] + }, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "adapters/temporal/**", "modules/commit/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0}, + "semantic_owner": "continuity", + "acceptance_scenarios": ["WS-002"], + "required_tests": ["unit", "recovery", "revalidation", "duplicate-trigger", "architecture"], + "terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP"] +} diff --git a/dev/tasks/DEV-000007-deterministic-cognitive-fake.json b/dev/tasks/DEV-000007-deterministic-cognitive-fake.json new file mode 100644 index 0000000..199dda9 --- /dev/null +++ b/dev/tasks/DEV-000007-deterministic-cognitive-fake.json @@ -0,0 +1,15 @@ +{ + "task_id": "DEV-000007", + "task_revision": 1, + "architecture_revision": "bootstrap-1", + "base_repo_revision": "AFTER_DEV_000006", + "risk_tier": "C", + "intent": "Expose a deterministic cognitive fake that emits typed ActionProposal candidates only and cannot mutate canonical state or invoke an Executor.", + "allowed_change_scope": {"paths": ["adapters/models/**", "cmd/cognitive-worker/**", "tests/cognitive_fake/**", "dev/tasks/DEV-000007-deterministic-cognitive-fake.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "modules/**", "adapters/postgres/**", "adapters/temporal/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0}, + "semantic_owner": "none-cognitive-worker", + "acceptance_scenarios": ["deterministic fake produces identical candidate for identical typed request", "candidate cannot mutate canonical state", "candidate cannot execute external side effect"], + "required_tests": ["unit", "HTTP contract", "architecture"], + "terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP"] +} diff --git a/dev/tasks/DEV-000008-continuity-postgres-api.json b/dev/tasks/DEV-000008-continuity-postgres-api.json new file mode 100644 index 0000000..ca06b9c --- /dev/null +++ b/dev/tasks/DEV-000008-continuity-postgres-api.json @@ -0,0 +1,15 @@ +{ + "task_id": "DEV-000008", + "task_revision": 1, + "architecture_revision": "bootstrap-1", + "base_repo_revision": "AFTER_DEV_000007", + "risk_tier": "D", + "intent": "Persist Continuity ProspectiveItems in its owner-local PostgreSQL schema and expose a continuity-runtime HTTP API without leaking PostgreSQL into domain code.", + "allowed_change_scope": {"paths": ["go.mod", "go.sum", "adapters/postgres/**", "modules/continuity/**", "cmd/continuity-runtime/**", "deploy/**", "tests/continuity_api/**", "dev/tasks/DEV-000008-continuity-postgres-api.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "modules/commit/**", "modules/work/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 1}, + "semantic_owner": "continuity", + "acceptance_scenarios": ["ProspectiveItem survives continuity-runtime restart", "continuity API writes only continuity schema", "domain package imports no PostgreSQL types"], + "required_tests": ["unit", "repository integration", "runtime HTTP", "architecture"], + "terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "REQUIRES_MIGRATION_DECISION"] +} diff --git a/dev/tasks/DEV-000009-situation-focus-postgres-api.json b/dev/tasks/DEV-000009-situation-focus-postgres-api.json new file mode 100644 index 0000000..ca459e7 --- /dev/null +++ b/dev/tasks/DEV-000009-situation-focus-postgres-api.json @@ -0,0 +1,15 @@ +{ + "task_id": "DEV-000009", + "task_revision": 1, + "architecture_revision": "bootstrap-1", + "base_repo_revision": "AFTER_DEV_000008", + "risk_tier": "D", + "intent": "Persist Situation focus bindings in its owner-local PostgreSQL schema and resolve focus only from runtime session/workspace bindings.", + "allowed_change_scope": {"paths": ["adapters/postgres/**", "modules/situation/**", "cmd/core-runtime/**", "database/bootstrap/**", "database/migrations/**", "deploy/**", "tests/situation_api/**", "dev/tasks/DEV-000009-situation-focus-postgres-api.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "modules/work/**", "modules/commit/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0}, + "semantic_owner": "situation", + "acceptance_scenarios": ["WS-001 runtime binding resolves its own focus", "ambiguous runtime binding has no mutation"], + "required_tests": ["unit", "repository integration", "runtime HTTP", "architecture"], + "terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "REQUIRES_MIGRATION_DECISION"] +} diff --git a/dev/tasks/DEV-000010-work-task-postgres-api.json b/dev/tasks/DEV-000010-work-task-postgres-api.json new file mode 100644 index 0000000..5462a94 --- /dev/null +++ b/dev/tasks/DEV-000010-work-task-postgres-api.json @@ -0,0 +1 @@ +{"task_id":"DEV-000010","task_revision":1,"architecture_revision":"bootstrap-1","base_repo_revision":"AFTER_DEV_000009","risk_tier":"D","intent":"Persist Work tasks and enforce owner-local optimistic revision commits.","allowed_change_scope":{"paths":["adapters/postgres/**","modules/work/**","cmd/core-runtime/**","database/bootstrap/**","database/migrations/**","deploy/**","dev/tasks/DEV-000010-work-task-postgres-api.json"]},"forbidden_change_scope":{"paths":["architecture/**","security/**","modules/commit/**"]},"allowed_architecture_delta":{"new_cross_module_dependencies":0,"new_canonical_owners":0,"new_privileges":0},"semantic_owner":"work","acceptance_scenarios":["WS-001 stale task revision is rejected"],"required_tests":["unit","repository integration","runtime HTTP","architecture"],"terminal_gap_states":["BLOCKED_BY_MISSING_CAPABILITY","REQUIRES_MIGRATION_DECISION"]} diff --git a/dev/tasks/DEV-000011-focus-guarded-task-commit.json b/dev/tasks/DEV-000011-focus-guarded-task-commit.json new file mode 100644 index 0000000..da273f4 --- /dev/null +++ b/dev/tasks/DEV-000011-focus-guarded-task-commit.json @@ -0,0 +1 @@ +{"task_id":"DEV-000011","task_revision":1,"architecture_revision":"bootstrap-1","base_repo_revision":"AFTER_DEV_000010","risk_tier":"D","intent":"Wire persisted Situation resolution to Work revision commit so wrong-project proposals cannot reach work mutation.","allowed_change_scope":{"paths":["cmd/core-runtime/**","tests/walking_skeleton/**","dev/tasks/DEV-000011-focus-guarded-task-commit.json"]},"forbidden_change_scope":{"paths":["architecture/**","security/**","modules/commit/**","adapters/**"]},"allowed_architecture_delta":{"new_cross_module_dependencies":0,"new_canonical_owners":0,"new_privileges":0},"semantic_owner":"situation","acceptance_scenarios":["WS-001 wrong-project mutation rejected before work commit"],"required_tests":["black-box","runtime HTTP","architecture"],"terminal_gap_states":["BLOCKED_BY_ARCHITECTURE_GAP"]} diff --git a/dev/tasks/DEV-000012-commit-postgres-api.json b/dev/tasks/DEV-000012-commit-postgres-api.json new file mode 100644 index 0000000..c178638 --- /dev/null +++ b/dev/tasks/DEV-000012-commit-postgres-api.json @@ -0,0 +1 @@ +{"task_id":"DEV-000012","task_revision":1,"architecture_revision":"bootstrap-1","base_repo_revision":"AFTER_DEV_000011","risk_tier":"D","intent":"Persist Commit proposals, authorization and attempts in owner-local PostgreSQL state before exposing Commit Runtime APIs.","allowed_change_scope":{"paths":["adapters/postgres/**","modules/commit/**","cmd/commit-runtime/**","database/bootstrap/**","database/migrations/**","deploy/**","tests/commit_api/**","dev/tasks/DEV-000012-commit-postgres-api.json"]},"forbidden_change_scope":{"paths":["architecture/**","security/**","modules/authority/**","modules/work/**"]},"allowed_architecture_delta":{"new_cross_module_dependencies":0,"new_canonical_owners":0,"new_privileges":0},"semantic_owner":"commit","acceptance_scenarios":["WS-003 action state is durable across Commit Runtime restart","UNKNOWN_OUTCOME blocks blind retry"],"required_tests":["unit","repository integration","runtime HTTP","recovery","architecture"],"terminal_gap_states":["BLOCKED_BY_MISSING_CAPABILITY","REQUIRES_MIGRATION_DECISION"]} diff --git a/dev/tasks/DEV-000013-topology-equivalence.json b/dev/tasks/DEV-000013-topology-equivalence.json new file mode 100644 index 0000000..676f242 --- /dev/null +++ b/dev/tasks/DEV-000013-topology-equivalence.json @@ -0,0 +1 @@ +{"task_id":"DEV-000013","task_revision":1,"architecture_revision":"bootstrap-1","base_repo_revision":"AFTER_DEV_000012","risk_tier":"C","intent":"Prove equivalent semantic outcomes for walking-skeleton state machines under in-process and port-mediated boundaries.","allowed_change_scope":{"paths":["tests/topology_equivalence/**","dev/tasks/DEV-000013-topology-equivalence.json"]},"forbidden_change_scope":{"paths":["architecture/**","security/**","modules/**","contracts/**","adapters/**"]},"allowed_architecture_delta":{"new_cross_module_dependencies":0,"new_canonical_owners":0,"new_privileges":0},"semantic_owner":"none-verification","acceptance_scenarios":["WS-004"],"required_tests":["topology equivalence","architecture"],"terminal_gap_states":["BLOCKED_BY_SPEC_GAP"]} diff --git a/dev/tasks/DEV-000014-no-llm-degraded-capability.json b/dev/tasks/DEV-000014-no-llm-degraded-capability.json new file mode 100644 index 0000000..d9d1d3d --- /dev/null +++ b/dev/tasks/DEV-000014-no-llm-degraded-capability.json @@ -0,0 +1,36 @@ +{ + "task_id": "DEV-000014", + "task_revision": 1, + "architecture_revision": "bootstrap-1", + "base_repo_revision": "AFTER_DEV_000013", + "risk_tier": "C", + "intent": "Implement owner-local Capability health state so WS-005 can explicitly expose cognitive-provider degradation without affecting continuity, integrity, or canonical owners.", + "allowed_change_scope": { + "paths": [ + "contracts/capability/**", + "modules/capability/**", + "adapters/postgres/**", + "cmd/core-runtime/**", + "database/bootstrap/**", + "database/migrations/**", + "deploy/**", + "tests/degraded_continuity/**", + "dev/tasks/DEV-000014-no-llm-degraded-capability.json" + ] + }, + "forbidden_change_scope": { + "paths": ["architecture/**", "security/**", "modules/commit/**", "modules/continuity/**", "modules/authority/**"] + }, + "allowed_architecture_delta": { + "new_cross_module_dependencies": 0, + "new_canonical_owners": 0, + "new_privileges": 0 + }, + "semantic_owner": "capability", + "acceptance_scenarios": [ + "WS-005 cognitive-provider degradation is an explicit canonical Capability state", + "absence of the cognitive worker does not prevent continuity, focus/work, or deterministic commit runtimes from serving their existing state" + ], + "required_tests": ["unit", "repository integration", "runtime HTTP", "degraded-mode", "architecture"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "REQUIRES_MIGRATION_DECISION"] +} diff --git a/dev/tasks/DEV-000015-capability-semantic-evolution.json b/dev/tasks/DEV-000015-capability-semantic-evolution.json new file mode 100644 index 0000000..7727729 --- /dev/null +++ b/dev/tasks/DEV-000015-capability-semantic-evolution.json @@ -0,0 +1,32 @@ +{ + "task_id": "DEV-000015", + "task_revision": 1, + "architecture_revision": "bootstrap-1", + "base_repo_revision": "AFTER_DEV_000014", + "risk_tier": "C", + "intent": "Execute WS-006 as a versioned owner-local Capability Health evolution from V1 to V2, preserving historical meaning and rebuilding a derived projection.", + "allowed_change_scope": { + "paths": [ + "contracts/capability/**", + "modules/capability/**", + "adapters/postgres/**", + "cmd/core-runtime/**", + "database/bootstrap/**", + "database/migrations/**", + "deploy/**", + "docs/MIGRATIONS.md", + "tests/semantic_evolution/**", + "dev/tasks/DEV-000015-capability-semantic-evolution.json" + ] + }, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "modules/commit/**", "modules/continuity/**", "modules/authority/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0}, + "semantic_owner": "capability", + "acceptance_scenarios": [ + "WS-006 V1 Capability Health remains interpretable after the V2 migration", + "V2 writer requires explicit observation time and unsupported versions are rejected", + "Capability Health projection is rebuilt from canonical owner-local state" + ], + "required_tests": ["unit", "migration", "repository integration", "runtime HTTP", "projection rebuild", "architecture"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "REQUIRES_MIGRATION_DECISION"] +} diff --git a/dev/tasks/DEV-000016-owner-acl-negative-test.json b/dev/tasks/DEV-000016-owner-acl-negative-test.json new file mode 100644 index 0000000..138f6fa --- /dev/null +++ b/dev/tasks/DEV-000016-owner-acl-negative-test.json @@ -0,0 +1,15 @@ +{ + "task_id": "DEV-000016", + "task_revision": 1, + "architecture_revision": "bootstrap-1", + "base_repo_revision": "AFTER_DEV_000015", + "risk_tier": "C", + "intent": "Prove INV-OWN-001 with a negative PostgreSQL ACL test: a non-owner role cannot write another owner's canonical schema.", + "allowed_change_scope": {"paths": ["tests/owner_acl/**", "dev/tasks/DEV-000016-owner-acl-negative-test.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "modules/**", "contracts/**", "adapters/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0}, + "semantic_owner": "none-verification", + "acceptance_scenarios": ["continuity_role is physically denied INSERT into work.tasks", "work_role remains able to read its own owner-local table"], + "required_tests": ["negative ACL", "architecture"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY"] +} diff --git a/dev/tasks/DEV-000017-runtime-authorized-action-validation.json b/dev/tasks/DEV-000017-runtime-authorized-action-validation.json new file mode 100644 index 0000000..7ad4511 --- /dev/null +++ b/dev/tasks/DEV-000017-runtime-authorized-action-validation.json @@ -0,0 +1,15 @@ +{ + "task_id": "DEV-000017", + "task_revision": 1, + "architecture_revision": "bootstrap-1", + "base_repo_revision": "AFTER_DEV_000016", + "risk_tier": "D", + "intent": "Require Commit Runtime to construct AuthorizedAction only after live owner validation of Identity binding, Authority grant, Scope, and Work task revision over typed runtime queries.", + "allowed_change_scope": {"paths": ["contracts/identity/**", "contracts/authority/**", "contracts/commit/**", "contracts/work/**", "modules/identity/**", "modules/authority/**", "modules/commit/**", "modules/work/**", "adapters/**", "cmd/core-runtime/**", "cmd/commit-runtime/**", "database/bootstrap/**", "database/migrations/**", "deploy/**", "tests/action_commit/**", "tests/runtime_authorization/**", "dev/tasks/DEV-000017-runtime-authorized-action-validation.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "modules/continuity/**", "modules/capability/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0}, + "semantic_owner": "commit", + "acceptance_scenarios": ["revoked or stale Authority grant cannot create AuthorizedAction", "missing Actor-Principal binding cannot create AuthorizedAction", "stale Work target revision cannot create AuthorizedAction", "Commit Runtime has no direct access to identity or work schemas"], + "required_tests": ["unit", "authority rejection", "runtime HTTP", "negative ACL", "architecture"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "REQUIRES_MIGRATION_DECISION"] +} diff --git a/dev/tasks/DEV-000018-foundation-exit-audit.json b/dev/tasks/DEV-000018-foundation-exit-audit.json new file mode 100644 index 0000000..13a42ad --- /dev/null +++ b/dev/tasks/DEV-000018-foundation-exit-audit.json @@ -0,0 +1 @@ +{"task_id":"DEV-000018","task_revision":1,"architecture_revision":"bootstrap-1","base_repo_revision":"AFTER_DEV_000017","risk_tier":"C","intent":"Audit Foundation V1 exit criteria using only reproducible repository and deployed-stack evidence; record gaps without weakening guardrails.","allowed_change_scope":{"paths":["docs/**","tests/**","dev/tasks/DEV-000018-foundation-exit-audit.json"]},"forbidden_change_scope":{"paths":["architecture/**","security/**","modules/**","contracts/**","adapters/**"]},"allowed_architecture_delta":{"new_cross_module_dependencies":0,"new_canonical_owners":0,"new_privileges":0},"semantic_owner":"none-verification","acceptance_scenarios":["all Foundation Exit criteria have explicit evidence or an explicit gap"],"required_tests":["architecture","deployment evidence"],"terminal_gap_states":["BLOCKED_BY_SPEC_GAP","BLOCKED_BY_ARCHITECTURE_GAP"]} diff --git a/dev/tasks/DEV-000019-creator-authentication.json b/dev/tasks/DEV-000019-creator-authentication.json new file mode 100644 index 0000000..cd54923 --- /dev/null +++ b/dev/tasks/DEV-000019-creator-authentication.json @@ -0,0 +1 @@ +{"task_id":"DEV-000019","task_revision":1,"architecture_revision":"bootstrap-1","base_repo_revision":"AFTER_DEV_000018","risk_tier":"D","intent":"Provide authenticated Creator sessions for Domovoy without embedding credentials in source or bypassing Identity and Authority boundaries.","allowed_change_scope":{"paths":["cmd/core-runtime/**","modules/identity/**","contracts/identity/**","deploy/**","tests/creator_auth/**","dev/tasks/DEV-000019-creator-authentication.json"]},"forbidden_change_scope":{"paths":["architecture/**","security/**","modules/commit/**","database/**"]},"allowed_architecture_delta":{"new_cross_module_dependencies":0,"new_canonical_owners":0,"new_privileges":0},"semantic_owner":"identity","acceptance_scenarios":["Creator can obtain a time-bounded authenticated session using a runtime-only bootstrap credential","invalid credential is denied","session does not imply authority grant"],"required_tests":["unit","runtime HTTP","architecture"],"terminal_gap_states":["BLOCKED_BY_SPEC_GAP","BLOCKED_BY_MISSING_CAPABILITY"]} diff --git a/dev/tasks/DEV-000020-test-executor-reconciliation.json b/dev/tasks/DEV-000020-test-executor-reconciliation.json new file mode 100644 index 0000000..f4c3e88 --- /dev/null +++ b/dev/tasks/DEV-000020-test-executor-reconciliation.json @@ -0,0 +1,15 @@ +{ + "task_id": "DEV-000020", + "task_revision": 1, + "architecture_revision": "bootstrap-1", + "base_repo_revision": "AFTER_DEV_000019", + "risk_tier": "D", + "intent": "Complete the WS-003 isolated Test Executor path with idempotent effect identity, injected acknowledgement loss, UNKNOWN_OUTCOME, and explicit reconciliation without blind retry.", + "allowed_change_scope": {"paths": ["modules/commit/**", "adapters/testexecutor/**", "tests/action_commit/**", "dev/tasks/DEV-000020-test-executor-reconciliation.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "cmd/**", "contracts/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0}, + "semantic_owner": "commit", + "acceptance_scenarios": ["executor applies effect once and loses acknowledgement", "UNKNOWN_OUTCOME blocks blind retry", "reconciliation verifies the existing effect", "duplicate executor delivery does not duplicate the effect"], + "required_tests": ["unit", "adversarial", "reconciliation", "architecture"], + "terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP"] +} diff --git a/dev/tasks/DEV-000021-architecture-sentinel-negative-tests.json b/dev/tasks/DEV-000021-architecture-sentinel-negative-tests.json new file mode 100644 index 0000000..bb63ad1 --- /dev/null +++ b/dev/tasks/DEV-000021-architecture-sentinel-negative-tests.json @@ -0,0 +1,15 @@ +{ + "task_id": "DEV-000021", + "task_revision": 1, + "architecture_revision": "bootstrap-1", + "base_repo_revision": "AFTER_DEV_000020", + "risk_tier": "E", + "intent": "Strengthen the Architecture Sentinel without changing architecture: validate declared module contract dependencies and prove critical enforcement with negative self-tests.", + "allowed_change_scope": {"paths": ["cmd/devctl/**", "tests/architecture/**", "dev/tasks/DEV-000021-architecture-sentinel-negative-tests.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "ci/architecture/**", "modules/**", "contracts/**", "adapters/**", "database/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0}, + "semantic_owner": "development-control-plane", + "acceptance_scenarios": ["domain infrastructure import is rejected", "cross-module internal import is rejected", "undeclared contract dependency is rejected", "clean repository passes unchanged architecture registry"], + "required_tests": ["negative guardrail self-tests", "full architecture sentinel", "go test", "go vet"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_CONFLICT"] +} diff --git a/dev/tasks/DEV-000022-remove-commit-identity-drift.json b/dev/tasks/DEV-000022-remove-commit-identity-drift.json new file mode 100644 index 0000000..45bd830 --- /dev/null +++ b/dev/tasks/DEV-000022-remove-commit-identity-drift.json @@ -0,0 +1,15 @@ +{ + "task_id": "DEV-000022", + "task_revision": 1, + "architecture_revision": "bootstrap-1", + "base_repo_revision": "AFTER_DEV_000020", + "risk_tier": "C", + "intent": "Remove observed Commit-to-Identity contract drift by making Authority own the composite authorization context consumed by Commit.", + "allowed_change_scope": {"paths": ["contracts/authority/**", "modules/commit/**", "cmd/commit-runtime/**", "tests/action_commit/**", "dev/tasks/DEV-000022-remove-commit-identity-drift.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "adapters/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0}, + "semantic_owner": "authority", + "acceptance_scenarios": ["Commit imports contracts.authority but not contracts.identity", "authorization rejection semantics remain unchanged"], + "required_tests": ["unit", "architecture"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP"] +} diff --git a/dev/tasks/DEV-000023-remote-topology-harness.json b/dev/tasks/DEV-000023-remote-topology-harness.json new file mode 100644 index 0000000..71fc4b9 --- /dev/null +++ b/dev/tasks/DEV-000023-remote-topology-harness.json @@ -0,0 +1,15 @@ +{ + "task_id": "DEV-000023", + "task_revision": 1, + "architecture_revision": "bootstrap-1", + "base_repo_revision": "AFTER_DEV_000022", + "risk_tier": "C", + "intent": "Make WS-004 reproducible with a black-box Docker harness that exercises WS-001, WS-002, and WS-003 through extracted runtimes.", + "allowed_change_scope": {"paths": ["tests/topology_equivalence/**", "dev/tasks/DEV-000023-remote-topology-harness.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "modules/**", "contracts/**", "adapters/**", "cmd/**", "database/**", "deploy/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0}, + "semantic_owner": "none-verification", + "acceptance_scenarios": ["WS-001 remote wrong target rejected", "WS-002 remote prospective item survives Continuity restart", "WS-003 remote Commit authorizes current target and records UNKNOWN_OUTCOME"], + "required_tests": ["in-process", "remote black-box", "architecture"], + "terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_ARCHITECTURE_GAP"] +} diff --git a/development/CODEX_DEVELOPMENT_CONTRACT.md b/development/CODEX_DEVELOPMENT_CONTRACT.md new file mode 100644 index 0000000..cc6ef08 --- /dev/null +++ b/development/CODEX_DEVELOPMENT_CONTRACT.md @@ -0,0 +1,109 @@ +# 5. CODEX_DEVELOPMENT_CONTRACT.md + +## 5.1 Role + +Codex is an implementation capability. It can inspect, design locally, code, test and propose findings. It is **not** architectural authority and does not receive production authority merely because it can edit source. + +## 5.2 Task Packet schema + +Every non-trivial Codex implementation task must resolve to a packet similar to: + +```yaml +task_id: DEV-000123 +task_revision: 3 +architecture_revision: +base_repo_revision: +risk_tier: B +intent: "..." +requirement_ids: [REQ-...] +allowed_change_scope: + modules: [work] + paths: [modules/work/**, contracts/work/**] +forbidden_change_scope: + paths: [architecture/**, security/**, modules/identity/**] +allowed_architecture_delta: + new_cross_module_dependencies: 0 + new_canonical_owners: 0 + new_privileges: 0 +contracts: + consume: [contracts.identity@v1, contracts.authority@v1] + may_change: [] +semantic_owner: work +assumptions: [] +acceptance_scenarios: [SCN-...] +required_tests: [unit, contract, architecture] +terminal_gap_states: + - BLOCKED_BY_SPEC_GAP + - BLOCKED_BY_ARCHITECTURE_GAP + - BLOCKED_BY_MISSING_CAPABILITY +``` + +## 5.3 Risk tiers + +- **Tier A — module-local:** no public contract/schema ownership/privilege changes. Fast path. +- **Tier B — module contract/persistence local:** owner-local schema or stable port changes. Migration/contract tests. +- **Tier C — cross-module:** integration contracts/events/topology interactions. Independent architecture review. +- **Tier D — authority/canonical migration/external side effects:** adversarial/property/recovery tests and stricter promotion. +- **Tier E — Core/guardrails/constitutional:** proposal first; changed verifier cannot self-certify. + +## 5.4 Codex preflight for Tier B+ + +Before write, produce structured implementation plan containing: + +- modules/files expected to change; +- semantic owner(s); +- contracts consumed/changed; +- canonical state and state-machine transitions affected; +- side effects/privileges; +- persistence/migrations/events; +- failure/retry/restart semantics; +- tests to prove invariants. + +Plan is machine-checked against Task Packet. Unexpected architecture delta becomes finding/gap. + +## 5.5 Allowed local freedom + +Inside an owned module Codex may choose ordinary data structures, algorithms, file decomposition and local refactoring, provided public contracts/invariants/task scope remain intact. Architecture defines constraints, not every function. + +## 5.6 Mandatory Codex completion report + +- changed files/modules; +- contracts/schemas/events/dependencies/privileges touched; +- tests actually executed (CI is authoritative evidence); +- assumptions made; +- known limitations; +- unrelated findings not changed; +- any specification/architecture gaps. + +## 5.7 Development Commit Gate + +Candidate patch → deterministic build/tests → architecture sentinel → DB/migration checks → contract checks → risk-specific review → exact-artifact promotion/merge. + +Ordinary Codex tasks never write protected main directly. Promotion moves the evaluated artifact/commit, not a rebuilt unknown variant. + +## 5.8 Architecture Sentinel responsibilities + +Deterministically compare declared vs observed architecture: + +- import/dependency graph; +- DB writes/FKs/views/triggers; +- event publishers/subscribers; +- protected path and diff scope; +- third-party dependency diff; +- public contract diff; +- privileged secret/capability access where enforceable; +- guardrail execution health. + +AI reviewer handles semantic smells that cannot be mechanically proven: God Ports, ownership leakage, false abstraction, orchestration absorbing domain semantics, hidden co-location assumptions, unsafe retry reasoning. + +## 5.9 Architecture exceptions + +No `nolint`/architecture ignore without registered exception ID containing reason, scope, expiry/removal condition. New debt cannot be introduced silently. Existing baseline debt can be ratcheted down. + +## 5.10 Codex failure is valid + +Codex is explicitly allowed to return a typed gap rather than “make it work somehow”. A gap becomes architecture/spec/research task and may revise the original task packet. This is preferred to an unapproved shortcut. + +## 5.11 Cold Agent requirement + +At major milestones, a fresh Codex session with no conversational history must be able to explain module ownership/boundaries and implement a bounded task using canonical repo artifacts only. Critical architecture cannot live only in chat history. diff --git a/development/CREDENTIAL_POLICY.md b/development/CREDENTIAL_POLICY.md new file mode 100644 index 0000000..7af90ac --- /dev/null +++ b/development/CREDENTIAL_POLICY.md @@ -0,0 +1,17 @@ +# Development credential policy + +Codex may use credential helpers already configured on the machine, but must not expose or persist their secrets. + +Allowed discovery: +- `git credential` / Git Credential Manager as invoked by Git; +- Docker credential store as invoked by Docker; +- environment variables explicitly supplied to the task/runtime; +- sibling project files only to learn endpoint names, secret variable names and deployment conventions. + +Not allowed without a new explicit user instruction: +- recursive searching for plaintext passwords/tokens in unrelated repositories; +- printing recovered credentials; +- copying a literal secret from one project into this repository; +- committing `.env.local`, Portainer keys, Gitea tokens or passwords. + +If Git push or Portainer deployment still needs credentials, ask the user for the minimal credential/API key needed. diff --git a/development/README.md b/development/README.md new file mode 100644 index 0000000..552e62c --- /dev/null +++ b/development/README.md @@ -0,0 +1,7 @@ +# Development control plane — V1 minimum + +V1 intentionally starts small: Git + task packets + `devctl` + CI + architecture registry. Do not build a separate development platform service yet. + +Development flow: + +`Task Packet -> isolated branch/worktree -> Codex -> tests -> Architecture Sentinel/devctl -> review according to risk -> merge exact evaluated commit`. diff --git a/docs/CREDENTIALS_AND_SECRETS.md b/docs/CREDENTIALS_AND_SECRETS.md new file mode 100644 index 0000000..b7c5dac --- /dev/null +++ b/docs/CREDENTIALS_AND_SECRETS.md @@ -0,0 +1,22 @@ +# Credentials and secrets + +## Git +Remote is fixed to `https://git.cin.su/m/Assistant.git`. + +The repository can be read externally, but a push may require authentication. First let normal Git credential helpers handle authentication. Do not add username/password to the remote URL. + +If authentication is not already configured, ask the user for either: +- a Gitea personal access token with the minimum repository scope, or +- username + token/password if their local setup requires it. + +Never store it in Git. + +## Portainer +Target is `https://docker.cin.su`. + +Preferred automation credential: `PORTAINER_API_KEY` supplied outside Git. `PORTAINER_ENDPOINT_ID` may also be required after environment discovery. + +Do not guess Portainer credentials and do not search other project source trees for literal passwords. Existing OS/environment secret stores may be used without revealing values. + +## Production services +Each privileged runtime gets only its required secret/capability bindings. `commit-runtime` is intentionally isolated from the general Core Runtime. diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..0e0538b --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,17 @@ +# Deployment V1 + +Target control plane: Portainer at `https://docker.cin.su`. + +Recommended V1 deployment topology: +- `assistant-core-runtime` +- `assistant-continuity-runtime` +- `assistant-commit-runtime` (isolated privileged runtime) +- `assistant-cognitive-worker` (replaceable, no canonical ownership) +- PostgreSQL 18.6 +- NATS as replaceable integration transport + +Temporal is intentionally **not** the canonical owner of future obligations. If/when Temporal is added, loss of Temporal must be recoverable from canonical continuity state. + +The initial stack definition is `deploy/docker-compose.prod.yml`. Portainer may deploy it from Git after the repository has been pushed. + +Credentials/API deployment automation is deliberately not embedded. Use `PORTAINER_API_KEY` from an external secret source or deploy through Portainer UI/API after authenticating. diff --git a/docs/FOUNDATION_EXIT_AUDIT.md b/docs/FOUNDATION_EXIT_AUDIT.md new file mode 100644 index 0000000..8629695 --- /dev/null +++ b/docs/FOUNDATION_EXIT_AUDIT.md @@ -0,0 +1,28 @@ +# Foundation Exit audit — V1 + +Status is based on executed repository tests and Docker-stack verification on 2026-08-16. + +| Criterion | Status | Evidence / gap | +|---|---|---| +| Cold agent can use repository artifacts | PASS | `AGENTS.md`, architecture documents, versioned task packets and `devctl preflight`. | +| Cross-owner DB write denied | PASS | `tests/owner_acl/verify.ps1` proves `continuity_role` cannot write `work.tasks`. | +| Wrong-project mutation rejected | PASS | focus isolation tests and the remote WS-004 guarded commit harness. | +| Stale/revoked authority rejected | PASS | action-commit tests and live HTTP 409 checks in DEV-000017. | +| Duplicate/replay side effect prevention | PASS | isolated Test Executor applies by ActionIntentID once, injects lost ACK, and reconciles without mutation. | +| Future obligation survives engine loss | PASS | prospective-memory recovery and reproducible remote Continuity restart harness. | +| No-LLM continuity | PASS | DEV-000014 stopped cognitive worker while Core, Continuity and Commit remained available. | +| Two process topologies | PASS | in-process suites plus `tests/topology_equivalence/verify-remote.ps1` for extracted runtimes. | +| Provider swap/fake | PASS | deterministic cognitive fake has no canonical mutation authority. | +| Declared/observed architecture | PASS | strengthened sentinel checks declared contract dependencies and implementation imports; discovered Commit→Identity drift was removed. | +| Semantic migration drill | PASS | Capability Health V1→V2 migration, rejection of V3 and projection rebuild. | +| Guardrails negative tests | PASS | negative self-tests cover infrastructure imports, cross-module implementation imports, undeclared contracts, protected-path task authority, and live DB ACL denial. | + +## Foundation conclusion + +Foundation V1 is eligible for freeze. Creator session authentication was also completed as DEV-000019 and does not carry Authority. + +Post-foundation product work is intentionally separate: + +1. Choose and specify the first real external operation before adding a production Executor credential. +2. Build Creator API/UI only through validated Identity, Authority and Commit boundaries. +3. Add exact diff-to-task scope comparison to promotion CI when the first Git baseline exists. diff --git a/docs/LOCAL_PATH_AND_RESOURCES.md b/docs/LOCAL_PATH_AND_RESOURCES.md new file mode 100644 index 0000000..e240f79 --- /dev/null +++ b/docs/LOCAL_PATH_AND_RESOURCES.md @@ -0,0 +1,11 @@ +# Local project and private resources + +Expected local root on the user's workstation: + +`Z:\mst\codex\assistent` + +These are user-controlled resources: +- Git: `https://git.cin.su/m/Assistant.git` +- Portainer: `https://docker.cin.su` + +Do not hardcode drive `Z:` into application semantics. It is a development/workspace locator only. diff --git a/docs/MIGRATIONS.md b/docs/MIGRATIONS.md new file mode 100644 index 0000000..b7feedb --- /dev/null +++ b/docs/MIGRATIONS.md @@ -0,0 +1,9 @@ +# Semantic migrations + +## Capability Health V1 to V2 + +`007_capability_health_v2.sql` assigns existing rows semantic version `1` without inventing an observation time. V2 writers must explicitly provide a positive `observed_at_unix_millis`; unsupported versions are rejected by the owner domain validator. + +Forward fix: deploy the migration before the V2 runtime, then rebuild `capability.health_projection` through `POST /v1/capabilities/health-projection/rebuild`. + +Rollback: retain the migrated canonical rows and run the prior runtime only against V1 rows. A V2 row must not be silently read by an older runtime; use a forward-fix deployment to restore a V2-capable reader. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b81d994 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module git.cin.su/m/Assistant + +go 1.26 + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.8.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/text v0.29.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2aca163 --- /dev/null +++ b/go.sum @@ -0,0 +1,19 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/artifact/README.arch.md b/modules/artifact/README.arch.md new file mode 100644 index 0000000..bfa25c1 --- /dev/null +++ b/modules/artifact/README.arch.md @@ -0,0 +1,7 @@ +# artifact + +**Owns:** Artifact metadata and lineage semantics. + +See `architecture/architecture-registry.yaml` for allowed dependencies and DB schema. + +This module may not write another owner's canonical state. Domain code does not import adapters/runtime/providers. diff --git a/modules/artifact/application/doc.go b/modules/artifact/application/doc.go new file mode 100644 index 0000000..c02688f --- /dev/null +++ b/modules/artifact/application/doc.go @@ -0,0 +1,3 @@ +package application + +// Package application coordinates artifact-owned use cases through explicit ports. diff --git a/modules/artifact/domain/doc.go b/modules/artifact/domain/doc.go new file mode 100644 index 0000000..549c936 --- /dev/null +++ b/modules/artifact/domain/doc.go @@ -0,0 +1,3 @@ +package domain + +// Package domain contains artifact domain meaning only. It must not import adapters or runtime packages. diff --git a/modules/artifact/module.go b/modules/artifact/module.go new file mode 100644 index 0000000..dcfff94 --- /dev/null +++ b/modules/artifact/module.go @@ -0,0 +1,4 @@ +package artifact + +// Module is an intentionally minimal V1 composition handle. Do not turn it into a God service. +type Module struct{} diff --git a/modules/artifact/ports/doc.go b/modules/artifact/ports/doc.go new file mode 100644 index 0000000..2116642 --- /dev/null +++ b/modules/artifact/ports/doc.go @@ -0,0 +1,3 @@ +package ports + +// Package ports declares artifact-owned external dependencies. Ports must be narrow and semantic. diff --git a/modules/authority/README.arch.md b/modules/authority/README.arch.md new file mode 100644 index 0000000..17146ff --- /dev/null +++ b/modules/authority/README.arch.md @@ -0,0 +1,7 @@ +# authority + +**Owns:** Scope and authority grant/validation semantics. + +See `architecture/architecture-registry.yaml` for allowed dependencies and DB schema. + +This module may not write another owner's canonical state. Domain code does not import adapters/runtime/providers. diff --git a/modules/authority/application/doc.go b/modules/authority/application/doc.go new file mode 100644 index 0000000..ec8a269 --- /dev/null +++ b/modules/authority/application/doc.go @@ -0,0 +1,3 @@ +package application + +// Package application coordinates authority-owned use cases through explicit ports. diff --git a/modules/authority/domain/creator.go b/modules/authority/domain/creator.go new file mode 100644 index 0000000..00cbd81 --- /dev/null +++ b/modules/authority/domain/creator.go @@ -0,0 +1,22 @@ +package domain + +import ( + "errors" + + "git.cin.su/m/Assistant/contracts/authority" + "git.cin.su/m/Assistant/contracts/identity" +) + +var ErrInvalidCreatorGrant = errors.New("creator grant must be active, global, and revisioned") + +// ValidateCreatorGrant validates the narrow bootstrap authority used by +// Domovoy. Execution paths must still perform their own target/revision checks. +func ValidateCreatorGrant(binding identity.PrincipalBinding, scope authority.Scope, grant authority.AuthorityGrant) error { + if binding.ActorID == "" || binding.PrincipalID == "" || !binding.Active || + !scope.Global || scope.ID == "" || + grant.ID == "" || grant.Class != authority.GrantClassCreator || !grant.Active || grant.Revision == 0 || + grant.ActorID != binding.ActorID || grant.PrincipalID != binding.PrincipalID || grant.ScopeID != scope.ID { + return ErrInvalidCreatorGrant + } + return nil +} diff --git a/modules/authority/domain/doc.go b/modules/authority/domain/doc.go new file mode 100644 index 0000000..73779cb --- /dev/null +++ b/modules/authority/domain/doc.go @@ -0,0 +1,3 @@ +package domain + +// Package domain contains authority domain meaning only. It must not import adapters or runtime packages. diff --git a/modules/authority/domain/grant.go b/modules/authority/domain/grant.go new file mode 100644 index 0000000..7c31a1c --- /dev/null +++ b/modules/authority/domain/grant.go @@ -0,0 +1,19 @@ +package domain + +import ( + "errors" + + "git.cin.su/m/Assistant/contracts/authority" + "git.cin.su/m/Assistant/contracts/identity" +) + +var ErrInvalidGrant = errors.New("authority grant is not currently valid for actor, principal, and scope") + +func ValidateGrant(binding identity.PrincipalBinding, scope authority.Scope, grant authority.AuthorityGrant) error { + if binding.ActorID == "" || binding.PrincipalID == "" || !binding.Active || + scope.ID == "" || grant.ID == "" || grant.Revision == 0 || !grant.Active || + grant.ActorID != binding.ActorID || grant.PrincipalID != binding.PrincipalID || grant.ScopeID != scope.ID { + return ErrInvalidGrant + } + return nil +} diff --git a/modules/authority/module.go b/modules/authority/module.go new file mode 100644 index 0000000..3eeb316 --- /dev/null +++ b/modules/authority/module.go @@ -0,0 +1,4 @@ +package authority + +// Module is an intentionally minimal V1 composition handle. Do not turn it into a God service. +type Module struct{} diff --git a/modules/authority/ports/doc.go b/modules/authority/ports/doc.go new file mode 100644 index 0000000..5566e1d --- /dev/null +++ b/modules/authority/ports/doc.go @@ -0,0 +1,3 @@ +package ports + +// Package ports declares authority-owned external dependencies. Ports must be narrow and semantic. diff --git a/modules/capability/README.arch.md b/modules/capability/README.arch.md new file mode 100644 index 0000000..1ab7ef5 --- /dev/null +++ b/modules/capability/README.arch.md @@ -0,0 +1,7 @@ +# capability + +**Owns:** Capability, binding and health semantics. + +See `architecture/architecture-registry.yaml` for allowed dependencies and DB schema. + +This module may not write another owner's canonical state. Domain code does not import adapters/runtime/providers. diff --git a/modules/capability/application/doc.go b/modules/capability/application/doc.go new file mode 100644 index 0000000..252b9f9 --- /dev/null +++ b/modules/capability/application/doc.go @@ -0,0 +1,3 @@ +package application + +// Package application coordinates capability-owned use cases through explicit ports. diff --git a/modules/capability/domain/doc.go b/modules/capability/domain/doc.go new file mode 100644 index 0000000..228003b --- /dev/null +++ b/modules/capability/domain/doc.go @@ -0,0 +1,3 @@ +package domain + +// Package domain contains capability domain meaning only. It must not import adapters or runtime packages. diff --git a/modules/capability/domain/health.go b/modules/capability/domain/health.go new file mode 100644 index 0000000..4ac0773 --- /dev/null +++ b/modules/capability/domain/health.go @@ -0,0 +1,41 @@ +package domain + +import ( + "errors" + + "git.cin.su/m/Assistant/contracts/capability" +) + +var ErrInvalidHealth = errors.New("invalid capability health") +var ErrUnsupportedHealthVersion = errors.New("unsupported capability health semantic version") + +func ValidateHealth(health capability.Health) error { + if health.CapabilityID == "" || health.Revision < 1 { + return ErrInvalidHealth + } + switch health.SemanticVersion { + case capability.HealthV1: + if health.ObservedAtUnixMillis != nil { + return ErrInvalidHealth + } + case capability.HealthV2: + if health.ObservedAtUnixMillis == nil || *health.ObservedAtUnixMillis < 1 { + return ErrInvalidHealth + } + default: + return ErrUnsupportedHealthVersion + } + switch health.State { + case capability.HealthAvailable: + if health.Reason != capability.ReasonNone { + return ErrInvalidHealth + } + case capability.HealthDegraded, capability.HealthUnavailable: + if health.Reason == capability.ReasonNone || health.Reason == "" { + return ErrInvalidHealth + } + default: + return ErrInvalidHealth + } + return nil +} diff --git a/modules/capability/module.go b/modules/capability/module.go new file mode 100644 index 0000000..3c5bf10 --- /dev/null +++ b/modules/capability/module.go @@ -0,0 +1,4 @@ +package capability + +// Module is an intentionally minimal V1 composition handle. Do not turn it into a God service. +type Module struct{} diff --git a/modules/capability/ports/doc.go b/modules/capability/ports/doc.go new file mode 100644 index 0000000..9aa7710 --- /dev/null +++ b/modules/capability/ports/doc.go @@ -0,0 +1,3 @@ +package ports + +// Package ports declares capability-owned external dependencies. Ports must be narrow and semantic. diff --git a/modules/capability/ports/repository.go b/modules/capability/ports/repository.go new file mode 100644 index 0000000..ae7328d --- /dev/null +++ b/modules/capability/ports/repository.go @@ -0,0 +1,13 @@ +package ports + +import ( + "context" + + "git.cin.su/m/Assistant/contracts/capability" +) + +type Repository interface { + SaveHealth(context.Context, capability.Health) error + LoadHealth(context.Context, capability.CapabilityID) (capability.Health, error) + RebuildHealthProjection(context.Context) (int64, error) +} diff --git a/modules/commit/README.arch.md b/modules/commit/README.arch.md new file mode 100644 index 0000000..06464e1 --- /dev/null +++ b/modules/commit/README.arch.md @@ -0,0 +1,7 @@ +# commit + +**Owns:** Action proposal, authorization/commit protocol and attempt records. + +See `architecture/architecture-registry.yaml` for allowed dependencies and DB schema. + +This module may not write another owner's canonical state. Domain code does not import adapters/runtime/providers. diff --git a/modules/commit/application/doc.go b/modules/commit/application/doc.go new file mode 100644 index 0000000..9d90ef0 --- /dev/null +++ b/modules/commit/application/doc.go @@ -0,0 +1,3 @@ +package application + +// Package application coordinates commit-owned use cases through explicit ports. diff --git a/modules/commit/domain/commit.go b/modules/commit/domain/commit.go new file mode 100644 index 0000000..1403915 --- /dev/null +++ b/modules/commit/domain/commit.go @@ -0,0 +1,78 @@ +package domain + +import ( + "errors" + + "git.cin.su/m/Assistant/contracts/authority" + "git.cin.su/m/Assistant/contracts/capability" + commitcontract "git.cin.su/m/Assistant/contracts/commit" + "git.cin.su/m/Assistant/modules/commit/ports" +) + +var ( + ErrInvalidProposal = errors.New("action proposal lacks target, revision, capability, or operation") + ErrUnknownOutcome = errors.New("unknown outcome requires reconciliation before retry") + ErrInvalidReconciliation = errors.New("reconciliation requires the matching UNKNOWN_OUTCOME attempt and a new attempt identity") +) + +type AuthorizationContext = authority.AuthorizationContext + +// Reconcile verifies an UNKNOWN_OUTCOME through a read-only external query. +// It never invokes Executor.Execute and therefore cannot repeat the effect. +func Reconcile(action commitcontract.AuthorizedAction, previous commitcontract.ActionAttempt, attemptID commitcontract.ActionAttemptID, reconciler ports.Reconciler) (commitcontract.ActionAttempt, error) { + if action.State != commitcontract.ActionAuthorized || previous.State != commitcontract.ActionUnknownOutcome || + previous.AuthorizedID != action.ID || previous.IntentID != action.Proposal.IntentID || attemptID == "" || attemptID == previous.ID { + return commitcontract.ActionAttempt{}, ErrInvalidReconciliation + } + attempt := commitcontract.ActionAttempt{ID: attemptID, AuthorizedID: action.ID, IntentID: action.Proposal.IntentID, State: commitcontract.ActionExecuting} + result := reconciler.Reconcile(action) + if !result.Known { + attempt.State = commitcontract.ActionUnknownOutcome + return attempt, ErrUnknownOutcome + } + if result.Succeeded { + attempt.State = commitcontract.ActionSucceeded + return attempt, nil + } + attempt.State = commitcontract.ActionFailed + return attempt, nil +} + +// Authorize is the Commit boundary. A proposal alone never reaches Executor. +func Authorize(proposal commitcontract.ActionProposal, context AuthorizationContext, validator ports.AuthorityValidator, authorizedID commitcontract.AuthorizedActionID) (commitcontract.AuthorizedAction, error) { + if proposal.IntentID == "" || proposal.ActorID == "" || proposal.CapabilityID == capability.CapabilityID("") || + proposal.Target.TaskID == "" || proposal.Target.GoalID == "" || proposal.Target.ScopeID == "" || proposal.Target.Revision == 0 || proposal.Operation == "" { + return commitcontract.AuthorizedAction{}, ErrInvalidProposal + } + if proposal.ActorID != context.Binding.ActorID || proposal.Target.ScopeID != context.Scope.ID { + return commitcontract.AuthorizedAction{}, ErrInvalidProposal + } + if err := validator.Validate(context); err != nil { + return commitcontract.AuthorizedAction{}, err + } + if authorizedID == "" { + return commitcontract.AuthorizedAction{}, ErrInvalidProposal + } + proposal.State = commitcontract.ActionProposed + return commitcontract.AuthorizedAction{ID: authorizedID, Proposal: proposal, GrantID: context.Grant.ID, ScopeID: context.Scope.ID, State: commitcontract.ActionAuthorized}, nil +} + +// Execute records UNKNOWN_OUTCOME rather than silently retrying an ambiguous +// external mutation. +func Execute(action commitcontract.AuthorizedAction, attemptID commitcontract.ActionAttemptID, executor ports.Executor) (commitcontract.ActionAttempt, error) { + if action.State != commitcontract.ActionAuthorized || attemptID == "" { + return commitcontract.ActionAttempt{}, ErrUnknownOutcome + } + attempt := commitcontract.ActionAttempt{ID: attemptID, AuthorizedID: action.ID, IntentID: action.Proposal.IntentID, State: commitcontract.ActionExecuting} + result := executor.Execute(action) + if !result.Known { + attempt.State = commitcontract.ActionUnknownOutcome + return attempt, ErrUnknownOutcome + } + if result.Succeeded { + attempt.State = commitcontract.ActionSucceeded + return attempt, nil + } + attempt.State = commitcontract.ActionFailed + return attempt, nil +} diff --git a/modules/commit/domain/doc.go b/modules/commit/domain/doc.go new file mode 100644 index 0000000..0d56045 --- /dev/null +++ b/modules/commit/domain/doc.go @@ -0,0 +1,3 @@ +package domain + +// Package domain contains commit domain meaning only. It must not import adapters or runtime packages. diff --git a/modules/commit/module.go b/modules/commit/module.go new file mode 100644 index 0000000..8d587b4 --- /dev/null +++ b/modules/commit/module.go @@ -0,0 +1,4 @@ +package commit + +// Module is an intentionally minimal V1 composition handle. Do not turn it into a God service. +type Module struct{} diff --git a/modules/commit/ports/doc.go b/modules/commit/ports/doc.go new file mode 100644 index 0000000..740dc7a --- /dev/null +++ b/modules/commit/ports/doc.go @@ -0,0 +1,3 @@ +package ports + +// Package ports declares commit-owned external dependencies. Ports must be narrow and semantic. diff --git a/modules/commit/ports/executor.go b/modules/commit/ports/executor.go new file mode 100644 index 0000000..2af4ff4 --- /dev/null +++ b/modules/commit/ports/executor.go @@ -0,0 +1,27 @@ +package ports + +import ( + "git.cin.su/m/Assistant/contracts/authority" + "git.cin.su/m/Assistant/contracts/commit" +) + +// Executor is the only commit-owned port permitted to reach an external world. +// Its implementation belongs in an isolated adapter/runtime, never in domain. +type Executor interface { + Execute(commit.AuthorizedAction) ExecutionResult +} + +// Reconciler verifies an ambiguous effect without applying it again. +type Reconciler interface { + Reconcile(commit.AuthorizedAction) ExecutionResult +} + +type ExecutionResult struct { + Succeeded bool + Known bool +} + +// AuthorityValidator is the remote-capable boundary to the Authority owner. +type AuthorityValidator interface { + Validate(authority.AuthorizationContext) error +} diff --git a/modules/continuity/README.arch.md b/modules/continuity/README.arch.md new file mode 100644 index 0000000..f51da30 --- /dev/null +++ b/modules/continuity/README.arch.md @@ -0,0 +1,7 @@ +# continuity + +**Owns:** Commitment and ProspectiveItem semantics. + +See `architecture/architecture-registry.yaml` for allowed dependencies and DB schema. + +This module may not write another owner's canonical state. Domain code does not import adapters/runtime/providers. diff --git a/modules/continuity/application/doc.go b/modules/continuity/application/doc.go new file mode 100644 index 0000000..057f5eb --- /dev/null +++ b/modules/continuity/application/doc.go @@ -0,0 +1,3 @@ +package application + +// Package application coordinates continuity-owned use cases through explicit ports. diff --git a/modules/continuity/domain/doc.go b/modules/continuity/domain/doc.go new file mode 100644 index 0000000..b94313e --- /dev/null +++ b/modules/continuity/domain/doc.go @@ -0,0 +1,3 @@ +package domain + +// Package domain contains continuity domain meaning only. It must not import adapters or runtime packages. diff --git a/modules/continuity/domain/prospective.go b/modules/continuity/domain/prospective.go new file mode 100644 index 0000000..2c52261 --- /dev/null +++ b/modules/continuity/domain/prospective.go @@ -0,0 +1,90 @@ +package domain + +import ( + "errors" + + contract "git.cin.su/m/Assistant/contracts/continuity" +) + +var ( + ErrProspectiveNotFound = errors.New("prospective item not found") + ErrProspectiveRevision = errors.New("prospective item revision conflict") + ErrInvalidTransition = errors.New("invalid prospective item transition") +) + +type Store struct { + items map[contract.ProspectiveItemID]contract.ProspectiveItem +} + +func Recover(items []contract.ProspectiveItem) Store { + stored := make(map[contract.ProspectiveItemID]contract.ProspectiveItem, len(items)) + for _, item := range items { + stored[item.ID] = item + } + return Store{items: stored} +} + +func (s *Store) Snapshot() []contract.ProspectiveItem { + items := make([]contract.ProspectiveItem, 0, len(s.items)) + for _, item := range s.items { + items = append(items, item) + } + return items +} + +func (s *Store) Trigger(id contract.ProspectiveItemID, revision contract.ProspectiveRevision) (contract.ProspectiveItem, error) { + item, err := s.current(id, revision) + if err != nil { + return contract.ProspectiveItem{}, err + } + if item.State != contract.ProspectiveArmed { + return contract.ProspectiveItem{}, ErrInvalidTransition + } + item.State = contract.ProspectiveTriggered + item.Revision++ + s.items[id] = item + return item, nil +} + +func (s *Store) BeginRevalidation(id contract.ProspectiveItemID, revision contract.ProspectiveRevision) (contract.ProspectiveItem, error) { + item, err := s.current(id, revision) + if err != nil { + return contract.ProspectiveItem{}, err + } + if item.State != contract.ProspectiveTriggered { + return contract.ProspectiveItem{}, ErrInvalidTransition + } + item.State = contract.ProspectiveRevalidating + item.Revision++ + s.items[id] = item + return item, nil +} + +func (s *Store) FinishRevalidation(id contract.ProspectiveItemID, revision contract.ProspectiveRevision, relevant bool) (contract.ProspectiveItem, error) { + item, err := s.current(id, revision) + if err != nil { + return contract.ProspectiveItem{}, err + } + if item.State != contract.ProspectiveRevalidating { + return contract.ProspectiveItem{}, ErrInvalidTransition + } + if relevant { + item.State = contract.ProspectiveDelivered + } else { + item.State = contract.ProspectiveSuppressed + } + item.Revision++ + s.items[id] = item + return item, nil +} + +func (s *Store) current(id contract.ProspectiveItemID, revision contract.ProspectiveRevision) (contract.ProspectiveItem, error) { + item, ok := s.items[id] + if !ok { + return contract.ProspectiveItem{}, ErrProspectiveNotFound + } + if item.Revision != revision { + return contract.ProspectiveItem{}, ErrProspectiveRevision + } + return item, nil +} diff --git a/modules/continuity/module.go b/modules/continuity/module.go new file mode 100644 index 0000000..aef4acd --- /dev/null +++ b/modules/continuity/module.go @@ -0,0 +1,4 @@ +package continuity + +// Module is an intentionally minimal V1 composition handle. Do not turn it into a God service. +type Module struct{} diff --git a/modules/continuity/ports/doc.go b/modules/continuity/ports/doc.go new file mode 100644 index 0000000..ea3a97e --- /dev/null +++ b/modules/continuity/ports/doc.go @@ -0,0 +1,3 @@ +package ports + +// Package ports declares continuity-owned external dependencies. Ports must be narrow and semantic. diff --git a/modules/continuity/ports/repository.go b/modules/continuity/ports/repository.go new file mode 100644 index 0000000..e465763 --- /dev/null +++ b/modules/continuity/ports/repository.go @@ -0,0 +1,12 @@ +package ports + +import ( + "context" + + "git.cin.su/m/Assistant/contracts/continuity" +) + +type Repository interface { + Save(context.Context, continuity.ProspectiveItem) error + Load(context.Context, continuity.ProspectiveItemID) (continuity.ProspectiveItem, error) +} diff --git a/modules/epistemics/README.arch.md b/modules/epistemics/README.arch.md new file mode 100644 index 0000000..4e3c852 --- /dev/null +++ b/modules/epistemics/README.arch.md @@ -0,0 +1,7 @@ +# epistemics + +**Owns:** Observation, Claim, Belief and Provenance semantics. + +See `architecture/architecture-registry.yaml` for allowed dependencies and DB schema. + +This module may not write another owner's canonical state. Domain code does not import adapters/runtime/providers. diff --git a/modules/epistemics/application/doc.go b/modules/epistemics/application/doc.go new file mode 100644 index 0000000..9881cfc --- /dev/null +++ b/modules/epistemics/application/doc.go @@ -0,0 +1,3 @@ +package application + +// Package application coordinates epistemics-owned use cases through explicit ports. diff --git a/modules/epistemics/domain/doc.go b/modules/epistemics/domain/doc.go new file mode 100644 index 0000000..9d6af15 --- /dev/null +++ b/modules/epistemics/domain/doc.go @@ -0,0 +1,3 @@ +package domain + +// Package domain contains epistemics domain meaning only. It must not import adapters or runtime packages. diff --git a/modules/epistemics/module.go b/modules/epistemics/module.go new file mode 100644 index 0000000..beec9f2 --- /dev/null +++ b/modules/epistemics/module.go @@ -0,0 +1,4 @@ +package epistemics + +// Module is an intentionally minimal V1 composition handle. Do not turn it into a God service. +type Module struct{} diff --git a/modules/epistemics/ports/doc.go b/modules/epistemics/ports/doc.go new file mode 100644 index 0000000..80e6c9f --- /dev/null +++ b/modules/epistemics/ports/doc.go @@ -0,0 +1,3 @@ +package ports + +// Package ports declares epistemics-owned external dependencies. Ports must be narrow and semantic. diff --git a/modules/identity/README.arch.md b/modules/identity/README.arch.md new file mode 100644 index 0000000..0aa4535 --- /dev/null +++ b/modules/identity/README.arch.md @@ -0,0 +1,7 @@ +# identity + +**Owns:** Identity, Actor and Principal binding semantics. + +See `architecture/architecture-registry.yaml` for allowed dependencies and DB schema. + +This module may not write another owner's canonical state. Domain code does not import adapters/runtime/providers. diff --git a/modules/identity/application/doc.go b/modules/identity/application/doc.go new file mode 100644 index 0000000..3e45769 --- /dev/null +++ b/modules/identity/application/doc.go @@ -0,0 +1,3 @@ +package application + +// Package application coordinates identity-owned use cases through explicit ports. diff --git a/modules/identity/domain/creator.go b/modules/identity/domain/creator.go new file mode 100644 index 0000000..fac74bb --- /dev/null +++ b/modules/identity/domain/creator.go @@ -0,0 +1,17 @@ +package domain + +import ( + "errors" + + "git.cin.su/m/Assistant/contracts/identity" +) + +var ErrInvalidCreatorBinding = errors.New("creator actor and principal binding must be explicit and active") + +// ValidateCreatorBinding makes the Creator's two semantic roles explicit. +func ValidateCreatorBinding(binding identity.PrincipalBinding) error { + if binding.ActorID == "" || binding.PrincipalID == "" || !binding.Active { + return ErrInvalidCreatorBinding + } + return nil +} diff --git a/modules/identity/domain/doc.go b/modules/identity/domain/doc.go new file mode 100644 index 0000000..67013bb --- /dev/null +++ b/modules/identity/domain/doc.go @@ -0,0 +1,3 @@ +package domain + +// Package domain contains identity domain meaning only. It must not import adapters or runtime packages. diff --git a/modules/identity/domain/session.go b/modules/identity/domain/session.go new file mode 100644 index 0000000..a2f01dc --- /dev/null +++ b/modules/identity/domain/session.go @@ -0,0 +1,54 @@ +package domain + +import ( + "crypto/hmac" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "encoding/json" + "errors" + "strings" + "time" + + "git.cin.su/m/Assistant/contracts/identity" +) + +var ErrInvalidCreatorSession = errors.New("invalid creator session") + +func IssueCreatorSession(secret []byte, now time.Time, ttl time.Duration) (string, identity.CreatorSession, error) { + if len(secret) < 32 || ttl <= 0 { + return "", identity.CreatorSession{}, ErrInvalidCreatorSession + } + session := identity.CreatorSession{ActorID: "domovoy-creator-actor", PrincipalID: "domovoy-creator-principal", ExpiresAtUnix: now.Add(ttl).Unix()} + payload, err := json.Marshal(session) + if err != nil { + return "", identity.CreatorSession{}, err + } + encoded := base64.RawURLEncoding.EncodeToString(payload) + return encoded + "." + signature(secret, encoded), session, nil +} + +func ValidateCreatorSession(secret []byte, token string, now time.Time) (identity.CreatorSession, error) { + parts := strings.Split(token, ".") + if len(parts) != 2 || len(secret) < 32 || subtle.ConstantTimeCompare([]byte(signature(secret, parts[0])), []byte(parts[1])) != 1 { + return identity.CreatorSession{}, ErrInvalidCreatorSession + } + payload, err := base64.RawURLEncoding.DecodeString(parts[0]) + if err != nil { + return identity.CreatorSession{}, ErrInvalidCreatorSession + } + var session identity.CreatorSession + if json.Unmarshal(payload, &session) != nil || session.ActorID != "domovoy-creator-actor" || session.PrincipalID != "domovoy-creator-principal" || now.Unix() >= session.ExpiresAtUnix { + return identity.CreatorSession{}, ErrInvalidCreatorSession + } + return session, nil +} + +func BootstrapMatches(expected, supplied string) bool { + return expected != "" && supplied != "" && subtle.ConstantTimeCompare([]byte(expected), []byte(supplied)) == 1 +} +func signature(secret []byte, payload string) string { + mac := hmac.New(sha256.New, secret) + mac.Write([]byte(payload)) + return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) +} diff --git a/modules/identity/module.go b/modules/identity/module.go new file mode 100644 index 0000000..fe455dc --- /dev/null +++ b/modules/identity/module.go @@ -0,0 +1,4 @@ +package identity + +// Module is an intentionally minimal V1 composition handle. Do not turn it into a God service. +type Module struct{} diff --git a/modules/identity/ports/doc.go b/modules/identity/ports/doc.go new file mode 100644 index 0000000..2263f08 --- /dev/null +++ b/modules/identity/ports/doc.go @@ -0,0 +1,3 @@ +package ports + +// Package ports declares identity-owned external dependencies. Ports must be narrow and semantic. diff --git a/modules/situation/README.arch.md b/modules/situation/README.arch.md new file mode 100644 index 0000000..7714f7e --- /dev/null +++ b/modules/situation/README.arch.md @@ -0,0 +1,7 @@ +# situation + +**Owns:** Situation and focus binding semantics. + +See `architecture/architecture-registry.yaml` for allowed dependencies and DB schema. + +This module may not write another owner's canonical state. Domain code does not import adapters/runtime/providers. diff --git a/modules/situation/application/doc.go b/modules/situation/application/doc.go new file mode 100644 index 0000000..0914edb --- /dev/null +++ b/modules/situation/application/doc.go @@ -0,0 +1,3 @@ +package application + +// Package application coordinates situation-owned use cases through explicit ports. diff --git a/modules/situation/domain/doc.go b/modules/situation/domain/doc.go new file mode 100644 index 0000000..7fbf131 --- /dev/null +++ b/modules/situation/domain/doc.go @@ -0,0 +1,3 @@ +package domain + +// Package domain contains situation domain meaning only. It must not import adapters or runtime packages. diff --git a/modules/situation/domain/focus.go b/modules/situation/domain/focus.go new file mode 100644 index 0000000..e52a7ee --- /dev/null +++ b/modules/situation/domain/focus.go @@ -0,0 +1,60 @@ +package domain + +import ( + "errors" + + contract "git.cin.su/m/Assistant/contracts/situation" + "git.cin.su/m/Assistant/contracts/work" +) + +var ( + ErrNoActiveFocus = errors.New("no active focus binding") + ErrStaleFocus = errors.New("stale focus binding") + ErrAmbiguousFocus = errors.New("ambiguous focus binding") + ErrTargetMismatch = errors.New("proposed target does not match resolved focus") +) + +// Resolver is a deterministic situation-owned resolver. It accepts runtime +// bindings only; RequestText must never be used to select a project. +type Resolver struct { + bindings []contract.FocusBinding +} + +func NewResolver(bindings []contract.FocusBinding) Resolver { + return Resolver{bindings: append([]contract.FocusBinding(nil), bindings...)} +} + +func (r Resolver) Resolve(runtime contract.RuntimeBinding) (contract.FocusBinding, error) { + var matches []contract.FocusBinding + var stale bool + for _, binding := range r.bindings { + if binding.SessionID != runtime.SessionID || binding.Workspace != runtime.Workspace { + continue + } + if !binding.Active { + stale = true + continue + } + matches = append(matches, binding) + } + switch len(matches) { + case 0: + if stale { + return contract.FocusBinding{}, ErrStaleFocus + } + return contract.FocusBinding{}, ErrNoActiveFocus + case 1: + return matches[0], nil + default: + return contract.FocusBinding{}, ErrAmbiguousFocus + } +} + +// ValidateTaskTarget prevents cognition from redirecting a request outside the +// resolved situation before a work owner considers its own revision guard. +func ValidateTaskTarget(focus contract.FocusBinding, target work.TaskTarget) error { + if focus.GoalID != target.GoalID || focus.ScopeID != target.ScopeID { + return ErrTargetMismatch + } + return nil +} diff --git a/modules/situation/module.go b/modules/situation/module.go new file mode 100644 index 0000000..b396bd4 --- /dev/null +++ b/modules/situation/module.go @@ -0,0 +1,4 @@ +package situation + +// Module is an intentionally minimal V1 composition handle. Do not turn it into a God service. +type Module struct{} diff --git a/modules/situation/ports/doc.go b/modules/situation/ports/doc.go new file mode 100644 index 0000000..6ed818e --- /dev/null +++ b/modules/situation/ports/doc.go @@ -0,0 +1,3 @@ +package ports + +// Package ports declares situation-owned external dependencies. Ports must be narrow and semantic. diff --git a/modules/work/README.arch.md b/modules/work/README.arch.md new file mode 100644 index 0000000..1fba9d8 --- /dev/null +++ b/modules/work/README.arch.md @@ -0,0 +1,7 @@ +# work + +**Owns:** Goal, Task and Decision semantics. + +See `architecture/architecture-registry.yaml` for allowed dependencies and DB schema. + +This module may not write another owner's canonical state. Domain code does not import adapters/runtime/providers. diff --git a/modules/work/application/doc.go b/modules/work/application/doc.go new file mode 100644 index 0000000..e814918 --- /dev/null +++ b/modules/work/application/doc.go @@ -0,0 +1,3 @@ +package application + +// Package application coordinates work-owned use cases through explicit ports. diff --git a/modules/work/domain/doc.go b/modules/work/domain/doc.go new file mode 100644 index 0000000..1ead298 --- /dev/null +++ b/modules/work/domain/doc.go @@ -0,0 +1,3 @@ +package domain + +// Package domain contains work domain meaning only. It must not import adapters or runtime packages. diff --git a/modules/work/domain/task.go b/modules/work/domain/task.go new file mode 100644 index 0000000..48c0493 --- /dev/null +++ b/modules/work/domain/task.go @@ -0,0 +1,51 @@ +package domain + +import ( + "errors" + + "git.cin.su/m/Assistant/contracts/authority" + contract "git.cin.su/m/Assistant/contracts/work" +) + +var ( + ErrTaskNotFound = errors.New("task not found") + ErrTaskTargetMismatch = errors.New("task target does not match canonical task") + ErrRevisionConflict = errors.New("task revision conflict") +) + +type Task struct { + ID contract.TaskID + GoalID contract.GoalID + ScopeID authority.ScopeID + Revision contract.TaskRevision +} + +// Store models owner-local task state for the walking skeleton. A caller must +// present all target integrity fields; no cross-owner lookup is performed. +type Store struct { + tasks map[contract.TaskID]Task +} + +func NewStore(tasks []Task) Store { + stored := make(map[contract.TaskID]Task, len(tasks)) + for _, task := range tasks { + stored[task.ID] = task + } + return Store{tasks: stored} +} + +func (s *Store) Commit(target contract.TaskTarget) (Task, error) { + task, ok := s.tasks[target.TaskID] + if !ok { + return Task{}, ErrTaskNotFound + } + if task.GoalID != target.GoalID || task.ScopeID != target.ScopeID { + return Task{}, ErrTaskTargetMismatch + } + if task.Revision != target.Revision { + return Task{}, ErrRevisionConflict + } + task.Revision++ + s.tasks[task.ID] = task + return task, nil +} diff --git a/modules/work/module.go b/modules/work/module.go new file mode 100644 index 0000000..047c730 --- /dev/null +++ b/modules/work/module.go @@ -0,0 +1,4 @@ +package work + +// Module is an intentionally minimal V1 composition handle. Do not turn it into a God service. +type Module struct{} diff --git a/modules/work/ports/doc.go b/modules/work/ports/doc.go new file mode 100644 index 0000000..722e083 --- /dev/null +++ b/modules/work/ports/doc.go @@ -0,0 +1,3 @@ +package ports + +// Package ports declares work-owned external dependencies. Ports must be narrow and semantic. diff --git a/ops/RUNBOOK.md b/ops/RUNBOOK.md new file mode 100644 index 0000000..e8e2a3a --- /dev/null +++ b/ops/RUNBOOK.md @@ -0,0 +1,8 @@ +# Operator runbook V1 + +1. Confirm Git commit/release manifest. +2. Confirm required secrets exist outside Git. +3. Run architecture/test gate. +4. Deploy exact evaluated artifact/stack through Portainer. +5. Verify `/healthz` on runtimes. +6. Do not treat startup success as semantic migration success; run release-specific verification before final promotion. diff --git a/platform/README.md b/platform/README.md new file mode 100644 index 0000000..f35471a --- /dev/null +++ b/platform/README.md @@ -0,0 +1,3 @@ +# platform + +Only shared **technical mechanisms** may live here. No shared domain objects. Platform packages cannot import semantic modules. diff --git a/platform/clock/clock.go b/platform/clock/clock.go new file mode 100644 index 0000000..146e0a1 --- /dev/null +++ b/platform/clock/clock.go @@ -0,0 +1,9 @@ +package clock + +import "time" + +type Clock interface{ Now() time.Time } + +type Real struct{} + +func (Real) Now() time.Time { return time.Now().UTC() } diff --git a/platform/errors/errors.go b/platform/errors/errors.go new file mode 100644 index 0000000..6cc23eb --- /dev/null +++ b/platform/errors/errors.go @@ -0,0 +1,22 @@ +package errors + +type Kind string + +const ( + Rejected Kind = "REJECTED" + Failed Kind = "FAILED" + UnknownOutcome Kind = "UNKNOWN_OUTCOME" + TemporarilyUnavailable Kind = "TEMPORARILY_UNAVAILABLE" + Conflict Kind = "CONFLICT" + Stale Kind = "STALE" + Unauthorized Kind = "UNAUTHORIZED" + Unsupported Kind = "UNSUPPORTED" +) + +type Error struct { + Kind Kind + Code string + Message string +} + +func (e Error) Error() string { return e.Message } diff --git a/platform/ids/ids.go b/platform/ids/ids.go new file mode 100644 index 0000000..d4391ff --- /dev/null +++ b/platform/ids/ids.go @@ -0,0 +1,19 @@ +package ids + +import ( + "crypto/rand" + "encoding/hex" + "fmt" +) + +// ID is a transport-neutral opaque identifier primitive. Domain packages wrap it +// in strong semantic ID types; do not pass bare ID across semantic boundaries. +type ID string + +func New() (ID, error) { + var b [16]byte + if _, err := rand.Read(b[:]); err != nil { + return "", fmt.Errorf("generate id: %w", err) + } + return ID(hex.EncodeToString(b[:])), nil +} diff --git a/runtime/health/server.go b/runtime/health/server.go new file mode 100644 index 0000000..a440385 --- /dev/null +++ b/runtime/health/server.go @@ -0,0 +1,22 @@ +package health + +import ( + "encoding/json" + "net/http" +) + +type Handler struct { + Runtime string + SemanticGeneration int + ArchitectureGeneration int +} + +func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "status": "ok", + "runtime": h.Runtime, + "semantic_generation": h.SemanticGeneration, + "architecture_generation": h.ArchitectureGeneration, + }) +} diff --git a/scripts/bootstrap.ps1 b/scripts/bootstrap.ps1 new file mode 100644 index 0000000..3bf0b25 --- /dev/null +++ b/scripts/bootstrap.ps1 @@ -0,0 +1,23 @@ +$ErrorActionPreference = "Stop" +$expected = "Z:\mst\codex\assistent" +Write-Host "Assistant bootstrap" +Write-Host "Current: $((Get-Location).Path)" +if ((Get-Location).Path -ne $expected) { Write-Warning "Expected local project folder is $expected (warning only)." } + +if (-not (Test-Path .git)) { git init } +$remotes = @(git remote 2>$null) +if ($remotes -notcontains "origin") { git remote add origin "https://git.cin.su/m/Assistant.git" } +else { + $origin = git remote get-url origin + if ($origin -ne "https://git.cin.su/m/Assistant.git") { Write-Warning "origin is $origin, expected https://git.cin.su/m/Assistant.git" } +} + +if (-not (Test-Path .env.local)) { Copy-Item .env.example .env.local; Write-Host "Created .env.local. Replace local-only placeholders before use." } + +Write-Host "Checking remote read access..." +git ls-remote origin 2>$null | Out-Null +if ($LASTEXITCODE -eq 0) { Write-Host "Git remote reachable." } else { Write-Warning "Git remote/auth not available. Let Git Credential Manager handle auth or ask user for token." } + +Write-Host "Next: go run ./cmd/devctl doctor" +Write-Host "Then: go run ./cmd/devctl check-architecture" +Write-Host "Then: go test ./..." diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100644 index 0000000..aed5cdd --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +set -eu +[ -d .git ] || git init +if ! git remote get-url origin >/dev/null 2>&1; then git remote add origin https://git.cin.su/m/Assistant.git; fi +[ -f .env.local ] || cp .env.example .env.local +printf '%s\n' 'Run: go run ./cmd/devctl doctor && go run ./cmd/devctl check-architecture && go test ./...' diff --git a/security/README.md b/security/README.md new file mode 100644 index 0000000..df57ceb --- /dev/null +++ b/security/README.md @@ -0,0 +1,5 @@ +# Security / privilege boundary + +Protected path. Ordinary feature tasks may not edit this directory. + +Core rule: stronger model/capability never implies more authority. External mutation credentials are restricted to Commit/Executor runtime or a narrower capability-specific executor. diff --git a/tests/action_commit/safe_commit_test.go b/tests/action_commit/safe_commit_test.go new file mode 100644 index 0000000..a38998d --- /dev/null +++ b/tests/action_commit/safe_commit_test.go @@ -0,0 +1,72 @@ +package action_commit_test + +import ( + "errors" + "testing" + + "git.cin.su/m/Assistant/contracts/authority" + "git.cin.su/m/Assistant/contracts/capability" + commitcontract "git.cin.su/m/Assistant/contracts/commit" + "git.cin.su/m/Assistant/contracts/identity" + "git.cin.su/m/Assistant/contracts/work" + authoritydomain "git.cin.su/m/Assistant/modules/authority/domain" + commitdomain "git.cin.su/m/Assistant/modules/commit/domain" + "git.cin.su/m/Assistant/modules/commit/ports" + "git.cin.su/m/Assistant/platform/ids" +) + +type validator struct{} + +func (validator) Validate(context authority.AuthorizationContext) error { + return authoritydomain.ValidateGrant(context.Binding, context.Scope, context.Grant) +} + +type unknownExecutor struct{} + +func (unknownExecutor) Execute(commitcontract.AuthorizedAction) ports.ExecutionResult { + return ports.ExecutionResult{Known: false} +} + +func TestWS003RejectsRevokedGrantAndNeverExecutesProposalDirectly(t *testing.T) { + context, proposal := testContextAndProposal() + context.Grant.Active = false + _, err := commitdomain.Authorize(proposal, context, validator{}, commitcontract.AuthorizedActionID(ids.ID("authorized"))) + if !errors.Is(err, authoritydomain.ErrInvalidGrant) { + t.Fatalf("authorization error = %v, want invalid grant", err) + } +} + +func TestWS003RejectsMissingActorPrincipalBinding(t *testing.T) { + context, proposal := testContextAndProposal() + context.Binding.Active = false + _, err := commitdomain.Authorize(proposal, context, validator{}, commitcontract.AuthorizedActionID(ids.ID("authorized"))) + if !errors.Is(err, authoritydomain.ErrInvalidGrant) { + t.Fatalf("authorization error = %v, want invalid grant", err) + } +} + +func TestWS003UnknownOutcomeBlocksBlindRetry(t *testing.T) { + context, proposal := testContextAndProposal() + action, err := commitdomain.Authorize(proposal, context, validator{}, commitcontract.AuthorizedActionID(ids.ID("authorized"))) + if err != nil { + t.Fatal(err) + } + attempt, err := commitdomain.Execute(action, commitcontract.ActionAttemptID(ids.ID("attempt-one")), unknownExecutor{}) + if !errors.Is(err, commitdomain.ErrUnknownOutcome) || attempt.State != commitcontract.ActionUnknownOutcome { + t.Fatalf("unknown result = %#v, %v", attempt, err) + } + action.State = commitcontract.ActionUnknownOutcome + if _, err := commitdomain.Execute(action, commitcontract.ActionAttemptID(ids.ID("attempt-two")), unknownExecutor{}); !errors.Is(err, commitdomain.ErrUnknownOutcome) { + t.Fatalf("blind retry error = %v, want unknown outcome", err) + } +} + +func testContextAndProposal() (commitdomain.AuthorizationContext, commitcontract.ActionProposal) { + actor := identity.ActorID(ids.ID("creator-actor")) + principal := identity.PrincipalID(ids.ID("creator-principal")) + scope := authority.Scope{ID: authority.ScopeID(ids.ID("global")), Global: true, Label: "all"} + grant := authority.AuthorityGrant{ID: authority.GrantID(ids.ID("grant")), ActorID: actor, PrincipalID: principal, ScopeID: scope.ID, Class: authority.GrantClassCreator, Revision: 1, Active: true} + context := commitdomain.AuthorizationContext{Binding: identity.PrincipalBinding{ActorID: actor, PrincipalID: principal, Active: true}, Scope: scope, Grant: grant} + proposal := commitcontract.ActionProposal{IntentID: commitcontract.ActionIntentID(ids.ID("intent")), ActorID: actor, CapabilityID: capability.CapabilityID(ids.ID("executor")), Target: work.TaskTarget{TaskID: work.TaskID(ids.ID("task")), GoalID: work.GoalID(ids.ID("goal")), ScopeID: scope.ID, Revision: 1}, Operation: "mutate-external-world"} + return context, proposal +} diff --git a/tests/action_commit/test_executor_reconciliation_test.go b/tests/action_commit/test_executor_reconciliation_test.go new file mode 100644 index 0000000..f4e99c0 --- /dev/null +++ b/tests/action_commit/test_executor_reconciliation_test.go @@ -0,0 +1,45 @@ +package action_commit_test + +import ( + "errors" + "testing" + + "git.cin.su/m/Assistant/adapters/testexecutor" + commitcontract "git.cin.su/m/Assistant/contracts/commit" + commitdomain "git.cin.su/m/Assistant/modules/commit/domain" + "git.cin.su/m/Assistant/platform/ids" +) + +func TestWS003UnknownOutcomeReconcilesWithoutDuplicateEffect(t *testing.T) { + context, proposal := testContextAndProposal() + action, err := commitdomain.Authorize(proposal, context, validator{}, commitcontract.AuthorizedActionID(ids.ID("authorized-reconcile"))) + if err != nil { + t.Fatal(err) + } + executor := testexecutor.New(true) + unknown, err := commitdomain.Execute(action, commitcontract.ActionAttemptID(ids.ID("attempt-execute")), executor) + if !errors.Is(err, commitdomain.ErrUnknownOutcome) || unknown.State != commitcontract.ActionUnknownOutcome || executor.EffectCount() != 1 { + t.Fatalf("execute = %#v, %v, effects=%d", unknown, err, executor.EffectCount()) + } + verified, err := commitdomain.Reconcile(action, unknown, commitcontract.ActionAttemptID(ids.ID("attempt-reconcile")), executor) + if err != nil || verified.State != commitcontract.ActionSucceeded || executor.EffectCount() != 1 { + t.Fatalf("reconcile = %#v, %v, effects=%d", verified, err, executor.EffectCount()) + } + // Even an adversarial duplicate delivery is idempotent at the external boundary. + _, _ = commitdomain.Execute(action, commitcontract.ActionAttemptID(ids.ID("attempt-duplicate")), executor) + if executor.EffectCount() != 1 { + t.Fatalf("duplicate delivery created %d effects", executor.EffectCount()) + } +} + +func TestWS003ReconciliationRequiresUnknownOutcome(t *testing.T) { + context, proposal := testContextAndProposal() + action, err := commitdomain.Authorize(proposal, context, validator{}, commitcontract.AuthorizedActionID(ids.ID("authorized-invalid-reconcile"))) + if err != nil { + t.Fatal(err) + } + previous := commitcontract.ActionAttempt{ID: "attempt-failed", AuthorizedID: action.ID, IntentID: action.Proposal.IntentID, State: commitcontract.ActionFailed} + if _, err = commitdomain.Reconcile(action, previous, "attempt-next", testexecutor.New(false)); !errors.Is(err, commitdomain.ErrInvalidReconciliation) { + t.Fatalf("reconciliation error = %v", err) + } +} diff --git a/tests/architecture/registry_test.go b/tests/architecture/registry_test.go new file mode 100644 index 0000000..bf0e056 --- /dev/null +++ b/tests/architecture/registry_test.go @@ -0,0 +1,21 @@ +package architecture_test + +import ( + "encoding/json" + "os" + "testing" +) + +func TestArchitectureRegistryParses(t *testing.T) { + b, err := os.ReadFile("../../architecture/architecture-registry.json") + if err != nil { + t.Fatal(err) + } + var v map[string]any + if err := json.Unmarshal(b, &v); err != nil { + t.Fatal(err) + } + if v["semantic_generation"] == nil { + t.Fatal("semantic_generation missing") + } +} diff --git a/tests/cognitive_fake/deterministic_test.go b/tests/cognitive_fake/deterministic_test.go new file mode 100644 index 0000000..fa5e3a7 --- /dev/null +++ b/tests/cognitive_fake/deterministic_test.go @@ -0,0 +1,24 @@ +package cognitive_fake_test + +import ( + "testing" + + "git.cin.su/m/Assistant/adapters/models" + "git.cin.su/m/Assistant/contracts/authority" + "git.cin.su/m/Assistant/contracts/capability" + commitcontract "git.cin.su/m/Assistant/contracts/commit" + "git.cin.su/m/Assistant/contracts/identity" + "git.cin.su/m/Assistant/contracts/work" + "git.cin.su/m/Assistant/platform/ids" +) + +func TestDeterministicFakeEmitsCandidateOnly(t *testing.T) { + request := models.DeterministicRequest{ActorID: identity.ActorID(ids.ID("creator")), CapabilityID: capability.CapabilityID(ids.ID("fake")), Target: work.TaskTarget{TaskID: work.TaskID(ids.ID("task")), GoalID: work.GoalID(ids.ID("goal")), ScopeID: authority.ScopeID(ids.ID("scope")), Revision: 1}, Operation: commitcontract.ExternalOperation("test-operation")} + first, second := models.Propose(request), models.Propose(request) + if first.Proposal.IntentID != second.Proposal.IntentID || first.Seed != second.Seed { + t.Fatal("same request produced different candidate") + } + if first.Proposal.State != commitcontract.ActionProposed { + t.Fatalf("state = %s, want PROPOSED", first.Proposal.State) + } +} diff --git a/tests/creator_auth/session_test.go b/tests/creator_auth/session_test.go new file mode 100644 index 0000000..70a2815 --- /dev/null +++ b/tests/creator_auth/session_test.go @@ -0,0 +1,44 @@ +package creator_auth_test + +import ( + "encoding/json" + "git.cin.su/m/Assistant/modules/identity/domain" + "strings" + "testing" + "time" +) + +func TestCreatorSessionIsTimeBoundAndDoesNotCarryAuthority(t *testing.T) { + now := time.Unix(1000, 0) + token, session, err := domain.IssueCreatorSession([]byte("01234567890123456789012345678901"), now, time.Minute) + if err != nil || session.ExpiresAtUnix != 1060 { + t.Fatal("issue failed") + } + if _, err = domain.ValidateCreatorSession([]byte("01234567890123456789012345678901"), token, now); err != nil { + t.Fatal(err) + } + if _, err = domain.ValidateCreatorSession([]byte("01234567890123456789012345678901"), token, now.Add(time.Minute)); err == nil { + t.Fatal("expired session accepted") + } + if _, err = domain.ValidateCreatorSession([]byte("01234567890123456789012345678901"), token+"tampered", now); err == nil { + t.Fatal("tampered session accepted") + } + payload, err := json.Marshal(session) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(payload), "Grant") || strings.Contains(string(payload), "Scope") { + t.Fatal("identity session must not carry authority") + } +} + +func TestCreatorBootstrapCredentialMustMatch(t *testing.T) { + if !domain.BootstrapMatches("expected", "expected") { + t.Fatal("matching bootstrap credential rejected") + } + for _, supplied := range []string{"", "wrong"} { + if domain.BootstrapMatches("expected", supplied) { + t.Fatalf("invalid bootstrap credential %q accepted", supplied) + } + } +} diff --git a/tests/creator_authority/creator_authority_test.go b/tests/creator_authority/creator_authority_test.go new file mode 100644 index 0000000..8a05d82 --- /dev/null +++ b/tests/creator_authority/creator_authority_test.go @@ -0,0 +1,33 @@ +package creator_authority_test + +import ( + "testing" + + "git.cin.su/m/Assistant/contracts/authority" + "git.cin.su/m/Assistant/contracts/identity" + authoritydomain "git.cin.su/m/Assistant/modules/authority/domain" + identitydomain "git.cin.su/m/Assistant/modules/identity/domain" + "git.cin.su/m/Assistant/platform/ids" +) + +func TestCreatorRequiresDistinctActiveBindingAndGlobalGrant(t *testing.T) { + binding := identity.PrincipalBinding{ActorID: identity.ActorID(ids.ID("creator-actor")), PrincipalID: identity.PrincipalID(ids.ID("creator-principal")), Active: true} + scope := authority.Scope{ID: authority.ScopeID(ids.ID("all-domovoy")), Label: "All Domovoy scopes", Global: true} + grant := authority.AuthorityGrant{ID: authority.GrantID(ids.ID("creator-grant")), ActorID: binding.ActorID, PrincipalID: binding.PrincipalID, ScopeID: scope.ID, Class: authority.GrantClassCreator, Revision: 1, Active: true} + if err := identitydomain.ValidateCreatorBinding(binding); err != nil { + t.Fatal(err) + } + if err := authoritydomain.ValidateCreatorGrant(binding, scope, grant); err != nil { + t.Fatal(err) + } + + grant.Active = false + if err := authoritydomain.ValidateCreatorGrant(binding, scope, grant); err == nil { + t.Fatal("revoked creator grant was accepted") + } + grant.Active = true + scope.Global = false + if err := authoritydomain.ValidateCreatorGrant(binding, scope, grant); err == nil { + t.Fatal("non-global creator grant was accepted") + } +} diff --git a/tests/degraded_continuity/capability_health_test.go b/tests/degraded_continuity/capability_health_test.go new file mode 100644 index 0000000..6540486 --- /dev/null +++ b/tests/degraded_continuity/capability_health_test.go @@ -0,0 +1,39 @@ +package degraded_continuity_test + +import ( + "testing" + + "git.cin.su/m/Assistant/contracts/capability" + "git.cin.su/m/Assistant/modules/capability/domain" + "git.cin.su/m/Assistant/platform/ids" +) + +func TestCognitiveProviderLossIsExplicitDegradedCapability(t *testing.T) { + observed := int64(1) + status := capability.Health{ + CapabilityID: capability.CapabilityID(ids.ID("cognitive-proposal")), + State: capability.HealthDegraded, + Reason: capability.ReasonCognitiveProvidersUnavailable, + Revision: 1, + SemanticVersion: capability.HealthV2, + ObservedAtUnixMillis: &observed, + } + if err := domain.ValidateHealth(status); err != nil { + t.Fatalf("valid degraded health rejected: %v", err) + } +} + +func TestAvailableCapabilityCannotClaimDegradedReason(t *testing.T) { + observed := int64(1) + status := capability.Health{ + CapabilityID: capability.CapabilityID(ids.ID("cognitive-proposal")), + State: capability.HealthAvailable, + Reason: capability.ReasonCognitiveProvidersUnavailable, + Revision: 1, + SemanticVersion: capability.HealthV2, + ObservedAtUnixMillis: &observed, + } + if err := domain.ValidateHealth(status); err == nil { + t.Fatal("available status with degraded reason was accepted") + } +} diff --git a/tests/owner_acl/verify.ps1 b/tests/owner_acl/verify.ps1 new file mode 100644 index 0000000..6ff4b56 --- /dev/null +++ b/tests/owner_acl/verify.ps1 @@ -0,0 +1,25 @@ +param( + [string]$DockerContext = "docker-prod", + [string]$PostgresContainer = "assistant-postgres-1" +) + +$ErrorActionPreference = "Stop" + +$environment = docker --context $DockerContext inspect --format '{{range .Config.Env}}{{println .}}{{end}}' $PostgresContainer +$userLine = $environment | Where-Object { $_.StartsWith("POSTGRES_USER=") } | Select-Object -First 1 +$databaseLine = $environment | Where-Object { $_.StartsWith("POSTGRES_DB=") } | Select-Object -First 1 +if (-not $userLine -or -not $databaseLine) { throw "PostgreSQL bootstrap identity is unavailable" } +$user = $userLine.Substring("POSTGRES_USER=".Length) +$database = $databaseLine.Substring("POSTGRES_DB=".Length) + +$previousErrorAction = $ErrorActionPreference +$ErrorActionPreference = "Continue" +$denied = "SET ROLE continuity_role; INSERT INTO work.tasks (task_id, goal_id, scope_id, revision) VALUES ('acl-negative-probe', 'goal', 'scope', 1);" | docker --context $DockerContext exec -i $PostgresContainer psql -U $user -d $database -v ON_ERROR_STOP=1 2>&1 +$deniedExitCode = $LASTEXITCODE +$ErrorActionPreference = $previousErrorAction +if ($deniedExitCode -eq 0) { throw "cross-owner write unexpectedly succeeded" } + +"SET ROLE work_role; SELECT count(*) FROM work.tasks;" | docker --context $DockerContext exec -i $PostgresContainer psql -U $user -d $database -v ON_ERROR_STOP=1 | Out-Null +if ($LASTEXITCODE -ne 0) { throw "owner-local read failed" } + +Write-Output "INV-OWN-001 owner ACL verification: PASS" diff --git a/tests/prospective_memory/recovery_test.go b/tests/prospective_memory/recovery_test.go new file mode 100644 index 0000000..1bc37aa --- /dev/null +++ b/tests/prospective_memory/recovery_test.go @@ -0,0 +1,39 @@ +package prospective_memory_test + +import ( + "errors" + "testing" + + "git.cin.su/m/Assistant/contracts/authority" + contract "git.cin.su/m/Assistant/contracts/continuity" + "git.cin.su/m/Assistant/contracts/identity" + "git.cin.su/m/Assistant/contracts/work" + "git.cin.su/m/Assistant/modules/continuity/domain" + "git.cin.su/m/Assistant/platform/ids" +) + +func TestWS002SurvivesSchedulerLossAndRevalidatesBeforeDelivery(t *testing.T) { + item := contract.ProspectiveItem{ID: contract.ProspectiveItemID(ids.ID("reminder")), CommitmentID: contract.CommitmentID(ids.ID("commitment")), OwnerPrincipalID: identity.PrincipalID(ids.ID("creator")), ScopeID: authority.ScopeID(ids.ID("global")), GoalID: work.GoalID(ids.ID("goal")), TriggerAtUnixMillis: 1_800_000_000_000, State: contract.ProspectiveArmed, Revision: 1} + store := domain.Recover([]contract.ProspectiveItem{item}) + + // Scheduler state is discarded; only the canonical snapshot reconstructs work. + recovered := domain.Recover(store.Snapshot()) + triggered, err := recovered.Trigger(item.ID, 1) + if err != nil { + t.Fatal(err) + } + if _, err := recovered.Trigger(item.ID, 1); !errors.Is(err, domain.ErrProspectiveRevision) { + t.Fatalf("duplicate trigger error = %v, want revision conflict", err) + } + revalidating, err := recovered.BeginRevalidation(item.ID, triggered.Revision) + if err != nil { + t.Fatal(err) + } + suppressed, err := recovered.FinishRevalidation(item.ID, revalidating.Revision, false) + if err != nil { + t.Fatal(err) + } + if suppressed.State != contract.ProspectiveSuppressed { + t.Fatalf("state = %s, want SUPPRESSED", suppressed.State) + } +} diff --git a/tests/semantic_evolution/capability_health_v2_test.go b/tests/semantic_evolution/capability_health_v2_test.go new file mode 100644 index 0000000..8a748ac --- /dev/null +++ b/tests/semantic_evolution/capability_health_v2_test.go @@ -0,0 +1,32 @@ +package semantic_evolution_test + +import ( + "errors" + "testing" + + "git.cin.su/m/Assistant/contracts/capability" + "git.cin.su/m/Assistant/modules/capability/domain" + "git.cin.su/m/Assistant/platform/ids" +) + +func TestHistoricalV1HealthRemainsExplicitlyInterpretable(t *testing.T) { + historical := capability.Health{CapabilityID: capability.CapabilityID(ids.ID("historical")), State: capability.HealthDegraded, Reason: capability.ReasonCognitiveProvidersUnavailable, Revision: 1, SemanticVersion: capability.HealthV1} + if err := domain.ValidateHealth(historical); err != nil { + t.Fatalf("historical V1 health rejected: %v", err) + } + if historical.ObservedAtUnixMillis != nil { + t.Fatal("V1 must preserve the absence of V2 observation time") + } +} + +func TestV2RequiresObservationTimeAndRejectsUnknownVersion(t *testing.T) { + observed := int64(1720000000000) + v2 := capability.Health{CapabilityID: capability.CapabilityID(ids.ID("current")), State: capability.HealthAvailable, Reason: capability.ReasonNone, Revision: 2, SemanticVersion: capability.HealthV2, ObservedAtUnixMillis: &observed} + if err := domain.ValidateHealth(v2); err != nil { + t.Fatalf("V2 health rejected: %v", err) + } + v2.SemanticVersion = 3 + if err := domain.ValidateHealth(v2); !errors.Is(err, domain.ErrUnsupportedHealthVersion) { + t.Fatalf("unknown version error = %v", err) + } +} diff --git a/tests/topology_equivalence/commit_test.go b/tests/topology_equivalence/commit_test.go new file mode 100644 index 0000000..2b6b8a0 --- /dev/null +++ b/tests/topology_equivalence/commit_test.go @@ -0,0 +1,23 @@ +package topology_equivalence_test + +import ( + "encoding/json" + commitcontract "git.cin.su/m/Assistant/contracts/commit" + "git.cin.su/m/Assistant/platform/ids" + "testing" +) + +func TestWS004UnknownOutcomeSurvivesSerialization(t *testing.T) { + attempt := commitcontract.ActionAttempt{ID: commitcontract.ActionAttemptID(ids.ID("attempt")), AuthorizedID: commitcontract.AuthorizedActionID(ids.ID("authorized")), IntentID: commitcontract.ActionIntentID(ids.ID("intent")), State: commitcontract.ActionUnknownOutcome} + raw, err := json.Marshal(attempt) + if err != nil { + t.Fatal(err) + } + var restored commitcontract.ActionAttempt + if err := json.Unmarshal(raw, &restored); err != nil { + t.Fatal(err) + } + if restored != attempt || restored.State != commitcontract.ActionUnknownOutcome { + t.Fatalf("semantic state changed: %#v", restored) + } +} diff --git a/tests/topology_equivalence/focus_test.go b/tests/topology_equivalence/focus_test.go new file mode 100644 index 0000000..ab54d0b --- /dev/null +++ b/tests/topology_equivalence/focus_test.go @@ -0,0 +1,36 @@ +package topology_equivalence_test + +import ( + "encoding/json" + "git.cin.su/m/Assistant/contracts/authority" + "git.cin.su/m/Assistant/contracts/identity" + situationcontract "git.cin.su/m/Assistant/contracts/situation" + "git.cin.su/m/Assistant/contracts/work" + situation "git.cin.su/m/Assistant/modules/situation/domain" + "git.cin.su/m/Assistant/platform/ids" + "testing" +) + +func TestWS004FocusContractSurvivesSerialization(t *testing.T) { + binding := situationcontract.FocusBinding{ID: situationcontract.FocusBindingID(ids.ID("focus")), ActorID: identity.ActorID(ids.ID("actor")), SessionID: situationcontract.SessionID(ids.ID("session")), Workspace: situationcontract.WorkspaceID(ids.ID("workspace")), ProjectID: situationcontract.ProjectID(ids.ID("project")), GoalID: work.GoalID(ids.ID("goal")), ScopeID: authority.ScopeID(ids.ID("scope")), Active: true} + runtime := situationcontract.RuntimeBinding{SessionID: binding.SessionID, Workspace: binding.Workspace} + direct, err := situation.NewResolver([]situationcontract.FocusBinding{binding}).Resolve(runtime) + if err != nil { + t.Fatal(err) + } + encoded, err := json.Marshal([]situationcontract.FocusBinding{binding}) + if err != nil { + t.Fatal(err) + } + var remote []situationcontract.FocusBinding + if err := json.Unmarshal(encoded, &remote); err != nil { + t.Fatal(err) + } + extracted, err := situation.NewResolver(remote).Resolve(runtime) + if err != nil { + t.Fatal(err) + } + if direct != extracted { + t.Fatalf("topology changed focus: %#v != %#v", direct, extracted) + } +} diff --git a/tests/topology_equivalence/verify-remote.ps1 b/tests/topology_equivalence/verify-remote.ps1 new file mode 100644 index 0000000..032c71d --- /dev/null +++ b/tests/topology_equivalence/verify-remote.ps1 @@ -0,0 +1,81 @@ +param([string]$DockerContext = "docker-prod") + +$ErrorActionPreference = "Stop" + +function Post-Json([string]$container, [string]$url, [object]$body) { + $json = $body | ConvertTo-Json -Depth 8 -Compress + $local = New-TemporaryFile + $remote = "/tmp/domovoy-$([guid]::NewGuid().ToString('N')).json" + try { + [IO.File]::WriteAllText($local, $json, [Text.UTF8Encoding]::new($false)) + docker --context $DockerContext cp $local "${container}:$remote" | Out-Null + docker --context $DockerContext exec $container wget -qO- --header "Content-Type: application/json" --post-file $remote $url + if ($LASTEXITCODE -ne 0) { throw "POST $url failed" } + } finally { + docker --context $DockerContext exec --user root $container rm -f $remote 2>$null + Remove-Item -LiteralPath $local -Force + } +} + +function Expect-PostStatus([string]$container, [string]$url, [object]$body, [int]$status) { + $json = $body | ConvertTo-Json -Depth 8 -Compress + $local = New-TemporaryFile + $remote = "/tmp/domovoy-$([guid]::NewGuid().ToString('N')).json" + try { + [IO.File]::WriteAllText($local, $json, [Text.UTF8Encoding]::new($false)) + docker --context $DockerContext cp $local "${container}:$remote" | Out-Null + $previous = $ErrorActionPreference + $ErrorActionPreference = "Continue" + $response = (docker --context $DockerContext exec $container wget -S -O /dev/null --header "Content-Type: application/json" --post-file $remote $url 2>&1) -join "`n" + $ErrorActionPreference = $previous + if (-not $response.Contains("$status")) { throw "POST $url did not return $status" } + } finally { + docker --context $DockerContext exec --user root $container rm -f $remote 2>$null + Remove-Item -LiteralPath $local -Force + } +} + +$suffix = [guid]::NewGuid().ToString("N") +$scope = "domovoy-global-scope" +$goal = "goal-$suffix" +$task = "task-$suffix" +$session = "session-$suffix" +$workspace = "workspace-$suffix" + +Post-Json assistant-core-runtime-1 http://localhost:8080/v1/focus-bindings @{ + ID="focus-$suffix"; ActorID="domovoy-creator-actor"; SessionID=$session; Workspace=$workspace + ProjectID="project-$suffix"; GoalID=$goal; ScopeID=$scope; Active=$true +} | Out-Null +Post-Json assistant-core-runtime-1 http://localhost:8080/v1/tasks @{ + TaskID=$task; GoalID=$goal; ScopeID=$scope; Revision=1 +} | Out-Null +$wrongCommit = @{ SessionID=$session; Workspace=$workspace; Target=@{TaskID=$task;GoalID="wrong-$goal";ScopeID=$scope;Revision=1} } +Expect-PostStatus assistant-core-runtime-1 http://localhost:8080/v1/focus-task-commits $wrongCommit 409 +$rightCommit = @{ SessionID=$session; Workspace=$workspace; Target=@{TaskID=$task;GoalID=$goal;ScopeID=$scope;Revision=1} } +$commitResult = Post-Json assistant-core-runtime-1 http://localhost:8080/v1/focus-task-commits $rightCommit +if (($commitResult | ConvertFrom-Json).Revision -ne 2) { throw "remote task revision did not advance" } + +$prospectiveID = "prospective-$suffix" +Post-Json assistant-continuity-runtime-1 http://localhost:8081/v1/prospective-items @{ + ID=$prospectiveID; CommitmentID="commitment-$suffix"; OwnerPrincipalID="domovoy-creator-principal" + ScopeID=$scope; GoalID=$goal; TriggerAtUnixMillis=1800000000000; State="ARMED"; Revision=1 +} | Out-Null +docker --context $DockerContext restart assistant-continuity-runtime-1 | Out-Null +for ($attempt=0; $attempt -lt 15; $attempt++) { + $restored = docker --context $DockerContext exec assistant-continuity-runtime-1 wget -qO- "http://localhost:8081/v1/prospective-items/$prospectiveID" 2>$null + if ($LASTEXITCODE -eq 0) { break } + Start-Sleep -Seconds 2 +} +if ($LASTEXITCODE -ne 0 -or $restored -notmatch $prospectiveID) { throw "prospective item did not survive restart" } + +$intent = "intent-$suffix" +$authorized = "authorized-$suffix" +Post-Json assistant-commit-runtime-1 http://localhost:8082/v1/authorized-actions @{ + Proposal=@{IntentID=$intent;ActorID="domovoy-creator-actor";CapabilityID="test-executor";Target=@{TaskID=$task;GoalID=$goal;ScopeID=$scope;Revision=2};Operation="test-idempotent-effect"} + AuthorizedID=$authorized;GrantID="domovoy-creator-grant";PrincipalID="domovoy-creator-principal" +} | Out-Null +Post-Json assistant-commit-runtime-1 http://localhost:8082/v1/action-attempts @{ + ID="attempt-$suffix";AuthorizedID=$authorized;IntentID=$intent;State="UNKNOWN_OUTCOME" +} | Out-Null + +Write-Output "WS-004 remote topology verification: PASS" diff --git a/tests/walking_skeleton/README.md b/tests/walking_skeleton/README.md new file mode 100644 index 0000000..1bd5b20 --- /dev/null +++ b/tests/walking_skeleton/README.md @@ -0,0 +1,5 @@ +# Walking skeleton tests + +Black-box semantic acceptance tests go here. They must test properties rather than implementation call sequences. + +Initial scenario IDs are defined in `architecture/WALKING_SKELETON_SPEC.md`. diff --git a/tests/walking_skeleton/focus_isolation_test.go b/tests/walking_skeleton/focus_isolation_test.go new file mode 100644 index 0000000..e0a7cc6 --- /dev/null +++ b/tests/walking_skeleton/focus_isolation_test.go @@ -0,0 +1,84 @@ +package walking_skeleton_test + +import ( + "errors" + "testing" + + "git.cin.su/m/Assistant/contracts/authority" + "git.cin.su/m/Assistant/contracts/identity" + situationcontract "git.cin.su/m/Assistant/contracts/situation" + workcontract "git.cin.su/m/Assistant/contracts/work" + situation "git.cin.su/m/Assistant/modules/situation/domain" + work "git.cin.su/m/Assistant/modules/work/domain" + "git.cin.su/m/Assistant/platform/ids" +) + +func TestWS001FocusIsolationPreventsWrongProjectMutation(t *testing.T) { + actor := identity.ActorID(ids.ID("actor")) + scope := authority.ScopeID(ids.ID("scope")) + project := situationcontract.ProjectID(ids.ID("same-repository")) + goalA := workcontract.GoalID(ids.ID("goal-a")) + goalB := workcontract.GoalID(ids.ID("goal-b")) + bindings := []situationcontract.FocusBinding{ + {ID: situationcontract.FocusBindingID(ids.ID("focus-a")), ActorID: actor, SessionID: situationcontract.SessionID(ids.ID("session-a")), Workspace: situationcontract.WorkspaceID(ids.ID("worktree-a")), ProjectID: project, GoalID: goalA, ScopeID: scope, Active: true}, + {ID: situationcontract.FocusBindingID(ids.ID("focus-b")), ActorID: actor, SessionID: situationcontract.SessionID(ids.ID("session-b")), Workspace: situationcontract.WorkspaceID(ids.ID("worktree-b")), ProjectID: project, GoalID: goalB, ScopeID: scope, Active: true}, + } + resolver := situation.NewResolver(bindings) + store := work.NewStore([]work.Task{ + {ID: workcontract.TaskID(ids.ID("task-a")), GoalID: goalA, ScopeID: scope, Revision: 1}, + {ID: workcontract.TaskID(ids.ID("task-b")), GoalID: goalB, ScopeID: scope, Revision: 1}, + }) + + // The identical phrase is intentionally not supplied to resolution: runtime + // session/workspace bindings, including two worktrees of one repository, + // are the only project-selection inputs. + focusA, err := resolver.Resolve(situationcontract.RuntimeBinding{SessionID: bindings[0].SessionID, Workspace: bindings[0].Workspace}) + if err != nil { + t.Fatal(err) + } + focusB, err := resolver.Resolve(situationcontract.RuntimeBinding{SessionID: bindings[1].SessionID, Workspace: bindings[1].Workspace}) + if err != nil { + t.Fatal(err) + } + if focusA.GoalID == focusB.GoalID { + t.Fatal("two worktrees resolved to the same goal") + } + + wrongTarget := workcontract.TaskTarget{TaskID: workcontract.TaskID(ids.ID("task-b")), GoalID: goalB, ScopeID: scope, Revision: 1} + if err := situation.ValidateTaskTarget(focusA, wrongTarget); !errors.Is(err, situation.ErrTargetMismatch) { + t.Fatalf("wrong cognition target error = %v, want ErrTargetMismatch", err) + } + + targetA := workcontract.TaskTarget{TaskID: workcontract.TaskID(ids.ID("task-a")), GoalID: goalA, ScopeID: scope, Revision: 1} + if err := situation.ValidateTaskTarget(focusA, targetA); err != nil { + t.Fatal(err) + } + committed, err := store.Commit(targetA) + if err != nil { + t.Fatal(err) + } + if committed.Revision != 2 { + t.Fatalf("revision = %d, want 2", committed.Revision) + } + if _, err := store.Commit(targetA); !errors.Is(err, work.ErrRevisionConflict) { + t.Fatalf("stale revision error = %v, want ErrRevisionConflict", err) + } + if _, err := store.Commit(wrongTarget); err != nil { + t.Fatalf("other context must retain its original revision: %v", err) + } +} + +func TestWS001RejectsStaleAndAmbiguousFocusWithoutMutation(t *testing.T) { + runtime := situationcontract.RuntimeBinding{SessionID: situationcontract.SessionID(ids.ID("session")), Workspace: situationcontract.WorkspaceID(ids.ID("workspace"))} + stale := situation.NewResolver([]situationcontract.FocusBinding{{SessionID: runtime.SessionID, Workspace: runtime.Workspace, Active: false}}) + if _, err := stale.Resolve(runtime); !errors.Is(err, situation.ErrStaleFocus) { + t.Fatalf("stale error = %v, want ErrStaleFocus", err) + } + ambiguous := situation.NewResolver([]situationcontract.FocusBinding{ + {ID: situationcontract.FocusBindingID(ids.ID("one")), SessionID: runtime.SessionID, Workspace: runtime.Workspace, Active: true}, + {ID: situationcontract.FocusBindingID(ids.ID("two")), SessionID: runtime.SessionID, Workspace: runtime.Workspace, Active: true}, + }) + if _, err := ambiguous.Resolve(runtime); !errors.Is(err, situation.ErrAmbiguousFocus) { + t.Fatalf("ambiguity error = %v, want ErrAmbiguousFocus", err) + } +}