Initial commit: Add project files
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
namespace __MODULE__;
|
||||
|
||||
public static class Module
|
||||
{
|
||||
public static IServiceCollection Add__MODULE__(this IServiceCollection services)
|
||||
{
|
||||
// Register only module-owned policies, application handlers, and adapters.
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using FastEndpoints;
|
||||
|
||||
namespace __MODULE__.Features.__SLICE__;
|
||||
|
||||
public sealed class Endpoint(Handler handler) : Endpoint<Request, Response>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
__HTTP_METHOD__("/__ROUTE__");
|
||||
Roles("__ROLE__");
|
||||
Description(x => x.WithTags("__MODULE__"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(Request req, CancellationToken ct)
|
||||
{
|
||||
var result = await handler.HandleAsync(req, HttpContext.TraceIdentifier, ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace __MODULE__.Features.__SLICE__;
|
||||
|
||||
public sealed class Handler(/* narrow ports only */)
|
||||
{
|
||||
public async Task<Response> HandleAsync(Request request, string correlationId, CancellationToken ct)
|
||||
{
|
||||
// 1) server-side PIT context 2) idempotency 3) transaction 4) policy 5) outbox
|
||||
throw new NotImplementedException("Complete the approved Slice Spec; do not guess missing contracts.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace __MODULE__.Domain;
|
||||
|
||||
public sealed class __POLICY__
|
||||
{
|
||||
public Decision Evaluate(ApprovedPointInTimeInput input)
|
||||
{
|
||||
// Pure decision only. No Dapper, HTTP, Hangfire, DateTime.Now, or mutable global state.
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# __SLICE__
|
||||
|
||||
- Requirement: __REQUIREMENT_ID__
|
||||
- Module: __MODULE__
|
||||
- Route: `__HTTP_METHOD__ /__ROUTE__`
|
||||
- Role: `__ROLE__`
|
||||
- Generated status: SCAFFOLD_ONLY / DECISION_REQUIRED contracts remain blocked.
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace __MODULE__.Features.__SLICE__;
|
||||
|
||||
public sealed record Request(/* approved request fields only */);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace __MODULE__.Features.__SLICE__;
|
||||
|
||||
public sealed record Response(/* approved response fields only */);
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace __MODULE__.Features.__SLICE__;
|
||||
|
||||
internal static class Sql
|
||||
{
|
||||
internal const string ReadApprovedContext = """
|
||||
-- SCAFFOLD_ONLY: replace only after schema ownership, explicit columns,
|
||||
-- PIT cutoff, quality gate and key contract are approved in the Slice Spec.
|
||||
select 1 as contract_not_ready
|
||||
where false;
|
||||
""";
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
public sealed class __SLICE__Tests
|
||||
{
|
||||
[Fact]
|
||||
public void Golden_case_is_reproducible() { }
|
||||
|
||||
[Fact]
|
||||
public void Boundary_and_forbidden_transition_fail_closed() { }
|
||||
|
||||
[Fact]
|
||||
public void Same_idempotency_key_has_one_side_effect() { }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using FastEndpoints;
|
||||
using FluentValidation;
|
||||
|
||||
namespace __MODULE__.Features.__SLICE__;
|
||||
|
||||
public sealed class Validator : Validator<Request>
|
||||
{
|
||||
public Validator()
|
||||
{
|
||||
// Translate approved boundary rules only; UNKNOWN and DECISION_REQUIRED must fail closed.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Hangfire;
|
||||
|
||||
public sealed class __JOB__(IJobRunRepository runs, IClock clock)
|
||||
{
|
||||
[Queue("__QUEUE__")]
|
||||
[AutomaticRetry(Attempts = 0)] // retry policy is explicit and evidence-driven
|
||||
public async Task ExecuteAsync(JobCommand command, CancellationToken ct)
|
||||
{
|
||||
// TryStart(idempotency/scope/version/hash) → heartbeat → work → Complete.
|
||||
// No current-value overwrite; reprocess creates a revision.
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
-- Immutable ordered DbUp migration. Never rewrite after release.
|
||||
create schema if not exists __schema__;
|
||||
|
||||
create table if not exists __schema__.__table__ (
|
||||
id uuid primary key,
|
||||
source_id text not null,
|
||||
published_at timestamptz not null,
|
||||
ingested_at timestamptz not null,
|
||||
revision_no integer not null,
|
||||
content_hash text not null,
|
||||
unique (source_id, revision_no)
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
using FastEndpoints;
|
||||
namespace KArtSell.Modules.__MODULE__.Features.__FEATURE__;
|
||||
public sealed class Endpoint(IHandler handler) : Endpoint<Request, Response>
|
||||
{
|
||||
public override void Configure() { Post("/api/v1/__MODULE__/__FEATURE__"); Policies("__SLICE_ID__"); }
|
||||
public override async Task HandleAsync(Request request, CancellationToken ct)
|
||||
=> await SendOkAsync(await handler.HandleAsync(request, ct), ct);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace KArtSell.Modules.__MODULE__.Features.__FEATURE__;
|
||||
public interface IHandler { Task<Response> HandleAsync(Request request, CancellationToken ct); }
|
||||
public sealed class Handler : IHandler
|
||||
{
|
||||
public Task<Response> HandleAsync(Request request, CancellationToken ct)
|
||||
=> throw new NotImplementedException("Load approved server-side PIT context, execute a pure policy, persist decision and Outbox in one transaction.");
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
# __SLICE_ID__ __MODULE__.__FEATURE__
|
||||
Required before implementation: Requirement/Policy/Data/API/DB/Job/UI/Test IDs, PIT cutoff, state transition, idempotency key, transaction boundary, Outbox event, Golden/boundary/failure/replay tests, metric/alert/runbook/rollback.
|
||||
@@ -0,0 +1,2 @@
|
||||
namespace KArtSell.Modules.__MODULE__.Features.__FEATURE__;
|
||||
public sealed record Request(string IdempotencyKey, string ScopeId, DateTimeOffset AsOf);
|
||||
@@ -0,0 +1,2 @@
|
||||
namespace KArtSell.Modules.__MODULE__.Features.__FEATURE__;
|
||||
public sealed record Response(Guid Id, string Status, string EvidenceHash, DateTimeOffset AsOf);
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace KArtSell.Modules.__MODULE__.Features.__FEATURE__;
|
||||
internal static class Sql
|
||||
{
|
||||
public const string LoadContext = """select /* explicit columns */ 1 where @as_of is not null;""";
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using FluentValidation;
|
||||
namespace KArtSell.Modules.__MODULE__.Features.__FEATURE__;
|
||||
public sealed class Validator : AbstractValidator<Request>
|
||||
{
|
||||
public Validator() { RuleFor(x => x.IdempotencyKey).NotEmpty().MaximumLength(100); RuleFor(x => x.ScopeId).NotEmpty(); RuleFor(x => x.AsOf).NotEmpty(); }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { api } from '@/shared/api/client'
|
||||
import { responseSchema, type RequestDto, type ResponseDto } from './schema'
|
||||
export async function execute(request: RequestDto): Promise<ResponseDto> {
|
||||
const response = await api.post('/api/v1/__MODULE__/__FEATURE__', request, { headers: { 'Idempotency-Key': request.idempotencyKey } })
|
||||
return responseSchema.parse(response.data)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<script setup lang="ts">import { CrudDetailPage } from '@/shared/ui/screen-types'</script>
|
||||
<template><CrudDetailPage title="__FEATURE__" status="IMPLEMENTATION_REQUIRED"><p>__SLICE_ID__ Slice packet. Replace this placeholder only after the Slice Ready Gate is approved.</p></CrudDetailPage></template>
|
||||
@@ -0,0 +1,4 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||
import { execute } from './api'
|
||||
export const queryKeys = { root: ['__MODULE__','__FEATURE__'] as const }
|
||||
export function useExecuteMutation() { const client=useQueryClient(); return useMutation({ mutationFn: execute, onSuccess: async()=>client.invalidateQueries({queryKey:queryKeys.root}) }) }
|
||||
@@ -0,0 +1,5 @@
|
||||
import { z } from 'zod'
|
||||
export const requestSchema = z.object({ idempotencyKey: z.string().min(1).max(100), scopeId: z.string().min(1), asOf: z.string().datetime() })
|
||||
export const responseSchema = z.object({ id: z.string().uuid(), status: z.string(), evidenceHash: z.string().min(16), asOf: z.string().datetime() })
|
||||
export type RequestDto = z.infer<typeof requestSchema>
|
||||
export type ResponseDto = z.infer<typeof responseSchema>
|
||||
@@ -0,0 +1,3 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { requestSchema } from '../schema'
|
||||
describe('__SLICE_ID__ request contract',()=>{it('rejects missing idempotency evidence',()=>{expect(requestSchema.safeParse({scopeId:'x',asOf:new Date().toISOString()}).success).toBe(false)})})
|
||||
@@ -0,0 +1,8 @@
|
||||
import { api } from '@/shared/api/client'
|
||||
import { requestSchema, responseSchema, type Request } from './schema'
|
||||
|
||||
export async function execute(request: Request) {
|
||||
const body = requestSchema.parse(request)
|
||||
const response = await api.post('/__ROUTE__', body)
|
||||
return responseSchema.parse(response.data)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
// Server state belongs to TanStack Query. Pinia is limited to session/UI preferences.
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<h1>__FEATURE__</h1>
|
||||
<p>Contract-first scaffold. Complete Zod request/response schemas before enabling the route.</p>
|
||||
</main>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { useMutation, useQuery } from '@tanstack/vue-query'
|
||||
import { execute } from './api'
|
||||
|
||||
export const featureKeys = { all: ['__FEATURE__'] as const }
|
||||
export function useExecute() { return useMutation({ mutationFn: execute }) }
|
||||
@@ -0,0 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { responseSchema } from './schema'
|
||||
|
||||
describe('__FEATURE__ response contract', () => {
|
||||
it('fails closed for an unapproved empty payload', () => {
|
||||
expect(responseSchema.safeParse({}).success).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const requestSchema = z.object({ /* approved contract */ })
|
||||
export const responseSchema = z.object({ /* runtime trust boundary */ })
|
||||
export type Request = z.infer<typeof requestSchema>
|
||||
export type Response = z.infer<typeof responseSchema>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { DetailReadPage, EvidenceVersionSet } from '@/shared/ui'
|
||||
const versionSet = { datasetId: 'DECISION_REQUIRED', dataHash: 'DECISION_REQUIRED', modelVersion: 'DECISION_REQUIRED', configVersion: 'DECISION_REQUIRED', codeSha: 'DECISION_REQUIRED', contractVersion: '__CONTRACT_VERSION__' }
|
||||
</script>
|
||||
<template>
|
||||
<DetailReadPage title="__TITLE__" state="READY" :evidence="{ version: '__CONTRACT_VERSION__' }">
|
||||
<section class="ks-card ks-section">상세 내용</section>
|
||||
<template #evidence><EvidenceVersionSet :value="versionSet" /></template>
|
||||
</DetailReadPage>
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { EditFormPage } from '@/shared/ui/screen-types'
|
||||
import { KsButton, KsTextField } from '@/shared/ui/components'
|
||||
</script>
|
||||
<template>
|
||||
<EditFormPage title="__TITLE__" state="READY" :evidence="{ version: '__CONTRACT_VERSION__' }" @submit="undefined">
|
||||
<KsTextField model-value="" label="이름" />
|
||||
<template #footer><KsButton label="저장" type="submit" /></template>
|
||||
</EditFormPage>
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { SearchListCrudPage } from '@/shared/ui/screen-types'
|
||||
import { KsButton, KsDataGrid } from '@/shared/ui/components'
|
||||
</script>
|
||||
<template>
|
||||
<SearchListCrudPage title="__TITLE__" state="LOADING" :evidence="{ version: '__CONTRACT_VERSION__' }">
|
||||
<template #actions><KsButton label="등록" /></template>
|
||||
<KsDataGrid :rows="[]" :columns="[]" />
|
||||
</SearchListCrudPage>
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ApprovalWorkbenchPage } from '@/shared/ui/screen-types'
|
||||
</script>
|
||||
<template>
|
||||
<ApprovalWorkbenchPage title="__TITLE__" state="READY" :evidence="{ version: '__CONTRACT_VERSION__' }">
|
||||
<template #queue>검토 대기열</template>
|
||||
<template #detail>증거와 상세</template>
|
||||
<template #decision>승인/보류/기각</template>
|
||||
</ApprovalWorkbenchPage>
|
||||
</template>
|
||||
Reference in New Issue
Block a user