@hellraisercenobit/ai-engineering-gate 0.8.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/README.md +82 -0
- package/agents/design-pattern-reviewer.md +40 -0
- package/agents/modern-typescript-reviewer.md +24 -0
- package/agents/testing-pattern-reviewer.md +23 -0
- package/bin/ai-engineering-gate.mjs +6 -0
- package/contracts/axi/axi-standard.md +273 -0
- package/contracts/axi/pin.json +36 -0
- package/contracts/members.json +58 -0
- package/contracts/schemas/arbitration.schema.json +45 -0
- package/contracts/schemas/decision-envelope.schema.json +215 -0
- package/contracts/schemas/declaration.schema.json +190 -0
- package/contracts/schemas/dispute.schema.json +67 -0
- package/contracts/schemas/evidence-append.schema.json +72 -0
- package/contracts/schemas/gate-output.schema.json +296 -0
- package/contracts/schemas/marker.schema.json +75 -0
- package/contracts/schemas/review-envelope.schema.json +378 -0
- package/contracts/schemas/verdict-record.schema.json +191 -0
- package/contracts/suite-contract.md +270 -0
- package/dist/ai-engineering-gate.mjs +2762 -0
- package/package.json +47 -0
- package/skills/engineering/review-design-patterns/LICENSE +6 -0
- package/skills/engineering/review-design-patterns/SKILL.md +196 -0
- package/skills/engineering/review-design-patterns/agents/openai.yaml +3 -0
- package/skills/engineering/review-design-patterns/references/smell-signatures.md +117 -0
- package/skills/engineering/review-design-patterns/review-design-patterns.yaml +25 -0
- package/skills/engineering/review-modern-typescript/LICENSE +21 -0
- package/skills/engineering/review-modern-typescript/SKILL.md +88 -0
- package/skills/engineering/review-modern-typescript/agents/openai.yaml +3 -0
- package/skills/engineering/review-modern-typescript/references/smell-signatures.md +36 -0
- package/skills/engineering/review-modern-typescript/review-modern-typescript.yaml +22 -0
- package/skills/engineering/review-testing-patterns/SKILL.md +108 -0
- package/skills/engineering/review-testing-patterns/agents/openai.yaml +4 -0
- package/skills/engineering/review-testing-patterns/references/compiler-profile.mjs +103 -0
- package/skills/engineering/review-testing-patterns/references/smell-signatures.md +30 -0
- package/skills/engineering/review-testing-patterns/review-testing-patterns.yaml +23 -0
- package/skills/engineering/transpose-design-patterns/LICENSE +6 -0
- package/skills/engineering/transpose-design-patterns/SKILL.md +231 -0
- package/skills/engineering/transpose-design-patterns/agents/openai.yaml +3 -0
- package/skills/engineering/transpose-design-patterns/references/arbitration.schema.json +45 -0
- package/skills/engineering/transpose-design-patterns/references/decision-envelope.schema.json +215 -0
- package/skills/engineering/transpose-design-patterns/references/declaration.schema.json +190 -0
- package/skills/engineering/transpose-design-patterns/references/design-decision-record.schema.json +731 -0
- package/skills/engineering/transpose-design-patterns/references/dispute.schema.json +67 -0
- package/skills/engineering/transpose-design-patterns/references/evidence-append.schema.json +72 -0
- package/skills/engineering/transpose-design-patterns/references/marker.schema.json +75 -0
- package/skills/engineering/transpose-design-patterns/references/pattern-catalog.md +305 -0
- package/skills/engineering/transpose-design-patterns/references/record.example.json +120 -0
- package/skills/engineering/transpose-design-patterns/references/review-envelope.schema.json +378 -0
- package/skills/engineering/transpose-design-patterns/references/suite-contract.md +270 -0
- package/skills/engineering/transpose-design-patterns/references/transpose-angular.md +224 -0
- package/skills/engineering/transpose-design-patterns/references/transpose-php.md +561 -0
- package/skills/engineering/transpose-design-patterns/references/transpose-quarkus.md +283 -0
- package/skills/engineering/transpose-design-patterns/references/transpose-react.md +256 -0
- package/skills/engineering/transpose-design-patterns/references/transpose-vanilla.md +204 -0
- package/skills/engineering/transpose-design-patterns/references/transpose-vue.md +234 -0
- package/skills/engineering/transpose-design-patterns/transpose-design-patterns.yaml +24 -0
- package/skills/engineering/transpose-modern-typescript/LICENSE +21 -0
- package/skills/engineering/transpose-modern-typescript/SKILL.md +90 -0
- package/skills/engineering/transpose-modern-typescript/agents/openai.yaml +3 -0
- package/skills/engineering/transpose-modern-typescript/references/arbitration.schema.json +45 -0
- package/skills/engineering/transpose-modern-typescript/references/catalog.md +38 -0
- package/skills/engineering/transpose-modern-typescript/references/collections.md +50 -0
- package/skills/engineering/transpose-modern-typescript/references/compatibility.md +61 -0
- package/skills/engineering/transpose-modern-typescript/references/decision-envelope.schema.json +215 -0
- package/skills/engineering/transpose-modern-typescript/references/decision-record.schema.json +830 -0
- package/skills/engineering/transpose-modern-typescript/references/declaration.schema.json +190 -0
- package/skills/engineering/transpose-modern-typescript/references/dispute.schema.json +67 -0
- package/skills/engineering/transpose-modern-typescript/references/evidence-append.schema.json +72 -0
- package/skills/engineering/transpose-modern-typescript/references/idioms.md +74 -0
- package/skills/engineering/transpose-modern-typescript/references/marker.schema.json +75 -0
- package/skills/engineering/transpose-modern-typescript/references/platform.md +147 -0
- package/skills/engineering/transpose-modern-typescript/references/record.example.json +172 -0
- package/skills/engineering/transpose-modern-typescript/references/record.md +37 -0
- package/skills/engineering/transpose-modern-typescript/references/review-envelope.schema.json +378 -0
- package/skills/engineering/transpose-modern-typescript/references/suite-contract.md +270 -0
- package/skills/engineering/transpose-modern-typescript/references/types.md +97 -0
- package/skills/engineering/transpose-modern-typescript/transpose-modern-typescript.yaml +22 -0
- package/skills/engineering/transpose-testing-patterns/SKILL.md +96 -0
- package/skills/engineering/transpose-testing-patterns/agents/openai.yaml +4 -0
- package/skills/engineering/transpose-testing-patterns/references/arbitration.schema.json +45 -0
- package/skills/engineering/transpose-testing-patterns/references/catalog.md +218 -0
- package/skills/engineering/transpose-testing-patterns/references/decision-envelope.schema.json +215 -0
- package/skills/engineering/transpose-testing-patterns/references/decision-record.schema.json +857 -0
- package/skills/engineering/transpose-testing-patterns/references/declaration.schema.json +190 -0
- package/skills/engineering/transpose-testing-patterns/references/detect-adapter.mjs +98 -0
- package/skills/engineering/transpose-testing-patterns/references/dispute.schema.json +67 -0
- package/skills/engineering/transpose-testing-patterns/references/doubles-data.md +45 -0
- package/skills/engineering/transpose-testing-patterns/references/evidence-append.schema.json +72 -0
- package/skills/engineering/transpose-testing-patterns/references/journal-event.schema.json +115 -0
- package/skills/engineering/transpose-testing-patterns/references/marker.schema.json +75 -0
- package/skills/engineering/transpose-testing-patterns/references/record.example.json +230 -0
- package/skills/engineering/transpose-testing-patterns/references/record.md +25 -0
- package/skills/engineering/transpose-testing-patterns/references/review-envelope.schema.json +378 -0
- package/skills/engineering/transpose-testing-patterns/references/suite-contract.md +270 -0
- package/skills/engineering/transpose-testing-patterns/references/tdd.md +62 -0
- package/skills/engineering/transpose-testing-patterns/references/transpose-codeception.md +84 -0
- package/skills/engineering/transpose-testing-patterns/references/transpose-karma-jasmine-angular.md +73 -0
- package/skills/engineering/transpose-testing-patterns/references/transpose-vitest.md +111 -0
- package/skills/engineering/transpose-testing-patterns/references/typescript.md +42 -0
- package/skills/engineering/transpose-testing-patterns/transpose-testing-patterns.yaml +23 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/hellraisercenobit/skills/blob/main/contracts/schemas/dispute.schema.json",
|
|
4
|
+
"title": "Dispute 1.0.0",
|
|
5
|
+
"description": "The builder's second and only other response to a finding, beside addressing it. It contests one finding with a pointer to counter-evidence that already exists; a dispute without such a pointer is refused. It goes to the user and its only exit is an arbitration.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": [
|
|
9
|
+
"document",
|
|
10
|
+
"documentVersion",
|
|
11
|
+
"dimension",
|
|
12
|
+
"report",
|
|
13
|
+
"finding",
|
|
14
|
+
"counterEvidence",
|
|
15
|
+
"position"
|
|
16
|
+
],
|
|
17
|
+
"properties": {
|
|
18
|
+
"document": {
|
|
19
|
+
"const": "dispute"
|
|
20
|
+
},
|
|
21
|
+
"documentVersion": {
|
|
22
|
+
"const": "1.0.0"
|
|
23
|
+
},
|
|
24
|
+
"dimension": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"minLength": 1
|
|
27
|
+
},
|
|
28
|
+
"report": {
|
|
29
|
+
"description": "The identifier of the report that carries the contested finding.",
|
|
30
|
+
"type": "string",
|
|
31
|
+
"minLength": 1
|
|
32
|
+
},
|
|
33
|
+
"finding": {
|
|
34
|
+
"description": "The contested finding's identifier.",
|
|
35
|
+
"type": "string",
|
|
36
|
+
"minLength": 1
|
|
37
|
+
},
|
|
38
|
+
"counterEvidence": {
|
|
39
|
+
"description": "Pointers to artifacts that exist now. Each path is checked; a dangling pointer refuses the dispute.",
|
|
40
|
+
"type": "array",
|
|
41
|
+
"minItems": 1,
|
|
42
|
+
"items": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"additionalProperties": false,
|
|
45
|
+
"required": [
|
|
46
|
+
"path",
|
|
47
|
+
"claim"
|
|
48
|
+
],
|
|
49
|
+
"properties": {
|
|
50
|
+
"path": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"minLength": 1
|
|
53
|
+
},
|
|
54
|
+
"claim": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"minLength": 1
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"position": {
|
|
62
|
+
"description": "The builder's position in one sentence.",
|
|
63
|
+
"type": "string",
|
|
64
|
+
"minLength": 1
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/hellraisercenobit/skills/blob/main/contracts/schemas/evidence-append.schema.json",
|
|
4
|
+
"title": "Evidence append 1.0.0",
|
|
5
|
+
"description": "Append-only. Journal events, check outputs, snapshots and any artifact a record planned are filed through `evidence append`, never written into the gate's index by hand. The gate stamps every append with the content hashes of the record's planned artifacts, by role, so an event proves the state of test and production artifacts at the moment it was filed.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": [
|
|
9
|
+
"document",
|
|
10
|
+
"documentVersion",
|
|
11
|
+
"dimension",
|
|
12
|
+
"record",
|
|
13
|
+
"kind",
|
|
14
|
+
"payload"
|
|
15
|
+
],
|
|
16
|
+
"properties": {
|
|
17
|
+
"document": {
|
|
18
|
+
"const": "evidence-append"
|
|
19
|
+
},
|
|
20
|
+
"documentVersion": {
|
|
21
|
+
"const": "1.0.0"
|
|
22
|
+
},
|
|
23
|
+
"dimension": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"minLength": 1
|
|
26
|
+
},
|
|
27
|
+
"record": {
|
|
28
|
+
"description": "The decision record this evidence belongs to.",
|
|
29
|
+
"type": "string",
|
|
30
|
+
"minLength": 1
|
|
31
|
+
},
|
|
32
|
+
"kind": {
|
|
33
|
+
"description": "A kind the member's evidence schemas name, which then validates the payload, or one of the generic kinds.",
|
|
34
|
+
"type": "string",
|
|
35
|
+
"minLength": 1
|
|
36
|
+
},
|
|
37
|
+
"produces": {
|
|
38
|
+
"description": "The planned artifact this append produces or extends. A planned artifact is satisfied by a file in the worktree or by an append that names it here.",
|
|
39
|
+
"type": "string",
|
|
40
|
+
"minLength": 1
|
|
41
|
+
},
|
|
42
|
+
"payload": {
|
|
43
|
+
"description": "Opaque to the envelope. Validated against the member's evidence schema when its kind names one."
|
|
44
|
+
},
|
|
45
|
+
"filedAt": {
|
|
46
|
+
"description": "Stamped by the gate when the append is stored.",
|
|
47
|
+
"type": "string",
|
|
48
|
+
"minLength": 1
|
|
49
|
+
},
|
|
50
|
+
"artifactHashes": {
|
|
51
|
+
"description": "Stamped by the gate: the content hash of every artifact the record planned, grouped by role, at the moment of the append.",
|
|
52
|
+
"type": "object",
|
|
53
|
+
"additionalProperties": {
|
|
54
|
+
"type": "object",
|
|
55
|
+
"additionalProperties": {
|
|
56
|
+
"type": [
|
|
57
|
+
"string",
|
|
58
|
+
"null"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"builder": {
|
|
64
|
+
"description": "Stamped by the gate: the identity it captured for the caller.",
|
|
65
|
+
"type": "object"
|
|
66
|
+
},
|
|
67
|
+
"replayed": {
|
|
68
|
+
"description": "Stamped by the gate: `true` only when the gate itself ran the scenario through `replay`.",
|
|
69
|
+
"type": "boolean"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/hellraisercenobit/skills/blob/main/contracts/schemas/marker.schema.json",
|
|
4
|
+
"title": "Suite marker 1.0.0",
|
|
5
|
+
"description": "Committed at the repository root as `.ai-engineering-suite.json`. Its presence is the project's opt-in and the only signal enforcement reads: without it every gate command allows and prints nothing that blocks.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": [
|
|
9
|
+
"markerVersion",
|
|
10
|
+
"dimensions"
|
|
11
|
+
],
|
|
12
|
+
"properties": {
|
|
13
|
+
"markerVersion": {
|
|
14
|
+
"const": "1.0.0"
|
|
15
|
+
},
|
|
16
|
+
"dimensions": {
|
|
17
|
+
"description": "`all` registers every member of the manifest; a list registers exactly those dimension identifiers.",
|
|
18
|
+
"oneOf": [
|
|
19
|
+
{
|
|
20
|
+
"const": "all"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"type": "array",
|
|
24
|
+
"minItems": 1,
|
|
25
|
+
"uniqueItems": true,
|
|
26
|
+
"items": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"minLength": 1
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"evidenceRoot": {
|
|
34
|
+
"description": "Overrides the default evidence root. `~` expands to the user's home directory. A relative path resolves against the repository root, which the change set then excludes.",
|
|
35
|
+
"type": "string",
|
|
36
|
+
"minLength": 1
|
|
37
|
+
},
|
|
38
|
+
"exportDirectory": {
|
|
39
|
+
"description": "Repository-relative directory `export` writes to. Excluded from the change set and from every fingerprint.",
|
|
40
|
+
"type": "string",
|
|
41
|
+
"minLength": 1
|
|
42
|
+
},
|
|
43
|
+
"ignore": {
|
|
44
|
+
"description": "Repository-relative glob patterns removed from the change set. `*` matches within one segment, `**` across segments.",
|
|
45
|
+
"type": "array",
|
|
46
|
+
"items": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"minLength": 1
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"base": {
|
|
52
|
+
"description": "Default comparison base when a declaration names none.",
|
|
53
|
+
"type": "string",
|
|
54
|
+
"minLength": 1
|
|
55
|
+
},
|
|
56
|
+
"conflictRoundCap": {
|
|
57
|
+
"description": "Consecutive cross-dimension conflict rounds before `can-review` is refused with `arbitration-required`.",
|
|
58
|
+
"type": "integer",
|
|
59
|
+
"minimum": 1
|
|
60
|
+
},
|
|
61
|
+
"roundCap": {
|
|
62
|
+
"description": "Total review rounds for one task before completion ends with `round-cap-reached`.",
|
|
63
|
+
"type": "integer",
|
|
64
|
+
"minimum": 1
|
|
65
|
+
},
|
|
66
|
+
"allowReplay": {
|
|
67
|
+
"description": "`false` forbids the gate-executed replay remedy, leaving a rerun of a full cycle as the only remedy for chronology-bound evidence.",
|
|
68
|
+
"type": "boolean"
|
|
69
|
+
},
|
|
70
|
+
"requireVerifiedIdentity": {
|
|
71
|
+
"description": "`true` refuses an attestation stored `identity-unverified`, for harnesses that provide agent identifiers.",
|
|
72
|
+
"type": "boolean"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# Design Pattern Catalog 1.0.0 - framework-agnostic (reference idioms in TypeScript)
|
|
2
|
+
|
|
3
|
+
Self-contained catalog for the `transpose-design-patterns` skill. Framework-agnostic on purpose:
|
|
4
|
+
select the pattern here, then apply the wiring from the matching `transpose-<framework>.md` guide.
|
|
5
|
+
A decision record declares this catalog version.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Core Principles
|
|
10
|
+
|
|
11
|
+
### 1. Separation of Concerns
|
|
12
|
+
|
|
13
|
+
Each layer has a single responsibility.
|
|
14
|
+
|
|
15
|
+
- UI handles rendering and interactions
|
|
16
|
+
- Services orchestrate business logic
|
|
17
|
+
- Domain models represent business concepts
|
|
18
|
+
- Infrastructure handles APIs, storage, logging, etc.
|
|
19
|
+
|
|
20
|
+
### 2. Prefer Composition Over Inheritance
|
|
21
|
+
|
|
22
|
+
Use dependency injection, functions, services, composition. Avoid deep class hierarchies and abstract
|
|
23
|
+
base classes everywhere.
|
|
24
|
+
|
|
25
|
+
### 3. Depend on Abstractions
|
|
26
|
+
|
|
27
|
+
High-level modules must not depend on low-level implementations. Use interfaces, contracts, injection
|
|
28
|
+
tokens, generic constraints.
|
|
29
|
+
|
|
30
|
+
### 4. Keep Business Logic Framework-Agnostic
|
|
31
|
+
|
|
32
|
+
Business logic should work without a UI framework, without the DOM, without HTTP. Prefer pure functions,
|
|
33
|
+
domain services, typed contracts.
|
|
34
|
+
|
|
35
|
+
### 5. Use Strong Typing Everywhere
|
|
36
|
+
|
|
37
|
+
Use the strongest typing the language offers, and a static analyzer where the language stops. TypeScript:
|
|
38
|
+
inferred unions, `as const`, `satisfies`, discriminated unions, generics, branded types for IDs. Java:
|
|
39
|
+
records, sealed interfaces, generics. PHP: native types, `readonly`, backed enums, PHPStan generics and array
|
|
40
|
+
shapes. Avoid `any` / `mixed` / `Object`, magic strings, weak object maps.
|
|
41
|
+
|
|
42
|
+
### 6. Runtime Validation Matters
|
|
43
|
+
|
|
44
|
+
Static types stop at the process boundary. Validate API payloads, runtime plugins and environment configs
|
|
45
|
+
at that boundary - zod / valibot / arktype in TypeScript, Bean Validation in Java, Validator constraints in
|
|
46
|
+
PHP.
|
|
47
|
+
|
|
48
|
+
### 7. Prefer Immutable Data
|
|
49
|
+
|
|
50
|
+
Use `readonly`, readonly arrays, immutable updates. Avoid object mutation and hidden side effects.
|
|
51
|
+
|
|
52
|
+
### 8. Explicitness Over Magic
|
|
53
|
+
|
|
54
|
+
Prefer explicit flows, explicit dependencies, explicit state transitions. Avoid hidden global state,
|
|
55
|
+
implicit effects, dynamic behavior without contracts.
|
|
56
|
+
|
|
57
|
+
### 9. Keep the Domain Layer Pure
|
|
58
|
+
|
|
59
|
+
The domain layer holds business types and rules only. Two rules every framework guide enforces:
|
|
60
|
+
|
|
61
|
+
- **No presentation or infrastructure in the domain** - view-models, CSS / design tokens, display
|
|
62
|
+
labels, locale formatting (presentation) and URLs, HTTP, runtime config (infrastructure) live in their
|
|
63
|
+
own layers, never in the domain.
|
|
64
|
+
- **Split ports by concern (ISP)** - never bundle a domain rule + an IO/output port + UI-copy strings in
|
|
65
|
+
one interface; separate them.
|
|
66
|
+
|
|
67
|
+
### 10. A Derived Value Has One Owner
|
|
68
|
+
|
|
69
|
+
A value computed from other values has one owner for its derivation and lifecycle:
|
|
70
|
+
|
|
71
|
+
- **Read-only derived fields stay out of persistence.** When a field exists only in the read model,
|
|
72
|
+
its owner defines both its derivation and its exclusion from writes; **every** write path uses that
|
|
73
|
+
boundary. Overwriting a stored field on read can hide an accidental persisted copy. Deliberately
|
|
74
|
+
materialized derived values instead need an explicit refresh/invalidation contract.
|
|
75
|
+
- **A memo key carries every input the value depends on**, fallback inputs included. A key built
|
|
76
|
+
from the nominal input alone can collide between cases the derived value distinguishes. Skip
|
|
77
|
+
caching a no-work branch unless negative caching is intentional and has a defined scope and
|
|
78
|
+
invalidation policy.
|
|
79
|
+
|
|
80
|
+
### 11. One Owner for a Set an API Replaces
|
|
81
|
+
|
|
82
|
+
When a call **replaces** a set instead of merging into it, callers providing the same logical context
|
|
83
|
+
must agree on the complete set. Omitting a member can silently change behavior. Give that context
|
|
84
|
+
one named builder used by each such caller. Distinct contexts can have distinct builders; a comment
|
|
85
|
+
asking duplicated builders to stay in step does not enforce their agreement.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Structural forces
|
|
90
|
+
|
|
91
|
+
A pattern is justified by a force in the change, never by taste. Walk all eight before you decide,
|
|
92
|
+
and answer each one with a value from its set below plus the site that carries it. Every force at its
|
|
93
|
+
`absent` value is a legitimate `none`. Values, not prose, are what a reviewer compares: an answer of
|
|
94
|
+
"several behaviors" that jumps straight to Strategy is exactly what the value set prevents.
|
|
95
|
+
|
|
96
|
+
| Force | Value | What it looks like in the change | Decision |
|
|
97
|
+
| --- | --- | --- | --- |
|
|
98
|
+
| `variability` | `absent` | one responsibility, one body | - |
|
|
99
|
+
| | `duplicated-body` | two copies of the same body, no discriminator | Command, not Strategy - see `reusable-action` |
|
|
100
|
+
| | `interchangeable-closed` | several interchangeable implementations of one responsibility, selected by a discriminator the compiler closes | Strategy |
|
|
101
|
+
| | `interchangeable-open` | the same, with the discriminator open to config, plugins or runtime | Strategy or Registry (tie-breaker below) |
|
|
102
|
+
| `extension` | `no-variant-expected` | no plausible next variant | - |
|
|
103
|
+
| | `declaration-only` | the next variant costs the declaration and its exhaustive branch, nothing else (extension-cost test, question 1) | None - exhaustive branch on a discriminated union |
|
|
104
|
+
| | `one-site` | the next variant costs one orchestration edit | - |
|
|
105
|
+
| | `many-sites` | the next variant costs more than one orchestration edit | Strategy or Registry (tie-breaker below) |
|
|
106
|
+
| `creation-policy` | `absent` | consumers construct directly, always the same way | - |
|
|
107
|
+
| | `runtime-input` | construction depends on a runtime value | Factory |
|
|
108
|
+
| | `business-rule` | construction depends on a business rule | Factory |
|
|
109
|
+
| `boundary-mismatch` | `absent` | one shape crosses the boundary unchanged | - |
|
|
110
|
+
| | `shape-differs` | an external DTO / API shape differs from the domain shape | Adapter / DTO Mapping |
|
|
111
|
+
| | `shape-and-semantics-differ` | the shapes differ and so do units, nullability or error meaning | Adapter / DTO Mapping |
|
|
112
|
+
| `reusable-action` | `absent` | no operation stands on its own | - |
|
|
113
|
+
| | `state-container-bound` | the operation only changes its own store's state | - (a store action, not a Command) |
|
|
114
|
+
| | `caller-independent` | the operation has meaning independent of its caller and of its state container | Command |
|
|
115
|
+
| `composition` | `absent` | one behavior | - |
|
|
116
|
+
| | `sequential-steps` | several steps of one behavior, in fixed order | - |
|
|
117
|
+
| | `independent-behaviors` | several independent behaviors combine into one | Composition |
|
|
118
|
+
| `shared-lifecycle` | `absent` | no identity or lifecycle is shared | - |
|
|
119
|
+
| | `per-consumer` | each consumer owns its instance and its lifecycle | - |
|
|
120
|
+
| | `application-wide` | application-wide identity or lifecycle is required | Singleton / shared state (a facade over a store records as `application-wide`) |
|
|
121
|
+
| `cross-cutting-behavior` | `absent` | no concern wraps the operations | - |
|
|
122
|
+
| | `one-operation` | a concern wraps a single operation | - |
|
|
123
|
+
| | `many-operations` | logging, metrics, auth, caching, retry or tracing wraps many operations | Composition (decorator / interceptor), per the framework guide |
|
|
124
|
+
|
|
125
|
+
UI state (signals, per the front-end guides) is framework wiring, not a design decision: it takes no record.
|
|
126
|
+
|
|
127
|
+
**Strategy or Registry.** Both answer the extension force. Strategy when the variants are compile-time
|
|
128
|
+
known and typed - a `satisfies Record<K, S>` table gives the completeness check. Registry when variants
|
|
129
|
+
arrive from plugins, config or runtime, or self-register.
|
|
130
|
+
|
|
131
|
+
**The extension question.** _If one plausible variant is added next, what existing code must change?_
|
|
132
|
+
Answer it by naming the files and symbols, and let the count pick the `extension` value. The question is
|
|
133
|
+
answered, never asserted: "open to extension" without that list is not an answer.
|
|
134
|
+
|
|
135
|
+
**Extension-cost test.** A _variation axis_ is the discriminator that selects a variant: a type tag, a
|
|
136
|
+
key, a config value. For each axis, two questions in this order:
|
|
137
|
+
|
|
138
|
+
1. _Is the set closed by declaration, and does it stay closed?_ Closed means a sealed type, a
|
|
139
|
+
discriminated union or a backed enum, exhaustiveness enforced by the compiler or analyzer, one call
|
|
140
|
+
site, and no variant arriving from config, plugins or runtime. A closed set is a legitimate `none`
|
|
141
|
+
even though adding a variant edits the branch: that edit is the declaration.
|
|
142
|
+
2. Otherwise, _count the edit sites the next legitimate variant costs_: files and symbols in
|
|
143
|
+
orchestration code (the consumer, its constructor, each branch); manifests such as `package.json` do
|
|
144
|
+
not count. More than one orchestration edit points at a pattern from the table. Never choose the
|
|
145
|
+
inline branch because it is shorter; count the next plausible variant, not the current one.
|
|
146
|
+
|
|
147
|
+
A force without a variant axis (`creation-policy`, `boundary-mismatch`, `reusable-action`,
|
|
148
|
+
`shared-lifecycle`, `composition`, `cross-cutting-behavior`) takes the same count on the next change of its kind: the next
|
|
149
|
+
environment, the next API field, the next caller, the next consumer, the next concern to stack. Today's
|
|
150
|
+
cost against the cost with the decision is the `extensionCost` the reviewer re-counts.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Patterns
|
|
155
|
+
|
|
156
|
+
### Strategy
|
|
157
|
+
|
|
158
|
+
**Use when** you need interchangeable business behaviors.
|
|
159
|
+
|
|
160
|
+
**Best practices** - use interfaces; use discriminated/inferred unions; inject implementations; keep
|
|
161
|
+
strategies stateless (no mutable per-call state; closing over injected dependencies is fine).
|
|
162
|
+
|
|
163
|
+
**Avoid** - `switch`/`case` on a type tag as the extension mechanism (a branch over a set that passes
|
|
164
|
+
question 1 of the extension-cost test is `none`, see below); `constructor.name` lookups; string magic;
|
|
165
|
+
giant strategy classes.
|
|
166
|
+
|
|
167
|
+
**Invariants** - the consumer depends on the behavioral contract, never on a concrete type; an
|
|
168
|
+
implementation can be replaced without editing the consumer; dispatch on a concrete type or a type tag
|
|
169
|
+
is not the extension mechanism.
|
|
170
|
+
|
|
171
|
+
### Registry
|
|
172
|
+
|
|
173
|
+
**Use when** you need extensibility / a plugin system.
|
|
174
|
+
|
|
175
|
+
**Best practices** - plugins self-register; no central switch; typed registries when plugins are
|
|
176
|
+
compile-time known; runtime validation for dynamic plugins.
|
|
177
|
+
|
|
178
|
+
**Avoid** - maintaining enums by hand for external plugins; the service-locator anti-pattern.
|
|
179
|
+
|
|
180
|
+
**Invariants** - registration is explicit at the composition root, or by the self-registration the
|
|
181
|
+
framework wires; consumers resolve through the registry contract by key; no central provider switch grows
|
|
182
|
+
with each plugin.
|
|
183
|
+
|
|
184
|
+
### Factory
|
|
185
|
+
|
|
186
|
+
**Use when** object creation contains logic.
|
|
187
|
+
|
|
188
|
+
**Best practices** - isolate creation complexity; return abstractions; keep factories lightweight.
|
|
189
|
+
|
|
190
|
+
**Avoid** - factories without creation logic; giant conditional factories.
|
|
191
|
+
|
|
192
|
+
**Invariants** - the factory holds real creation logic (a decision, an environment choice, an async
|
|
193
|
+
setup); it returns an abstraction; no caller repeats the creation decision.
|
|
194
|
+
|
|
195
|
+
### Command
|
|
196
|
+
|
|
197
|
+
**Use when** actions must be reusable or composable.
|
|
198
|
+
|
|
199
|
+
**Best practices** - encapsulate side effects; keep commands focused; prefer functions over heavy classes.
|
|
200
|
+
|
|
201
|
+
**Avoid** - burying the command inside a store/facade method (or any state container) when it must be
|
|
202
|
+
reusable; mixing the action with UI-state, persistence, or presentation concerns.
|
|
203
|
+
|
|
204
|
+
**Invariants** - the action exists as its own function or injectable, independent of UI or store
|
|
205
|
+
state; it can be tested without standing up the store; a store or facade delegates to it when
|
|
206
|
+
surrounding state is needed.
|
|
207
|
+
|
|
208
|
+
**A store action/method is _not automatically_ the Command.** When the action must be reusable,
|
|
209
|
+
composable, or unit-testable in isolation, keep it a **distinct use-case** (function or small injectable)
|
|
210
|
+
and have the store **delegate** to it - the store then only manages the surrounding state (status flags,
|
|
211
|
+
journal, reset). Inline it in the store only for a one-off with no reuse/test pressure (YAGNI). In a
|
|
212
|
+
strict layering the use-case lives in an _application_ layer depending only on _domain_ + ports; the
|
|
213
|
+
store (an adapter) invokes it. Each `transpose-<framework>.md` only binds this to its store (Angular
|
|
214
|
+
`@ngrx/signals` `withMethods`, React/Zustand, Vue/Pinia, Vanilla closure store).
|
|
215
|
+
|
|
216
|
+
### Adapter / DTO Mapping
|
|
217
|
+
|
|
218
|
+
**Use when** backend models differ from frontend domain models.
|
|
219
|
+
|
|
220
|
+
**Best practices** - separate DTO and domain; use pure `from`/`to` functions; map at infrastructure boundaries.
|
|
221
|
+
|
|
222
|
+
**Avoid** - leaking DTOs into the UI; framework-dependent mappers.
|
|
223
|
+
|
|
224
|
+
**Invariants** - the external shape does not cross the boundary; the mapping is a pure function
|
|
225
|
+
testable on its own; UI and application code receive the domain representation.
|
|
226
|
+
|
|
227
|
+
**Canonical pure mapper** - each `transpose-<framework>.md` shows only the framework wrapper that calls it:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
interface UserDto {
|
|
231
|
+
id: string;
|
|
232
|
+
created_at: string;
|
|
233
|
+
}
|
|
234
|
+
interface User {
|
|
235
|
+
id: string;
|
|
236
|
+
createdAt: Date;
|
|
237
|
+
}
|
|
238
|
+
const userFromDto = (d: UserDto): User => ({ id: d.id, createdAt: new Date(d.created_at) });
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Composition
|
|
242
|
+
|
|
243
|
+
**Use when** you want modular, decoupled systems.
|
|
244
|
+
|
|
245
|
+
**Best practices** - compose behavior through services/functions; isolate concerns.
|
|
246
|
+
|
|
247
|
+
**Avoid** - inheritance chains; tightly coupled modules.
|
|
248
|
+
|
|
249
|
+
**Invariants** - behavior is combined through injection, functions or decorators, not through a class
|
|
250
|
+
hierarchy; each part has one concern and can be swapped alone.
|
|
251
|
+
|
|
252
|
+
### Singleton
|
|
253
|
+
|
|
254
|
+
**Use when** shared application-wide state/services are needed.
|
|
255
|
+
|
|
256
|
+
**Best practices** - keep singleton state minimal; expose readonly APIs.
|
|
257
|
+
|
|
258
|
+
**Avoid** - global mutable state; hidden shared state.
|
|
259
|
+
|
|
260
|
+
**Invariants** - state is private to the service; the public surface is readonly or computed;
|
|
261
|
+
consumers cannot mutate shared state directly.
|
|
262
|
+
|
|
263
|
+
### None - no named pattern
|
|
264
|
+
|
|
265
|
+
**Use when** no structural force is present, or the only force is a variant set that is intentionally
|
|
266
|
+
closed, compiler-checked, and branched on at one site (question 1 of the extension-cost test).
|
|
267
|
+
|
|
268
|
+
**Best practices** - keep the exhaustive branch on a discriminated union (TypeScript `switch` with a
|
|
269
|
+
`never` check, Java `sealed` + `switch`, PHP backed enum + `match`); declare that the set is closed;
|
|
270
|
+
name the trigger that reopens the decision.
|
|
271
|
+
|
|
272
|
+
**Avoid** - `none` chosen to save ceremony while a force is present; an "exhaustive" branch duplicated
|
|
273
|
+
at several call sites; a closed set that receives variants from config, plugins or runtime.
|
|
274
|
+
|
|
275
|
+
**Invariants** - for a closed set: the branch stays exhaustive and the compiler or analyzer enforces it,
|
|
276
|
+
the variant set is closed by declaration, not by accident, and adding a variant is a deliberate reopening
|
|
277
|
+
of the decision. When no force is present: the change adds no layer, alias or indirection, and the shape
|
|
278
|
+
that exists is preserved.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Reference idioms (TypeScript)
|
|
283
|
+
|
|
284
|
+
The catalog's examples are TypeScript. Each `transpose-<framework>.md` carries the same idioms in its own
|
|
285
|
+
language (records and sealed interfaces for Quarkus, `readonly` classes and backed enums for PHP).
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
// Prefer `satisfies`
|
|
289
|
+
const exporters = {
|
|
290
|
+
pdf: pdfExporter,
|
|
291
|
+
csv: csvExporter,
|
|
292
|
+
} as const satisfies Record<string, Exporter>;
|
|
293
|
+
|
|
294
|
+
// Prefer inferred unions
|
|
295
|
+
type ExportFormat = keyof typeof exporters;
|
|
296
|
+
|
|
297
|
+
// Prefer discriminated unions
|
|
298
|
+
type Result = { success: true; data: User } | { success: false; error: string };
|
|
299
|
+
|
|
300
|
+
// Prefer readonly APIs
|
|
301
|
+
readonly items: readonly Item[];
|
|
302
|
+
|
|
303
|
+
// Prefer pure functions
|
|
304
|
+
export const userFromDto = (dto: UserDto): User => ({ /* ... */ });
|
|
305
|
+
```
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dimension": "design-patterns",
|
|
3
|
+
"schemaVersion": "1.0.0",
|
|
4
|
+
"catalogVersion": "1.0.0",
|
|
5
|
+
"contractVersions": [
|
|
6
|
+
"1.0.0",
|
|
7
|
+
"1.1.0"
|
|
8
|
+
],
|
|
9
|
+
"need": "Price an order for one of several tax regimes, with new regimes arriving from configuration.",
|
|
10
|
+
"scope": [
|
|
11
|
+
"src/pricing/"
|
|
12
|
+
],
|
|
13
|
+
"base": "origin/main",
|
|
14
|
+
"revision": {
|
|
15
|
+
"number": 1,
|
|
16
|
+
"previous": null,
|
|
17
|
+
"reason": "Illustrative initial decision; replace all facts for a real execution."
|
|
18
|
+
},
|
|
19
|
+
"cites": [
|
|
20
|
+
{
|
|
21
|
+
"path": "src/pricing/price-order.ts",
|
|
22
|
+
"checkedAt": "2026-09-18",
|
|
23
|
+
"claim": "One function branches on a regime string read from configuration, and both callers repeat the branch.",
|
|
24
|
+
"covers": [
|
|
25
|
+
"src/pricing/price-order.ts:priceOrder"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"path": "config/regimes.json",
|
|
30
|
+
"checkedAt": "2026-09-18",
|
|
31
|
+
"claim": "Regimes are listed in configuration, so the variant set is not closed by declaration.",
|
|
32
|
+
"covers": [
|
|
33
|
+
"extension",
|
|
34
|
+
"variability"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"plans": [
|
|
39
|
+
{
|
|
40
|
+
"path": "src/pricing/tax-regime.ts",
|
|
41
|
+
"role": "implementation"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"path": "src/pricing/price-order.ts",
|
|
45
|
+
"role": "implementation"
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"forces": [
|
|
49
|
+
{
|
|
50
|
+
"force": "variability",
|
|
51
|
+
"value": "interchangeable-open",
|
|
52
|
+
"site": "src/pricing/price-order.ts branches on a regime key that config supplies."
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"force": "extension",
|
|
56
|
+
"value": "many-sites",
|
|
57
|
+
"site": "The next regime edits priceOrder's branch, its two callers and the regime union."
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"force": "creation-policy",
|
|
61
|
+
"value": "absent",
|
|
62
|
+
"site": "Regime objects are plain data; nothing decides how to construct them."
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"force": "boundary-mismatch",
|
|
66
|
+
"value": "absent",
|
|
67
|
+
"site": "The configuration shape and the domain shape are the same record."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"force": "reusable-action",
|
|
71
|
+
"value": "absent",
|
|
72
|
+
"site": "Pricing has no operation that stands on its own away from the order."
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"force": "composition",
|
|
76
|
+
"value": "sequential-steps",
|
|
77
|
+
"site": "Subtotal then tax then rounding, in one fixed order."
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"force": "shared-lifecycle",
|
|
81
|
+
"value": "per-consumer",
|
|
82
|
+
"site": "Each request prices its own order; nothing is shared."
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"force": "cross-cutting-behavior",
|
|
86
|
+
"value": "absent",
|
|
87
|
+
"site": "No concern wraps the pricing calls."
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
"alternatives": [
|
|
91
|
+
"Current shape: a switch on the regime key inside priceOrder, repeated by both callers. Rejected because the set is open and the next regime costs four edits.",
|
|
92
|
+
"Registry: regimes self-register at load. Rejected because the regimes are known at build time and a registry hides which ones exist.",
|
|
93
|
+
"Strategy with a typed table: one regime module per regime, selected by a `satisfies Record<RegimeKey, TaxRegime>` table. Chosen."
|
|
94
|
+
],
|
|
95
|
+
"decision": {
|
|
96
|
+
"pattern": "strategy",
|
|
97
|
+
"reason": "Interchangeable implementations of one responsibility, selected by a discriminator, with the table giving the compiler a completeness check.",
|
|
98
|
+
"extensionCost": "Today the next regime edits priceOrder's branch, both callers and the regime union: four sites. With the table it adds one module and one table entry, and the compiler reports the entry if it is missing.",
|
|
99
|
+
"reconsiderWhen": "Regimes start arriving at runtime from plugins, which makes this a Registry."
|
|
100
|
+
},
|
|
101
|
+
"framework": {
|
|
102
|
+
"name": "vanilla",
|
|
103
|
+
"transposition": "transpose-vanilla.md, Strategy: a module per variant and one `satisfies Record<K, S>` table at the composition root."
|
|
104
|
+
},
|
|
105
|
+
"artifacts": [
|
|
106
|
+
{
|
|
107
|
+
"file": "src/pricing/tax-regime.ts",
|
|
108
|
+
"symbol": "taxRegimes"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"file": "src/pricing/price-order.ts",
|
|
112
|
+
"symbol": "priceOrder"
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
"invariants": [
|
|
116
|
+
"The table is typed with `satisfies Record<RegimeKey, TaxRegime>`, so a missing regime is a compile error.",
|
|
117
|
+
"priceOrder holds no regime branch: it looks the regime up and calls it.",
|
|
118
|
+
"Selection happens once, at the composition root; no caller repeats it."
|
|
119
|
+
]
|
|
120
|
+
}
|