create-rigel 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/cli.js +122 -0
- package/package.json +47 -0
- package/templates/express/.claude/CLAUDE.md +114 -0
- package/templates/express/.claude/agents/arch-validator.md +85 -0
- package/templates/express/.claude/agents/db-optimizer.md +72 -0
- package/templates/express/.claude/agents/doc-gardener.md +67 -0
- package/templates/express/.claude/agents/garbage-collector.md +94 -0
- package/templates/express/.claude/agents/gate-checker.md +176 -0
- package/templates/express/.claude/agents/reviewer.md +84 -0
- package/templates/express/.claude/agents/security-auditor.md +82 -0
- package/templates/express/.claude/hooks/post-write.sh +93 -0
- package/templates/express/.claude/rules/api.md +149 -0
- package/templates/express/.claude/rules/architecture.md +78 -0
- package/templates/express/.claude/rules/database.md +104 -0
- package/templates/express/.claude/rules/jobs.md +81 -0
- package/templates/express/.claude/rules/security.md +92 -0
- package/templates/express/.claude/rules/testing.md +97 -0
- package/templates/express/.claude/settings.json +35 -0
- package/templates/express/.claude/skills/00-infra-setup/SKILL.md +479 -0
- package/templates/express/.claude/skills/01-write-roadmap/SKILL.md +72 -0
- package/templates/express/.claude/skills/02-write-spec/SKILL.md +119 -0
- package/templates/express/.claude/skills/03-write-plan/SKILL.md +124 -0
- package/templates/express/.claude/skills/04-build-layer/SKILL.md +216 -0
- package/templates/express/.claude/skills/05-validate-layer/SKILL.md +43 -0
- package/templates/express/.claude/skills/06-push-layer/SKILL.md +59 -0
- package/templates/express/.claude/skills/07-layer-check/SKILL.md +62 -0
- package/templates/express/.claude/skills/08-garbage-collect/SKILL.md +86 -0
- package/templates/express/.claude/skills/09-doc-garden/SKILL.md +64 -0
- package/templates/express/.claude/skills/10-db-optimize/SKILL.md +96 -0
- package/templates/express/.claude/skills/11-load-test/SKILL.md +77 -0
- package/templates/express/.dockerignore +43 -0
- package/templates/express/.env.example +42 -0
- package/templates/express/.github/CODEOWNERS +22 -0
- package/templates/express/.github/dependabot.yml +48 -0
- package/templates/express/.github/pull_request_template.md +58 -0
- package/templates/express/.github/workflows/ci.yml +278 -0
- package/templates/express/.github/workflows/load-test.yml +41 -0
- package/templates/express/.gitleaks.toml +18 -0
- package/templates/express/.lintstagedrc.json +3 -0
- package/templates/express/.nvmrc +1 -0
- package/templates/express/.prettierrc +8 -0
- package/templates/express/.sequelizerc +8 -0
- package/templates/express/AGENTS.md +94 -0
- package/templates/express/ARCHITECTURE.md +161 -0
- package/templates/express/Dockerfile +40 -0
- package/templates/express/Makefile +79 -0
- package/templates/express/QUICKSTART.md +131 -0
- package/templates/express/docker-compose.yml +114 -0
- package/templates/express/docs/PLANS.md +111 -0
- package/templates/express/docs/QUALITY_SCORE.md +66 -0
- package/templates/express/docs/design-docs/core-beliefs.md +89 -0
- package/templates/express/docs/design-docs/decisions/ADR-000-infrastructure.md +70 -0
- package/templates/express/docs/design-docs/decisions/ADR-001-observability.md +82 -0
- package/templates/express/docs/design-docs/decisions/index.md +23 -0
- package/templates/express/docs/design-docs/index.md +11 -0
- package/templates/express/docs/design-docs/observability.md +194 -0
- package/templates/express/docs/design-docs/team-workflow.md +64 -0
- package/templates/express/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/express/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/express/docs/exec-plans/tech-debt-tracker.md +31 -0
- package/templates/express/docs/generated/.gitkeep +0 -0
- package/templates/express/docs/product-specs/ROADMAP.md +49 -0
- package/templates/express/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/express/docs/product-specs/index.md +21 -0
- package/templates/express/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/express/eslint.config.mjs +94 -0
- package/templates/express/gitignore +36 -0
- package/templates/express/infra/monitoring/alloy/config.alloy +59 -0
- package/templates/express/jest.config.ts +61 -0
- package/templates/express/package.json +45 -0
- package/templates/express/scripts/openapi.export.ts +77 -0
- package/templates/express/tests/architecture/isolation.test.ts +51 -0
- package/templates/express/tests/architecture/layers.test.ts +83 -0
- package/templates/express/tests/integration/health.test.ts +40 -0
- package/templates/express/tests/integration/isolation.test.template.ts +88 -0
- package/templates/express/tests/load/smoke.js +46 -0
- package/templates/express/tests/load/soak.js +45 -0
- package/templates/express/tests/load/stress.js +44 -0
- package/templates/express/tests/unit/utils/response.util.test.ts +53 -0
- package/templates/express/tsconfig.build.json +8 -0
- package/templates/express/tsconfig.json +20 -0
- package/templates/fastapi/.claude/CLAUDE.md +122 -0
- package/templates/fastapi/.claude/agents/arch-validator.md +64 -0
- package/templates/fastapi/.claude/agents/db-optimizer.md +59 -0
- package/templates/fastapi/.claude/agents/doc-gardener.md +55 -0
- package/templates/fastapi/.claude/agents/garbage-collector.md +74 -0
- package/templates/fastapi/.claude/agents/gate-checker.md +133 -0
- package/templates/fastapi/.claude/agents/reviewer.md +74 -0
- package/templates/fastapi/.claude/agents/security-auditor.md +69 -0
- package/templates/fastapi/.claude/hooks/post-write.sh +65 -0
- package/templates/fastapi/.claude/rules/api.md +119 -0
- package/templates/fastapi/.claude/rules/architecture.md +84 -0
- package/templates/fastapi/.claude/rules/database.md +153 -0
- package/templates/fastapi/.claude/rules/jobs.md +85 -0
- package/templates/fastapi/.claude/rules/security.md +102 -0
- package/templates/fastapi/.claude/rules/testing.md +207 -0
- package/templates/fastapi/.claude/settings.json +50 -0
- package/templates/fastapi/.claude/skills/00-infra-setup/SKILL.md +311 -0
- package/templates/fastapi/.claude/skills/01-write-roadmap/SKILL.md +61 -0
- package/templates/fastapi/.claude/skills/02-write-spec/SKILL.md +82 -0
- package/templates/fastapi/.claude/skills/03-write-plan/SKILL.md +83 -0
- package/templates/fastapi/.claude/skills/04-build-layer/SKILL.md +170 -0
- package/templates/fastapi/.claude/skills/05-validate-layer/SKILL.md +20 -0
- package/templates/fastapi/.claude/skills/06-push-layer/SKILL.md +31 -0
- package/templates/fastapi/.claude/skills/07-layer-check/SKILL.md +51 -0
- package/templates/fastapi/.claude/skills/08-garbage-collect/SKILL.md +47 -0
- package/templates/fastapi/.claude/skills/09-doc-garden/SKILL.md +9 -0
- package/templates/fastapi/.claude/skills/10-db-optimize/SKILL.md +53 -0
- package/templates/fastapi/.claude/skills/11-load-test/SKILL.md +81 -0
- package/templates/fastapi/.dockerignore +30 -0
- package/templates/fastapi/.github/dependabot.yml +24 -0
- package/templates/fastapi/AGENTS.md +87 -0
- package/templates/fastapi/ARCHITECTURE.md +141 -0
- package/templates/fastapi/Makefile +42 -0
- package/templates/fastapi/README.md +179 -0
- package/templates/fastapi/docs/PLANS.md +26 -0
- package/templates/fastapi/docs/QUALITY_SCORE.md +26 -0
- package/templates/fastapi/docs/design-docs/core-beliefs.md +46 -0
- package/templates/fastapi/docs/design-docs/decisions/ADR-000-infrastructure.md +79 -0
- package/templates/fastapi/docs/design-docs/decisions/index.md +7 -0
- package/templates/fastapi/docs/design-docs/index.md +9 -0
- package/templates/fastapi/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/fastapi/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/fastapi/docs/exec-plans/tech-debt-tracker.md +19 -0
- package/templates/fastapi/docs/generated/.gitkeep +0 -0
- package/templates/fastapi/docs/product-specs/ROADMAP.md +49 -0
- package/templates/fastapi/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/fastapi/docs/product-specs/index.md +18 -0
- package/templates/fastapi/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/fastapi/gitignore +46 -0
- package/templates/fastapi/scripts/gate.sh +68 -0
- package/templates/nestjs/.claude/CLAUDE.md +121 -0
- package/templates/nestjs/.claude/agents/db-optimizer.md +51 -0
- package/templates/nestjs/.claude/agents/doc-gardener.md +45 -0
- package/templates/nestjs/.claude/agents/garbage-collector.md +50 -0
- package/templates/nestjs/.claude/agents/gate-checker.md +127 -0
- package/templates/nestjs/.claude/agents/reviewer.md +68 -0
- package/templates/nestjs/.claude/agents/security-auditor.md +72 -0
- package/templates/nestjs/.claude/hooks/post-write.sh +91 -0
- package/templates/nestjs/.claude/rules/api.md +87 -0
- package/templates/nestjs/.claude/rules/architecture.md +104 -0
- package/templates/nestjs/.claude/rules/database.md +111 -0
- package/templates/nestjs/.claude/rules/security.md +110 -0
- package/templates/nestjs/.claude/rules/testing.md +145 -0
- package/templates/nestjs/.claude/settings.json +33 -0
- package/templates/nestjs/.claude/skills/00-infra-setup/SKILL.md +222 -0
- package/templates/nestjs/.claude/skills/01-write-spec/SKILL.md +25 -0
- package/templates/nestjs/.claude/skills/02-write-plan/SKILL.md +18 -0
- package/templates/nestjs/.claude/skills/03-build-layer/SKILL.md +242 -0
- package/templates/nestjs/.claude/skills/04-validate-layer/SKILL.md +4 -0
- package/templates/nestjs/.claude/skills/05-push-layer/SKILL.md +6 -0
- package/templates/nestjs/.claude/skills/06-layer-check/SKILL.md +12 -0
- package/templates/nestjs/.claude/skills/07-garbage-collect/SKILL.md +2 -0
- package/templates/nestjs/.claude/skills/08-doc-garden/SKILL.md +4 -0
- package/templates/nestjs/.claude/skills/09-api-sync/SKILL.md +36 -0
- package/templates/nestjs/.claude/skills/10-load-test/SKILL.md +29 -0
- package/templates/nestjs/.env.example +11 -0
- package/templates/nestjs/AGENTS.md +88 -0
- package/templates/nestjs/ARCHITECTURE.md +192 -0
- package/templates/nestjs/docs/PLANS.md +22 -0
- package/templates/nestjs/docs/QUALITY_SCORE.md +27 -0
- package/templates/nestjs/docs/design-docs/core-beliefs.md +18 -0
- package/templates/nestjs/docs/design-docs/decisions/ADR-000-infrastructure.md +58 -0
- package/templates/nestjs/docs/design-docs/decisions/index.md +7 -0
- package/templates/nestjs/docs/design-docs/index.md +7 -0
- package/templates/nestjs/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/nestjs/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/nestjs/docs/exec-plans/tech-debt-tracker.md +16 -0
- package/templates/nestjs/docs/generated/.gitkeep +0 -0
- package/templates/nestjs/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/nestjs/docs/product-specs/index.md +7 -0
- package/templates/nestjs/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/nestjs/gitignore +8 -0
- package/templates/nextjs/.claude/CLAUDE.md +139 -0
- package/templates/nextjs/.claude/agents/arch-validator.md +66 -0
- package/templates/nextjs/.claude/agents/contract-checker.md +78 -0
- package/templates/nextjs/.claude/agents/doc-gardener.md +52 -0
- package/templates/nextjs/.claude/agents/garbage-collector.md +61 -0
- package/templates/nextjs/.claude/agents/gate-checker.md +129 -0
- package/templates/nextjs/.claude/agents/perf-auditor.md +109 -0
- package/templates/nextjs/.claude/agents/reviewer.md +79 -0
- package/templates/nextjs/.claude/agents/security-auditor.md +50 -0
- package/templates/nextjs/.claude/hooks/post-write.sh +121 -0
- package/templates/nextjs/.claude/rules/api-contract.md +166 -0
- package/templates/nextjs/.claude/rules/architecture.md +99 -0
- package/templates/nextjs/.claude/rules/components.md +122 -0
- package/templates/nextjs/.claude/rules/security.md +122 -0
- package/templates/nextjs/.claude/rules/testing.md +187 -0
- package/templates/nextjs/.claude/scripts/infra-setup.sh +550 -0
- package/templates/nextjs/.claude/settings.json +33 -0
- package/templates/nextjs/.claude/skills/00-infra-setup/SKILL.md +471 -0
- package/templates/nextjs/.claude/skills/01-write-roadmap/SKILL.md +77 -0
- package/templates/nextjs/.claude/skills/02-write-spec/SKILL.md +78 -0
- package/templates/nextjs/.claude/skills/03-write-plan/SKILL.md +91 -0
- package/templates/nextjs/.claude/skills/04-build-layer/SKILL.md +141 -0
- package/templates/nextjs/.claude/skills/05-validate-layer/SKILL.md +13 -0
- package/templates/nextjs/.claude/skills/06-push-layer/SKILL.md +20 -0
- package/templates/nextjs/.claude/skills/07-layer-check/SKILL.md +28 -0
- package/templates/nextjs/.claude/skills/08-garbage-collect/SKILL.md +14 -0
- package/templates/nextjs/.claude/skills/09-doc-garden/SKILL.md +12 -0
- package/templates/nextjs/.claude/skills/10-api-sync/SKILL.md +50 -0
- package/templates/nextjs/.claude/skills/11-perf-budget/SKILL.md +51 -0
- package/templates/nextjs/.dockerignore +25 -0
- package/templates/nextjs/.gitattributes +10 -0
- package/templates/nextjs/.github/CODEOWNERS +24 -0
- package/templates/nextjs/.github/dependabot.yml +31 -0
- package/templates/nextjs/.github/pull_request_template.md +59 -0
- package/templates/nextjs/.github/workflows/ci.yml +143 -0
- package/templates/nextjs/.github/workflows/lighthouse.yml +54 -0
- package/templates/nextjs/.github/workflows/load-test.yml +54 -0
- package/templates/nextjs/.husky/pre-commit +1 -0
- package/templates/nextjs/.lintstagedrc.json +4 -0
- package/templates/nextjs/.mcp.json +8 -0
- package/templates/nextjs/.prettierignore +13 -0
- package/templates/nextjs/.prettierrc +9 -0
- package/templates/nextjs/AGENTS.md +88 -0
- package/templates/nextjs/ARCHITECTURE.md +165 -0
- package/templates/nextjs/Dockerfile +38 -0
- package/templates/nextjs/Makefile +50 -0
- package/templates/nextjs/QUICKSTART.md +128 -0
- package/templates/nextjs/docs/PLANS.md +23 -0
- package/templates/nextjs/docs/QUALITY_SCORE.md +27 -0
- package/templates/nextjs/docs/design-docs/core-beliefs.md +43 -0
- package/templates/nextjs/docs/design-docs/decisions/ADR-000-infrastructure.md +72 -0
- package/templates/nextjs/docs/design-docs/decisions/index.md +7 -0
- package/templates/nextjs/docs/design-docs/index.md +7 -0
- package/templates/nextjs/docs/design-docs/team-workflow.md +66 -0
- package/templates/nextjs/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/nextjs/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/nextjs/docs/exec-plans/tech-debt-tracker.md +19 -0
- package/templates/nextjs/docs/generated/.gitkeep +0 -0
- package/templates/nextjs/docs/product-specs/ROADMAP.md +59 -0
- package/templates/nextjs/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/nextjs/docs/product-specs/index.md +18 -0
- package/templates/nextjs/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/nextjs/eslint.config.mjs +150 -0
- package/templates/nextjs/gitignore +35 -0
- package/templates/nextjs/lighthouserc.js +37 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI exporter — the single machine-readable contract for the harness family.
|
|
3
|
+
*
|
|
4
|
+
* The whole harness shares one response envelope, one error-code enum, and one
|
|
5
|
+
* cursor format (see .claude/rules/api.md). This script turns the Zod schemas +
|
|
6
|
+
* route registrations into an OpenAPI 3.1 document that the Next.js frontend
|
|
7
|
+
* consumes via `openapi-fetch` — so the contract is generated, never hand-synced
|
|
8
|
+
* across backend and frontend.
|
|
9
|
+
*
|
|
10
|
+
* How it works:
|
|
11
|
+
* - Routes/schemas register themselves into an OpenAPIRegistry exported from
|
|
12
|
+
* `src/runtime/openapi.ts` (created by /infra-setup; products add paths there).
|
|
13
|
+
* - This script imports that registry if it exists, generates the document, and
|
|
14
|
+
* writes docs/generated/openapi.json + openapi.yaml.
|
|
15
|
+
* - If the registry is absent (fresh template, pre-/infra-setup) it emits a
|
|
16
|
+
* minimal-but-valid base document so the pipeline + CI drift-check still work.
|
|
17
|
+
*
|
|
18
|
+
* Run: `npm run openapi:export` (CI runs it and fails if the committed doc drifts).
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
21
|
+
import { dirname, resolve } from 'node:path'
|
|
22
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
23
|
+
import { OpenApiGeneratorV31, OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'
|
|
24
|
+
import { stringify as yamlStringify } from 'yaml'
|
|
25
|
+
|
|
26
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
27
|
+
const repoRoot = resolve(here, '..')
|
|
28
|
+
const OUT_DIR = resolve(repoRoot, 'docs/generated')
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Pull the product's populated registry from src/runtime/openapi.ts when present.
|
|
32
|
+
* Falls back to an empty registry so the template (no src/ yet) still produces a
|
|
33
|
+
* valid base document.
|
|
34
|
+
*/
|
|
35
|
+
async function loadRegistry(): Promise<OpenAPIRegistry> {
|
|
36
|
+
const candidates = [
|
|
37
|
+
resolve(repoRoot, 'dist/runtime/openapi.js'),
|
|
38
|
+
resolve(repoRoot, 'src/runtime/openapi.ts'),
|
|
39
|
+
]
|
|
40
|
+
for (const file of candidates) {
|
|
41
|
+
if (!existsSync(file)) continue
|
|
42
|
+
const mod = (await import(pathToFileURL(file).href)) as { registry?: OpenAPIRegistry }
|
|
43
|
+
if (mod.registry) return mod.registry
|
|
44
|
+
}
|
|
45
|
+
return new OpenAPIRegistry()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function main(): Promise<void> {
|
|
49
|
+
const registry = await loadRegistry()
|
|
50
|
+
const generator = new OpenApiGeneratorV31(registry.definitions)
|
|
51
|
+
|
|
52
|
+
const document = generator.generateDocument({
|
|
53
|
+
openapi: '3.1.0',
|
|
54
|
+
info: {
|
|
55
|
+
title: process.env.SERVICE_NAME ?? 'harness-service',
|
|
56
|
+
version: process.env.APP_VERSION ?? '0.0.1',
|
|
57
|
+
description:
|
|
58
|
+
'Generated contract. Canonical response envelope + error-code enum are identical across the harness family — see .claude/rules/api.md.',
|
|
59
|
+
},
|
|
60
|
+
servers: [{ url: '/api/v1' }],
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
mkdirSync(OUT_DIR, { recursive: true })
|
|
64
|
+
const json = `${JSON.stringify(document, null, 2)}\n`
|
|
65
|
+
writeFileSync(resolve(OUT_DIR, 'openapi.json'), json)
|
|
66
|
+
writeFileSync(resolve(OUT_DIR, 'openapi.yaml'), yamlStringify(document))
|
|
67
|
+
|
|
68
|
+
const pathCount = Object.keys(document.paths ?? {}).length
|
|
69
|
+
// eslint-disable-next-line no-console -- this is a build script, not app code
|
|
70
|
+
console.log(`openapi: wrote ${pathCount} path(s) to docs/generated/openapi.{json,yaml}`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
main().catch((err: unknown) => {
|
|
74
|
+
// eslint-disable-next-line no-console -- build script
|
|
75
|
+
console.error('openapi export failed:', err)
|
|
76
|
+
process.exit(1)
|
|
77
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-user isolation contract — mechanical enforcement of the invariant in
|
|
3
|
+
* ARCHITECTURE.md (Repo layer): a resource owned by user A must be invisible to
|
|
4
|
+
* user B (the API responds 404, never 403).
|
|
5
|
+
*
|
|
6
|
+
* For every repo that enforces ownership (a userId-scoped query), an isolation
|
|
7
|
+
* integration test MUST exist at `tests/integration/<resource>.isolation.test.ts`.
|
|
8
|
+
* This replaces the old "copy isolation.test.template.ts by hand" honor system:
|
|
9
|
+
* the suite fails CI if an owned resource ships without its isolation test.
|
|
10
|
+
*
|
|
11
|
+
* Fresh repos (no src/repo yet) pass; the check tightens as repos appear.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
|
|
16
|
+
const REPO_DIR = 'src/repo'
|
|
17
|
+
const INTEGRATION_DIR = 'tests/integration'
|
|
18
|
+
|
|
19
|
+
// A repo enforces ownership if it scopes queries by the owning user — the harness
|
|
20
|
+
// pattern is `findByIdAndUser` / `where: { ..., userId }`.
|
|
21
|
+
const OWNERSHIP_RE = /findByIdAndUser|userId|user_id/
|
|
22
|
+
|
|
23
|
+
/** Resource base names (e.g. "application") for repos that enforce ownership. */
|
|
24
|
+
function ownedResources(): string[] {
|
|
25
|
+
if (!existsSync(REPO_DIR)) return []
|
|
26
|
+
const out: string[] = []
|
|
27
|
+
for (const file of readdirSync(REPO_DIR)) {
|
|
28
|
+
if (!file.endsWith('.repo.ts')) continue
|
|
29
|
+
const src = readFileSync(join(REPO_DIR, file), 'utf8')
|
|
30
|
+
if (OWNERSHIP_RE.test(src)) out.push(file.replace(/\.repo\.ts$/, ''))
|
|
31
|
+
}
|
|
32
|
+
return out
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('architecture: cross-user isolation contract', () => {
|
|
36
|
+
const resources = ownedResources()
|
|
37
|
+
|
|
38
|
+
if (resources.length === 0) {
|
|
39
|
+
it('no owned resources yet — isolation contract not applicable', () => {
|
|
40
|
+
expect(resources).toEqual([])
|
|
41
|
+
})
|
|
42
|
+
} else {
|
|
43
|
+
it.each(resources)(
|
|
44
|
+
'owned resource "%s" must ship its tests/integration/<resource>.isolation.test.ts',
|
|
45
|
+
(resource) => {
|
|
46
|
+
const expected = join(INTEGRATION_DIR, `${resource}.isolation.test.ts`)
|
|
47
|
+
expect(existsSync(expected)).toBe(true)
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Architecture structural tests — enforce the layer import boundaries from
|
|
3
|
+
* ARCHITECTURE.md mechanically. These run in CI and in the gate-checker.
|
|
4
|
+
*
|
|
5
|
+
* Circular-import detection is handled separately by `npx madge --circular`
|
|
6
|
+
* (see .claude/agents/gate-checker.md). This file enforces the *direction* of
|
|
7
|
+
* allowed imports: a lower layer must never import from a higher one.
|
|
8
|
+
*
|
|
9
|
+
* Layers that do not exist yet (fresh repo, mid-build) are skipped, so this
|
|
10
|
+
* suite passes immediately after /infra-setup and tightens as layers appear.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
|
|
15
|
+
const SRC = 'src'
|
|
16
|
+
|
|
17
|
+
/** Recursively collect every .ts file under a directory. */
|
|
18
|
+
function tsFiles(dir: string): string[] {
|
|
19
|
+
if (!existsSync(dir)) return []
|
|
20
|
+
const out: string[] = []
|
|
21
|
+
for (const entry of readdirSync(dir)) {
|
|
22
|
+
const full = join(dir, entry)
|
|
23
|
+
if (statSync(full).isDirectory()) out.push(...tsFiles(full))
|
|
24
|
+
else if (entry.endsWith('.ts')) out.push(full)
|
|
25
|
+
}
|
|
26
|
+
return out
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Extract the specifier from every `import ... from '...'` / `require('...')`. */
|
|
30
|
+
function importSpecifiers(file: string): string[] {
|
|
31
|
+
const src = readFileSync(file, 'utf8')
|
|
32
|
+
const specs: string[] = []
|
|
33
|
+
const re = /(?:from\s+|require\(\s*)['"]([^'"]+)['"]/g
|
|
34
|
+
let m: RegExpExecArray | null
|
|
35
|
+
while ((m = re.exec(src)) !== null) { if (m[1] !== undefined) specs.push(m[1]) }
|
|
36
|
+
return specs
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** True if any file in `layerDir` imports something matching a forbidden layer. */
|
|
40
|
+
function findViolations(layerDir: string, forbidden: string[]): string[] {
|
|
41
|
+
const violations: string[] = []
|
|
42
|
+
for (const file of tsFiles(join(SRC, layerDir))) {
|
|
43
|
+
for (const spec of importSpecifiers(file)) {
|
|
44
|
+
for (const bad of forbidden) {
|
|
45
|
+
if (spec.includes(`/${bad}/`) || spec.includes(`/${bad}`) || spec === bad) {
|
|
46
|
+
violations.push(`${file} imports forbidden layer "${bad}" via "${spec}"`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return violations
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('architecture: layer import boundaries', () => {
|
|
55
|
+
it('types imports nothing from other layers', () => {
|
|
56
|
+
const v = findViolations('types', ['config', 'models', 'repo', 'services', 'runtime', 'providers', 'utils'])
|
|
57
|
+
expect(v).toEqual([])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('config imports only from types', () => {
|
|
61
|
+
const v = findViolations('config', ['models', 'repo', 'services', 'runtime', 'providers'])
|
|
62
|
+
expect(v).toEqual([])
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('repo does not import from service or runtime', () => {
|
|
66
|
+
const v = findViolations('repo', ['services', 'runtime'])
|
|
67
|
+
expect(v).toEqual([])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('service does not import express, runtime, or providers', () => {
|
|
71
|
+
const v = findViolations('services', ['runtime', 'providers'])
|
|
72
|
+
const expressImports = tsFiles(join(SRC, 'services')).filter((f) =>
|
|
73
|
+
importSpecifiers(f).some((s) => s === 'express')
|
|
74
|
+
)
|
|
75
|
+
expect(v).toEqual([])
|
|
76
|
+
expect(expressImports).toEqual([])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('utils has zero domain imports', () => {
|
|
80
|
+
const v = findViolations('utils', ['config', 'models', 'repo', 'services', 'runtime', 'providers'])
|
|
81
|
+
expect(v).toEqual([])
|
|
82
|
+
})
|
|
83
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EXAMPLE integration test — the model for runtime/route tests.
|
|
3
|
+
*
|
|
4
|
+
* Activates once /infra-setup generates `src/runtime/app.ts` with the health
|
|
5
|
+
* routes. Demonstrates the supertest HTTP-contract pattern: hit the real
|
|
6
|
+
* Express app, assert status + the canonical response envelope.
|
|
7
|
+
*
|
|
8
|
+
* `/health` — liveness: process is up (always 200, no dependencies)
|
|
9
|
+
* `/ready` — readiness: DB ping + Redis ping (200 when both reachable)
|
|
10
|
+
*/
|
|
11
|
+
import request from 'supertest'
|
|
12
|
+
import { app } from '../../src/runtime/app.js'
|
|
13
|
+
|
|
14
|
+
describe('GET /health', () => {
|
|
15
|
+
it('returns 200 (liveness — no dependencies)', async () => {
|
|
16
|
+
const res = await request(app).get('/health')
|
|
17
|
+
expect(res.status).toBe(200)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('returns the canonical success envelope', async () => {
|
|
21
|
+
const res = await request(app).get('/health')
|
|
22
|
+
expect(res.body.ok).toBe(true)
|
|
23
|
+
expect(res.body).toHaveProperty('data')
|
|
24
|
+
expect(res.body).toHaveProperty('meta.requestId')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('propagates the X-Request-ID header', async () => {
|
|
28
|
+
const res = await request(app).get('/health').set('X-Request-ID', 'test-req-id')
|
|
29
|
+
expect(res.body.meta.requestId).toBe('test-req-id')
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('GET /ready', () => {
|
|
34
|
+
it('returns 200 when dependencies are reachable', async () => {
|
|
35
|
+
const res = await request(app).get('/ready')
|
|
36
|
+
// 200 healthy, 503 when a dependency is down — both are valid contract responses
|
|
37
|
+
expect([200, 503]).toContain(res.status)
|
|
38
|
+
expect(res.body).toHaveProperty('ok')
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEMPLATE — Cross-User Isolation Test (the most important security test).
|
|
3
|
+
*
|
|
4
|
+
* This file is named `*.template.ts` so Jest does NOT run it (the matcher is
|
|
5
|
+
* `*.test.ts`). For every user-owned resource, COPY this to
|
|
6
|
+
* `tests/integration/<resource>.isolation.test.ts` (where <resource> matches the
|
|
7
|
+
* repo base name, e.g. `application.repo.ts` → `application.isolation.test.ts`),
|
|
8
|
+
* replace the placeholders, and make it pass.
|
|
9
|
+
*
|
|
10
|
+
* This is ENFORCED, not optional: `tests/architecture/isolation.test.ts` fails CI
|
|
11
|
+
* for any repo that scopes queries by userId but has no matching isolation test.
|
|
12
|
+
*
|
|
13
|
+
* The invariant (ARCHITECTURE.md, Repo layer):
|
|
14
|
+
* A resource owned by user A must be invisible to user B. When B requests A's
|
|
15
|
+
* resource, the API responds 404 — NEVER 403. A 403 confirms the resource
|
|
16
|
+
* exists; a 404 reveals nothing.
|
|
17
|
+
*
|
|
18
|
+
* Replace:
|
|
19
|
+
* - `/api/v1/applications` → your resource route
|
|
20
|
+
* - `createApplication(...)` → your create helper / factory
|
|
21
|
+
* - `validBody` → a valid create payload
|
|
22
|
+
* - `authTokenFor(userId)` → your test auth-token helper
|
|
23
|
+
*/
|
|
24
|
+
import request from 'supertest'
|
|
25
|
+
import { app } from '../../src/runtime/app.js'
|
|
26
|
+
// import { authTokenFor, createUser, resetDb } from './setup.js'
|
|
27
|
+
|
|
28
|
+
describe('cross-user isolation: applications', () => {
|
|
29
|
+
let userA: string
|
|
30
|
+
let userB: string
|
|
31
|
+
let tokenA: string
|
|
32
|
+
let tokenB: string
|
|
33
|
+
let resourceId: string
|
|
34
|
+
|
|
35
|
+
beforeAll(async () => {
|
|
36
|
+
// await resetDb()
|
|
37
|
+
// userA = (await createUser('a@example.com')).id
|
|
38
|
+
// userB = (await createUser('b@example.com')).id
|
|
39
|
+
// tokenA = authTokenFor(userA)
|
|
40
|
+
// tokenB = authTokenFor(userB)
|
|
41
|
+
|
|
42
|
+
// User A creates a resource
|
|
43
|
+
const created = await request(app)
|
|
44
|
+
.post('/api/v1/applications')
|
|
45
|
+
.set('Authorization', `Bearer ${tokenA}`)
|
|
46
|
+
.send(/* validBody */ {})
|
|
47
|
+
resourceId = created.body.data.id
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("user B cannot READ user A's resource (404, not 403)", async () => {
|
|
51
|
+
const res = await request(app)
|
|
52
|
+
.get(`/api/v1/applications/${resourceId}`)
|
|
53
|
+
.set('Authorization', `Bearer ${tokenB}`)
|
|
54
|
+
expect(res.status).toBe(404)
|
|
55
|
+
expect(res.body.ok).toBe(false)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("user B cannot UPDATE user A's resource (404)", async () => {
|
|
59
|
+
const res = await request(app)
|
|
60
|
+
.patch(`/api/v1/applications/${resourceId}`)
|
|
61
|
+
.set('Authorization', `Bearer ${tokenB}`)
|
|
62
|
+
.send({ note: 'hijack attempt' })
|
|
63
|
+
expect(res.status).toBe(404)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it("user B cannot DELETE user A's resource (404)", async () => {
|
|
67
|
+
const res = await request(app)
|
|
68
|
+
.delete(`/api/v1/applications/${resourceId}`)
|
|
69
|
+
.set('Authorization', `Bearer ${tokenB}`)
|
|
70
|
+
expect(res.status).toBe(404)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("user A's resource never appears in user B's list", async () => {
|
|
74
|
+
const res = await request(app)
|
|
75
|
+
.get('/api/v1/applications')
|
|
76
|
+
.set('Authorization', `Bearer ${tokenB}`)
|
|
77
|
+
const ids = (res.body.data.items as Array<{ id: string }>).map((r) => r.id)
|
|
78
|
+
expect(ids).not.toContain(resourceId)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('user A (the owner) can still read their own resource (200)', async () => {
|
|
82
|
+
const res = await request(app)
|
|
83
|
+
.get(`/api/v1/applications/${resourceId}`)
|
|
84
|
+
.set('Authorization', `Bearer ${tokenA}`)
|
|
85
|
+
expect(res.status).toBe(200)
|
|
86
|
+
expect(res.body.data.id).toBe(resourceId)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* k6 SMOKE test — minimal load, run on every change / before a release.
|
|
3
|
+
* Goal: confirm the service is up and fast under light traffic.
|
|
4
|
+
*
|
|
5
|
+
* k6 run tests/load/smoke.js
|
|
6
|
+
* k6 run --env BASE_URL=https://staging.example.com --env AUTH_TOKEN=xyz tests/load/smoke.js
|
|
7
|
+
*
|
|
8
|
+
* Runs out of the box against /health (no auth, exists after /infra-setup).
|
|
9
|
+
* Copy the commented block to exercise a real authenticated endpoint per feature.
|
|
10
|
+
*
|
|
11
|
+
* Thresholds: P95 < 500ms, error rate < 1%.
|
|
12
|
+
*/
|
|
13
|
+
import http from 'k6/http'
|
|
14
|
+
import { check, sleep } from 'k6'
|
|
15
|
+
|
|
16
|
+
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000'
|
|
17
|
+
const AUTH_TOKEN = __ENV.AUTH_TOKEN || ''
|
|
18
|
+
|
|
19
|
+
export const options = {
|
|
20
|
+
vus: 10,
|
|
21
|
+
duration: '30s',
|
|
22
|
+
thresholds: {
|
|
23
|
+
http_req_duration: ['p(95)<500'],
|
|
24
|
+
http_req_failed: ['rate<0.01'],
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default function () {
|
|
29
|
+
// --- Liveness: always available, no auth ---
|
|
30
|
+
const health = http.get(`${BASE_URL}/health`)
|
|
31
|
+
check(health, {
|
|
32
|
+
'health: status 200': (r) => r.status === 200,
|
|
33
|
+
'health: ok=true': (r) => r.json('ok') === true,
|
|
34
|
+
'health: < 500ms': (r) => r.timings.duration < 500,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// --- Authenticated feature path (copy & adapt per feature) ---
|
|
38
|
+
// const headers = { Authorization: `Bearer ${AUTH_TOKEN}`, 'Content-Type': 'application/json' }
|
|
39
|
+
// const list = http.get(`${BASE_URL}/api/v1/applications`, { headers })
|
|
40
|
+
// check(list, {
|
|
41
|
+
// 'list: status 200': (r) => r.status === 200,
|
|
42
|
+
// 'list: has data': (r) => r.json('data') !== undefined,
|
|
43
|
+
// })
|
|
44
|
+
|
|
45
|
+
sleep(1)
|
|
46
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* k6 SOAK test — sustained moderate load over a long window. Catches memory
|
|
3
|
+
* leaks, connection-pool exhaustion, and slow degradation.
|
|
4
|
+
*
|
|
5
|
+
* k6 run --env BASE_URL=https://staging.example.com tests/load/soak.js
|
|
6
|
+
* k6 run --env DURATION=2h --env BASE_URL=... tests/load/soak.js
|
|
7
|
+
*
|
|
8
|
+
* Default duration is 30m (override with --env DURATION). Runs out of the box
|
|
9
|
+
* against /health. Copy the commented block for real endpoints.
|
|
10
|
+
*
|
|
11
|
+
* Thresholds: P95 < 800ms, error rate < 2% — must hold steady for the whole run.
|
|
12
|
+
*/
|
|
13
|
+
import http from 'k6/http'
|
|
14
|
+
import { check, sleep } from 'k6'
|
|
15
|
+
|
|
16
|
+
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000'
|
|
17
|
+
const AUTH_TOKEN = __ENV.AUTH_TOKEN || ''
|
|
18
|
+
const DURATION = __ENV.DURATION || '30m'
|
|
19
|
+
|
|
20
|
+
export const options = {
|
|
21
|
+
stages: [
|
|
22
|
+
{ duration: '2m', target: 20 }, // ramp to moderate load
|
|
23
|
+
{ duration: DURATION, target: 20 }, // hold steady (the soak)
|
|
24
|
+
{ duration: '2m', target: 0 }, // ramp down
|
|
25
|
+
],
|
|
26
|
+
thresholds: {
|
|
27
|
+
http_req_duration: ['p(95)<800'],
|
|
28
|
+
http_req_failed: ['rate<0.02'],
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default function () {
|
|
33
|
+
const health = http.get(`${BASE_URL}/health`)
|
|
34
|
+
check(health, {
|
|
35
|
+
'health: status 200': (r) => r.status === 200,
|
|
36
|
+
'health: ok=true': (r) => r.json('ok') === true,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// --- Authenticated feature path (copy & adapt per feature) ---
|
|
40
|
+
// const headers = { Authorization: `Bearer ${AUTH_TOKEN}`, 'Content-Type': 'application/json' }
|
|
41
|
+
// const list = http.get(`${BASE_URL}/api/v1/applications`, { headers })
|
|
42
|
+
// check(list, { 'list: status 200': (r) => r.status === 200 })
|
|
43
|
+
|
|
44
|
+
sleep(1)
|
|
45
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* k6 STRESS test — find the breaking point. Run before a release / capacity planning.
|
|
3
|
+
* Ramps virtual users up to 100 and holds, then ramps down.
|
|
4
|
+
*
|
|
5
|
+
* k6 run --env BASE_URL=https://staging.example.com --env AUTH_TOKEN=xyz tests/load/stress.js
|
|
6
|
+
*
|
|
7
|
+
* Runs out of the box against /health. Copy the commented block for real endpoints.
|
|
8
|
+
*
|
|
9
|
+
* Thresholds: P95 < 1000ms, P99 < 2000ms, error rate < 5%.
|
|
10
|
+
*/
|
|
11
|
+
import http from 'k6/http'
|
|
12
|
+
import { check, sleep } from 'k6'
|
|
13
|
+
|
|
14
|
+
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000'
|
|
15
|
+
const AUTH_TOKEN = __ENV.AUTH_TOKEN || ''
|
|
16
|
+
|
|
17
|
+
export const options = {
|
|
18
|
+
stages: [
|
|
19
|
+
{ duration: '1m', target: 25 }, // warm up
|
|
20
|
+
{ duration: '2m', target: 50 }, // ramp to moderate load
|
|
21
|
+
{ duration: '2m', target: 100 }, // ramp to peak
|
|
22
|
+
{ duration: '3m', target: 100 }, // hold at peak
|
|
23
|
+
{ duration: '1m', target: 0 }, // ramp down
|
|
24
|
+
],
|
|
25
|
+
thresholds: {
|
|
26
|
+
http_req_duration: ['p(95)<1000', 'p(99)<2000'],
|
|
27
|
+
http_req_failed: ['rate<0.05'],
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default function () {
|
|
32
|
+
const health = http.get(`${BASE_URL}/health`)
|
|
33
|
+
check(health, {
|
|
34
|
+
'health: status 200': (r) => r.status === 200,
|
|
35
|
+
'health: < 1000ms': (r) => r.timings.duration < 1000,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// --- Authenticated feature path (copy & adapt per feature) ---
|
|
39
|
+
// const headers = { Authorization: `Bearer ${AUTH_TOKEN}`, 'Content-Type': 'application/json' }
|
|
40
|
+
// const list = http.get(`${BASE_URL}/api/v1/applications`, { headers })
|
|
41
|
+
// check(list, { 'list: status 200': (r) => r.status === 200 })
|
|
42
|
+
|
|
43
|
+
sleep(1)
|
|
44
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EXAMPLE unit test — the model for utils-layer tests (100% coverage required).
|
|
3
|
+
*
|
|
4
|
+
* Activates once /infra-setup generates `src/utils/response.util.ts`. It pins
|
|
5
|
+
* the canonical response envelope (see .claude/rules/api.md):
|
|
6
|
+
* ok(data, requestId) → { ok: true, data, meta: { requestId, timestamp } }
|
|
7
|
+
* err(code, message, reqId) → { ok: false, error: { code, message }, meta: { requestId } }
|
|
8
|
+
*
|
|
9
|
+
* Utils tests must cover every branch and edge case — no exceptions.
|
|
10
|
+
*/
|
|
11
|
+
import { ok, err } from '../../../src/utils/response.util.js'
|
|
12
|
+
|
|
13
|
+
describe('response.util — ok()', () => {
|
|
14
|
+
it('wraps data with ok: true and the request id', () => {
|
|
15
|
+
const res = ok({ id: 'abc' }, 'req-123')
|
|
16
|
+
expect(res.ok).toBe(true)
|
|
17
|
+
expect(res.data).toEqual({ id: 'abc' })
|
|
18
|
+
expect(res.meta.requestId).toBe('req-123')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('includes an ISO timestamp in meta', () => {
|
|
22
|
+
const res = ok({ id: 'abc' }, 'req-123')
|
|
23
|
+
expect(typeof res.meta.timestamp).toBe('string')
|
|
24
|
+
expect(() => new Date(res.meta.timestamp).toISOString()).not.toThrow()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('preserves array payloads without mutation', () => {
|
|
28
|
+
const items = [1, 2, 3]
|
|
29
|
+
const res = ok(items, 'req-123')
|
|
30
|
+
expect(res.data).toBe(items)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('handles null data', () => {
|
|
34
|
+
const res = ok(null, 'req-123')
|
|
35
|
+
expect(res.ok).toBe(true)
|
|
36
|
+
expect(res.data).toBeNull()
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('response.util — err()', () => {
|
|
41
|
+
it('wraps an error with ok: false and the canonical shape', () => {
|
|
42
|
+
const res = err('NOT_FOUND', 'Application not found', 'req-456')
|
|
43
|
+
expect(res.ok).toBe(false)
|
|
44
|
+
expect(res.error.code).toBe('NOT_FOUND')
|
|
45
|
+
expect(res.error.message).toBe('Application not found')
|
|
46
|
+
expect(res.meta.requestId).toBe('req-456')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('never leaks a data field on errors', () => {
|
|
50
|
+
const res = err('INTERNAL_ERROR', 'boom', 'req-456')
|
|
51
|
+
expect(res).not.toHaveProperty('data')
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"exactOptionalPropertyTypes": true,
|
|
10
|
+
"noUncheckedIndexedAccess": true,
|
|
11
|
+
"experimentalDecorators": true,
|
|
12
|
+
"emitDecoratorMetadata": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"forceConsistentCasingInFileNames": true,
|
|
16
|
+
"resolveJsonModule": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*", "scripts/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
20
|
+
}
|