@tenkara/afm-linux-arm64 0.0.0-stage → 4.0.0-alpha.6
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/bin/afm +4 -0
- package/content/.coupling-baseline.tsv +1 -0
- package/content/.pending-commands.txt +2 -0
- package/content/CHANGELOG-archive.md +806 -0
- package/content/CHANGELOG.md +4256 -0
- package/content/PRINCIPLES.md +121 -0
- package/content/README-light.md +160 -0
- package/content/README.md +152 -0
- package/content/code-style/_any/file-size.md +62 -0
- package/content/code-style/_any/imports.md +68 -0
- package/content/code-style/_any/naming.md +72 -0
- package/content/code-style/_any/test-placement.md +57 -0
- package/content/code-style/nextjs-app-router/imports.md +50 -0
- package/content/code-style/nextjs-app-router/naming.md +50 -0
- package/content/code-style/prisma-postgres/error-boundary.md +55 -0
- package/content/code-style/prisma-postgres/imports.md +61 -0
- package/content/code-style/shadcn-ui/naming.md +48 -0
- package/content/code-style/stripe-billing/naming.md +76 -0
- package/content/code-style/trigger-dev-v4/imports.md +58 -0
- package/content/code-style/trigger-dev-v4/naming.md +49 -0
- package/content/code-style/trigger-dev-v4/test-placement.md +61 -0
- package/content/code-style/trpc-v11/error-boundary.md +51 -0
- package/content/code-style/trpc-v11/file-size.md +44 -0
- package/content/code-style/trpc-v11/naming.md +60 -0
- package/content/code-style/trpc-v11/test-placement.md +52 -0
- package/content/core/ach.md +265 -0
- package/content/core/afm.md +496 -0
- package/content/core/gotchas.md +18 -0
- package/content/core/prd.md +143 -0
- package/content/core/protocol.md +160 -0
- package/content/core/ust.md +65 -0
- package/content/guardrails/README.md +55 -0
- package/content/guardrails/dsl-com-ordem-obrigatoria.md +47 -0
- package/content/guardrails/exaustividade-de-union.md +48 -0
- package/content/guardrails/fronteira-de-import.md +57 -0
- package/content/hooks/README.md +72 -0
- package/content/interview/bootstrap.md +415 -0
- package/content/interview/promotion.md +117 -0
- package/content/interview/refactor.md +381 -0
- package/content/interview/triggers.md +109 -0
- package/content/modules/nextjs-app-router/ach.patch.md +44 -0
- package/content/modules/nextjs-app-router/gotchas.seed.md +78 -0
- package/content/modules/nextjs-app-router/rules.patch.md +35 -0
- package/content/modules/prisma-postgres/ach.patch.md +65 -0
- package/content/modules/prisma-postgres/rules.patch.md +28 -0
- package/content/modules/shadcn-ui/ach.patch.md +42 -0
- package/content/modules/stripe-billing/ach.patch.md +46 -0
- package/content/modules/stripe-billing/gotchas.seed.md +73 -0
- package/content/modules/stripe-billing/rules.patch.md +30 -0
- package/content/modules/trigger-dev-v4/ach.patch.md +64 -0
- package/content/modules/trigger-dev-v4/gotchas.seed.md +53 -0
- package/content/modules/trigger-dev-v4/rules.patch.md +35 -0
- package/content/modules/trpc-v11/ach.patch.md +56 -0
- package/content/modules/trpc-v11/rules.patch.md +34 -0
- package/content/modules/vireya-ui/ach.patch.md +85 -0
- package/content/modules/vireya-ui/gotchas.seed.md +98 -0
- package/content/modules/vireya-ui/rules.patch.md +36 -0
- package/content/ops/adr.md +54 -0
- package/content/ops/analyze.md +129 -0
- package/content/ops/clarify.md +86 -0
- package/content/ops/deliver.md +211 -0
- package/content/ops/diagnose.md +146 -0
- package/content/ops/evolve.md +317 -0
- package/content/ops/export-template.md +97 -0
- package/content/ops/generalize.md +120 -0
- package/content/ops/gotcha.md +46 -0
- package/content/ops/plan.md +61 -0
- package/content/ops/policy.md +63 -0
- package/content/ops/promote.md +72 -0
- package/content/ops/protocol.md +51 -0
- package/content/ops/reconcile.md +44 -0
- package/content/ops/reflect.md +180 -0
- package/content/ops/remediate.md +146 -0
- package/content/ops/restructure.md +145 -0
- package/content/ops/rule.md +165 -0
- package/content/ops/specify.md +62 -0
- package/content/ops/tasks.md +67 -0
- package/content/ops/update.md +223 -0
- package/content/ops/verify.md +69 -0
- package/content/patterns/adapter.md +46 -0
- package/content/patterns/facade.md +47 -0
- package/content/patterns/factory.md +46 -0
- package/content/patterns/null-object.md +55 -0
- package/content/patterns/result-vs-exception.md +52 -0
- package/content/patterns/strategy.md +45 -0
- package/content/patterns/template-method-vs-composition.md +50 -0
- package/content/questions/adr.questions.yaml +14 -0
- package/content/questions/analyze.questions.yaml +12 -0
- package/content/questions/bootstrap-mapping.questions.yaml +14 -0
- package/content/questions/bootstrap-rules.questions.yaml +10 -0
- package/content/questions/bootstrap-stack.questions.yaml +23 -0
- package/content/questions/clarify.questions.yaml +10 -0
- package/content/questions/code-style.questions.yaml +12 -0
- package/content/questions/confirm.questions.yaml +12 -0
- package/content/questions/deliver.questions.yaml +23 -0
- package/content/questions/evolve.questions.yaml +15 -0
- package/content/questions/export-template-apply.questions.yaml +11 -0
- package/content/questions/export-template.questions.yaml +23 -0
- package/content/questions/gates.questions.yaml +27 -0
- package/content/questions/generalize.questions.yaml +13 -0
- package/content/questions/gotcha.questions.yaml +11 -0
- package/content/questions/init-template.questions.yaml +10 -0
- package/content/questions/init.questions.yaml +28 -0
- package/content/questions/plan.questions.yaml +14 -0
- package/content/questions/refactor.questions.yaml +23 -0
- package/content/questions/reflect.questions.yaml +13 -0
- package/content/questions/remediate.questions.yaml +27 -0
- package/content/questions/restructure.questions.yaml +15 -0
- package/content/questions/rf-mapping.questions.yaml +12 -0
- package/content/questions/rule-scope.questions.yaml +12 -0
- package/content/questions/rule.questions.yaml +12 -0
- package/content/questions/specify.questions.yaml +23 -0
- package/content/questions/update-artifacts.questions.yaml +10 -0
- package/content/questions/update-strings.questions.yaml +14 -0
- package/content/questions/update.questions.yaml +10 -0
- package/content/questions/verify.questions.yaml +9 -0
- package/content/roles/executor.md +18 -0
- package/content/roles/explorer.md +22 -0
- package/content/roles/reviewer.md +23 -0
- package/content/rubrics/enum-vs-union-vs-branded.md +40 -0
- package/content/rubrics/episodic-vs-semantic-boundary.md +37 -0
- package/content/rubrics/error-classification.md +98 -0
- package/content/rubrics/failure-classification.md +113 -0
- package/content/rubrics/negative-filters.md +38 -0
- package/content/rubrics/solid-triggers.md +121 -0
- package/content/rubrics/template-vs-streaming-precedence.md +34 -0
- package/content/rubrics/validation-boundary.md +94 -0
- package/content/rubrics/when-to-create-dsl.md +65 -0
- package/content/rubrics/when-to-create-lib.md +49 -0
- package/content/rubrics/when-to-create-module.md +39 -0
- package/content/rubrics/when-to-evolve-methodology.md +62 -0
- package/content/skills/adr.md +42 -0
- package/content/skills/analyze.md +34 -0
- package/content/skills/clarify.md +34 -0
- package/content/skills/deliver.md +42 -0
- package/content/skills/diagnose.md +50 -0
- package/content/skills/evolve.md +67 -0
- package/content/skills/export-template.md +40 -0
- package/content/skills/generalize.md +47 -0
- package/content/skills/gotcha.md +40 -0
- package/content/skills/init.md +82 -0
- package/content/skills/plan.md +34 -0
- package/content/skills/policy.md +31 -0
- package/content/skills/promote.md +41 -0
- package/content/skills/protocol.md +42 -0
- package/content/skills/reconcile.md +49 -0
- package/content/skills/refactor.md +84 -0
- package/content/skills/reflect.md +53 -0
- package/content/skills/remediate.md +39 -0
- package/content/skills/restructure.md +52 -0
- package/content/skills/rule.md +44 -0
- package/content/skills/specify.md +34 -0
- package/content/skills/tasks.md +34 -0
- package/content/skills/update.md +56 -0
- package/content/skills/verify.md +31 -0
- package/content/templates/adr.template.md +51 -0
- package/content/templates/evolution-report.template.md +129 -0
- package/content/templates/feature-plan.template.md +86 -0
- package/content/templates/feature-research.template.md +57 -0
- package/content/templates/feature-spec.template.md +86 -0
- package/content/templates/feature-tasks.induced.template.md +67 -0
- package/content/templates/feature-tasks.template.md +60 -0
- package/content/templates/focus.template.md +30 -0
- package/content/templates/gotcha.template.md +31 -0
- package/content/templates/guardrail-runner.template.md +81 -0
- package/content/templates/guardrail.template.md +63 -0
- package/content/templates/learning.template.md +40 -0
- package/content/templates/procedure.template.md +47 -0
- package/content/templates/protocols/p1.md +29 -0
- package/content/templates/protocols/p2.md +28 -0
- package/content/templates/protocols/p3.md +47 -0
- package/content/templates/research.template.md +50 -0
- package/content/templates/session-handoff.template.md +58 -0
- package/content/templates/us.template.md +39 -0
- package/content/tools/gate/core-rules/PLUGIN-13-secrets.sh +36 -0
- package/content/tools/gate/core-rules/PLUGIN-21-code-language.sh +27 -0
- package/content/tools/gate/core-rules/PLUGIN-35-gate-complete.sh +19 -0
- package/content/tools/gate/core-rules/PLUGIN-36-absolute-path.sh +38 -0
- package/content/tools/gate/core-rules/PLUGIN-37-prose-language.sh +56 -0
- package/content/tools/gate/core-rules/PLUGIN-38-parser-marker.sh +82 -0
- package/content/tools/gate/core-rules/PLUGIN-39-frontmatter-ops.sh +325 -0
- package/content/tools/gate/core-rules/PLUGIN-40-dead-reference.sh +74 -0
- package/content/tools/gate/core-rules/README.md +55 -0
- package/content/tools/gate/guardrails.sh +740 -0
- package/content/tools/gate/lib/ratchet.sh +119 -0
- package/content/tools/gate/lib/scope.sh +85 -0
- package/content/tools/gate/ratchet.seed.tsv +1 -0
- package/content/tools/gate/rules/README.md +50 -0
- package/package.json +19 -5
- package/plugin/agy/hooks.json +29 -0
- package/plugin/agy/mcp_config.json +7 -0
- package/plugin/agy/plugin.json +6 -0
- package/plugin/agy/skills/afm-adr/SKILL.md +44 -0
- package/plugin/agy/skills/afm-analyze/SKILL.md +36 -0
- package/plugin/agy/skills/afm-clarify/SKILL.md +36 -0
- package/plugin/agy/skills/afm-deliver/SKILL.md +44 -0
- package/plugin/agy/skills/afm-diagnose/SKILL.md +50 -0
- package/plugin/agy/skills/afm-evolve/SKILL.md +69 -0
- package/plugin/agy/skills/afm-export-template/SKILL.md +42 -0
- package/plugin/agy/skills/afm-generalize/SKILL.md +49 -0
- package/plugin/agy/skills/afm-gotcha/SKILL.md +42 -0
- package/plugin/agy/skills/afm-init/SKILL.md +84 -0
- package/plugin/agy/skills/afm-plan/SKILL.md +36 -0
- package/plugin/agy/skills/afm-policy/SKILL.md +31 -0
- package/plugin/agy/skills/afm-promote/SKILL.md +41 -0
- package/plugin/agy/skills/afm-protocol/SKILL.md +42 -0
- package/plugin/agy/skills/afm-reconcile/SKILL.md +49 -0
- package/plugin/agy/skills/afm-refactor/SKILL.md +86 -0
- package/plugin/agy/skills/afm-reflect/SKILL.md +55 -0
- package/plugin/agy/skills/afm-remediate/SKILL.md +41 -0
- package/plugin/agy/skills/afm-restructure/SKILL.md +54 -0
- package/plugin/agy/skills/afm-rule/SKILL.md +46 -0
- package/plugin/agy/skills/afm-specify/SKILL.md +36 -0
- package/plugin/agy/skills/afm-tasks/SKILL.md +34 -0
- package/plugin/agy/skills/afm-update/SKILL.md +58 -0
- package/plugin/agy/skills/afm-verify/SKILL.md +33 -0
- package/plugin/claude-code/.claude-plugin/marketplace.json +15 -0
- package/plugin/claude-code/.claude-plugin/plugin.json +9 -0
- package/plugin/claude-code/.mcp.json +7 -0
- package/plugin/claude-code/agents/afm-executor.md +18 -0
- package/plugin/claude-code/agents/afm-explorer.md +20 -0
- package/plugin/claude-code/agents/afm-reviewer.md +21 -0
- package/plugin/claude-code/hooks/hooks.json +85 -0
- package/plugin/claude-code/skills/adr/SKILL.md +44 -0
- package/plugin/claude-code/skills/analyze/SKILL.md +36 -0
- package/plugin/claude-code/skills/clarify/SKILL.md +36 -0
- package/plugin/claude-code/skills/deliver/SKILL.md +44 -0
- package/plugin/claude-code/skills/diagnose/SKILL.md +50 -0
- package/plugin/claude-code/skills/evolve/SKILL.md +69 -0
- package/plugin/claude-code/skills/export-template/SKILL.md +42 -0
- package/plugin/claude-code/skills/generalize/SKILL.md +49 -0
- package/plugin/claude-code/skills/gotcha/SKILL.md +42 -0
- package/plugin/claude-code/skills/init/SKILL.md +84 -0
- package/plugin/claude-code/skills/plan/SKILL.md +36 -0
- package/plugin/claude-code/skills/policy/SKILL.md +31 -0
- package/plugin/claude-code/skills/promote/SKILL.md +41 -0
- package/plugin/claude-code/skills/protocol/SKILL.md +42 -0
- package/plugin/claude-code/skills/reconcile/SKILL.md +49 -0
- package/plugin/claude-code/skills/refactor/SKILL.md +86 -0
- package/plugin/claude-code/skills/reflect/SKILL.md +55 -0
- package/plugin/claude-code/skills/remediate/SKILL.md +41 -0
- package/plugin/claude-code/skills/restructure/SKILL.md +54 -0
- package/plugin/claude-code/skills/rule/SKILL.md +46 -0
- package/plugin/claude-code/skills/specify/SKILL.md +36 -0
- package/plugin/claude-code/skills/tasks/SKILL.md +34 -0
- package/plugin/claude-code/skills/update/SKILL.md +58 -0
- package/plugin/claude-code/skills/verify/SKILL.md +33 -0
- package/plugin/opencode/afm.js +173 -0
- package/plugin/opencode/agents/afm-executor.md +31 -0
- package/plugin/opencode/agents/afm-explorer.md +47 -0
- package/plugin/opencode/agents/afm-reviewer.md +48 -0
- package/plugin/opencode/skills/afm-adr/SKILL.md +44 -0
- package/plugin/opencode/skills/afm-analyze/SKILL.md +36 -0
- package/plugin/opencode/skills/afm-clarify/SKILL.md +36 -0
- package/plugin/opencode/skills/afm-deliver/SKILL.md +44 -0
- package/plugin/opencode/skills/afm-diagnose/SKILL.md +50 -0
- package/plugin/opencode/skills/afm-evolve/SKILL.md +69 -0
- package/plugin/opencode/skills/afm-export-template/SKILL.md +42 -0
- package/plugin/opencode/skills/afm-generalize/SKILL.md +49 -0
- package/plugin/opencode/skills/afm-gotcha/SKILL.md +42 -0
- package/plugin/opencode/skills/afm-init/SKILL.md +84 -0
- package/plugin/opencode/skills/afm-plan/SKILL.md +36 -0
- package/plugin/opencode/skills/afm-policy/SKILL.md +31 -0
- package/plugin/opencode/skills/afm-promote/SKILL.md +41 -0
- package/plugin/opencode/skills/afm-protocol/SKILL.md +42 -0
- package/plugin/opencode/skills/afm-reconcile/SKILL.md +49 -0
- package/plugin/opencode/skills/afm-refactor/SKILL.md +86 -0
- package/plugin/opencode/skills/afm-reflect/SKILL.md +55 -0
- package/plugin/opencode/skills/afm-remediate/SKILL.md +41 -0
- package/plugin/opencode/skills/afm-restructure/SKILL.md +54 -0
- package/plugin/opencode/skills/afm-rule/SKILL.md +46 -0
- package/plugin/opencode/skills/afm-specify/SKILL.md +36 -0
- package/plugin/opencode/skills/afm-tasks/SKILL.md +34 -0
- package/plugin/opencode/skills/afm-update/SKILL.md +58 -0
- package/plugin/opencode/skills/afm-verify/SKILL.md +33 -0
- package/README.md +0 -3
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: rubric
|
|
3
|
+
description: Separates what decays from what is permanent, and says where each one lives.
|
|
4
|
+
load: warm
|
|
5
|
+
fires_on: op-call
|
|
6
|
+
called_by: ["reconcile", "reflect"]
|
|
7
|
+
fires_when: 'an artifact needs a tier: does it decay or is it permanent'
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Rubric — episodic vs semantic boundary (what decays vs what is permanent)
|
|
11
|
+
|
|
12
|
+
> This rubric applies invariant principle #1 ("if another agent without my memory needs to know it, it is doc") on the **time** dimension. Not every artifact in `/docs/` is permanent knowledge. v3.1.0+ (Phase 3 of the v3.2 initiative). Ancestor: CoALA (arXiv 2309.02427) — **episodic** memory (what happened) differs from **semantic** memory (what is known) and from **procedural** memory (how to do it).
|
|
13
|
+
|
|
14
|
+
## The tiers in AFM
|
|
15
|
+
|
|
16
|
+
| Tier | What it is | Where it lives | Does it decay? | Enters diagnose axes A1-A7? |
|
|
17
|
+
|---|---|---|---|---|
|
|
18
|
+
| **Semantic** | durable knowledge: rules, architecture, decisions, surprises | `afm.md`, `ach.md`, `prd.md`, `ust.md`, `gotchas.md`, `adr/`, `learnings/`, `rubrics/` | **No** — superseded (ADR Superseded, tombstone), never deleted | **Yes** |
|
|
19
|
+
| **Procedural** | repeatable sequences | `procedures/` | frequency/staleness (`restructure`) | Yes (inherited coverage) |
|
|
20
|
+
| **Episodic** | what happened in one session: work state, handoff | handoff block in `sessions/`, `_focus.md` (current focus) | **Yes** — an aged `open` handoff decays (A2). `_focus` is OVERWRITE | **NO** (it is state, not knowledge) |
|
|
21
|
+
| **Telemetry** | append-only log of events/failures. **real use of the methodology** | `.afm-log/`, `.afm-log-failures/`. **timeline and `afm_findings` in `sessions/`** | only by rotation/retention. audit trail = git | **NO** (append-only) |
|
|
22
|
+
|
|
23
|
+
> **`sessions/` is dual-tier** (v4/F9). One file carries both parts. The **handoff block** decays: `state: open|consumed` governs **only that block**. `consumed` means "already read", never "file is dead". The **telemetry** part does not decay: what was asked, how AFM was used, `afm_findings`. This part is versioned and travels with the repo. It is what an agent in the plugin repo reads to improve AFM from real use. Rule 17's exclusion still applies by closed path — the file is append-per-session, never rewritten.
|
|
24
|
+
|
|
25
|
+
## Decision criteria
|
|
26
|
+
|
|
27
|
+
- **Does another agent without my memory need this to avoid breaking something?** Then it is **semantic**: permanent, enters diagnose, never decays on its own. This is #1.
|
|
28
|
+
- **Is it "where I was" or "what happened in this session"?** Then it is **episodic**: it decays and stays out of diagnose. Giving this permanence fills diagnose with noise.
|
|
29
|
+
- **Promotion from episodic to semantic is the point of `reconcile`.** An episode can reveal a load-bearing decision, rule, or gotcha. When it does, `reconcile` proposes materializing it in the semantic tier. This proposal needs a human gate (#6). The episode is the **buffer** where evolution finds candidates, not the evolution itself.
|
|
30
|
+
|
|
31
|
+
## Anti-patterns
|
|
32
|
+
|
|
33
|
+
- ❌ **Treating handoff or `_focus` as a normative doc.** This fills diagnose (A1-A7) with volatile state. These artifacts are episodic.
|
|
34
|
+
- ❌ **Reading `state: consumed` as "the file can disappear".** `state` governs only the handoff block. The telemetry from the same session stays alive and versioned.
|
|
35
|
+
- ❌ **Letting semantic knowledge decay.** A rule, decision, or gotcha does not decay with age. It is superseded (ADR) or consolidated (`generalize`). It is never deleted for age (#5: institutional memory).
|
|
36
|
+
- ❌ **Letting `_focus.md` grow.** It must stay small state that gets overwritten (rule 20), not a log.
|
|
37
|
+
- ❌ **Promoting an episode to semantic without checking against #1.** Not everything that "happened" is something another agent needs to know.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: rubric
|
|
3
|
+
description: Separates a domain error from a procedure error and from an infrastructure error.
|
|
4
|
+
load: warm
|
|
5
|
+
fires_on: file-shape
|
|
6
|
+
trigger_path: '*/domain/*'
|
|
7
|
+
trigger_grep: '(\b(TRPCError|HttpException|HTTPException|ApiError|ResponseError)\b|throw[[:space:]]+new[[:space:]]+Error\()'
|
|
8
|
+
fires_when: 'a transport error type or a bare `Error` is raised inside a `domain/` path'
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Rubric — error classification (Domain vs. Procedure vs. Infra)
|
|
12
|
+
|
|
13
|
+
## The 3 levels
|
|
14
|
+
|
|
15
|
+
### Domain error — a business rule is violated
|
|
16
|
+
|
|
17
|
+
**Who throws it:** `domain_*` functions (pure business rules).
|
|
18
|
+
|
|
19
|
+
**How to represent it:**
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// Option A: Result<T, E> (preferred for clean flow control)
|
|
23
|
+
type Result<T, E> =
|
|
24
|
+
| { ok: true; value: T }
|
|
25
|
+
| { ok: false; code: E };
|
|
26
|
+
|
|
27
|
+
const result = await domain_createUserAuth({ ctx, input });
|
|
28
|
+
if (!result.ok) {
|
|
29
|
+
switch (result.code) {
|
|
30
|
+
case "EMAIL_TAKEN": ...
|
|
31
|
+
case "WEAK_PASSWORD": ...
|
|
32
|
+
default: {
|
|
33
|
+
const _exhaustive: never = result.code;
|
|
34
|
+
throw new Error(`unhandled domain code: ${_exhaustive}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Option B: throw DomainError when the caller rarely handles each case
|
|
40
|
+
class DomainError<C extends string> extends Error {
|
|
41
|
+
constructor(public readonly code: C, message?: string) { super(message); }
|
|
42
|
+
}
|
|
43
|
+
throw new DomainError("MAGIC_LINK_INVALID");
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**The code must be a literal `as const`.** The switch then guarantees coverage in the caller, as long as `default` assigns the rest to `never` (see example). Without a literal code, a refactor breaks silently.
|
|
47
|
+
|
|
48
|
+
### Procedure error — transport boundary
|
|
49
|
+
|
|
50
|
+
**Who throws it:** procedures (tRPC handlers, route handlers, controllers).
|
|
51
|
+
|
|
52
|
+
**How to represent it:** `TRPCError` (or the framework equivalent — `HTTPException` in FastAPI, `BadRequestException` in Nest).
|
|
53
|
+
|
|
54
|
+
**The boundary is one-way.** The procedure knows Domain and maps code to HTTP status. Domain never imports `TRPCError` (hard rule 15).
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// the procedure does the translation
|
|
58
|
+
const result = await domain_createUserAuth({ ctx, input });
|
|
59
|
+
if (!result.ok) {
|
|
60
|
+
switch (result.code) {
|
|
61
|
+
case "EMAIL_TAKEN":
|
|
62
|
+
throw new TRPCError({ code: "CONFLICT", message: "Email já cadastrado" });
|
|
63
|
+
case "WEAK_PASSWORD":
|
|
64
|
+
throw new TRPCError({ code: "BAD_REQUEST", message: "Senha fraca" });
|
|
65
|
+
default: {
|
|
66
|
+
const _exhaustive: never = result.code;
|
|
67
|
+
throw new Error(`unhandled domain code: ${_exhaustive}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Infra error — external resource failure
|
|
74
|
+
|
|
75
|
+
**Who throws it:** no one on purpose. It rises from the HTTP client, the ORM, or the queue.
|
|
76
|
+
|
|
77
|
+
**How it propagates:**
|
|
78
|
+
- In a **Task-like** context (Trigger.dev, BullMQ): let it rise. The orchestrator's retry policy handles it.
|
|
79
|
+
- In a **Procedure-like** context: catch at the final boundary, log it, and map it to `INTERNAL_SERVER_ERROR`. Never leak the stack trace to the client.
|
|
80
|
+
|
|
81
|
+
## Anti-patterns
|
|
82
|
+
|
|
83
|
+
- ❌ **Domain imports `TRPCError`.** This couples Domain to the transport layer. Hard rule 15 checks it: `grep -rn "TRPCError" {{src-path}}/server/modules/**/domain/` must return 0.
|
|
84
|
+
- ❌ **`throw new Error("generic string")` in domain code.** Without a literal code, the caller cannot handle the error specifically. Use `DomainError({ code: "..." as const })` instead.
|
|
85
|
+
- ❌ **A generic try/catch around every `await`.** Typed errors beat a generic catch. Catch only when you will act on the error — map it, log it specifically, or apply a deliberate fallback. Otherwise let it rise.
|
|
86
|
+
- ❌ **A procedure that returns `{ error: "..." }` instead of throwing.** This breaks the framework contract and forces the client to check `data.error` instead of using the standard mechanism.
|
|
87
|
+
- ❌ **Mixing Domain error and Infra error in the same switch.** Domain code is closed — you know every case. Infra is open — the network can fail in N ways. Handle them separately.
|
|
88
|
+
- ❌ **An "exhaustive" switch with no `never` branch.** When a new code is added to a Domain error, existing callers still compile. They silently ignore the new case. Hard rule 4 catches this only when `default` assigns the rest to `never`.
|
|
89
|
+
|
|
90
|
+
## Decision flow
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
Where am I?
|
|
94
|
+
├── domain function → Result<T, E> with a literal `as const` code
|
|
95
|
+
├── procedure → maps Domain code to TRPCError; lets Infra rise
|
|
96
|
+
├── task → carries idempotency; lets Infra rise (retry handles it)
|
|
97
|
+
└── pure library → Result<T, E> or DomainError with a discriminant code
|
|
98
|
+
```
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: rubric
|
|
3
|
+
description: Routes a recognized failure to a guardrail, a procedure, a learning or to nothing.
|
|
4
|
+
load: warm
|
|
5
|
+
fires_on: op-call
|
|
6
|
+
called_by: ["remediate", "reflect"]
|
|
7
|
+
fires_when: 'a failure was recognized by an external signal and needs a destination'
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Rubric — failure classification (mechanizable → guardrail | mechanical → procedure | conceptual → learning | transient → nothing)
|
|
11
|
+
|
|
12
|
+
> **In this file:** Entry gate (common to all three) · The four destinations · Decision flow · Anti-patterns
|
|
13
|
+
|
|
14
|
+
> The CRITIC loop (`ops/remediate.md`) uses this rubric to remediate a failure recognized and recorded in the state store (`afm failures record`). The discriminator is **executable**. It is not a "feeling". Ancestor: CRITIC (arXiv 2305.11738) — recognize a failure only by external signal. Self-criticism without a tool degrades (PRINCIPLES #3).
|
|
15
|
+
|
|
16
|
+
## Entry gate (common to all three)
|
|
17
|
+
|
|
18
|
+
A failure **exists only** if an **external signal** recognized it.
|
|
19
|
+
|
|
20
|
+
Signals: a test stays red (cap of §5.1 used up), or `tsc`/type-check returns non-zero.
|
|
21
|
+
|
|
22
|
+
Other signals: a step command returns non-zero, or a hard rule breaks after the fact (trigger fired on the wrong side).
|
|
23
|
+
|
|
24
|
+
A user's explicit correction is also a valid signal. "I think I made a mistake" is **not** a failure (CRITIC).
|
|
25
|
+
|
|
26
|
+
Every failure becomes **one record** in the state store, written by `afm failures record --sig <signature>` (the recurrence key).
|
|
27
|
+
|
|
28
|
+
## The four destinations
|
|
29
|
+
|
|
30
|
+
### Mechanizable → `docs/guardrails/<slug>.md` (**takes precedence**)
|
|
31
|
+
|
|
32
|
+
**Both conditions apply:** (1) a **0/1 check can detect the failure without judgment** — a lint rule, type-check, `grep`, test, or verification script. (2) It recurred (same promotion gate as the other destinations: `sig=` appears ≥2 times **OR** the user gave 1 explicit correction).
|
|
33
|
+
|
|
34
|
+
**Why this destination takes precedence over procedure and learning.** The two destinations below produce **documents that inform a reader**. They depend on someone remembering to open the runbook or having read the lesson. A guardrail **does not depend on memory**. The gate fires on its own, in the development environment, before the error reaches review. When a failure fits both categories, choose guardrail. A repeated error fixed by non-mechanical analysis fits exactly this case: the analysis costs context every time, but the check costs effort only once.
|
|
35
|
+
|
|
36
|
+
*Examples:* a broken boundary between modules (an import crossing a layer). A union of literals that does not break code that skips the new case. A required order in an internal DSL. An identifier outside the language required by rule 21.
|
|
37
|
+
|
|
38
|
+
**Where the check lands — discovery is mandatory before any proposal.** Before you propose an installation, **read the verification surfaces the project already has**.
|
|
39
|
+
|
|
40
|
+
Examples: linter config, `tsconfig`/type-check, CI steps, git hooks, test scripts.
|
|
41
|
+
|
|
42
|
+
The proposal **extends an existing surface** — a new rule in the linter that already runs, a step in the workflow that already exists. **Introducing a new tool is a last resort.** State that choice explicitly in the proposal, with its cost. A guardrail that forces the operator to adopt an extra tool to catch one error is often more expensive than the error.
|
|
43
|
+
|
|
44
|
+
**Installing writes outside `docs/`.** So it is a **proposal to the operator**, with surface options (linter · CI · git hook · free text).
|
|
45
|
+
|
|
46
|
+
AFM materializes only the record at `docs/guardrails/<slug>.md`, from [`../templates/guardrail.template.md`](../templates/guardrail.template.md). The record holds `signal:`, `trigger:` once installed, and the chosen `surface:` — the same frontmatter keys the catalog uses.
|
|
47
|
+
|
|
48
|
+
It also holds `origin: recurring-failure`, `Origin: sig=`, and the required **`## What would refute this`** (What would refute this) section.
|
|
49
|
+
|
|
50
|
+
> **There is one procedure for installation**, in [`../../ops/rule.md` § Install the gate](../ops/rule.md) — it covers finding the existing surface, building the runner from the template, measuring HEAD before you turn it on, cost, doc-to-runner sync, and the record. This destination **composes** that procedure. It does not repeat it (rule 34).
|
|
51
|
+
>
|
|
52
|
+
> **`docs/guardrails/` has two origins, and this rubric produces one of them.** `origin: recurring-failure` starts here, from a recurring `sig=`. The other, `origin: hard-rule`, starts from a hard rule already written in `afm.md § 3` whose 0/1 trigger nobody ran. Same artifact, same burden of refutation, different entry point. These are not two families. Describing the same object in two vocabularies is the drift that rule 34 exists to prevent.
|
|
53
|
+
|
|
54
|
+
> **Why the rationale is mandatory.** A future agent who finds the check inconvenient will want to loosen it. The `## What would refute this` section reverses that burden. Changing the guardrail requires **refuting the recorded reason**, not just claiming friction. This is the same discipline as "a broken rule is not a wrong rule" (`ops/reflect.md § 2`), applied to the gate instead of the text.
|
|
55
|
+
|
|
56
|
+
### Mechanical → `docs/procedures/<slug>.md`
|
|
57
|
+
|
|
58
|
+
**All three conditions apply:** (1) it is a **repeatable sequence of steps** with a 0/1 gate per step. (2) It recurred: `afm failures promotable` lists `sig=<hash>` with a count ≥2.
|
|
59
|
+
|
|
60
|
+
A user correction also counts as recurrence, because the user is the oracle. (3) It has a **mechanical start trigger** ("when X, run this runbook").
|
|
61
|
+
|
|
62
|
+
*Example:* the build breaks with `type:module` in tsdown, and this recurred. It became a runbook: "when you edit the emit step, do A, then B, then C, and check 0/1".
|
|
63
|
+
|
|
64
|
+
### Conceptual → `docs/learnings/<slug>.md`
|
|
65
|
+
|
|
66
|
+
**Condition:** the value is in the **cause** (a counterintuitive lesson), not in a mechanical sequence of steps. Same promotion gate (1 user correction OR ≥2 mechanical recurrences). It carries a `**Trigger:**` (trigger) by path, and `afm-pre-edit.sh` injects it for anyone who edits that area.
|
|
67
|
+
*Example:* "I assumed the webhook arrives once. It arrived twice. Always handle it as idempotent." This became a learning with trigger `webhook`.
|
|
68
|
+
|
|
69
|
+
### Transient → nothing (only evidence stays in the log)
|
|
70
|
+
|
|
71
|
+
**Condition:** the retry in §5.1 resolved it (it never reached the terminal). Or no durable fix exists. Or the claim **does not survive a fix to its cause**.
|
|
72
|
+
|
|
73
|
+
Example of the last case: "tool X is broken" hardens into a future refusal even after the tool is fixed — a negative filter.
|
|
74
|
+
|
|
75
|
+
It stays as a line in `.afm-log-failures/`. It does **not** become an artifact. This is principle #5 (do not invent a constraint without a durable case) and the negative filter from Phase 4.
|
|
76
|
+
|
|
77
|
+
## Decision flow
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
Failure recognized by an external signal? ── no → NOT a failure (CRITIC). Stop.
|
|
81
|
+
│ yes
|
|
82
|
+
▼
|
|
83
|
+
Record it in the state store (afm failures record, sig=).
|
|
84
|
+
│
|
|
85
|
+
Resolved by the §5.1 retry / no durable antidote / claim dies when the cause is fixed?
|
|
86
|
+
├── yes → TRANSIENT: stays as evidence, is not promoted.
|
|
87
|
+
└── no
|
|
88
|
+
│
|
|
89
|
+
1 user correction OR sig= recurred ≥2×?
|
|
90
|
+
├── no → still only evidence (waits for recurrence).
|
|
91
|
+
└── yes
|
|
92
|
+
│
|
|
93
|
+
Detectable by a 0/1 check without judgment (lint/type/grep/test)?
|
|
94
|
+
├── yes → GUARDRAIL (docs/guardrails/ + installation proposal)
|
|
95
|
+
└── no
|
|
96
|
+
│
|
|
97
|
+
Repeatable step sequence with 0/1 gates + a start trigger?
|
|
98
|
+
├── yes → PROCEDURE (docs/procedures/)
|
|
99
|
+
└── no → LEARNING (docs/learnings/)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Anti-patterns
|
|
103
|
+
|
|
104
|
+
- ❌ **Recognizing a failure by self-judgment** (with no external signal). CRITIC: this degrades results.
|
|
105
|
+
- ❌ **Promoting one mechanical failure** to an artifact (creates noise, a dead constraint). Wait for ≥2 recurrences, except a user correction, which counts as 1.
|
|
106
|
+
- ❌ **Promoting a transient failure** resolved in §5.1. It never reaches the terminal, so it is not logged as a failure.
|
|
107
|
+
- ❌ **Sending everything to `procedures/`**. A procedure is a *mechanical sequence*. A conceptual lesson is a `learning`.
|
|
108
|
+
- ❌ **Sending everything to `learnings/`**. A repeatable runbook is a `procedure`.
|
|
109
|
+
- ❌ **Writing a procedure or learning for what a check can catch** — this is the central anti-pattern of this rubric. A document that depends on memory loses to a gate that fires on its own. If the check exists, the destination is guardrail.
|
|
110
|
+
- ❌ **Proposing a new tool without reading what the project already runs** — the existing surface comes first. A new tool is a last resort, and you must state its cost.
|
|
111
|
+
- ❌ **A guardrail with no `## What would refute this`** — without the recorded burden of refutation, the next agent loosens the gate by claiming friction.
|
|
112
|
+
- ❌ **Installing the guardrail alone** — installing writes outside `docs/`, so it is a proposal to the operator (#6).
|
|
113
|
+
- ❌ **A procedure, learning, or guardrail with no source `sig=` cited** (traceability — hard rule 19 / R3).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: rubric
|
|
3
|
+
description: Rejects what must not become a durable doc, because a false constraint is poison.
|
|
4
|
+
load: warm
|
|
5
|
+
fires_on: op-call
|
|
6
|
+
called_by: ["remediate", "reconcile"]
|
|
7
|
+
fires_when: 'something is about to become a durable doc'
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Rubric — negative-filters (what NOT to promote to durable doc)
|
|
11
|
+
|
|
12
|
+
> This is a rejection checklist for the entry gate of `gotcha`/`reconcile`/`remediate`. It is the negative side of principle #5 ("do not invent"): **a false constraint is poison**. It is worse than no doc, because it hardens into a future wrong refusal or error. This applies from v3.1.0 (Phase 4 of the v3.2 initiative). Field evidence (ai-memory): in one real corpus, about 28% of pages were low-signal sessions that only polluted retrieval.
|
|
13
|
+
|
|
14
|
+
## Reject (this does NOT become a gotcha, learning, rule, or procedure)
|
|
15
|
+
|
|
16
|
+
| Anti-pattern | Why it is poison | What to do instead |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| **Negative claim about a tool** ("tool X is broken", "library Y does not work") | It hardens into a **fossil refusal**. After the tool is fixed, the doc still makes the next agent avoid it for no reason. | Record the concrete *workaround*, if it is durable. Do not record the verdict "it is broken". Remove the entry when the cause is fixed, because it is transient. |
|
|
19
|
+
| **Transient failure** (fixed on retry, see § 5.1, a missing credential or binary at that moment, a setup state) | It becomes a **dead constraint**. The condition no longer exists, so the doc lies to the future reader. | Keep it as evidence in the state store, not as an artifact. Capture the *retry or fix pattern*, not the temporary failure. |
|
|
20
|
+
| **One-off narrative** ("in this session I did A, B, C") | The timeline already lives in the event ledger and the session handoffs, in the state store. As a normative doc, it is only noise. | Use an episodic handoff, not a semantic doc. |
|
|
21
|
+
| **User-visible status** ("the build passes now", "deploy ok") | This is a momentary state, not knowledge. | Use `_focus.md` (state) or a handoff. Do not use `gotcha` or `adr`. |
|
|
22
|
+
| **Release marker or smoke test** ("v0.1.1 published", "echo ok") | This is operational, not a reusable lesson. | Use `notes` or a log. Never a rule or gotcha. |
|
|
23
|
+
| **Vibe-rule with no trigger** (KISS, DRY treated as a "rule") | Without an executable 0/1 trigger, it is not a hard rule (see #3). | Record it as a principle in `afm.md § 1.3`, not in § 3. |
|
|
24
|
+
|
|
25
|
+
## Promotion test (pass the gate IF)
|
|
26
|
+
|
|
27
|
+
1. **Does it have a durable antidote?** It must be a concrete action that prevents the problem, not just a verdict.
|
|
28
|
+
2. **Does the claim survive a fix to its cause?** If it "goes away when X is fixed", it is transient. Reject it.
|
|
29
|
+
3. **Does another agent without my memory need this to avoid breaking something?** (#1 — otherwise it is memory or state, not doc.)
|
|
30
|
+
4. **Did it happen ≥2 times?** Or is it a user correction? Or is it external SDK documentation? (#5 — otherwise it is evidence, not yet an artifact.)
|
|
31
|
+
|
|
32
|
+
All four **yes** answers promote the item. Any structural **no** keeps it as evidence or state, not a durable doc.
|
|
33
|
+
|
|
34
|
+
## Anti-patterns (of the gate itself)
|
|
35
|
+
|
|
36
|
+
- ❌ **Do not promote out of caution** ("better to record it just in case"). A false constraint is worse than nothing (#5).
|
|
37
|
+
- ❌ **Do not confuse transient with durable.** The test is always "does it survive a fix to its cause?".
|
|
38
|
+
- ❌ **Do not treat every error as a gotcha.** A recognized error goes to the CRITIC loop (`remediate`), which applies this filter.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: rubric
|
|
3
|
+
description: Turns each SOLID principle into a mechanical trigger instead of folklore.
|
|
4
|
+
load: warm
|
|
5
|
+
fires_on: file-shape
|
|
6
|
+
trigger_basename: 'utils.* helpers.* util.* helper.* *-utils.* *-helpers.* *_utils.* *_helpers.* *-manager.* *_manager.* manager.* *-and-*'
|
|
7
|
+
trigger_any: yes
|
|
8
|
+
trigger_grep: 'switch[[:space:]]*\([[:space:]]*[a-zA-Z_.]*(provider|adapter|driver|backend|strategy|channel|kind|variant)'
|
|
9
|
+
fires_when: 'a file name collects unrelated responsibilities, or a provider `switch` is spread across files'
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Rubric — SOLID as a concrete trigger (not folklore)
|
|
13
|
+
|
|
14
|
+
> **In this file:** SRP — Single Responsibility · OCP — Open/Closed: extend without modifying · LSP — Liskov Substitution: interchangeable implementations · ISP — Interface Segregation: minimal port · DIP — Dependency Inversion: domain does not know infra · Summary — what to ask when
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
SOLID is easy to use as an argument and hard to use as a trigger. This rubric converts each principle into a **mechanical trigger** — when to apply it, the anti-pattern, an example.
|
|
18
|
+
|
|
19
|
+
## SRP — Single Responsibility (already hard rule 5)
|
|
20
|
+
|
|
21
|
+
**Trigger:** the file or function name has "and" / "manager" / "utils" / "helpers" without a domain prefix.
|
|
22
|
+
|
|
23
|
+
**Apply:** split it. One file, one responsibility.
|
|
24
|
+
|
|
25
|
+
Anti-pattern: `notification-and-billing.ts`, `user-manager.ts`, `helpers.ts`.
|
|
26
|
+
|
|
27
|
+
## OCP — Open/Closed: extend without modifying
|
|
28
|
+
|
|
29
|
+
**Trigger:** you are about to add a **new provider**. Examples: registry NPM/GitHub/Custom, storage S3/R2/MinIO, a new plan, a new payment method, a new notification channel.
|
|
30
|
+
|
|
31
|
+
**Apply:** add a **new file** that implements a typed port. A central registry injects it. **No** `switch (provider)` spread across 2+ files.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// ✅ port + adapters + registry
|
|
35
|
+
type RegistryAdapter = {
|
|
36
|
+
publish(tarball: Buffer, opts: PublishOpts): Promise<PublishResult>;
|
|
37
|
+
};
|
|
38
|
+
const adapters: Record<RegistryKind, RegistryAdapter> = {
|
|
39
|
+
npm: npmAdapter,
|
|
40
|
+
github: githubAdapter,
|
|
41
|
+
custom: customAdapter,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// ❌ switch spread across 3 files
|
|
45
|
+
function publish(kind: RegistryKind, ...) {
|
|
46
|
+
if (kind === "npm") { /* npm logic */ }
|
|
47
|
+
else if (kind === "github") { /* github logic */ }
|
|
48
|
+
// ... and again in other functions
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## LSP — Liskov Substitution: interchangeable implementations
|
|
53
|
+
|
|
54
|
+
**Trigger:** you have **2+ implementations of the same port** (RegistryAdapter, StorageAdapter, EmailAdapter).
|
|
55
|
+
|
|
56
|
+
**Apply:** all implementations return the **same classified result shape**. Adapter A must not return `null` on conflict while adapter B throws an exception.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
// ✅ every implementation returns the same discriminated Result
|
|
60
|
+
type PublishResult =
|
|
61
|
+
| { code: "PUBLISHED"; version: string }
|
|
62
|
+
| { code: "CONFLICT"; existingVersion: string }
|
|
63
|
+
| { code: "FAILED"; reason: string };
|
|
64
|
+
|
|
65
|
+
// ❌ adapter A: returns null on conflict
|
|
66
|
+
async function publishNpm(): Promise<{ version: string } | null> { ... }
|
|
67
|
+
// ❌ adapter B: throws an exception
|
|
68
|
+
async function publishGithub(): Promise<{ version: string }> {
|
|
69
|
+
throw new ConflictError(...);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## ISP — Interface Segregation: minimal port
|
|
74
|
+
|
|
75
|
+
**Trigger:** a domain function receives the whole `ctx: TRPCContext` but uses only `ctx.db` and `ctx.log`.
|
|
76
|
+
|
|
77
|
+
**Apply:** declare only what you use: `ctx: Pick<TRPCContext, "db" | "log">`. Otherwise the test must mock everything.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// ✅ minimal port
|
|
81
|
+
async function domain_createSessionAuth({
|
|
82
|
+
ctx,
|
|
83
|
+
input,
|
|
84
|
+
}: {
|
|
85
|
+
ctx: Pick<TRPCContext, "db" | "log">;
|
|
86
|
+
input: { userId: string };
|
|
87
|
+
}): Promise<Result<IssuedSession, "USER_NOT_FOUND">> { ... }
|
|
88
|
+
|
|
89
|
+
// ❌ whole ctx, hides the real dependency
|
|
90
|
+
async function domain_createSessionAuth({ ctx, input }: DomainInput<...>) {
|
|
91
|
+
// uses only ctx.db, but the test must mock ctx.user, ctx.session, ctx.headers, ...
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## DIP — Dependency Inversion: domain does not know infra
|
|
96
|
+
|
|
97
|
+
**Trigger:** a domain function or pure lib is about to import something from a framework (`TRPCError`, `req.headers`, a direct ORM client with no abstraction).
|
|
98
|
+
|
|
99
|
+
**Apply:** invert the direction. Domain defines the interface. Infra implements it. Domain imports only from layers below it (pure lib, types).
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
✅ correct direction (all paths rooted at {{src-path}}/):
|
|
103
|
+
app/ → server/ → server/modules/X/ → domain/X/ → lib/ → types
|
|
104
|
+
|
|
105
|
+
Each layer knows the layer below it, never the layer above it.
|
|
106
|
+
|
|
107
|
+
❌ common inversions:
|
|
108
|
+
- {{src-path}}/domain/X/ importing TRPCError (transport leaks up into domain)
|
|
109
|
+
- {{src-path}}/lib/Y/ importing from server (lib is no longer publishable)
|
|
110
|
+
- {{src-path}}/lib/Y/ importing an ORM type (couples lib to infra)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Summary — what to ask when
|
|
114
|
+
|
|
115
|
+
| Symptom | Principle | Fix |
|
|
116
|
+
| --- | --- | --- |
|
|
117
|
+
| File is growing, doing 2+ things | SRP | Split it |
|
|
118
|
+
| You are adding a 4th variant of something | OCP | Port + adapters, not a switch |
|
|
119
|
+
| 2 implementations of the same thing return different shapes | LSP | Standardize on a discriminated Result |
|
|
120
|
+
| Function receives a giant ctx but uses 2 fields | ISP | Pick / minimal interface |
|
|
121
|
+
| Domain wants to import something from above | DIP | Invert: domain defines, infra implements |
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: rubric
|
|
3
|
+
description: Picks one source when the frozen template and the streaming learning disagree.
|
|
4
|
+
load: warm
|
|
5
|
+
fires_on: op-call
|
|
6
|
+
called_by: ["export-template", "reconcile"]
|
|
7
|
+
fires_when: 'frozen template knowledge and a streaming learning are in conflict'
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Rubric — frozen template (offline) vs. streaming learning (online): precedence, never sum
|
|
11
|
+
|
|
12
|
+
> **Load-bearing finding from AWM (arXiv:2409.07429):** offline knowledge (the frozen architectural template from `export-template`) and online knowledge (the learning that `reconcile`/`restructure` accumulate in streaming during the project life) are **non-additive**. Combining the two hurts. The combined result falls *between* the two and matches neither. AFM has both layers. AFM needs an explicit rule of **precedence + tombstone**. AFM must never merge the two.
|
|
13
|
+
|
|
14
|
+
The single rule: when offline and online conflict, **pick ONE and tombstone the other**. Keep the provenance. Never keep both as a "sum". Never merge the content.
|
|
15
|
+
|
|
16
|
+
## Decision table
|
|
17
|
+
|
|
18
|
+
| Situation | Rule |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| The project has a **template origin** (`docs/.template` was the seed) AND `reconcile` proposes a rule or gotcha that **contradicts** an artifact inherited from the template | **Streaming wins locally.** Apply the new rule or gotcha. Mark the inherited artifact with `*(overridden — see reconcile NNN / cycle NNN)*`. **Do not merge** the two. Non-additivity means mixing is worse than either alone. |
|
|
21
|
+
| The **same knowledge** exists in the frozen template **and** in a streaming learning (a duplicate, not a conflict) | **Pick ONE** (usually the more recent, local copy). Tombstone the other. Do not keep both copies as a "sum". That creates the inflation `diagnose A6` later flags. |
|
|
22
|
+
| `export-template` is about to **freeze** a project that already accumulated streaming learnings | Consolidate the streaming learnings **via `afm generalize` BEFORE** the export. They become part of the frozen template (offline), not a parallel layer. This avoids freezing half the knowledge and leaving the other half to collide with the consumer's own reconcile later. |
|
|
23
|
+
|
|
24
|
+
## Why not merge
|
|
25
|
+
|
|
26
|
+
- **AWM measured this:** combined offline and online knowledge lands *between* the two and matches neither. Silent fusion is the worst outcome.
|
|
27
|
+
- **Tombstone, not deletion:** precedence preserves provenance. The losing artifact becomes a note. It does not disappear. This matches the discipline of `generalize` (a merged gotcha becomes a tombstone) and principle #6 (deletion is a bright line).
|
|
28
|
+
- **`reflect` and `generalize` stay valid** *within* each layer. This rubric forbids only mixing **between** layers without deciding precedence.
|
|
29
|
+
|
|
30
|
+
## Counter-examples
|
|
31
|
+
|
|
32
|
+
- ❌ Do not keep the inherited template rule **and** the new reconcile rule that contradicts it, hoping "both count". This violates non-additivity. Decide the precedence instead.
|
|
33
|
+
- ❌ Do not let `export-template` freeze a project with pending streaming learnings unconsolidated. The consumer then inherits a partial layer that will fight its own reconcile.
|
|
34
|
+
- ❌ Do not merge the text of both into a third "combined" version. That is exactly the *between-the-two* result AWM showed to be worse.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: rubric
|
|
3
|
+
description: Says where the parser goes, so the domain never validates its own input.
|
|
4
|
+
load: warm
|
|
5
|
+
fires_on: file-shape
|
|
6
|
+
trigger_path: '*/domain/*'
|
|
7
|
+
trigger_grep: '(from[[:space:]]+[''"]zod[''"]|require\([''"]zod[''"]\)|\bz\.(object|string|number|enum|array)\()'
|
|
8
|
+
fires_when: 'a schema parser is used inside a `domain/` path'
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Rubric — where Zod (or the parser) goes
|
|
12
|
+
|
|
13
|
+
## The single rule
|
|
14
|
+
|
|
15
|
+
The schema parser (Zod or the stack equivalent) **validates in exactly 2 places**:
|
|
16
|
+
|
|
17
|
+
1. **Procedure input / transport handler** — request to validated type.
|
|
18
|
+
2. **External payload parsing** — webhook, crawled source, env var, queue message, DB JSON with unknown shape.
|
|
19
|
+
|
|
20
|
+
**Domain receives an already validated, typed shape.** Pure lib code does too. Re-validating inside domain "for safety" duplicates the schema's source of truth.
|
|
21
|
+
|
|
22
|
+
## Why this rule exists
|
|
23
|
+
|
|
24
|
+
- **Validating twice wastes effort** (CPU cost, plus a schema kept in 2 places).
|
|
25
|
+
- **Domain tests logic, not parsing.** A mock input with the wrong type does not compile. TS is already the barrier.
|
|
26
|
+
- **Schema lives near the boundary** where external data arrives, not wherever "seemed like a good idea."
|
|
27
|
+
|
|
28
|
+
## Examples
|
|
29
|
+
|
|
30
|
+
### ✅ Correct: procedure validates input before it calls domain
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
const router_apps = router({
|
|
34
|
+
procedure_createApps: publicProcedure
|
|
35
|
+
.input(z.object({
|
|
36
|
+
packageName: z.string().min(1),
|
|
37
|
+
schemaUrl: z.string().url(),
|
|
38
|
+
}))
|
|
39
|
+
.mutation(async ({ ctx, input }) => {
|
|
40
|
+
// input is already { packageName: string; schemaUrl: string }
|
|
41
|
+
const result = await domain_createApps({ ctx, input });
|
|
42
|
+
// ...
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### ✅ Correct: webhook handler validates the external payload
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
export async function POST(req: Request) {
|
|
51
|
+
const raw = await req.text();
|
|
52
|
+
const event = stripe_verifyWebhook(raw, signature); // validates the signature
|
|
53
|
+
const payload = zStripeEvent.parse(event); // validates the shape
|
|
54
|
+
await domain_handleStripeEventBillings({ ctx, input: payload });
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### ❌ Wrong: domain re-validates "for safety"
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
async function domain_createApps({ ctx, input }: DomainInput<{ packageName: string; schemaUrl: string }>) {
|
|
62
|
+
// ❌ Wrong: the procedure already validated this. It adds noise and duplicates the schema.
|
|
63
|
+
const validated = z.object({
|
|
64
|
+
packageName: z.string().min(1),
|
|
65
|
+
schemaUrl: z.string().url(),
|
|
66
|
+
}).parse(input);
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### ✅ Correct: pure lib receives the shape and does not validate
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// lib/sdk-generator/index.ts
|
|
74
|
+
export function generateSdk({ schema, options }: { schema: OpenApiV3; options: GenOpts }): SdkFiles {
|
|
75
|
+
// No z.parse call here. The schema arrives already validated from the caller.
|
|
76
|
+
// If the caller passed the wrong shape, that is the caller's bug, not something the lib must guard against.
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## When "validate at the boundary" grows
|
|
81
|
+
|
|
82
|
+
Suppose 6 similar procedures all validate the same `apps` shape.
|
|
83
|
+
|
|
84
|
+
- Extract the schema to `{{src-path}}/server/modules/apps/schemas.ts`.
|
|
85
|
+
- Each procedure imports it: `.input(zCreateAppsInput)`.
|
|
86
|
+
- The schema is still declared **once**, but reused across procedures.
|
|
87
|
+
- This stays a boundary pattern. Only the duplication is gone.
|
|
88
|
+
|
|
89
|
+
## More anti-patterns
|
|
90
|
+
|
|
91
|
+
- ❌ Wrong: validating input inside a domain function "for DI" or "for defense in depth." TS plus the boundary schema already cover this.
|
|
92
|
+
- ❌ Wrong: declaring a schema inside the domain file. Move it to `schemas.ts` or `*.types.ts` in the module (hard rule 7).
|
|
93
|
+
- ❌ Wrong: `z.unknown().parse(...)` to dodge a type. This validates nothing and gives false safety. Use a real `z.object({...})` or accept the upstream type.
|
|
94
|
+
- ❌ Wrong: validating env vars with scattered `process.env.X || throw` calls. Centralize this in `{{src-path}}/server/env.ts` with Zod, and validate once at boot.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: rubric
|
|
3
|
+
description: Decides when a DSL is worth suggesting, and when it is premature.
|
|
4
|
+
load: warm
|
|
5
|
+
fires_on: repo-shape
|
|
6
|
+
detector: dsl
|
|
7
|
+
fires_when: 'three or more call sites repeat one fixed-order sequence'
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Rubric — when to create a DSL (suggest, do not build)
|
|
11
|
+
|
|
12
|
+
> DSL here means any domain mini-language that collapses repeated boilerplate of primitive calls: a fluent builder, a combinator pipeline, a declarative object that gets interpreted, or a parsed language. **DSL-first is the complement of the Rule of Three, not the opposite of KISS.** The agent does not abstract early. But when primitive boilerplate scales AND has grammar, a DSL pays for itself. The agent **suggests** (proposes and sketches). The agent **does not build alone** (hard rule 11 — a DSL is a first-class layer or abstraction, so it stops and asks).
|
|
13
|
+
|
|
14
|
+
## Binary decision
|
|
15
|
+
|
|
16
|
+
**Suggest a DSL only when BOTH signals are true:**
|
|
17
|
+
|
|
18
|
+
- ✅ **Repetition (Rule of Three of sequence):** 3 or more call sites repeat the same sequence of N or more primitive calls, in fixed order. This is not 3 loose calls. The *sequence* itself repeats. Only the parameters vary, not the structure.
|
|
19
|
+
- ✅ **Boilerplate dominates the intent (ratio):** at these sites, most lines are mechanical wiring, setup, or glue, not real business logic. The reader cannot see *what* the code wants through *how* it wires the primitives together.
|
|
20
|
+
|
|
21
|
+
**A third condition tells a DSL apart from "extract a function":**
|
|
22
|
+
|
|
23
|
+
- ✅ **Reusable grammar exists** — composition, required ordering, branching, or chaining that repeats. This is what makes a DSL pay more than a helper. Without grammar (just "call 5 functions in the same fixed sequence"), **a named helper wins.** That case is not a DSL.
|
|
24
|
+
|
|
25
|
+
**Do NOT suggest a DSL when:**
|
|
26
|
+
|
|
27
|
+
- ❌ **Fewer than 3 repeated sequences.** The Rule of Three has not been met. Wait for the third case (YAGNI).
|
|
28
|
+
- ❌ **A simple helper or builder already collapses the boilerplate.** KISS beats DSL here. A named function (`stripe_createCheckoutSession`) or a one-level builder wins over a mini-language. Use a DSL only when the helper cannot capture the grammar.
|
|
29
|
+
- ❌ **The "boilerplate" is real per-site logic.** Genuine variation between sites is not boilerplate. Abstracting it would hide a difference that matters.
|
|
30
|
+
- ❌ **"This will scale" or "we can parametrize everything."** This is YAGNI. Do not build a speculative DSL for the general case before 3 real cases exist.
|
|
31
|
+
|
|
32
|
+
## Why these rules
|
|
33
|
+
|
|
34
|
+
- **Both signals together, not one.** Repetition without dominant boilerplate: a helper solves it. High boilerplate without repetition (1 site): it is just a large function — split it or inline it. A DSL is expensive (new layer, indirection, a learning curve, debugging through the abstraction). It pays off only at the intersection of both signals.
|
|
35
|
+
- **Grammar is the DSL-versus-helper discriminator.** Extracting a function removes duplication. A DSL gives you a *language* to express the intent. If there is nothing to "compose," a function is enough, and cheaper.
|
|
36
|
+
- **Suggest, do not build (rule 11 / autonomy principle 6, bright line d).** Creating a DSL introduces a first-class layer or abstraction. This is a load-bearing architectural decision. The agent brings the proposal, with an API sketch and a candidate ADR. The architecture owner decides. Building a DSL on its own would cross the bright line.
|
|
37
|
+
|
|
38
|
+
## Form — pick the lightest one that removes the boilerplate (KISS inside the DSL)
|
|
39
|
+
|
|
40
|
+
In increasing order of cost — **stop at the first form that captures the grammar:**
|
|
41
|
+
|
|
42
|
+
1. **Fluent builder / factory function** — simple chaining (`q().where().limit()`). This is the cheapest form. It covers most cases.
|
|
43
|
+
|
|
44
|
+
**Purity:** chain methods only assemble immutable data. IO stays in one terminal call (`.exec()` or `.run()`). A side effect in the middle of the chain forces you to mock the world to test a pure rule. This is the "mock that simulates more than needed" smell (see § 5). **Hard rule 7** separates pure domain from glue for this exact reason. A builder placed outside `{{src-path}}/<domain>/` escapes that separation.
|
|
45
|
+
|
|
46
|
+
Counter-example: `checkout().comValor(v).cobrarNoStripe().comDesconto(d)`. Here `cobrarNoStripe()` sits mid-chain and mixes IO with data assembly.
|
|
47
|
+
2. **Combinators / pipeline** — small functions that compose (`pipe(parse, validate, persist)`). Use when the grammar is composition.
|
|
48
|
+
3. **Declarative interpreted object** — data-driven config that a runner walks through. Use when the "language" is a data structure.
|
|
49
|
+
4. **Parsed mini-language** (string to AST to exec) — **last resort**, rarely justified in a product app. It needs a parser, error handling, and tests for the parser itself. Use it only when the 3 forms above genuinely cannot express the grammar.
|
|
50
|
+
|
|
51
|
+
## Before proposing — self-check (verification corollary 6)
|
|
52
|
+
|
|
53
|
+
1. Do the 3 sites repeat the same *sequence*, or do they just use the same primitives in different orders? (Different orders mean it is not one DSL.)
|
|
54
|
+
2. Would a one-level helper collapse 80% of the boilerplate? If yes, propose the helper, not the DSL.
|
|
55
|
+
3. Does the proposed DSL hide a real difference between the sites? If yes, it will leak — rethink the boundary.
|
|
56
|
+
4. Which form above is the lightest one that captures the grammar? Do not propose a parser if a builder solves it.
|
|
57
|
+
5. Is there a required order between chain steps? If yes, the proposed form must reject the wrong order at **compile time**, not a runtime `throw`. Use type-state: each method returns a type that exposes only the next legal methods. **If type-state gets expensive, do not propose the builder.** A single function with all required arguments solves the same problem without the technique.
|
|
58
|
+
|
|
59
|
+
## Counter-examples
|
|
60
|
+
|
|
61
|
+
- ❌ Building a standalone query DSL at the 2nd call site — the Rule of Three has not been met. Wait for the third case, or use a helper.
|
|
62
|
+
- ❌ Proposing a parsed mini-language for 3 sequences that a 20-line fluent builder would solve — too expensive a form (use level 1 instead).
|
|
63
|
+
- ❌ Calling 3 handlers that check different things "boilerplate" — this is per-site logic, not repeated wiring. A DSL would hide the difference.
|
|
64
|
+
- ❌ Building the DSL alone because "it is obviously needed" — this is rule 11. Propose it, sketch the API, and open a candidate ADR. The human approves the new layer.
|
|
65
|
+
- ❌ A DSL for the general case ("supports any future provider") built from 3 real providers — this is YAGNI. Model the 3 real cases and generalize when a 4th case actually diverges.
|