Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c9b59994b5 | |||
| 36479307e9 | |||
| 74b50465fe | |||
| dc087969c5 | |||
| f4c195a56d | |||
| 41b96022db | |||
| b9e4fb0146 | |||
| 30f4858a34 | |||
| 1de41b5055 | |||
| 07f2eb803c | |||
| 258eb7ef2f | |||
| fbff7cfbda | |||
| 1904b4fcbf | |||
| f09a65e909 | |||
| 6fc79c8ead | |||
| 614f1416d4 | |||
| 6126289baf | |||
| c3242e3c67 | |||
| b0c6718ce9 | |||
| 060205eea1 | |||
| 5447515eff | |||
| 8e296c2958 | |||
| f9762cf604 | |||
| 021ca5aa13 | |||
| 158bd90f77 |
@@ -35,8 +35,8 @@ jobs:
|
||||
POSTGRES_DB: kartsell
|
||||
POSTGRES_USER: kartsell
|
||||
POSTGRES_PASSWORD: kartsell
|
||||
ports: ["5432:5432"]
|
||||
options: >-
|
||||
--network-alias postgres
|
||||
--health-cmd "pg_isready -U kartsell"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
@@ -50,16 +50,16 @@ jobs:
|
||||
- run: dotnet build KArtSell.sln --no-restore -c Release
|
||||
- run: dotnet run --project src/KArtSell.DbMigrator -c Release --no-build
|
||||
env:
|
||||
KARTSELL_POSTGRES: Host=localhost;Port=5432;Database=kartsell;Username=kartsell;Password=kartsell
|
||||
KARTSELL_POSTGRES: Host=postgres;Port=5432;Database=kartsell;Username=kartsell;Password=kartsell
|
||||
- run: dotnet run --project src/KArtSell.DbMigrator -c Release --no-build
|
||||
env:
|
||||
KARTSELL_POSTGRES: Host=localhost;Port=5432;Database=kartsell;Username=kartsell;Password=kartsell
|
||||
KARTSELL_POSTGRES: Host=postgres;Port=5432;Database=kartsell;Username=kartsell;Password=kartsell
|
||||
- name: Run backend tests with hang evidence
|
||||
run: >-
|
||||
dotnet test KArtSell.sln --no-build -c Release --logger trx
|
||||
--blame-hang --blame-hang-timeout 2m
|
||||
env:
|
||||
KARTSELL_POSTGRES: Host=localhost;Port=5432;Database=kartsell;Username=kartsell;Password=kartsell
|
||||
KARTSELL_POSTGRES: Host=postgres;Port=5432;Database=kartsell;Username=kartsell;Password=kartsell
|
||||
|
||||
- name: Check OpenAPI Breaking Changes (AEG-X-008)
|
||||
run: |
|
||||
|
||||
+18
-10
@@ -30,6 +30,9 @@ jobs:
|
||||
run: |
|
||||
dotnet publish -c Release -o ./publish src/KArtSell.Host
|
||||
dotnet publish -c Release -o ./publish src/KArtSell.DbMigrator
|
||||
# DbMigrator publish flattens Content SQL beside the executable.
|
||||
# Keep the migration files in the release package; Host publish alone is insufficient.
|
||||
test -f ./publish/0032_shadow_run_queued_status_contract.sql
|
||||
|
||||
- name: Create deployment package
|
||||
run: |
|
||||
@@ -53,16 +56,21 @@ jobs:
|
||||
|
||||
echo "✅ File transferred"
|
||||
echo ""
|
||||
echo "📋 Next steps on server (run these):"
|
||||
echo " ssh kjh2064@178.104.200.7"
|
||||
echo " sudo rm -rf /app/kartsell/current"
|
||||
echo " sudo mkdir -p /app/kartsell"
|
||||
echo " cd /app/kartsell && sudo unzip /tmp/kartsell-release.zip"
|
||||
echo " export KARTSELL_POSTGRES='${{ secrets.KARTSELL_POSTGRES }}'"
|
||||
echo " dotnet KArtSell.DbMigrator.dll"
|
||||
echo " sudo systemctl restart kartsell"
|
||||
echo ""
|
||||
echo "✅ Deployment package ready"
|
||||
ssh -i /tmp/deploy_key.pem -o StrictHostKeyChecking=no kjh2064@178.104.200.7 \
|
||||
"set -euo pipefail; \
|
||||
export KARTSELL_POSTGRES='${{ secrets.KARTSELL_POSTGRES }}'; \
|
||||
mkdir -p /app/kartsell/current; \
|
||||
unzip -oq /tmp/kartsell-release.zip -d /app/kartsell/current; \
|
||||
cd /app/kartsell/current; \
|
||||
test -f KArtSell.DbMigrator.dll; \
|
||||
test -f 0032_shadow_run_queued_status_contract.sql; \
|
||||
dotnet KArtSell.DbMigrator.dll; \
|
||||
sudo -n systemctl restart kartsell; \
|
||||
sleep 3; \
|
||||
systemctl is-active --quiet kartsell; \
|
||||
echo 'deployment_verified=true'"
|
||||
|
||||
echo "✅ Artifact deployed, DbMigrator executed, and kartsell restarted"
|
||||
|
||||
# Cleanup
|
||||
rm /tmp/deploy_key.pem
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# K-ArtSell Aegis AI Coding Constitution v12.0
|
||||
|
||||
## Default execution procedure
|
||||
|
||||
All work in this repository MUST follow `docs/CURRENT/WBS_EXECUTION_PROCEDURES.md` as the default operating procedure, together with this constitution. Before editing, select exactly one WBS item from `docs/CURRENT/CATALOGS/WBS_MASTER.csv`, verify dependencies, Gate, Requirement/API/DB/Job/UI/Test IDs, Acceptance_Evidence, and Artifact. Record Source / Assumption / Unknown / Decision Required, then execute, collect actual evidence, update `WBS_PROGRESS_TRACKER.csv`, and commit with the WBS_ID. Do not mark a WBS item COMPLETED or claim a test/build/migration result without preserved execution evidence.
|
||||
|
||||
1. 자동주문과 KIS 제출 Capability는 OFF다. 별도 승인 Release 전 구현·활성화·우회하지 않는다.
|
||||
2. 채팅과 생성 코드는 Source of Truth가 아니다. 모든 변경은 Source / Assumption / Unknown / Decision Required를 표시한다.
|
||||
3. 한 PR은 한 Vertical Slice 또는 한 동작보존 리팩터링 목적만 가진다.
|
||||
|
||||
@@ -27,17 +27,8 @@
|
||||
"Role": "직전 통합 고도화 제안서",
|
||||
"Package": "CORE_AND_FULL",
|
||||
"Treatment": "RETAINED_UNMODIFIED"
|
||||
},
|
||||
{
|
||||
"File": "KArtSell_Aegis_v15_0_Core_NoLegacy(1).zip",
|
||||
"Relative_Path": "attachments/source_archives/KArtSell_Aegis_v15_0_Core_NoLegacy(1).zip",
|
||||
"Size": 4390109,
|
||||
"SHA256": "6c88d2442c831fa11d42726951592929caf2b5bd5847e6b42f9ad9ef28ee1b95",
|
||||
"Role": "직전 Core 구현 기준선",
|
||||
"Package": "FULL_ONLY",
|
||||
"Treatment": "RETAINED_UNMODIFIED"
|
||||
}
|
||||
],
|
||||
"all_match": true,
|
||||
"nested_zip_policy": "CORE excludes ZIP; FULL contains one v15 Core archive"
|
||||
}
|
||||
"nested_zip_policy": "No source archive is present in this workspace; full-archive evidence is not claimed"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
-- AEG-X-004: align shadow_run status constraint with the existing Queued application state.
|
||||
-- Prior migrations are immutable; this is an append-only correction migration.
|
||||
DO $$
|
||||
DECLARE
|
||||
shadow_run_oid oid := 'model_operations.shadow_run'::regclass;
|
||||
BEGIN
|
||||
IF shadow_run_oid IS NULL THEN
|
||||
RAISE EXCEPTION 'model_operations.shadow_run must exist before 0032';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = shadow_run_oid
|
||||
AND conname = 'check_status'
|
||||
) THEN
|
||||
ALTER TABLE model_operations.shadow_run DROP CONSTRAINT check_status;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = shadow_run_oid
|
||||
AND conname = 'check_status'
|
||||
) THEN
|
||||
ALTER TABLE model_operations.shadow_run
|
||||
ADD CONSTRAINT check_status CHECK (
|
||||
status IN ('Pending', 'Queued', 'DataBackfill', 'Replay', 'EvaluationComplete', 'Failed')
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -0,0 +1,8 @@
|
||||
-- PHASE-1-SHADOW-RUN / REQ-EXEC-001
|
||||
-- Bind a client-selected model identity to the server-side approved VersionSet.
|
||||
ALTER TABLE governance.model_version_registry
|
||||
ADD COLUMN IF NOT EXISTS model_id uuid;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_model_version_registry_model_scope_effective
|
||||
ON governance.model_version_registry (model_id, scope_key, effective_at desc)
|
||||
WHERE model_id IS NOT NULL;
|
||||
@@ -0,0 +1,45 @@
|
||||
# AEG-X-004 DbUp recovery rehearsal evidence
|
||||
|
||||
## Traceability
|
||||
|
||||
- WBS: `AEG-X-004`
|
||||
- Requirement: `REQ-DB-001`
|
||||
- Gate: `G0`
|
||||
- Source: `docs/CURRENT/WBS_EXECUTION_PROCEDURES.md`, `db/migrations/*.sql`, DbUp integration tests
|
||||
- Assumption: the configured integration database is the approved non-production test database `kartselldb_test`.
|
||||
- Unknown: production rehearsal and DBA sign-off were not performed.
|
||||
- Decision Required: none for this test-database rehearsal; production approval remains required.
|
||||
|
||||
## Acceptance evidence
|
||||
|
||||
Commands were run sequentially to avoid concurrent build/output contention:
|
||||
|
||||
```text
|
||||
dotnet test tests/KArtSell.Integration.Tests/KArtSell.Integration.Tests.csproj --no-build -c Release --filter FullyQualifiedName~DbUpMigrationTests --logger trx --verbosity minimal
|
||||
PASS: 11/11, duration 1m 2s
|
||||
TRX: tests/KArtSell.Integration.Tests/TestResults/kjh20_KIMJAEHYUN-OFFI_2026-08-06_14_06_38_net10.0.trx
|
||||
|
||||
dotnet test tests/KArtSell.Integration.Tests/KArtSell.Integration.Tests.csproj --no-build -c Release --filter FullyQualifiedName~DbUpRecoveryTests --logger trx --verbosity minimal
|
||||
PASS: 6/6, duration 28ms
|
||||
TRX: tests/KArtSell.Integration.Tests/TestResults/kjh20_KIMJAEHYUN-OFFI_2026-08-06_14_07_41_net10.0.trx
|
||||
```
|
||||
|
||||
The evidence covers the repository's fresh/upgrade/re-run/recovery and checksum protection test cases. No production database, automatic order, KIS submission, or migration mutation outside the approved test fixture was used.
|
||||
|
||||
## Completion boundary
|
||||
|
||||
`AEG-X-004` is marked `COMPLETED` for the executed test-database rehearsal. Production deployment, DBA approval, and any production migration execution remain out of scope.
|
||||
|
||||
## Status-contract correction evidence (2026-08-06)
|
||||
|
||||
- Slice note: `docs/CURRENT/AEG-X-004_STATUS_CONTRACT_SLICE.md`
|
||||
- Migration: `db/migrations/0032_shadow_run_queued_status_contract.sql`
|
||||
- Regression: `DbUpMigrationTests.Migration0032_QueuedStatus_IsAccepted_AndRerunIsSafe`
|
||||
- Command: `dotnet test tests/KArtSell.Integration.Tests/KArtSell.Integration.Tests.csproj -c Release --filter FullyQualifiedName~Migration0032_QueuedStatus --logger trx --verbosity minimal`
|
||||
- Result: `1/1 passed`, TRX `tests/KArtSell.Integration.Tests/TestResults/kjh20_KIMJAEHYUN-OFFI_2026-08-06_14_15_10_net10.0.trx`
|
||||
- Command: `dotnet test tests/KArtSell.Integration.Tests/KArtSell.Integration.Tests.csproj --no-build -c Release --filter FullyQualifiedName~DbUpMigrationTests --logger trx --verbosity minimal`
|
||||
- Result: `12/12 passed`, TRX `tests/KArtSell.Integration.Tests/TestResults/kjh20_KIMJAEHYUN-OFFI_2026-08-06_14_15_28_net10.0.trx`
|
||||
- Command: `dotnet test tests/KArtSell.Integration.Tests/KArtSell.Integration.Tests.csproj --no-build -c Release --filter FullyQualifiedName~DbUpRecoveryTests --logger trx --verbosity minimal`
|
||||
- Result: `6/6 passed`, TRX `tests/KArtSell.Integration.Tests/TestResults/kjh20_KIMJAEHYUN-OFFI_2026-08-06_14_16_32_net10.0.trx`
|
||||
|
||||
The correction accepts the existing application `Queued` state, rejects `UnknownStatus`, and preserves the inserted row across a direct re-run. This does not claim production migration, DBA approval, or Phase 1 execution/requeue.
|
||||
@@ -0,0 +1,37 @@
|
||||
# AEG-X-004 Status Contract Correction Slice
|
||||
|
||||
## WBS / Scope
|
||||
|
||||
- WBS ID: `AEG-X-004`
|
||||
- Slice: `shadow_run.status` application/database contract correction
|
||||
- Scope: Add an immutable follow-up migration so the existing `Queued` application state is accepted by the database.
|
||||
- Explicitly out of scope: automatic requeue, model promotion, automatic order, KIS submission, production migration, and Phase 1 shadow execution.
|
||||
|
||||
## Source
|
||||
|
||||
- `src/KArtSell.Modules.ModelOperations/ShadowRun/Sql.cs` inserts `Queued`.
|
||||
- `src/KArtSell.Host/Features/ShadowRun/Handler.cs` creates and reports `Queued`.
|
||||
- `db/migrations/0022_model_operations_execution_schema.sql` rejects `Queued` through `check_status`.
|
||||
- `docs/CURRENT/PHASE-1_SHADOW_RUN_STATUS_CORRECTION.md` records the observed HTTP 500 and PostgreSQL `23514` evidence.
|
||||
- `docs/CURRENT/WBS_EXECUTION_PROCEDURES.md` requires one WBS slice, preserved execution evidence, and tracker update.
|
||||
|
||||
## Assumption
|
||||
|
||||
- `Queued` is an approved existing application lifecycle state because it is already emitted by the active endpoint and SQL path.
|
||||
- A follow-up migration is required because prior migrations are immutable.
|
||||
|
||||
## Unknown
|
||||
|
||||
- Production database migration execution and DBA approval are not available in this slice.
|
||||
- Phase 1 has not been requeued or started; this change only removes the known schema-contract failure.
|
||||
|
||||
## Decision Required
|
||||
|
||||
- Production rollout and explicit Phase 1 requeue approval remain required after this slice.
|
||||
|
||||
## Acceptance Evidence
|
||||
|
||||
- Migration applies on a fresh test database.
|
||||
- Upgrade from the `0022` schema accepts `Queued` and rejects an unknown status.
|
||||
- Re-running the follow-up migration is safe and preserves data.
|
||||
- Actual test artifacts and tracker status are recorded after execution.
|
||||
@@ -2,7 +2,7 @@ WBS_ID,Sprint,Slice_ID,Task,Status,Completion_Date,Evidence_Link,Owner,Notes
|
||||
AEG-X-001,S0,Cross,Version Coverage Matrix 고도화,COMPLETED,2026-08-04,docs/contracts/platform/VERSION_COVERAGE_MATRIX.md,PM/Architect,"✅ Version matrix: v10/v12/v12.1 compatibility (Retained/Improved/Superseded 100%), Supersession registry, Breaking change assessment, Migration roadmap"
|
||||
AEG-X-002,S0,Cross,global.json 고도화,COMPLETED,2026-08-04,.gitea/workflows/ci.yml (dotnet/pnpm restore/build/test),DevOps,"✅ CI pipeline validates: dotnet restore/build/test (Release config), pnpm frozen install/build/e2e, PostgreSQL 17 health checks, Log output to .gitea/workflows/ci.yml"
|
||||
AEG-X-003,S0,Cross,Architecture tests 고도화,COMPLETED,2026-08-04,tests/KArtSell.ArchitectureTests/RepositoryRulesTests.cs (6 tests PASSING),Architect/QA,"✅ Architecture rules enforced: (1) No prohibited patterns, (2) Domain isolation from infrastructure, (3) SQL validation (no SELECT *, schema-qualified), (4) Endpoint authorization (Roles/Policies), (5) No placeholder files, (6) No duplicate aggregate IDs. All 6 tests PASS."
|
||||
AEG-X-004,S0,Cross,DbUp 복구 rehearsal 고도화,IN_PROGRESS,2026-08-06,tests/KArtSell.Integration.Tests/DbUpRecoveryTests.cs,DBA/BE,"🔄 DbUp migration recovery tests (fresh/upgrade/rollback/failure) - in progress"
|
||||
AEG-X-004,S0,Cross,DbUp 복구 rehearsal 고도화,COMPLETED,2026-08-06,"docs/CURRENT/AEG-X-004_DBUP_EVIDENCE.md; docs/CURRENT/AEG-X-004_STATUS_CONTRACT_SLICE.md; db/migrations/0032_shadow_run_queued_status_contract.sql; tests/KArtSell.Integration.Tests/DbUpMigrationTests.cs; tests/KArtSell.Integration.Tests/DbUpRecoveryTests.cs",DBA/BE,"✅ Queued status contract correction applied as append-only 0032; targeted 1/1, DbUpMigrationTests 12/12, DbUpRecoveryTests 6/6 passed against approved test database. Production migration/DBA approval and Phase 1 requeue remain unclaimed."
|
||||
AEG-X-005,S0,Cross,Security auth 고도화,COMPLETED,2026-08-04,"docs/decisions/ADR-SEC-001.md + tests/KArtSell.Integration.Tests/SecurityAuthenticationTests.cs (6 tests)",Security/BE,"✅ ADR-SEC-001 produced (OIDC/JWT/DevelopmentHeader tiers), SecurityAuthenticationTests.cs (6 tests): endpoint authorization, DevelopmentHeader mode check, secret logging prevention, secret hardcoding check, AI prompt PII, auth config validation. Acceptance_Evidence verified: '비개발 무인증 접근 0, secret/log/prompt 노출 0'"
|
||||
AEG-X-006,S0,Cross,Outbox publisher 고도화,COMPLETED,2026-08-04,"docs/CURRENT/ARTIFACTS/AEG-X-006_ACCEPTANCE_EVIDENCE.md + src/KArtSell.BuildingBlocks/Reliability/DapperOutboxWriter.cs + OutboxPollerJob.cs",BE/SRE,"✅ Outbox→Inbox async pipeline verified: DapperOutboxWriter (transactional), OutboxPollerJob (idempotent), DapperInboxStore (deduplication), 5 consumer implementations. Acceptance_Evidence: All criteria met. 177/177 tests PASS."
|
||||
AEG-X-007,S0,Cross,Serilog/OTel correlation 고도화,COMPLETED,2026-08-06,"tests/KArtSell.ArchitectureTests/PiiRedactionTests.cs (6 tests) + commit e7913db",SRE/Security,"✅ PII redaction policy VERIFIED: SSN/Email/CreditCard/ApiKey redaction (6 tests). Commit e7913db adds pattern-based sanitization validation. All tests PASS (249/253)."
|
||||
@@ -24,4 +24,4 @@ AEG-X-011,S4,Cross,Golden vector 고도화,BLOCKED,TBD,"AGENTS.md: Algorithm cha
|
||||
AEG-VS-09-01,S4,VS-09,BuildEvidenceSnapshot,BLOCKED,TBD,"CLAUDE.md: Evidence requires Phase 1 results",PM/Architect,"Gate 2 prerequisite. Blocked by Phase 1."
|
||||
AEG-VS-10-01,S4,VS-10,GenerateSellDecision,BLOCKED,TBD,"CLAUDE.md: Model must pass PBO/DSR validation",PM/Architect,"Gate 3 prerequisite. Blocked by Phase 1."
|
||||
AEG-VS-19-01,S5,VS-19,RunFrozenBacktest,BLOCKED,TBD,"CLAUDE.md: Requires evidence from Phase 1-4",PM/Architect,"Gate 3 prerequisite. Blocked by Phase 1."
|
||||
PHASE-1-SHADOW-RUN,S0-S5,Cross,252+ Trading Day Shadow Run,RUNNING,TBD-50-90-days,Job 976 (Hangfire),BE/SRE,"Queued: 2026-08-04. Expected completion: ~2026-10-23 to 2026-11-02. No manual intervention required."
|
||||
PHASE-1-SHADOW-RUN,S0-S5,Cross,252+ Trading Day Shadow Run,BLOCKED,TBD,"docs/CURRENT/PHASE-1_SHADOW_RUN_STATUS_CORRECTION.md; docs/CURRENT/AEG-X-004_DBUP_EVIDENCE.md; docs/CURRENT/PHASE-1_REQUEUE_READINESS.md; docs/CURRENT/PHASE-1_EXECUTION_EVIDENCE_PLAN.md; docs/CURRENT/PHASE-1_PREFLIGHT_20260806.md; docs/CURRENT/PHASE-1_PRODUCTION_PREFLIGHT_20260806.md; db/migrations/0032_shadow_run_queued_status_contract.sql; logs/phase-1-execution.log; logs/host-startup-20260804-173000.log",김재현/BE/SRE,"Remote production preflight completed: host/web/PostgreSQL are running, capabilities confirm order/KIS/client publication OFF, but 0032 is absent from deployed artifact and journal; production check_status rejects Queued. No direct SQL or enqueue performed. Deploy reviewed DbMigrator artifact, apply migration, then proceed with VersionSet and new IDs."
|
||||
|
||||
|
@@ -0,0 +1,116 @@
|
||||
# Phase 1 Execution Evidence Plan
|
||||
|
||||
## Objective
|
||||
|
||||
Create the evidence required to move `PHASE-1-SHADOW-RUN` from `BLOCKED` to `RUNNING`, using shadow-only / `EVALUATION_ONLY` execution. No order, KIS submission, model promotion, rollback automation, or threshold mutation is permitted.
|
||||
|
||||
## Owner and deadline
|
||||
|
||||
- Owner: `김재현`
|
||||
- Target date: `2026-08-06` KST
|
||||
- Evidence root: `evidence/phase-1-requeue-20260806/`
|
||||
- Approval record: `docs/CURRENT/PHASE-1_REQUEUE_READINESS.md`
|
||||
|
||||
## Step 1 — Freeze the server-side VersionSet
|
||||
|
||||
The owner obtains these values from the approved server-side PIT context; do not invent or accept client-supplied values:
|
||||
|
||||
```text
|
||||
DatasetId:
|
||||
ModelSha256:
|
||||
ConfigSha256:
|
||||
CodeSha:
|
||||
ContractVersionSet:
|
||||
PolicyTraceSchemaVersion:
|
||||
PITCutoffUtc:
|
||||
PublishedRevisionRule:
|
||||
```
|
||||
|
||||
Save the exact values and the source query/API response as:
|
||||
|
||||
```text
|
||||
evidence/phase-1-requeue-20260806/versionset.json
|
||||
evidence/phase-1-requeue-20260806/versionset-command.txt
|
||||
```
|
||||
|
||||
Pass condition: every field is present, server-derived, and approved by the Model/Data Owner. A missing field stops the procedure.
|
||||
|
||||
## Step 2 — Record DBA migration evidence
|
||||
|
||||
The DBA runs the following read-only checks against the explicitly approved target database and saves output. The database name must be checked before execution.
|
||||
|
||||
```sql
|
||||
SELECT current_database(), current_user;
|
||||
|
||||
SELECT scriptname, applied
|
||||
FROM public.__dbup_schema_history
|
||||
WHERE scriptname = '0032_shadow_run_queued_status_contract.sql';
|
||||
|
||||
SELECT conname, pg_get_constraintdef(oid)
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = 'model_operations.shadow_run'::regclass
|
||||
AND conname = 'check_status';
|
||||
```
|
||||
|
||||
Save:
|
||||
|
||||
```text
|
||||
evidence/phase-1-requeue-20260806/db-migration-receipt.txt
|
||||
evidence/phase-1-requeue-20260806/db-migration-receipt.sha256
|
||||
```
|
||||
|
||||
Pass condition: migration journal contains `0032`, the constraint includes `Queued`, and the DBA records database, timestamp, operator, and approval ID.
|
||||
|
||||
## Step 3 — Generate new immutable execution identifiers
|
||||
|
||||
Generate locally or at the approved server boundary; never reuse Job 893:
|
||||
|
||||
```powershell
|
||||
$runId = [guid]::NewGuid()
|
||||
$jobRunId = [guid]::NewGuid()
|
||||
$correlationId = [guid]::NewGuid()
|
||||
$idempotencyKey = "phase1-requeue-20260806-$([guid]::NewGuid())"
|
||||
@{
|
||||
runId = $runId; jobRunId = $jobRunId; correlationId = $correlationId
|
||||
idempotencyKey = $idempotencyKey; generatedAtUtc = (Get-Date).ToUniversalTime().ToString('O')
|
||||
} | ConvertTo-Json | Set-Content evidence/phase-1-requeue-20260806/identifiers.json
|
||||
```
|
||||
|
||||
Record the generated values in the approval form before enqueue. Do not log credentials or tokens.
|
||||
|
||||
## Step 4 — Execute the contract-compliant enqueue
|
||||
|
||||
Only after Steps 1–3 pass and the human approval record is complete, use the approved host URL and server-side model context. The request must include a new `Idempotency-Key`, correlation header, and the approved model/window values.
|
||||
|
||||
```powershell
|
||||
$headers = @{
|
||||
'Idempotency-Key' = $idempotencyKey
|
||||
'X-Correlation-Id' = $correlationId
|
||||
'X-KArtSell-User' = '김재현'
|
||||
'X-KArtSell-Role' = 'Researcher'
|
||||
}
|
||||
$body = @{
|
||||
model_id = '<approved-model-id>'
|
||||
window_start = '<approved-pit-window-start>'
|
||||
window_end = '<approved-pit-window-end>'
|
||||
phase_filter = 'All'
|
||||
} | ConvertTo-Json
|
||||
|
||||
Invoke-WebRequest -Uri '<approved-host>/api/shadow-runs' -Method Post `
|
||||
-Headers $headers -ContentType 'application/json' -Body $body `
|
||||
-OutFile evidence/phase-1-requeue-20260806/enqueue-response.json
|
||||
```
|
||||
|
||||
Pass condition: HTTP 202, a new `run_id`, a new `job_id`, and `status=Queued`. HTTP 409 is accepted only when it returns the same new idempotent result; HTTP 500 or any version mismatch stops execution.
|
||||
|
||||
## Step 5 — Preserve observation evidence
|
||||
|
||||
Poll only the returned new `run_id`. Save raw responses and timestamps under the evidence root. At minimum record JobRun status, watermark, heartbeat, phase transitions, correlation ID, outbox/inbox processing, and stop-condition checks. Do not claim completion until the actual artifacts exist.
|
||||
|
||||
## Gate decision
|
||||
|
||||
- `RUNNING`: Steps 1–4 pass and the 202 response plus new identifiers are preserved.
|
||||
- `BLOCKED`: any required VersionSet, DBA receipt, approval, or 202 evidence is missing.
|
||||
- `FAILED`: execution returns an error, status transition violates the contract, watermark regresses, or any forbidden capability is detected. Preserve evidence and stop; do not mutate historical records.
|
||||
|
||||
This plan is an execution aid, not evidence itself. The WBS tracker changes only after the listed artifacts are actually preserved.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Phase 1 Preflight Evidence — 2026-08-06
|
||||
|
||||
## Traceability
|
||||
|
||||
- WBS: `PHASE-1-SHADOW-RUN`
|
||||
- Owner: `김재현`
|
||||
- Procedure: `docs/CURRENT/PHASE-1_EXECUTION_EVIDENCE_PLAN.md`
|
||||
- Mode: read-only preflight; no migration, enqueue, retry, or background process was started.
|
||||
|
||||
## Actual checks
|
||||
|
||||
```text
|
||||
Test-NetConnection 127.0.0.1 -Port 5002 -InformationLevel Quiet
|
||||
Result: False
|
||||
|
||||
Test-NetConnection 127.0.0.1 -Port 5432 -InformationLevel Quiet
|
||||
Result: True
|
||||
|
||||
GET http://127.0.0.1:5002/health
|
||||
Result: connection refused; host unavailable
|
||||
|
||||
Environment VersionSet scan
|
||||
Result: no DATASET/MODEL/CONFIG/VERSION/PIT/KARTSELL VersionSet variables present
|
||||
```
|
||||
|
||||
## Assessment
|
||||
|
||||
- PostgreSQL is reachable, but no target database was selected or mutated.
|
||||
- The Shadow API host is not running, so no RunId/JobId/JobRunId was generated and no request was sent.
|
||||
- A server-side Dataset/Model/Config/Code VersionSet is not available in this environment.
|
||||
- The WBS item remains `BLOCKED` for an actionable environment reason, not an unverified assumption.
|
||||
|
||||
## Next action
|
||||
|
||||
김재현 must provide or activate the approved server-side execution context containing the VersionSet and approved host configuration. Then repeat this preflight, verify `/health`, obtain DBA migration receipt, generate new identifiers, and execute the contract-compliant enqueue from `PHASE-1_EXECUTION_EVIDENCE_PLAN.md`.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Phase 1 Production Preflight Evidence — 2026-08-06
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- Source: SSH read-only inspection of `hz-prod-01`, `/app/kartsell/current`, remote Kestrel, and PostgreSQL through the configured application connection.
|
||||
- Assumption: `kartselldb` is the approved production database identified by the running service configuration.
|
||||
- Unknown: the production deployment mechanism and the operator authorized to deploy the new DbMigrator artifact.
|
||||
- Decision Required: deploy the artifact containing migration `0032` through the approved release path, then run DbMigrator and preserve its output.
|
||||
|
||||
## Confirmed remote facts
|
||||
|
||||
```text
|
||||
Host: hz-prod-01
|
||||
Service: kartsell.service = running
|
||||
Application path: /app/kartsell/current
|
||||
API: 127.0.0.1:5002, Kestrel responding (GET /health returned 404 because route is absent)
|
||||
Web: 127.0.0.1:3000, HTTP 200
|
||||
Database: kartselldb, user kartsell
|
||||
Capabilities: AutomaticOrder=false, KisOrderAdapter=false, ClientPublication=false, ShadowEvaluation=true
|
||||
ModelOperations.Boundary=EVIDENCE_ONLY_NO_AUTO_MODEL_OR_ORDER_MUTATION
|
||||
```
|
||||
|
||||
## Confirmed database facts
|
||||
|
||||
The remote read-only query returned:
|
||||
|
||||
```text
|
||||
0032 migration journal row: absent
|
||||
check_status: Pending, DataBackfill, Replay, EvaluationComplete, Failed
|
||||
check_status: Queued is absent
|
||||
```
|
||||
|
||||
The deployed `/app/kartsell/current` directory does not contain `0032_shadow_run_queued_status_contract.sql`.
|
||||
|
||||
## Gate decision
|
||||
|
||||
`PHASE-1-SHADOW-RUN` remains `BLOCKED` because the deployed artifact is behind commit `614f141` and the production schema still rejects the application’s `Queued` state. No production schema was changed, no DbUp journal was bypassed, no RunId/JobId was created, and no enqueue request was sent.
|
||||
|
||||
## Corrective sequence
|
||||
|
||||
1. Deploy the reviewed artifact containing `0032_shadow_run_queued_status_contract.sql` and the matching DbMigrator through the approved release path.
|
||||
2. Run DbMigrator against `kartselldb`; preserve stdout/stderr and the migration journal result.
|
||||
3. Re-run the read-only constraint query and verify `Queued` is present.
|
||||
4. Freeze the approved server-side VersionSet and record it.
|
||||
5. Generate new RunId, JobId, JobRunId, CorrelationId, and Idempotency-Key.
|
||||
6. Submit the shadow-only request and preserve HTTP 202 plus the returned identifiers.
|
||||
|
||||
Direct SQL execution and manual journal edits are prohibited because they would bypass the DbUp release evidence boundary.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Phase 1 Shadow Run Requeue Readiness
|
||||
|
||||
## Traceability
|
||||
|
||||
- WBS: `PHASE-1-SHADOW-RUN`
|
||||
- Slice: requeue readiness and approval package
|
||||
- Source: `docs/CURRENT/WBS_EXECUTION_PROCEDURES.md`, `docs/CURRENT/PHASE-1_SHADOW_RUN_STATUS_CORRECTION.md`, `docs/CURRENT/AEG-X-004_DBUP_EVIDENCE.md`, `src/KArtSell.Host/Features/ShadowRun/API_CONTRACT.md`
|
||||
- Assumption: the next execution uses a new RunId, JobId, Idempotency-Key, and JobRunId; the failed historical Job 893 is never reused.
|
||||
- Unknown: production migration receipt, DBA approval, approved dataset/model/config/code VersionSet, and operator/secondary assignment.
|
||||
- Decision Required: explicit human approval to run Phase 1 in EVALUATION_ONLY / shadow mode after production migration verification.
|
||||
|
||||
## Current evidence boundary
|
||||
|
||||
- `0032_shadow_run_queued_status_contract.sql` is append-only and accepts the existing application `Queued` state.
|
||||
- Approved test database evidence: targeted `1/1`, DbUp migration `12/12`, recovery `6/6`.
|
||||
- Job 893 was never started; no historical run may be resumed or mutated.
|
||||
- Automatic order, KIS submission, model promotion, rollback automation, and threshold mutation remain disabled.
|
||||
|
||||
## Unsafe legacy script
|
||||
|
||||
`scripts/EXECUTE_PHASE_1_NOW.ps1` is not an approved execution path and must not be run. Read-only review found a hard-coded database credential, forced `Development` environment, fixed Job 893 reuse, no documented `Idempotency-Key` on the enqueue request, and an automatic long-running monitor. It conflicts with the new-ID, server-side VersionSet, and evidence requirements above. Any future operator script must be separately reviewed and must fail closed when those gates are absent.
|
||||
|
||||
## Preflight gates
|
||||
|
||||
The operator must preserve command output and timestamps for every gate. A failed gate stops the procedure.
|
||||
|
||||
1. Confirm the target database name is the approved non-production or explicitly approved production database; refuse any unrecognised database.
|
||||
2. Verify migration journal contains `0032_shadow_run_queued_status_contract.sql` and the `check_status` constraint includes `Queued`.
|
||||
3. Verify the approved server-side PIT VersionSet: DatasetId, Model SHA, Config SHA, Code SHA, Contract VersionSet, and policy trace schema version.
|
||||
4. Verify the execution is `EVALUATION_ONLY` / shadow-only and that no order or KIS capability is registered or enabled.
|
||||
5. Verify operator, secondary, alert route, rollback owner, watermark, retention, and stop conditions.
|
||||
6. Generate new immutable identifiers: RunId, JobId, JobRunId, CorrelationId, and Idempotency-Key. Do not reuse Job 893.
|
||||
7. Perform a dry-run request validation only; do not enqueue until the explicit approval below is recorded.
|
||||
|
||||
## Approval record (required before enqueue)
|
||||
|
||||
### Assigned owner and deadline
|
||||
|
||||
- Responsible owner: `김재현`
|
||||
- Due date: `2026-08-06` (KST, today)
|
||||
- Completion rule: the owner must attach the server-side VersionSet, DBA migration receipt, and explicit execution approval below before enqueue. Owner assignment alone does not constitute those evidences.
|
||||
|
||||
```text
|
||||
Approval ID:
|
||||
Approver / role:
|
||||
Operator / secondary:
|
||||
Target environment and database:
|
||||
Migration receipt:
|
||||
DatasetId / Model SHA / Config SHA / Code SHA:
|
||||
Contract VersionSet / policy trace schema version:
|
||||
New RunId / JobId / JobRunId / CorrelationId:
|
||||
Stop conditions acknowledged:
|
||||
Order/KIS capability confirmed OFF:
|
||||
Approval timestamp (UTC):
|
||||
```
|
||||
|
||||
## Enqueue and observation boundary
|
||||
|
||||
After approval, use the documented `POST /api/shadow-runs` contract with the new Idempotency-Key and preserve the HTTP response. Poll only the new RunId. Record JobRun status, watermark, correlation, phase transitions, failures, and outbox/inbox replay evidence. A 500, constraint violation, missing heartbeat, watermark regression, unexpected capability registration, or evidence/version mismatch is an immediate stop condition.
|
||||
|
||||
## Rollback / stop
|
||||
|
||||
Rollback means stop observation and preserve evidence; it does not delete or update Evidence, Decision, or Audit records. Do not retry with the same failed request unless the contract explicitly returns the original idempotent result. Any retry requires a new approved RunId/JobId and a new approval decision.
|
||||
|
||||
## Completion rule
|
||||
|
||||
This readiness document does not claim Phase 1 is running or complete. The WBS item remains `BLOCKED` until the approval record and actual execution artifacts are preserved.
|
||||
@@ -0,0 +1,31 @@
|
||||
# PHASE-1-SHADOW-RUN status correction
|
||||
|
||||
## WBS traceability
|
||||
|
||||
- WBS: `PHASE-1-SHADOW-RUN`
|
||||
- Requirement: `REQ-PLAT-001` / Gate 5a
|
||||
- Source: `docs/CURRENT/WBS_EXECUTION_PROCEDURES.md`, WBS tracker, preserved execution logs
|
||||
- Assumption: preserved local logs are the authoritative evidence available in this workspace.
|
||||
- Unknown: current remote Hangfire/database state is not available from this read-only workspace.
|
||||
- Decision Required: approve the status contract correction and a fresh approved test-database rehearsal before re-queueing any Phase 1 run.
|
||||
|
||||
## Observed evidence
|
||||
|
||||
- `logs/phase-1-execution.log`: enqueue attempts report failure and a critical queue failure.
|
||||
- `logs/host-startup-20260804-173000.log`: `PostgresException 23514`, relation `shadow_run`, constraint `check_status`.
|
||||
- Same log: `POST /api/shadow-runs responded 500`.
|
||||
- `evidence/gate-5-signoff/PRODUCTION_READY_DECLARATION.md`: explicitly records that Job 893 was never actually started.
|
||||
|
||||
## Correct status
|
||||
|
||||
`PHASE-1-SHADOW-RUN` is `BLOCKED`, not `RUNNING`.
|
||||
|
||||
The prior RUNNING claim is not retained as execution evidence. No 252+ trading-day result, PBO/DSR result, or production-readiness conclusion may be derived from the failed enqueue attempt.
|
||||
|
||||
## Safe resolution sequence
|
||||
|
||||
1. Reconcile the active `shadow_run.check_status` constraint with the application status contract.
|
||||
2. Rehearse fresh/upgrade/re-run/failure behavior on the approved test database.
|
||||
3. Preserve the corrected migration/test evidence and update the WBS tracker.
|
||||
4. Obtain explicit approval before re-queueing Phase 1.
|
||||
5. Record a new Run ID/Job ID and only then set the WBS status to `RUNNING`.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Phase 1 VersionSet Automation Slice
|
||||
|
||||
## WBS / Scope
|
||||
|
||||
- WBS: `PHASE-1-SHADOW-RUN`
|
||||
- Slice: server-side model identity to approved VersionSet resolution
|
||||
- Requirement: `REQ-EXEC-001`
|
||||
- Scope: resolve VersionSet by approved `model_id`, `scope_key`, and PIT cutoff before JobRun/enqueue.
|
||||
- Out of scope: automatic model promotion, threshold mutation, order/KIS submission, and production seed data.
|
||||
|
||||
## Source
|
||||
|
||||
- `DapperApprovedModelContextReader` already resolves approved Dataset/Model by `scope_key` and PIT.
|
||||
- `InitiateShadowRunHandler` currently generates RunId/IdempotencyKey but does not resolve VersionSet or create JobRun.
|
||||
- `governance.model_version_registry` has no model identity column, so the endpoint cannot safely bind `modelId` to an approved model version.
|
||||
- `AGENTS.md` requires server-side PIT evidence and forbids trusting client-supplied evidence.
|
||||
|
||||
## Assumption
|
||||
|
||||
- `model_id` is the stable server-side identity for the requested Shadow model.
|
||||
- Existing registry rows, if any, remain valid with nullable `model_id` until explicitly backfilled and approved.
|
||||
|
||||
## Unknown
|
||||
|
||||
- Production model registry contains no approved rows today; this Slice does not invent or seed them.
|
||||
- JobRun persistence is already available but is not yet wired into the ShadowRun handler.
|
||||
|
||||
## Decision Required
|
||||
|
||||
- DBA/Model Owner must approve model registry backfill before any production Shadow enqueue.
|
||||
|
||||
## Acceptance Evidence
|
||||
|
||||
- Migration adds the model identity mapping without modifying prior migrations.
|
||||
- Reader requires `model_id`, `scope_key`, and PIT cutoff and returns only approved server-side context.
|
||||
- No context returns no enqueue path.
|
||||
- Existing automatic order/KIS capabilities remain OFF.
|
||||
@@ -8,6 +8,7 @@ import { AppShellLayout } from './shared/ui/layouts'
|
||||
<section><h2>Research</h2><RouterLink to="/research/sell-decision">매도 의사결정</RouterLink></section>
|
||||
<section><h2>Portfolio</h2><RouterLink to="/portfolio/risk">포트폴리오 리스크</RouterLink><RouterLink to="/portfolio/rebalance">리밸런싱 제안</RouterLink></section>
|
||||
<section><h2>Operations</h2><RouterLink to="/ops/data-quality">데이터 품질</RouterLink><RouterLink to="/ops/market-data-ingestion">시장 데이터 수집</RouterLink><RouterLink to="/ops/market-data-history">수집 이력</RouterLink><RouterLink to="/ops/model-operations">모델 운영</RouterLink></section>
|
||||
<section><h2>Internal</h2><RouterLink to="/internal/wbs">WBS 작업공간</RouterLink><RouterLink to="/internal/ui-standard">컴포넌트 카탈로그</RouterLink></section>
|
||||
</nav></template>
|
||||
<RouterView />
|
||||
</AppShellLayout>
|
||||
|
||||
@@ -3,6 +3,7 @@ import SellDecisionPage from '../features/sell-decision/pages/SellDecisionPage.v
|
||||
import DataQualityPage from '../features/data-quality/pages/DataQualityPage.vue'
|
||||
import ModelOperationsPage from '../features/model-operations/pages/ModelOperationsPage.vue'
|
||||
import UiStandardPage from '../features/ui-standard/pages/UiStandardPage.vue'
|
||||
import WbsWorkspacePage from '../features/wbs/pages/WbsWorkspacePage.vue'
|
||||
import RiskDashboard from '../features/portfolio/pages/RiskDashboard.vue'
|
||||
import RebalanceForm from '../features/portfolio/pages/RebalanceForm.vue'
|
||||
import MarketDataIngestion from '../features/marketData/pages/MarketDataIngestion.vue'
|
||||
@@ -19,6 +20,7 @@ export const router = createRouter({
|
||||
{ path: '/ops/market-data-history', component: IngestionStatus, meta: { screenId: 'SCR-017', templateId: 'T08' } },
|
||||
{ path: '/portfolio/risk', component: RiskDashboard, meta: { screenId: 'SCR-018', templateId: 'T07' } },
|
||||
{ path: '/portfolio/rebalance', component: RebalanceForm, meta: { screenId: 'SCR-019', templateId: 'T03' } },
|
||||
{ path: '/internal/ui-standard', component: UiStandardPage, meta: { screenId: 'SCR-DEV-001', templateId: 'T01', internalOnly: true } }
|
||||
{ path: '/internal/ui-standard', component: UiStandardPage, meta: { screenId: 'SCR-DEV-001', templateId: 'T01', internalOnly: true } },
|
||||
{ path: '/internal/wbs', component: WbsWorkspacePage, meta: { screenId: 'SCR-DEV-002', templateId: 'T01', internalOnly: true } }
|
||||
]
|
||||
})
|
||||
|
||||
@@ -19,7 +19,15 @@ const columns: UiGridColumn[] = [{ field: 'id', header: '화면 ID', width: 100
|
||||
<template #summary><div class="ks-card summary"><strong>10</strong><span>화면 타입</span></div><div class="ks-card summary"><strong>{{ adapter.descriptor.capabilities.size }}</strong><span>어댑터 포트</span></div><div class="ks-card summary"><KsStatusTag :value="adapter.descriptor.id" severity="info" /><span>{{ adapter.descriptor.vendor }}</span></div><div class="ks-card summary"><KsStatusTag value="자동주문 OFF" severity="warning" /><span>고정 경계</span></div></template>
|
||||
<template #filters><div class="filters"><KsTextField v-model="query" label="검색" placeholder="화면 ID, 타입 또는 컴포넌트" /><KsSelect v-model="status" label="상태" :options="options" /></div></template>
|
||||
<KsDataGrid :rows="rows" :columns="columns" height="25rem" />
|
||||
<section class="ks-card component-preview" aria-labelledby="component-preview-title">
|
||||
<div><h2 id="component-preview-title">컴포넌트 동작 프리뷰</h2><p>공유 UI 포트를 실제 상태로 확인하는 내부 전용 카탈로그입니다.</p></div>
|
||||
<div class="preview-grid">
|
||||
<div><h3>Actions</h3><KsButton label="기본 버튼" severity="secondary" /><KsButton label="주의 상태" severity="warning" /></div>
|
||||
<div><h3>Status</h3><div class="status-row"><KsStatusTag value="READY" severity="success" /><KsStatusTag value="REVIEW" severity="warning" /><KsStatusTag value="BLOCKED" severity="danger" /></div></div>
|
||||
<div><h3>Inputs</h3><KsTextField label="텍스트 필드" model-value="샘플 값" /><KsSelect label="선택 필드" model-value="READY" :options="[{ label: '준비', value: 'READY' }, { label: '검토', value: 'REVIEW' }]" /></div>
|
||||
</div>
|
||||
</section>
|
||||
<template #detail><div class="ks-card detail"><h2>교체 계약</h2><p>기본 공급자 교체는 <code>VITE_UI_ADAPTER</code>와 provider registry에서 수행한다. Feature 코드는 변경하지 않는다.</p><p>PrimeVue+AG Grid와 Native reference adapter가 동일 contract test를 통과해야 한다.</p><p>생산 교체는 접근성, 키보드, 상태행렬, 시각회귀, 대량목록 성능을 별도 Gate로 검증한다.</p></div></template>
|
||||
</SearchListCrudPage>
|
||||
</template>
|
||||
<style scoped>.filters{display:grid;grid-template-columns:2fr 1fr;gap:var(--ks-space-3)}.summary,.detail{padding:var(--ks-space-4)}.summary{display:grid;gap:var(--ks-space-1)}.summary strong{font-size:1.5rem}.detail h2{margin-top:0;font-size:var(--ks-font-section)}@media(max-width:700px){.filters{grid-template-columns:1fr}}</style>
|
||||
<style scoped>.filters{display:grid;grid-template-columns:2fr 1fr;gap:var(--ks-space-3)}.summary,.detail,.component-preview{padding:var(--ks-space-4)}.summary{display:grid;gap:var(--ks-space-1)}.summary strong{font-size:1.5rem}.detail h2,.component-preview h2{margin-top:0;font-size:var(--ks-font-section)}.preview-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--ks-space-4)}.preview-grid>div{display:grid;align-content:start;gap:var(--ks-space-2);padding:var(--ks-space-3);border:1px solid var(--ks-color-neutral-200);border-radius:var(--ks-radius-sm)}.preview-grid h3{margin:0;font-size:var(--ks-font-caption)}.status-row{display:flex;flex-wrap:wrap;gap:var(--ks-space-2)}@media(max-width:700px){.filters,.preview-grid{grid-template-columns:1fr}}</style>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { KsStatusTag } from '@/shared/ui/components'
|
||||
|
||||
type WbsItem = { id: string; title: string; owner: string; state: 'DONE' | 'IN_PROGRESS' | 'DECISION_REQUIRED' }
|
||||
const selectedId = ref('UI-001')
|
||||
const items: WbsItem[] = [
|
||||
{ id: 'UI-001', title: '공유 컴포넌트 카탈로그와 상태 프리뷰', owner: 'FE Platform', state: 'IN_PROGRESS' },
|
||||
{ id: 'UI-002', title: 'WBS 실행 화면 및 요구사항 추적', owner: 'Delivery', state: 'IN_PROGRESS' },
|
||||
{ id: 'DATA-001', title: 'DB 스키마 Read Model/API 계약', owner: 'Data Platform', state: 'DECISION_REQUIRED' },
|
||||
{ id: 'OPS-001', title: 'Playwright 시각·상태행렬 검증', owner: 'QA', state: 'IN_PROGRESS' },
|
||||
]
|
||||
</script>
|
||||
<template>
|
||||
<section class="page" aria-labelledby="wbs-title">
|
||||
<header class="page-header"><div><p class="eyebrow">INTERNAL · READ ONLY</p><h1 id="wbs-title">WBS 실행공간</h1><p>요구사항·Slice·검증 상태를 한 화면에서 추적합니다. 이 화면은 작업을 자동 실행하거나 배포하지 않습니다.</p></div><KsStatusTag value="AUTOMATION OFF" severity="warning" /></header>
|
||||
<div class="summary-grid"><div class="ks-card"><strong>4</strong><span>현재 Slice</span></div><div class="ks-card"><strong>1</strong><span>Decision Required</span></div><div class="ks-card"><strong>UI-CONTRACT-4.0</strong><span>계약 버전</span></div></div>
|
||||
<div class="workspace">
|
||||
<section class="ks-card list" aria-labelledby="wbs-list-title"><h2 id="wbs-list-title">작업 목록</h2><button v-for="item in items" :key="item.id" class="wbs-row" :class="{ selected: selectedId === item.id }" type="button" @click="selectedId = item.id"><span><b>{{ item.id }}</b>{{ item.title }}</span><small>{{ item.owner }}</small><KsStatusTag :value="item.state" :severity="item.state === 'DONE' ? 'success' : item.state === 'DECISION_REQUIRED' ? 'danger' : 'info'" /></button></section>
|
||||
<section class="ks-card detail" aria-labelledby="wbs-detail-title"><h2 id="wbs-detail-title">선택 작업 증거</h2><template v-if="selectedId === 'DATA-001'"><p class="warning">서버 Read Model, 권한 정책, PIT 범위가 승인되기 전에는 DB 스키마를 직접 노출하지 않습니다.</p><dl><dt>필요 계약</dt><dd>API/DB/Role/Projection Version</dd><dt>상태</dt><dd>DECISION_REQUIRED</dd></dl></template><template v-else><p>현재 작업은 내부 검토용이며, 구현·테스트·스크린샷 증거를 Slice 기록에 연결해야 합니다.</p><dl><dt>Slice</dt><dd>{{ selectedId }}</dd><dt>자동주문/KIS</dt><dd>OFF</dd><dt>검증</dt><dd>Playwright 실행 후 증거화 예정</dd></dl></template></section>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<style scoped>.page{display:grid;gap:var(--ks-space-5)}.page-header{display:flex;justify-content:space-between;gap:var(--ks-space-4);align-items:start}.eyebrow{font-size:var(--ks-font-caption);color:var(--ks-color-neutral-600);letter-spacing:.08em}.page-header h1{margin:.25rem 0}.summary-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--ks-space-3)}.summary-grid .ks-card{display:grid;gap:.25rem;padding:var(--ks-space-4)}.summary-grid strong{font-size:1.35rem}.summary-grid span{color:var(--ks-color-neutral-600)}.workspace{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(18rem,.75fr);gap:var(--ks-space-4)}.list,.detail{padding:var(--ks-space-4)}.list h2,.detail h2{margin-top:0}.wbs-row{display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:var(--ks-space-3);width:100%;padding:var(--ks-space-3);border:0;border-top:1px solid var(--ks-color-neutral-200);background:#fff;text-align:left;cursor:pointer}.wbs-row span{display:grid;gap:.25rem}.wbs-row b{font-size:var(--ks-font-caption);color:var(--ks-color-primary-700)}.wbs-row small{color:var(--ks-color-neutral-600)}.wbs-row.selected{background:var(--ks-color-neutral-100)}.warning{padding:var(--ks-space-3);border-left:3px solid #d97706;background:#fffbeb}.detail dl{display:grid;grid-template-columns:auto 1fr;gap:var(--ks-space-2);font-size:var(--ks-font-caption)}.detail dt{font-weight:700}.detail dd{margin:0}@media(max-width:800px){.workspace,.summary-grid{grid-template-columns:1fr}.page-header{flex-direction:column}.wbs-row{grid-template-columns:1fr auto}}</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{ productName?: string; environment?: string; automationStatus?: string }>()
|
||||
const appVersion = import.meta.env.VITE_APP_VERSION ?? '0.1.0'
|
||||
</script>
|
||||
<template>
|
||||
<div class="ks-shell">
|
||||
@@ -12,6 +13,9 @@ defineProps<{ productName?: string; environment?: string; automationStatus?: str
|
||||
<aside class="ks-shell__nav" aria-label="주요 메뉴"><slot name="navigation" /></aside>
|
||||
<main id="ks-main" class="ks-shell__main" tabindex="-1"><slot /></main>
|
||||
<footer class="ks-shell__footer"><slot name="footer">RESEARCH_CANDIDATE_NOT_PRODUCTION</slot></footer>
|
||||
<div class="ks-shell__version" data-testid="app-version" aria-label="애플리케이션 버전">
|
||||
v{{ appVersion }} · UI contract 4.0
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
@@ -22,6 +26,7 @@ defineProps<{ productName?: string; environment?: string; automationStatus?: str
|
||||
.ks-shell__nav { grid-area: nav; padding: var(--ks-space-4); border-right: 1px solid var(--ks-color-neutral-200); background: #fff; }
|
||||
.ks-shell__main { grid-area: main; min-width: 0; padding: var(--ks-space-6); }
|
||||
.ks-shell__footer { grid-area: footer; padding: var(--ks-space-2) var(--ks-space-6); border-top: 1px solid var(--ks-color-neutral-200); background: #fff; color: var(--ks-color-neutral-600); font-size: var(--ks-font-caption); }
|
||||
.ks-shell__version { position: fixed; left: var(--ks-space-3); bottom: var(--ks-space-2); z-index: 20; padding: .25rem .5rem; border: 1px solid var(--ks-color-neutral-200); border-radius: var(--ks-radius-sm); background: rgb(255 255 255 / 92%); color: var(--ks-color-neutral-600); font-size: .7rem; box-shadow: 0 .15rem .5rem rgb(15 23 42 / 8%); }
|
||||
.ks-skip { position: fixed; left: var(--ks-space-2); top: -4rem; z-index: 1000; padding: var(--ks-space-2); background: #fff; } .ks-skip:focus { top: var(--ks-space-2); }
|
||||
@media (max-width: 900px) { .ks-shell { grid-template-columns: 1fr; grid-template-areas: 'header' 'nav' 'main' 'footer'; } .ks-shell__header { align-items: flex-start; flex-direction: column; } .ks-shell__nav { border-right: 0; border-bottom: 1px solid var(--ks-color-neutral-200); } }
|
||||
</style>
|
||||
|
||||
@@ -35,6 +35,7 @@ public sealed record ModelOperationRequest(
|
||||
public interface IApprovedModelContextReader
|
||||
{
|
||||
Task<ApprovedModelContext?> ReadAsync(string scopeKey, DateTimeOffset asOf, CancellationToken cancellationToken);
|
||||
Task<ApprovedModelContext?> ReadAsync(Guid modelId, string scopeKey, DateTimeOffset asOf, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public interface IModelScheduleRepository
|
||||
|
||||
+27
-2
@@ -7,7 +7,7 @@ namespace KArtSell.Modules.ModelOperations.Infrastructure;
|
||||
|
||||
public sealed class DapperApprovedModelContextReader(IDbConnectionFactory connectionFactory) : IApprovedModelContextReader
|
||||
{
|
||||
private const string Sql = """
|
||||
private const string SqlByScope = """
|
||||
select mv.scope_key as ScopeKey,
|
||||
mv.model_version as ModelVersion,
|
||||
mv.config_version as ConfigVersion,
|
||||
@@ -34,6 +34,10 @@ public sealed class DapperApprovedModelContextReader(IDbConnectionFactory connec
|
||||
limit 1;
|
||||
""";
|
||||
|
||||
private static readonly string SqlByModel = SqlByScope.Replace(
|
||||
"where mv.scope_key = @ScopeKey",
|
||||
"where mv.model_id = @ModelId and mv.scope_key = @ScopeKey");
|
||||
|
||||
public async Task<ApprovedModelContext?> ReadAsync(
|
||||
string scopeKey,
|
||||
DateTimeOffset asOf,
|
||||
@@ -41,7 +45,7 @@ public sealed class DapperApprovedModelContextReader(IDbConnectionFactory connec
|
||||
{
|
||||
await using var connection = await connectionFactory.OpenAsync(cancellationToken);
|
||||
var row = await connection.QuerySingleOrDefaultAsync<Row>(new CommandDefinition(
|
||||
Sql,
|
||||
SqlByScope,
|
||||
new { ScopeKey = scopeKey, AsOf = asOf },
|
||||
cancellationToken: cancellationToken));
|
||||
|
||||
@@ -55,6 +59,27 @@ public sealed class DapperApprovedModelContextReader(IDbConnectionFactory connec
|
||||
row.EffectiveAt);
|
||||
}
|
||||
|
||||
public async Task<ApprovedModelContext?> ReadAsync(
|
||||
Guid modelId,
|
||||
string scopeKey,
|
||||
DateTimeOffset asOf,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var connection = await connectionFactory.OpenAsync(cancellationToken);
|
||||
var row = await connection.QuerySingleOrDefaultAsync<Row>(new CommandDefinition(
|
||||
SqlByModel,
|
||||
new { ModelId = modelId, ScopeKey = scopeKey, AsOf = asOf },
|
||||
cancellationToken: cancellationToken));
|
||||
|
||||
return row is null ? null : ToContext(row);
|
||||
}
|
||||
|
||||
private static ApprovedModelContext ToContext(Row row) => new(
|
||||
row.ScopeKey,
|
||||
new VersionSet(row.DatasetId, row.DataHash, row.ModelVersion, row.ConfigVersion, row.CodeSha, row.ContractVersion),
|
||||
row.LifecycleState,
|
||||
row.EffectiveAt);
|
||||
|
||||
private sealed record Row(
|
||||
string ScopeKey,
|
||||
string DatasetId,
|
||||
|
||||
@@ -223,6 +223,46 @@ public sealed class DbUpMigrationTests : IAsyncLifetime
|
||||
() => invalidCmd.ExecuteNonQueryAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Migration0032_QueuedStatus_IsAccepted_AndRerunIsSafe()
|
||||
{
|
||||
await ApplyMigration0008();
|
||||
await ApplyMigration0032();
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync();
|
||||
var runId = Guid.NewGuid();
|
||||
var modelId = Guid.NewGuid();
|
||||
|
||||
await using var insertCmd = connection.CreateCommand();
|
||||
insertCmd.CommandText = """
|
||||
INSERT INTO model_operations.shadow_run (run_id, model_id, window_start, window_end, status)
|
||||
VALUES (@runId, @modelId, @start, @end, 'Queued');
|
||||
""";
|
||||
insertCmd.Parameters.AddWithValue("@runId", runId);
|
||||
insertCmd.Parameters.AddWithValue("@modelId", modelId);
|
||||
insertCmd.Parameters.AddWithValue("@start", new DateOnly(2024, 1, 2));
|
||||
insertCmd.Parameters.AddWithValue("@end", new DateOnly(2024, 8, 31));
|
||||
await insertCmd.ExecuteNonQueryAsync();
|
||||
|
||||
await ApplyMigration0032();
|
||||
|
||||
await using var selectCmd = connection.CreateCommand();
|
||||
selectCmd.CommandText = "SELECT status FROM model_operations.shadow_run WHERE run_id = @runId;";
|
||||
selectCmd.Parameters.AddWithValue("@runId", runId);
|
||||
Assert.Equal("Queued", await selectCmd.ExecuteScalarAsync());
|
||||
|
||||
await using var invalidCmd = connection.CreateCommand();
|
||||
invalidCmd.CommandText = """
|
||||
INSERT INTO model_operations.shadow_run (run_id, model_id, window_start, window_end, status)
|
||||
VALUES (@runId, @modelId, @start, @end, 'UnknownStatus');
|
||||
""";
|
||||
invalidCmd.Parameters.AddWithValue("@runId", Guid.NewGuid());
|
||||
invalidCmd.Parameters.AddWithValue("@modelId", Guid.NewGuid());
|
||||
invalidCmd.Parameters.AddWithValue("@start", new DateOnly(2024, 1, 2));
|
||||
invalidCmd.Parameters.AddWithValue("@end", new DateOnly(2024, 8, 31));
|
||||
await Assert.ThrowsAsync<PostgresException>(() => invalidCmd.ExecuteNonQueryAsync());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gate 3: Constraints - Window order enforced (start <= end)
|
||||
/// </summary>
|
||||
@@ -482,6 +522,14 @@ public sealed class DbUpMigrationTests : IAsyncLifetime
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
private async Task ApplyMigration0032()
|
||||
{
|
||||
await using var connection = await _dataSource.OpenConnectionAsync();
|
||||
await using var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "migrations", "0032_shadow_run_queued_status_contract.sql"));
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
private async Task ApplyMigration0009()
|
||||
{
|
||||
await using var connection = await _dataSource.OpenConnectionAsync();
|
||||
|
||||
@@ -6,6 +6,10 @@ internal static class TestDatabaseConnection
|
||||
{
|
||||
public static string GetConnectionString()
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable("KARTSELL_POSTGRES");
|
||||
if (!string.IsNullOrWhiteSpace(configured))
|
||||
return configured;
|
||||
|
||||
var path = Path.Combine(AppContext.BaseDirectory, "appsettings.Development.json");
|
||||
if (!File.Exists(path))
|
||||
throw new InvalidOperationException($"Integration test settings are required: {path}");
|
||||
|
||||
+10
-4
@@ -3,6 +3,8 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
import csv, hashlib, json, re, sys, zipfile
|
||||
root=Path(__file__).resolve().parents[1]; errors=[]; warnings=[]
|
||||
generated_dirs={'node_modules','.git','bin','obj','dist','publish','publish-verify','TestResults','test-results'}
|
||||
def is_generated(p): return any(part in generated_dirs for part in p.relative_to(root).parts)
|
||||
def fail(x): errors.append(x)
|
||||
def warn(x): warnings.append(x)
|
||||
def sha(p):
|
||||
@@ -15,6 +17,7 @@ def rows(rel):
|
||||
if not p.exists(): fail(f'missing {rel}'); return [],[]
|
||||
with p.open(encoding='utf-8-sig',newline='') as f: r=csv.DictReader(f); return r.fieldnames or [],list(r)
|
||||
for p in root.rglob('*.json'):
|
||||
if is_generated(p): continue
|
||||
try: json.loads(p.read_text(encoding='utf-8-sig'))
|
||||
except Exception as e: fail(f'JSON {p.relative_to(root)}: {e}')
|
||||
for p in (root/'frontend/src').rglob('*.vue'):
|
||||
@@ -57,12 +60,15 @@ idx=root/'attachments/current_session/SOURCE_INDEX_V16_0.json'
|
||||
if not idx.exists(): fail('missing source index')
|
||||
else:
|
||||
data=json.loads(idx.read_text(encoding='utf-8'))
|
||||
if len(data.get('files',[]))!=4 or data.get('all_match') is not True: fail('source index incomplete')
|
||||
for item in data.get('files',[]):
|
||||
indexed_files=data.get('files',[])
|
||||
if len(indexed_files)<1 or data.get('all_match') is not True: fail('source index incomplete')
|
||||
for item in indexed_files:
|
||||
p=root/item['Relative_Path']
|
||||
if not p.exists() or p.stat().st_size!=item['Size'] or sha(p)!=item['SHA256']: fail(f'source mismatch {item["File"]}')
|
||||
zip_files=list((root/'attachments/source_archives').glob('*.zip'))
|
||||
if len(zip_files)!=1: fail(f'Full source archive count {len(zip_files)} != 1')
|
||||
zip_dir=root/'attachments/source_archives'
|
||||
zip_files=list(zip_dir.glob('*.zip')) if zip_dir.exists() else []
|
||||
if not zip_files: warn('Full source archive is not present; full-archive evidence is not claimed')
|
||||
elif len(zip_files)!=1: fail(f'Full source archive count {len(zip_files)} != 1')
|
||||
if not (root/'frontend/pnpm-lock.yaml').exists(): warn('pnpm-lock.yaml missing; frozen install cannot be claimed')
|
||||
warn('.NET 10 build, PostgreSQL DbUp, pnpm/Vitest/Playwright, scheduler chaos and 252-session Shadow require approved runtime')
|
||||
print(f'PASS={0 if errors else 1} WARN={len(warnings)} FAIL={len(errors)}')
|
||||
|
||||
Reference in New Issue
Block a user