@tenkara/afm-linux-arm64 0.0.0-stage → 4.0.0-alpha.7
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 +196 -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,223 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: op
|
|
3
|
+
description: Brings a materialized instance up to the current plugin version.
|
|
4
|
+
load: warm
|
|
5
|
+
writes: ["docs/.afm-version", "docs/evolution/*.md"]
|
|
6
|
+
composes: []
|
|
7
|
+
---
|
|
8
|
+
# Sub-flow: sync of the materialized instance with the current plugin version (`update`)
|
|
9
|
+
|
|
10
|
+
> **In this file:** What update is / is not · Procedure · What the codemods can do · Instances older than 4.0 · 1.6 Language migration · Opt-in deltas · Post-conditions · Suppression contract · Anti-patterns
|
|
11
|
+
|
|
12
|
+
> **Form** — effect: deltas in the instance + bump · trigger: `afm update`, composition · precondition: pin ≠ the shipped version · composed by: `evolve` 2.2 · **degree of freedom: low**
|
|
13
|
+
|
|
14
|
+
Triggers: `afm update`, or natural language for **version bump / sync with the plugin**. Examples:
|
|
15
|
+
"update AFM here", "bump the methodology version", "sync `docs/` with the new plugin", "migrate to the new
|
|
16
|
+
AFM version", "my `.afm-version` is behind".
|
|
17
|
+
|
|
18
|
+
> **The write target is the local docs (`docs/`), NEVER the plugin.** `afm update` reads the shipped
|
|
19
|
+
> content only to **discover the deltas**. It **materializes** the consumer-facing ones
|
|
20
|
+
> in the local instance. It never edits the plugin (bright line #7). See `afm path ops/evolve` § The
|
|
21
|
+
> target of evolution.
|
|
22
|
+
>
|
|
23
|
+
> **Sync mechanism, not orchestrator.** `update` resolves the **instance-to-plugin version drift** (axis
|
|
24
|
+
> A3, which `afm diagnose` *detects* but does not fix). It runs à la carte (this doc, with its own gate)
|
|
25
|
+
> or inside the `evolve` cycle, which composes it when `afm diagnose` flags A3, with the own gate
|
|
26
|
+
> suppressed (see § Suppression contract).
|
|
27
|
+
|
|
28
|
+
The command owns everything mechanical: reading the pin, comparing versions, running the **codemod
|
|
29
|
+
registry** of every release between the pin and the binary (ADR-0013), proving each op, keeping the manifest
|
|
30
|
+
and moving the pin at the end. This op owns the three things it cannot do: the agent tasks, the language
|
|
31
|
+
migration and the post-conditions.
|
|
32
|
+
|
|
33
|
+
## When it is another op instead
|
|
34
|
+
|
|
35
|
+
- `init` / `refactor` (bootstrap) **create** the instance from zero. `update` assumes the instance
|
|
36
|
+
**already exists** and only brings it forward. `afm refactor` refuses a project with `.afm-version`;
|
|
37
|
+
`update` is the correct path for that case.
|
|
38
|
+
- `diagnose` only **detects** A3 (read-only). `update` **resolves** it. Diagnose finds it, update fixes it.
|
|
39
|
+
- `evolve` is the full health cycle. `update` is **one lens** (version sync), composed when A3 fires.
|
|
40
|
+
- `promote` **logs** a finding meant for the plugin. `update` goes plugin to **instance**. Neither op
|
|
41
|
+
touches the shipped content: an absolute boundary (#7).
|
|
42
|
+
- Not a resolver of `[TBD]`. Not an inventor of product content (#4/#5). It materializes only what the
|
|
43
|
+
plugin already provides: stack-agnostic rubrics, hygiene rules, template sections. It never fills in a
|
|
44
|
+
product decision.
|
|
45
|
+
|
|
46
|
+
## Procedure
|
|
47
|
+
|
|
48
|
+
1. **Preview:** `afm update --dry-run`. It runs the pre-check and prints every codemod with its result
|
|
49
|
+
against an overlay of the docs, writing nothing:
|
|
50
|
+
- Exit 3: no `docs/.afm-version`. Stop and run `afm init` (greenfield) or `afm refactor` (brownfield).
|
|
51
|
+
- Exit 5: the pin and the binary differ by a major version. Run `afm migrate` (the 3.x to 4.0 bridge),
|
|
52
|
+
then `afm update` again.
|
|
53
|
+
- *already up to date*: no drift means nothing to do, and an empty diagnosis is a valid result.
|
|
54
|
+
- *anomaly*: the pin is **ahead** of the binary. Someone edited the instance by hand, or the binary
|
|
55
|
+
is old. Report it, upgrade the binary, and never roll the pin back.
|
|
56
|
+
2. **Read the preview as one package, not N loose items.** The tally counts what each op did:
|
|
57
|
+
- *applied*: the op wrote (a literal replacement, a move, a created or synced file, a frontmatter key,
|
|
58
|
+
a section, a regenerated surface).
|
|
59
|
+
- *already applied or absent*: the proof of need failed (the old text is not there), so the delta was
|
|
60
|
+
already applied or never existed in this instance. Silent, and not work done.
|
|
61
|
+
- *held*: `[TBD]` content protects the text (#4), or the owner edited a file a codemod would delete.
|
|
62
|
+
**Stop and report it**: the owner's stance wins.
|
|
63
|
+
- *failed*: a proof of application did not hold. The pin stays; fix the cause and run again.
|
|
64
|
+
- *agent task(s) pending*: work only judgment can do (below).
|
|
65
|
+
- a release with nothing to materialize counts toward the bump and prints no line. Do not invent work
|
|
66
|
+
where the plugin declared there is none.
|
|
67
|
+
3. **Own gate (à la carte).** Show the preview as evidence (which version to which version, what each
|
|
68
|
+
codemod changes in the instance), then ask `afm ask update` (questions in
|
|
69
|
+
`content/questions/update.questions.yaml`). Under the `evolve` cycle this gate is **suppressed**: the
|
|
70
|
+
migration item moves up to the orchestrator's single gate.
|
|
71
|
+
4. **Apply:** `afm update`. It runs every codemod, proves each one, prints the tally and **moves
|
|
72
|
+
`docs/.afm-version` only at the end**. The exit code tells what is left:
|
|
73
|
+
- Exit 0: done, the pin moved.
|
|
74
|
+
- Exit 1: a codemod failed (or the pin is ahead). The pin stays (marker honesty: never bump as if the
|
|
75
|
+
migration were complete). Fix it and run again.
|
|
76
|
+
- **Exit 6: agent tasks are pending.** Do them (step 5), then run `afm update` again. The pin stays until
|
|
77
|
+
every task passes its check.
|
|
78
|
+
- **Protected surfaces** are never materialized by an update: the shipped content itself,
|
|
79
|
+
`PRINCIPLES`, `[TBD]` content, the format the hooks parse, the rubric that decides when the evolution
|
|
80
|
+
cycle runs (`when-to-evolve-methodology`) and the `triggers` entries that fire the evolution
|
|
81
|
+
mechanisms. That is self-reference: the sync does not materialize the document that decides when
|
|
82
|
+
the cycle runs. New stack-agnostic rubrics, yes.
|
|
83
|
+
5. **Do each agent task** listed after the tally. A line reads `agent <codemod>: <brief> (scope: <paths>)`
|
|
84
|
+
followed by the lines of its check that still fail. Work only inside the scope, through the write ops
|
|
85
|
+
(`afm adr`, `afm gotcha`, `afm rule`, `afm new <type>`), suppressing each op's own per-artifact
|
|
86
|
+
question. Run `afm lint <path>` to see the frontmatter findings and `afm lint --fix <path>` to fix
|
|
87
|
+
the ones that have one exact answer. Never invent content: what the operator did not say stays `[TBD]`.
|
|
88
|
+
A `[TBD]` placeholder alone lints clean and does not hold the pin.
|
|
89
|
+
6. **Close a sync on an edited file** with `afm update --accept <path>`. When a codemod would overwrite a
|
|
90
|
+
materialized file the owner edited (its hash in the manifest no longer matches), it hands the merge to
|
|
91
|
+
the agent instead. Merge the new shipped text into the owner's file, then run
|
|
92
|
+
`afm update --accept docs/<path>`: it records the file as it is now as the accepted version. It never
|
|
93
|
+
edits the file.
|
|
94
|
+
7. **Run `afm update` again** until it prints `pin: <from> -> <to>` (or *already up to date*). Then the
|
|
95
|
+
**post-conditions** (below) and the **log**.
|
|
96
|
+
|
|
97
|
+
## What the codemods can do
|
|
98
|
+
|
|
99
|
+
A release is a module in `packages/core/src/features/codemod-releases/data/` with an ordered list of typed ops. The rules that
|
|
100
|
+
make them safe are 0/1 and none depends on judgment:
|
|
101
|
+
|
|
102
|
+
| Op | What it does | Proof of need | When it hands over |
|
|
103
|
+
| --- | --- | --- | --- |
|
|
104
|
+
| `replace` | literal substitution in one file | the old text is in the file | `[TBD]` there: *held* |
|
|
105
|
+
| `move` | renames a file or a folder | the source exists | a target that already exists |
|
|
106
|
+
| `create` | writes a shipped file the instance lacks | the file is absent | never overwrites |
|
|
107
|
+
| `sync` | overwrites a materialized file with the new shipped text | the manifest hash still equals the file | an edited file becomes an agent task (`--accept`) |
|
|
108
|
+
| `delete` | removes a file the plugin retired | the manifest hash still equals the file | an edited file is *held* |
|
|
109
|
+
| `frontmatter` | sets or renames a key | the key is missing or old | `[TBD]` there: *held* |
|
|
110
|
+
| `section` | adds a section to a doc | the heading is absent | never rewrites an existing one |
|
|
111
|
+
| `regenerate` | rebuilds a generated surface (agents block, stubs, index) | the surface is stale | none |
|
|
112
|
+
| `custom` | a program for what the others cannot say (the OKF lift) | its own | its own agent tasks |
|
|
113
|
+
| `agent` | what only judgment can do (a rewrite, a merge) | its own | always, with a mechanical check |
|
|
114
|
+
|
|
115
|
+
**The rules:**
|
|
116
|
+
|
|
117
|
+
1. **Proof of need before a file is touched.** Zero matches means already applied: skip it silently.
|
|
118
|
+
2. **Literal substitution, never a regular expression.** A pattern on an instance the owner edited by
|
|
119
|
+
hand is how a sync destroys text nobody asked to change.
|
|
120
|
+
3. **Proof of application afterwards.** The old text is gone **and** the new text appears at least as often.
|
|
121
|
+
4. **A file missing from the instance is skipped.** A doc the consumer never materialized does not arrive
|
|
122
|
+
through the back door, except through an explicit `create`.
|
|
123
|
+
5. **`[TBD]` is untouchable** (#4): text inside a block the owner marked this way is *held*.
|
|
124
|
+
6. **The manifest is the memory of what AFM wrote.** `docs/.afm-manifest.json` maps each file AFM wrote to
|
|
125
|
+
the hash of the text it wrote. `sync` and `delete` act only on a file whose hash still matches. A file
|
|
126
|
+
the owner changed is never overwritten or removed silently: the old "update never deletes" is now
|
|
127
|
+
"update never deletes what the owner changed". Commit the manifest with the docs.
|
|
128
|
+
7. **The proof of need decides, not only the pin.** A codemod whose precondition still holds runs even when
|
|
129
|
+
the pin is past its release.
|
|
130
|
+
8. **The pin moves last**, and only when every agent task passes its check.
|
|
131
|
+
|
|
132
|
+
> **Discover-first (#2):** the proof of need is the discover-first rule. A delta the operator already copied
|
|
133
|
+
> by hand never reappears.
|
|
134
|
+
|
|
135
|
+
## Instances older than 4.0
|
|
136
|
+
|
|
137
|
+
`afm update` refuses to cross a major version (exit 5). A 3.x instance goes through `afm migrate` first
|
|
138
|
+
(`--dry-run` to preview): the bridge to 4.0, the split of `docs/gotchas.md` into `docs/gotchas/` and of
|
|
139
|
+
`afm.md` section 3 into `docs/rules/`, and the report names. Run it once and **commit before anything
|
|
140
|
+
else** (a structural migration mixed with a content edit hides both in the diff). Then run `afm update`
|
|
141
|
+
to receive the codemods of the 4.x releases (the OKF frontmatter lift among them), and
|
|
142
|
+
`afm index --materialize` (hard rule 39 fails on a missing or stale index).
|
|
143
|
+
|
|
144
|
+
## 1.6 Language migration (runs when the instance is not English)
|
|
145
|
+
|
|
146
|
+
Artifacts are English: the docs, the code, the comments, the tests, the rule text. The ONE exception is
|
|
147
|
+
the strings the product shows its own users, and that is a product decision.
|
|
148
|
+
|
|
149
|
+
**The order below is the mechanism, and reversing it is the defect.** Read the field. Derive the excluded
|
|
150
|
+
scope. Only then read a file. "Scan first, remember to skip" makes the exclusion depend on the agent
|
|
151
|
+
noticing during the work, and that is the branch that damages a product.
|
|
152
|
+
|
|
153
|
+
1. **Read `docs/prd.md` § 11.1 FIRST.** It holds the app-facing string language. An instance
|
|
154
|
+
materialized before this field existed has no such section, which reads the same as `[TBD]`.
|
|
155
|
+
2. **`[TBD]` or absent: stop and ask `afm ask update-strings`** (questions in
|
|
156
|
+
`content/questions/update-strings.questions.yaml`). Never choose a language. The irreducible mistake
|
|
157
|
+
is "no field, so nothing is a product string, so migrate everything". A translated UI is not something
|
|
158
|
+
`git revert` gives back to the user who saw it.
|
|
159
|
+
3. **Derive the excluded scope from the answer, before any file is read.** The exclusion is a file and
|
|
160
|
+
directory list computed once, not a judgment made per file. An English app language makes the excluded
|
|
161
|
+
scope empty, and the rest is unchanged. `afm lang` reports the language each file is written in.
|
|
162
|
+
4. **Ask ONE question about the artifacts: `afm ask update-artifacts`** (questions in
|
|
163
|
+
`content/questions/update-artifacts.questions.yaml`). Ask it once, never per file. `Keep the current language` records the choice, because the next update must not ask again.
|
|
164
|
+
5. **On migrate, fan out over the artifacts outside the excluded scope**: the local `docs/`, the code
|
|
165
|
+
comments, the test names. Each file gets a rewrite. Then an adversarial check asks what fact, number,
|
|
166
|
+
path, marker or caveat disappeared. The checker reports; it never rewrites.
|
|
167
|
+
6. **Never touch the excluded scope.** Not the UI strings, not the user-facing error messages, not the
|
|
168
|
+
email copy, not the seed data a user reads. An ambiguous file stays.
|
|
169
|
+
|
|
170
|
+
**Post-condition.** `prd.md` § 11.1 is unchanged by this step. The migration reads that field and never
|
|
171
|
+
writes it. The product language is the owner's decision, not a consequence of a version bump.
|
|
172
|
+
|
|
173
|
+
## Opt-in deltas: a release that adds a capability, not a requirement
|
|
174
|
+
|
|
175
|
+
A capability delta is not a migration. Nothing breaks when the operator ignores it, and `update` must not
|
|
176
|
+
apply it silently, because opting in is the operator's decision. The release notes (`content/CHANGELOG.md`) name it, and no codemod applies it.
|
|
177
|
+
Report an opt-in delta, and never apply it. A project that has not opted in has not decided against the
|
|
178
|
+
feature: it has not been asked.
|
|
179
|
+
|
|
180
|
+
| Release | Capability | How to opt in | What happens if you do nothing |
|
|
181
|
+
| --- | --- | --- | --- |
|
|
182
|
+
| 3.10.0 | organisation policy | set `[org] dir` in the global config, pointing at a bundle that holds `policy.md` that your user owns (it was the `AFM_GLOBAL_DIR` variable before 4.0) | nothing. Every reader returns before any file test when no bundle is set |
|
|
183
|
+
| 3.11.0 | `policy_blocking` is REMOVED | delete the key if an instance added it | nothing. It decided nothing while it existed |
|
|
184
|
+
|
|
185
|
+
## Post-conditions
|
|
186
|
+
|
|
187
|
+
1. **Parseable format.** Re-extraction by the hooks over the touched docs must preserve the format:
|
|
188
|
+
`afm lint` exits 0, `afm index --check` returns 0 and `afm gate --audit` is no worse than before. Otherwise roll back the
|
|
189
|
+
delta and report it. Never fail silently.
|
|
190
|
+
2. **The instance/plugin boundary is MECHANICAL.** Run the two-branch check in `afm path ops/evolve`
|
|
191
|
+
§ Phase 4.3 at the end. The boundary must come back clean: any change in the shipped content means
|
|
192
|
+
**rollback + report CRITICAL**. `update` only reads the plugin.
|
|
193
|
+
|
|
194
|
+
## Log and handoff
|
|
195
|
+
|
|
196
|
+
- **À la carte:** report in chat what migrated (applied deltas, pending deltas, the newly pinned version).
|
|
197
|
+
A simple sync does not need its own `docs/evolution/` entry.
|
|
198
|
+
- **Under `evolve`:** it feeds the cycle report (the A3 migration becomes one line in
|
|
199
|
+
`docs/evolution/<date>-NNN.md`).
|
|
200
|
+
- **Handoff to `afm reconcile`** to watch, in the following sessions, whether the materialized delta now
|
|
201
|
+
guides well.
|
|
202
|
+
|
|
203
|
+
## Suppression contract (back-pointer)
|
|
204
|
+
|
|
205
|
+
`evolve` **composes `update`** on axis A3. Under the cycle, `evolve` suppresses the gate of step 3 and
|
|
206
|
+
moves the item up to its single gate; the log feeds the cycle report. The authoritative table lives in
|
|
207
|
+
`afm path ops/evolve` § Suppression contract. À la carte, `update` runs on its own, with its own gate.
|
|
208
|
+
|
|
209
|
+
## Anti-patterns
|
|
210
|
+
|
|
211
|
+
- ❌ **Editing the shipped content.** `update` only **reads** the plugin to discover deltas. It writes
|
|
212
|
+
only to the instance.
|
|
213
|
+
- ❌ **Materializing a consumer-noop release.** A plugin-internal patch with no content to materialize
|
|
214
|
+
only bumps the version. Do not invent work.
|
|
215
|
+
- ❌ **Re-materializing a delta that is already in the instance.** The proof of need exists for this.
|
|
216
|
+
- ❌ **Overwriting or deleting a file the owner edited.** The manifest hash is the guard: merge by hand and close it with `afm update --accept <path>`.
|
|
217
|
+
- ❌ **Materializing a protected surface:** the evolution cycle's own rubric, the evolution `triggers`,
|
|
218
|
+
and `[TBD]`. This is self-reference, and it is the operator's own stance.
|
|
219
|
+
- ❌ **Bumping `.afm-version` as if the migration were complete.** If a delta was left out, the marker
|
|
220
|
+
must show the gap honestly.
|
|
221
|
+
- ❌ **Inventing product content.** `update` materializes only what the plugin already provides
|
|
222
|
+
(stack-agnostic). A missing product decision stays `[TBD]`, for `afm reconcile` to handle (#4/#5).
|
|
223
|
+
- ❌ **Running as bootstrap.** A missing instance calls for `init` or `refactor`, not `update`.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: op
|
|
3
|
+
description: Stamps a human verification on a doc, the only writer of the OKF `verified` field.
|
|
4
|
+
load: warm
|
|
5
|
+
writes: ["docs/**/*.md"]
|
|
6
|
+
composes: []
|
|
7
|
+
---
|
|
8
|
+
# Sub-flow: stamp a human verification (`verify`)
|
|
9
|
+
|
|
10
|
+
> **In this file:** What verify is / is not · Why only a human · Procedure · What it must never do · Invariants
|
|
11
|
+
|
|
12
|
+
> **Form** — effect: adds `verified:` to the frontmatter of one file · trigger: `afm verify <path>` · precondition: `.afm-version` · **degree of freedom: low**
|
|
13
|
+
|
|
14
|
+
Triggers: `afm verify <path>`. Also natural language that states a person checked a document.
|
|
15
|
+
Examples: "I reviewed this rule", "mark this gotcha as verified", "sign off on this ADR".
|
|
16
|
+
|
|
17
|
+
## What `verify` is, and is not
|
|
18
|
+
|
|
19
|
+
OKF v0.2 §5.2 keeps two facts apart. `generated` records what PRODUCED the content. `verified`
|
|
20
|
+
records who CONFIRMED it. They are separate because the writer and the checker are different
|
|
21
|
+
actors, and a document can change without re-confirmation.
|
|
22
|
+
|
|
23
|
+
From those two fields OKF derives a trust tier (§5.3):
|
|
24
|
+
|
|
25
|
+
| `verified` | Tier |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| absent | **unverified** |
|
|
28
|
+
| present, no `human:` actor | **machine-confirmed** |
|
|
29
|
+
| present with a `human:<id>` actor | **human-reviewed** |
|
|
30
|
+
|
|
31
|
+
AFM is autonomy-first. `reflect`, `reconcile` and `remediate` all WRITE documentation. Without this
|
|
32
|
+
operation, "an agent wrote this and no person read it" and "a person approved it" look the same.
|
|
33
|
+
|
|
34
|
+
## Why only a human, and why only here
|
|
35
|
+
|
|
36
|
+
An automatic writer would destroy the field. The agent commits under the operator's git identity. A
|
|
37
|
+
hook that stamps `verified` on every commit would claim human review of every agent commit. That is
|
|
38
|
+
worse than no field: it converts an honest absence into a confident lie.
|
|
39
|
+
|
|
40
|
+
So the rule is one line. **Absence means unverified, and only an explicit human stamp writes it.**
|
|
41
|
+
The command enforces it: it refuses when `AFM_ACTOR` names a role or a machine instead of a person.
|
|
42
|
+
|
|
43
|
+
## Procedure
|
|
44
|
+
|
|
45
|
+
1. `afm preflight`. Exit 3: no instance, stop.
|
|
46
|
+
2. **Confirm that a person read the file.** Show it, or ask `afm ask verify` (questions in
|
|
47
|
+
`content/questions/verify.questions.yaml`). Never infer review from ordinary work. `No` stops.
|
|
48
|
+
3. **Stamp it:** `afm verify <path under docs/>`. The actor is `human:<id>`, where `<id>` comes from
|
|
49
|
+
`--as <id>`, from `AFM_ACTOR=human:<id>` or from the git `user.name` in lowercase with no space.
|
|
50
|
+
The command appends `verified: [{ by: "human:<id>", at: <ISO 8601 UTC> }]`, keeps every other
|
|
51
|
+
key, and adds an entry when `verified` is already a list. A second check is a second event; the
|
|
52
|
+
tier reads the latest `at`.
|
|
53
|
+
4. **Report the tier that resulted**, and say what it means. The command prints it.
|
|
54
|
+
|
|
55
|
+
A file with no frontmatter carries no contract to stamp, and the command says so.
|
|
56
|
+
|
|
57
|
+
## What it must never do
|
|
58
|
+
|
|
59
|
+
- Never stamp a file the person did not read.
|
|
60
|
+
- Never write `verified` from a hook, a gate, a commit, or another op.
|
|
61
|
+
- Never remove a verification. A doc that changed after a verification stays verified at that date,
|
|
62
|
+
and the date is the signal. Silently dropping it hides that the content moved.
|
|
63
|
+
- Never write `generated` here. That belongs to whoever produced the content.
|
|
64
|
+
|
|
65
|
+
## Invariant principles
|
|
66
|
+
|
|
67
|
+
The single canonical source is `afm path PRINCIPLES`. Universals §1-8 apply. #5 applies hardest:
|
|
68
|
+
this operation records a fact about a person. Inventing one is the worst kind of invention, because
|
|
69
|
+
nothing downstream can tell.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: pattern
|
|
3
|
+
description: Wraps a vendor SDK type or payload shape at the boundary so domain and lib code depend on an owned interface instead of the vendor's.
|
|
4
|
+
load: cold
|
|
5
|
+
smell: 'a vendor SDK type (a Stripe object, an S3 response, an ORM row) is imported and used directly inside domain or lib code, past the boundary it arrived at'
|
|
6
|
+
when_to_use: the vendor type crosses more than one file, or it is expected to change independently of the domain model that consumes it
|
|
7
|
+
when_not_to_use: the vendor call happens in exactly one file and its result is consumed nowhere else
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Pattern — Adapter
|
|
11
|
+
|
|
12
|
+
> Suggestion only. This pattern does not become project convention by existing here — it
|
|
13
|
+
> materializes into `code-style/`, `ach.md`, or an ADR only when the operator accepts it for
|
|
14
|
+
> this codebase.
|
|
15
|
+
|
|
16
|
+
Continues DIP in `rubrics/solid-triggers.md` (domain must not import infra) and
|
|
17
|
+
`rubrics/validation-boundary.md` (the parser lives at exactly 2 places). Adapter is what sits at
|
|
18
|
+
that same boundary for a *type*, not a schema: it converts the vendor's shape to an owned one on
|
|
19
|
+
the way in, once.
|
|
20
|
+
|
|
21
|
+
## Decision
|
|
22
|
+
|
|
23
|
+
| Situation | Verdict | Anti-pattern |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `Stripe.Customer` (or equivalent) is read in 2+ files | Adapter: one function maps it to an owned `Customer` type at the boundary, everything past that line uses the owned type | Passing the SDK type through domain and re-reading its fields in each consumer |
|
|
26
|
+
| The SDK's field names or nesting are unstable across major versions (checked: it has broken before) | Adapter absorbs the next breaking change in 1 file | The breaking change ships and touches every call site that read the SDK type directly |
|
|
27
|
+
| The SDK call and its one consumer live in the same file, never exported | No adapter yet — see `when-to-create-lib.md`, one caller does not earn extraction | A `StripeCustomerAdapter` class for a value read once and discarded |
|
|
28
|
+
| The domain needs to call back into the vendor (not just read its output) | Wrap the vendor client behind a port your domain defines (this is DIP, Adapter's usual partner) | Domain importing the vendor SDK client directly to make the call itself |
|
|
29
|
+
|
|
30
|
+
## Why this works
|
|
31
|
+
|
|
32
|
+
A vendor's shape is out of your control; a domain model is not. Every field of the vendor type
|
|
33
|
+
read directly in domain code is a place a vendor upgrade can break — with the adapter, an upgrade
|
|
34
|
+
that renames or restructures the vendor type is absorbed in the 1 function that does the mapping,
|
|
35
|
+
never in the N places that use the resulting owned type.
|
|
36
|
+
|
|
37
|
+
## Anti-patterns
|
|
38
|
+
|
|
39
|
+
- ❌ An adapter that just renames fields 1:1 and adds no real translation, guarding a type used in
|
|
40
|
+
exactly one place. That's indirection with no boundary to protect — see `when-to-create-lib.md`.
|
|
41
|
+
- ❌ The adapter returns the vendor's own error type unchanged. Errors need the same translation as
|
|
42
|
+
data — see `error-classification.md` for what the domain-facing shape should be.
|
|
43
|
+
- ❌ Two adapters for the same vendor type, one per module, silently drifting. Vendor type → owned
|
|
44
|
+
type is a boundary fact, not a per-module preference; it belongs in one shared place.
|
|
45
|
+
- ❌ Calling the adapter function conditionally, so some call sites still see the raw vendor type.
|
|
46
|
+
A boundary that is optional is not a boundary.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: pattern
|
|
3
|
+
description: Bundles a repeated sequence of calls across 3 or more collaborators, that always represents one business step, behind a single function.
|
|
4
|
+
load: cold
|
|
5
|
+
smell: 'a call site wires 3 or more collaborators in the same order, in more than one place, to carry out one coherent business step'
|
|
6
|
+
when_to_use: the same sequence of calls across 3+ collaborators repeats at 2+ call sites and always represents one step
|
|
7
|
+
when_not_to_use: the collaborators are used independently at each call site — a different subset, or a different order, each time
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Pattern — Facade
|
|
11
|
+
|
|
12
|
+
> Suggestion only. This pattern does not become project convention by existing here — it
|
|
13
|
+
> materializes into `code-style/`, `ach.md`, or an ADR only when the operator accepts it for
|
|
14
|
+
> this codebase.
|
|
15
|
+
|
|
16
|
+
Continues ISP in `rubrics/solid-triggers.md`, from the other direction: ISP narrows what a single
|
|
17
|
+
function receives; Facade narrows what a *caller* must know to perform one step, by hiding the
|
|
18
|
+
fan-out to several collaborators behind one call.
|
|
19
|
+
|
|
20
|
+
## Decision
|
|
21
|
+
|
|
22
|
+
| Situation | Verdict | Anti-pattern |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| 2+ call sites each wire the same 3+ collaborators, in the same order, for the same outcome | Extract a facade function; the call sites call it once and stop knowing the collaborators exist | Each call site re-derives the correct order, one of them getting it wrong under time pressure |
|
|
25
|
+
| One of the wired collaborators is itself a vendor SDK crossing a boundary | Apply Adapter to that one collaborator first, then wrap the sequence in the Facade | The Facade re-exports the raw vendor type through its own return value, leaking the boundary it should hide |
|
|
26
|
+
| The call sites use *different* subsets or orders of the same collaborators | Do not extract — that variation is the actual logic, and a facade would hide it, not simplify it | A facade with a boolean flag per caller's variant — the flags are the switch this pattern was meant to avoid |
|
|
27
|
+
| Only one call site exists | No facade yet — inline the sequence until a 2nd real caller needs the same one | A `CheckoutFacade` built for a single call site "to keep the handler short" |
|
|
28
|
+
|
|
29
|
+
## Why this works
|
|
30
|
+
|
|
31
|
+
A caller wiring C collaborators must know all C APIs, their call order, and how errors from each
|
|
32
|
+
propagate — that knowledge is repeated at every call site that performs the same step. A facade
|
|
33
|
+
moves it to 1 place: each call site's coupling drops from C collaborators to 1 function, and a
|
|
34
|
+
change to the internal wiring (a 4th collaborator added, the order changed) touches that 1 place.
|
|
35
|
+
|
|
36
|
+
## Anti-patterns
|
|
37
|
+
|
|
38
|
+
- ❌ A facade over collaborators that are each already simple to call alone. If wiring them takes
|
|
39
|
+
1 line each, the facade adds a name to remember without reducing real complexity.
|
|
40
|
+
- ❌ A "God facade" that grows to wrap every collaborator in the module, becoming the module's only
|
|
41
|
+
entry point for unrelated steps. Facade wraps *one* coherent business step, not the whole module
|
|
42
|
+
— see `rubrics/solid-triggers.md` § SRP if it starts doing 2+ things.
|
|
43
|
+
- ❌ Swallowing or generically catching every collaborator's error inside the facade. Each
|
|
44
|
+
collaborator's failure still needs its own classification — see `error-classification.md`;
|
|
45
|
+
a facade changes who calls, not how errors are represented.
|
|
46
|
+
- ❌ Building the facade before the 2nd call site exists, on the belief the sequence will repeat.
|
|
47
|
+
That is the same premature-extraction risk `when-to-create-lib.md` names for helpers in general.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: pattern
|
|
3
|
+
description: Centralizes a multi-step object construction that is copy-pasted at 2 or more call sites into one named function.
|
|
4
|
+
load: cold
|
|
5
|
+
smell: 'the same multi-step construction (apply defaults, validate, wire 2+ collaborators) is copy-pasted at 2 or more call sites to build the same kind of object'
|
|
6
|
+
when_to_use: constructing the object takes more than one step, and that construction already repeats at 2+ call sites
|
|
7
|
+
when_not_to_use: the value is a single-field object literal, or only one call site builds it today
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Pattern — Factory
|
|
11
|
+
|
|
12
|
+
> Suggestion only. This pattern does not become project convention by existing here — it
|
|
13
|
+
> materializes into `code-style/`, `ach.md`, or an ADR only when the operator accepts it for
|
|
14
|
+
> this codebase.
|
|
15
|
+
|
|
16
|
+
Continues the Rule-of-Three trigger already used in `rubrics/when-to-create-lib.md`: that rubric
|
|
17
|
+
extracts a helper on the 3rd caller. Factory is the same threshold applied specifically to
|
|
18
|
+
*construction* logic — defaults, validation, wiring 2+ collaborators into one object.
|
|
19
|
+
|
|
20
|
+
## Decision
|
|
21
|
+
|
|
22
|
+
| Situation | Verdict | Anti-pattern |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| 2+ call sites build the same kind of object with the same defaults and the same wiring steps | Extract a `create<X>()` factory function; call sites pass only what varies | Each call site re-applies the same defaults, one of them drifting when a default changes |
|
|
25
|
+
| The object needs 1+ collaborator injected (a clock, an id generator, a config) to be testable | Factory takes the collaborators as parameters, with production defaults | The object reaches for `Date.now()` or a singleton internally — untestable without mocking modules |
|
|
26
|
+
| Construction is a single field literal (`{ id, name }`) | No factory. A function that returns its own argument is ceremony | `createUser({ id, name })` that does nothing `{ id, name }` didn't already do |
|
|
27
|
+
| Only one call site exists today | No factory yet — wait for the 2nd real caller, per `when-to-create-lib.md` | A `UserFactory` class built ahead of the 2nd caller, on the promise it is coming |
|
|
28
|
+
|
|
29
|
+
## Why this works
|
|
30
|
+
|
|
31
|
+
Construction logic duplicated at K call sites means a changed default, a new required field, or a
|
|
32
|
+
new invariant must be applied K times — and K-1 of those edits are typically missed. A factory
|
|
33
|
+
collapses that to 1 place: the invariant is enforced once, and every call site that changes still
|
|
34
|
+
compiles because they pass arguments to a function, not fields to a literal.
|
|
35
|
+
|
|
36
|
+
## Anti-patterns
|
|
37
|
+
|
|
38
|
+
- ❌ A factory with a config object with 12 optional fields, most unused by every caller. That is
|
|
39
|
+
the object's own complexity leaking into the factory's signature — fix the object first.
|
|
40
|
+
- ❌ A factory that also performs I/O (saves to the DB, calls an API) as a side effect of
|
|
41
|
+
"constructing." Construction builds a value; persistence is a separate step the caller invokes.
|
|
42
|
+
- ❌ A factory function per call site (`createUserForSignup`, `createUserForAdmin`) that each
|
|
43
|
+
duplicate 90% of the same logic. That is the smell this pattern exists to remove, reintroduced
|
|
44
|
+
one layer down — collapse them into one factory with a parameter for the 10% that differs.
|
|
45
|
+
- ❌ Naming it `UserFactory.getInstance()` to imitate a singleton when nothing requires only one
|
|
46
|
+
instance to exist. A plain exported function is the smaller design (KISS).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: pattern
|
|
3
|
+
description: Replaces a null guard repeated at 3 or more call sites with a default implementation that behaves like the absent case's known, safe outcome.
|
|
4
|
+
load: cold
|
|
5
|
+
smell: 'the same `if (x == null) { ...default behavior... }` guard, followed by using `x`, is repeated at 3 or more call sites'
|
|
6
|
+
when_to_use: the absent case has one well-known default behavior and every caller currently branches on null to reach it
|
|
7
|
+
when_not_to_use: callers must react differently to absence, or must distinguish absent from present-but-default
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Pattern — Null Object
|
|
11
|
+
|
|
12
|
+
> Suggestion only. This pattern does not become project convention by existing here — it
|
|
13
|
+
> materializes into `code-style/`, `ach.md`, or an ADR only when the operator accepts it for
|
|
14
|
+
> this codebase.
|
|
15
|
+
|
|
16
|
+
A narrower cousin of Strategy in this same library: instead of one implementation per real case,
|
|
17
|
+
it adds exactly one extra implementation standing in for "nothing here" — so the caller's `switch`
|
|
18
|
+
or `if (x == null)` collapses to zero branches, not one fewer.
|
|
19
|
+
|
|
20
|
+
## Decision
|
|
21
|
+
|
|
22
|
+
- ✅ 3+ call sites do `if (x == null) return fallbackValue` (or an equivalent guard) right before
|
|
23
|
+
using `x`, and `fallbackValue` is the same at every one → give the absent case a real
|
|
24
|
+
implementation of the same interface (an empty cart, a no-op logger, a zero-total invoice) and
|
|
25
|
+
stop returning `null` in the first place.
|
|
26
|
+
- ✅ The type already has a real interface with 1+ methods (a `Logger`, a `Cart`) → the Null Object
|
|
27
|
+
implements that interface with safe, inert behavior (`NullLogger.log()` does nothing).
|
|
28
|
+
- ❌ Fewer than 3 call sites guard the same way, or two callers want a *different* fallback → a
|
|
29
|
+
shared Null Object would force one fallback where callers legitimately need their own; keep the
|
|
30
|
+
guards local, or pass the fallback in per caller instead.
|
|
31
|
+
- ❌ The caller must tell "absent" apart from "present with the default value" (e.g. a form field
|
|
32
|
+
the user explicitly cleared vs. one never touched) → collapsing both into one Null Object erases
|
|
33
|
+
that distinction; `Result`/`Option` is the honest fit — see `result-vs-exception.md`.
|
|
34
|
+
|
|
35
|
+
## Why this works
|
|
36
|
+
|
|
37
|
+
Every `if (x == null)` guard is a branch a reader and the compiler must both track; N call sites
|
|
38
|
+
with the same guard is N places that branch identically and N places a future change to the
|
|
39
|
+
fallback must be kept in sync. A Null Object moves the branch inside the 1 place that decides
|
|
40
|
+
"use the real thing or the null thing" (usually the factory or lookup that produces `x`), so the
|
|
41
|
+
N call sites go back to unconditional code — 1 branch total instead of N.
|
|
42
|
+
|
|
43
|
+
## Anti-patterns
|
|
44
|
+
|
|
45
|
+
- ❌ A Null Object that throws when any of its methods is called. That is not a null object, it is
|
|
46
|
+
a differently-shaped exception — it defeats the purpose, which is *safe* default behavior.
|
|
47
|
+
- ❌ Using Null Object where the caller actually needs to know absence happened (an audit log that
|
|
48
|
+
must record "no discount was found," not silently apply a $0 discount and look identical to a
|
|
49
|
+
real $0 discount). Prefer an explicit `Option`/`Result` there instead.
|
|
50
|
+
- ❌ A Null Object with mutable shared state (`NullCart` that collects items across unrelated
|
|
51
|
+
calls). The point of the pattern is a stateless, inert stand-in — shared mutable state turns it
|
|
52
|
+
into a hidden singleton bug.
|
|
53
|
+
- ❌ Silently swapping `null` for a Null Object across a serialization boundary (DB, API response).
|
|
54
|
+
Null Object is an in-process convenience; a stored `null` still means "absent" to every reader
|
|
55
|
+
outside this process, so translate at that boundary instead of upstream.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: pattern
|
|
3
|
+
description: Replaces a thrown exception for an expected, listable failure with a typed Result value the immediate caller must handle.
|
|
4
|
+
load: cold
|
|
5
|
+
smell: 'a function throws for an outcome the caller already anticipates and branches on (validation failure, not-found, conflict), and the caller wraps the call in try/catch to recover that one branch'
|
|
6
|
+
when_to_use: the failure modes are known, finite, and the immediate caller needs to react differently to each one
|
|
7
|
+
when_not_to_use: the failure is unexpected or unrecoverable — a network drop, a timeout, a bug
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Pattern — Result / Either over exception-as-control-flow
|
|
11
|
+
|
|
12
|
+
> Suggestion only. This pattern does not become project convention by existing here — it
|
|
13
|
+
> materializes into `code-style/`, `ach.md`, or an ADR only when the operator accepts it for
|
|
14
|
+
> this codebase.
|
|
15
|
+
|
|
16
|
+
`rubrics/error-classification.md` already prescribes the shape (`Result<T, E>` with a literal
|
|
17
|
+
`as const` code) for the Domain layer specifically. This pattern generalizes the same call: apply
|
|
18
|
+
it to *any* function, domain or not, where a `try { } catch { }` exists only to recover one
|
|
19
|
+
specific, anticipated branch — not to handle a genuine surprise.
|
|
20
|
+
|
|
21
|
+
## Decision
|
|
22
|
+
|
|
23
|
+
- ✅ The function has 2 or more named, expected outcomes (`EMAIL_TAKEN`, `NOT_FOUND`,
|
|
24
|
+
`WEAK_PASSWORD`) → return `Result<T, E>` with `E` a literal union, and force the caller to
|
|
25
|
+
`switch` with an exhaustive `never` default (see `error-classification.md`).
|
|
26
|
+
- ✅ Today's code throws a custom error class only to `catch (e) { if (e instanceof X) }` one line
|
|
27
|
+
above the call → that `catch` is the tell; replace both sides with `Result`.
|
|
28
|
+
- ❌ The failure is a bug, a timeout, or anything the immediate caller cannot meaningfully act on
|
|
29
|
+
→ let it rise. Wrapping every infra call in `Result` just relocates the `try/catch`, it does not
|
|
30
|
+
remove it — see `error-classification.md`'s Infra-error row.
|
|
31
|
+
- ❌ The function is called in exactly one place and that place already has a correct
|
|
32
|
+
`try/catch` → converting it buys nothing; the try/catch already IS the handling.
|
|
33
|
+
|
|
34
|
+
## Why this works
|
|
35
|
+
|
|
36
|
+
An exception's type is invisible at the call site — nothing in a function's signature says it can
|
|
37
|
+
throw, so the caller either wraps every call defensively or misses a case entirely. A `Result<T,
|
|
38
|
+
E>` return type puts the failure in the signature: TypeScript's exhaustiveness check on the
|
|
39
|
+
`switch` (the `never` default) then makes an unhandled case a compile error instead of a runtime
|
|
40
|
+
surprise, at zero added `try/catch` blocks.
|
|
41
|
+
|
|
42
|
+
## Anti-patterns
|
|
43
|
+
|
|
44
|
+
- ❌ Wrapping infra failures (DB down, network drop) in `Result` "for consistency." Those are not
|
|
45
|
+
anticipated outcomes — see `error-classification.md`: infra errors rise and get caught once at
|
|
46
|
+
the process boundary, they do not become a case in every caller's switch.
|
|
47
|
+
- ❌ A `Result<T, string>` with a free-text message instead of a literal code. The caller cannot
|
|
48
|
+
`switch` on a string; the whole benefit (exhaustiveness) is gone. The code must be `as const`.
|
|
49
|
+
- ❌ Mixing `Result` and exceptions in the same function — return `Result` for the expected branch,
|
|
50
|
+
then also `throw` for another branch of the same function. Callers must handle two mechanisms.
|
|
51
|
+
- ❌ A generic `try/catch` around a `Result`-returning call "just in case." If the function no
|
|
52
|
+
longer throws for its expected failures, the surrounding `try` should not exist either.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: pattern
|
|
3
|
+
description: Replaces a branching conditional keyed on a kind, type, or provider field with one interface and one implementation per case, selected through a registry.
|
|
4
|
+
load: cold
|
|
5
|
+
smell: 'a `switch` or `if`/`else if` chain keyed on a kind, type, or provider field, duplicated across 2+ files or functions'
|
|
6
|
+
when_to_use: a new case of the same kind arrives on a predictable cadence and every case can share one call shape
|
|
7
|
+
when_not_to_use: only one implementation exists today, or the cases would return incompatible shapes
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Pattern — Strategy
|
|
11
|
+
|
|
12
|
+
> Suggestion only. This pattern does not become project convention by existing here — it
|
|
13
|
+
> materializes into `code-style/`, `ach.md`, or an ADR only when the operator accepts it for
|
|
14
|
+
> this codebase.
|
|
15
|
+
|
|
16
|
+
Continues the OCP trigger in `rubrics/solid-triggers.md`: that rubric tells you a `switch` on a
|
|
17
|
+
provider is a problem the moment a 2nd `switch` on the same field appears elsewhere. Strategy is
|
|
18
|
+
the concrete extraction once that trigger fires.
|
|
19
|
+
|
|
20
|
+
## Decision
|
|
21
|
+
|
|
22
|
+
| Situation | Verdict | Anti-pattern |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| A `switch(kind)` selects behavior, and a second `switch(kind)` on the same field lives in another function or file | Apply Strategy: one file per case implementing a shared interface, wired through a lookup table | Adding a 4th `case` to both switches and hoping they stay in sync |
|
|
25
|
+
| Every case already returns the same shape (LSP, see `solid-triggers.md`) | Safe to extract now — the interface is exactly that shared shape | Extracting before the shapes agree — the interface degrades to `any` or a union of incompatibles |
|
|
26
|
+
| Only one case exists and no second is concretely planned | Do not extract. A single `if` is not a smell | A `StrategyRegistry` with one entry — OCP paid for a case that never happened |
|
|
27
|
+
| The active case is fixed for the process lifetime (an env-driven backend chosen once at boot) | Plain dependency injection of the one active implementation | A registry of N strategies when exactly 1 is ever selected at runtime |
|
|
28
|
+
|
|
29
|
+
## Why this works
|
|
30
|
+
|
|
31
|
+
A `switch` on a variant duplicates its list of cases every time the branch is copy-pasted into a
|
|
32
|
+
new function: N call sites × M cases means up to N×M places that drift when case M+1 arrives.
|
|
33
|
+
Strategy collapses that to N call sites each holding one lookup, plus M single-purpose files —
|
|
34
|
+
adding case M+1 then touches exactly 1 new file and 1 registry line, never the N call sites.
|
|
35
|
+
|
|
36
|
+
## Anti-patterns
|
|
37
|
+
|
|
38
|
+
- ❌ A `StrategyRegistry` holding one entry "for when we add the second provider." Inline the one
|
|
39
|
+
implementation. Extract on the second real case, not the imagined one.
|
|
40
|
+
- ❌ Interfaces that leak the framework in the port (`RegistryAdapter.publish(ctx: TRPCContext)`).
|
|
41
|
+
The port must be as narrow as ISP allows — see `solid-triggers.md` § ISP.
|
|
42
|
+
- ❌ A registry keyed by a raw `string` instead of the closed literal union of kinds. A typo in the
|
|
43
|
+
key then fails at runtime instead of at compile time.
|
|
44
|
+
- ❌ Keeping the old `switch` alongside the new registry "just in case." Delete it in the same
|
|
45
|
+
change — two selection mechanisms for the same kind is worse than the one you started with.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: pattern
|
|
3
|
+
description: Chooses between an inherited algorithm skeleton with overridable steps and a plain function that takes the varying step as a parameter.
|
|
4
|
+
load: cold
|
|
5
|
+
smell: 'two or more functions or classes repeat the same ordered sequence of steps, differing only in 1-2 of those steps'
|
|
6
|
+
when_to_use: the skeleton has 3+ fixed steps and only 1-2 vary across callers
|
|
7
|
+
when_not_to_use: the varying step is the only step, or fewer than 3 callers share the skeleton
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Pattern — Template Method vs. composition
|
|
11
|
+
|
|
12
|
+
> Suggestion only. This pattern does not become project convention by existing here — it
|
|
13
|
+
> materializes into `code-style/`, `ach.md`, or an ADR only when the operator accepts it for
|
|
14
|
+
> this codebase.
|
|
15
|
+
|
|
16
|
+
Shares the Rule-of-Three threshold from `rubrics/when-to-create-lib.md`, applied to a *sequence*
|
|
17
|
+
of steps instead of a single helper — and it decides between two ways to remove the duplication,
|
|
18
|
+
not just whether to remove it.
|
|
19
|
+
|
|
20
|
+
## Decision
|
|
21
|
+
|
|
22
|
+
| Situation | Verdict | Anti-pattern |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| 3+ callers share an ordered skeleton (validate → transform → persist → notify), 1-2 steps vary, and the language has first-class functions | Composition: one function takes the varying step(s) as parameters/callbacks | Copy-pasting the whole skeleton per caller so the 1 varying line can differ |
|
|
25
|
+
| Same skeleton, but the varying steps need to read or mutate protected state shared across steps (a running total, an accumulator built up across the sequence) | Template Method: a base class fixes the sequence, subclasses override only the varying steps | Threading that shared state through 4 separate callback parameters to fake what inheritance gives for free |
|
|
26
|
+
| Only 1-2 callers share the skeleton today | Do not extract either way — inline duplication for 2 callers costs less than an abstraction (Rule of Three, `when-to-create-lib.md`) | A base class with exactly one subclass, built ahead of the 2nd real caller |
|
|
27
|
+
| The "skeleton" is actually just the 1 step that varies, wrapped in ceremony | There is no skeleton to extract — this is a Strategy situation instead, see `strategy.md` | Template Method with a base class whose every method is abstract — nothing is actually shared |
|
|
28
|
+
|
|
29
|
+
## Why this works
|
|
30
|
+
|
|
31
|
+
A skeleton with S total steps where V of them vary means V×N points of difference across N
|
|
32
|
+
callers when duplicated in full. Extracting the skeleton — either way — reduces that to N call
|
|
33
|
+
sites providing only the V varying steps, plus 1 shared definition of the S−V fixed ones.
|
|
34
|
+
Composition and Template Method both hit that number; the choice between them is only about
|
|
35
|
+
where the shared state between steps needs to live — a closure (composition) or `this`
|
|
36
|
+
(inheritance).
|
|
37
|
+
|
|
38
|
+
## Anti-patterns
|
|
39
|
+
|
|
40
|
+
- ❌ Reaching for inheritance by default. Composition is the smaller design (KISS) whenever no
|
|
41
|
+
step needs shared mutable state across the sequence — prefer it, and use Template Method only
|
|
42
|
+
when composition would require threading that state through every callback.
|
|
43
|
+
- ❌ A Template Method base class with a step marked `abstract` that every subclass overrides
|
|
44
|
+
identically. That step isn't varying — fold it back into the fixed skeleton.
|
|
45
|
+
- ❌ Composition where the "varying step" callback secretly depends on 3+ pieces of the caller's
|
|
46
|
+
outer scope. At that point the callback IS the logic and the shared skeleton is the smaller
|
|
47
|
+
part — question whether extracting the skeleton was the right cut at all.
|
|
48
|
+
- ❌ Extracting the skeleton before the 3rd caller exists. Two callers sharing steps can be
|
|
49
|
+
coincidence, not a real pattern — the same Rule-of-Three reasoning `when-to-create-lib.md` uses
|
|
50
|
+
for helpers applies here.
|