Review: Ash DSL and extension system
Review: ash-dsl-ext (notes/research/02-ash-dsl-and-extensions.md)
Review of: Ash DSL and extension system.
VERDICT: ACCEPT
Reviewer: independent fact-check, 2026-10-01. Ground truth: clones under scratch/ash-src/ (spark 2.7.3, ash 3.33.11, igniter 0.8.4, ash_state_machine 0.2.13, ash_paper_trail 0.7.0, ash_archival 2.0.3, ash_oban 0.9.0, ash_events 0.8.2; versions confirmed) plus gh and forum.elixirforum.com.
Paths below are relative to scratch/ash-src/ unless they are URLs.
Overall. Most of the document is right: the structure, the inventories, the line counts and most citations. The case-study line counts are exact. But seven high-severity mechanism errors sit in exactly the parts that will shape mesh’s extension architecture: verifier error handling, after_compile? timing, the semantics of before?/after? conflicts, the Ash.Extension behaviour (including migrate/1), the install hook, and a maintainer quote that was made up and that implication #1 relies on. The verdict is ACCEPT-WITH-FIXES only because every fix is a local edit. All rows marked high must be fixed before anyone uses the document for design.
1. Error table
| Doc line | Claim | What the source says | Sev |
|---|---|---|---|
| 46, 932, 1245 | Ash.Resource registers 21 transformers / “21 transformers and 28 verifiers” / “21 + 8 + 28” |
20 transformers, 8 persisters, 27 verifiers (ash/lib/ash/resource/dsl.ex:1807-1828, :1830-1839, :1841-1869). The doc’s own list at 447-457 has 27 names under a “28 of them” heading. The status report says 20/8/28. Neither count is right. Fix all three places and the report. |
medium |
| 857-860 (and 7.3 heading “confirmed in source”, Summary 64-68, report) | “There is no migrate/1 callback on data layers.” codegen/1/setup/1/tear_down/1 are “plain public functions”, “duck-typed, not a behaviour” |
Ash.Extension (ash/lib/ash/extension.ex:5-45) is a behaviour whose optional callbacks are migrate/1, reset/1, rollback/1, setup/1, tear_down/1, codegen/1, install/5. mix ash.migrate calls extension.migrate(argv) if exported (ash/lib/mix/tasks/ash.migrate.ex:19-28), and so do ash.rollback (ash.rollback.ex:19-29), ash.tear_down (:19-28) and ash.setup (:52-63). AshPostgres implements migrate/1 at ash_postgres/lib/data_layer.ex:459 and rollback/1 at :464. The accurate statement: the mix tasks invoke these duck-typed with function_exported?/3, a behaviour declares them as optional callbacks, and no extension in the clones declares @behaviour Ash.Extension (grep finds none). name/0 is the only one that is not in the behaviour. The reset/1 callback is declared, but mix ash.reset just runs ash.tear_down + ash.setup (ash.reset.ex:15-16), so nothing calls reset/1. |
high |
| 852, 864-867 | install/1 “via Igniter.Mix.Task”; ash.gen.*: “extensions are not automatically plugged into them” |
The extension hook is install/5 on Ash.Extension (ash/lib/ash/extension.ex:28-34). mix ash.extend calls extension.install(igniter, module, kind, path, argv) when it is exported (ash/lib/mix/tasks/ash.extend.ex:178-180), and mix ash.gen.resource --extend … composes ash.extend (ash/lib/mix/tasks/gen/ash.gen.resource.ex:202-207). Implementers: AshPostgres.DataLayer (ash_postgres/lib/data_layer.ex:4854), AshSqlite, AshGraphql Resource and Domain, AshJsonApi Resource and Domain, Ash.Policy.Authorizer. So extensions are plugged into resource generation. The Igniter callback is igniter/1 (or /2), not install/1 (igniter/lib/mix/task.ex:32-36). |
high |
| 361-364, 1039 | Verifier hook steps: “2. runs the after_compile? transformers, 3. runs all verifiers”; catalogue row 9: “after compilation, before verifiers” |
__verify_spark_dsl__ runs the verifiers first. It runs the after_compile? transformers only when final_errors == [], i.e. after verifiers and only if they passed (spark/lib/spark/dsl.ex:468-523). Maintainer in spark#104: transformers with after_compile? are “called with specific transformers … not all transformers”. |
high |
| 363-364 | “on error: reraise a single DslError … or … Multiple Errors Occurred” (implying compilation fails) |
The whole generated function has a catch kind, reason clause that turns any raised error into Spark.Warning.warn → IO.warn (spark/lib/spark/dsl.ex:558-574, spark/lib/spark/warning.ex:35-54). Verifier errors do not fail compilation; they become stderr warnings. Spark documents this: “Verifier errors raised inside Spark’s @after_verify hook are caught by the framework and emitted as stderr warnings instead of propagated as exceptions” (spark/documentation/how_to/test-spark-verifiers.md:9-12, spark/lib/spark/test.ex:9-13). This is the reason Spark.Test exists, not a side detail. Transformer errors, in contrast, do abort compilation (spark/lib/spark/dsl/extension.ex:704-722). |
high |
| 381-385, 1197 | Conflict is dropped only “if a transformer declares both before?(_) -> true and after?(_) -> true”; “cycles … degrade to declaration order” |
(a) The drop rule is per pair: for any pair where left.before?(right) || right.after?(left) and left.after?(right) || right.before?(left) are both true, no edge is added (spark/lib/spark/dsl/transformer.ex:410-418). The pattern Ash’s own guide recommends, “after?(_) -> true … except I go before SomeOtherTransformer” (ash/documentation/topics/advanced/writing-extensions.md:128-136), is therefore silently discarded. A real victim: AshPaperTrail…RelateVersionResource has before?(SetRelationshipSource) + after?(_) -> true (ash_paper_trail/lib/resource/transformers/relate_version_resource.ex:29-32), so its before? is dead. Ash core contains a mutual contradiction that gets dropped the same way: SetPrimaryActions.after?(DefaultAccept) (ash/lib/ash/resource/transformers/set_primary_actions.ex:24) and DefaultAccept.after?(SetPrimaryActions) (default_accept.ex:190). (b) “Declaration order” is wrong twice over. Ties among ready vertices are broken by :digraph.vertices/1 order (ETS order), not declaration order (transformer.ex:452). When there is a cycle, the chosen vertex (the earliest-declared remaining one) and any sink are appended with acc ++ [vertex] to an accumulator that is reversed at the end (transformer.ex:445-471), so they land at the front of the final order, ahead of transformers already emitted. Reviewer’s reading of the code; not executed. |
high |
| 1171-1172 (analysis #1) | “issue #2670 … caused precisely by validating at entity-build time, and the maintainer’s answer was ‘do it in the verifier’” | The only maintainer comment (zachdaniel, 2026-04-09) says to “fix it on the caller end not to have a compile time dependency on the calling resource” by avoiding struct pattern matches in function heads, and that compile-time init “is much better”. The issue was closed in the same minute (https://github.com/ash-project/ash/issues/2670). Verifiers are not mentioned anywhere. The cause was the new init/1 callback, not validation. The quote is made up, and analysis #1 rests on it. §8.2 (901-919) quotes it correctly but leaves out that the issue was closed as a caller-side problem. |
high |
| 69-73, 934-938 | spark#86: “a ~1k-line resource taking 30 s” listed as documented pain | The module is a SeedFactory schema (a non-Ash Spark DSL), not an Ash resource. The issue was closed 2024-06-26 by the maintainer: “improvement on the order of 40s -> 6s” (https://github.com/ash-project/spark/issues/86). Say it was resolved. | medium |
| 959-1006 | Pain points #239, #103, #259, #94, #193, #29, #57, #78, #231 presented as current | All are closed (gh: #239 closed 2026-03-01, #103 2025-06-10, #259 2026-02-07, #94 2024-05-14, #193 same day with fix commit 30b45a7, #29 2024-03-19, #57 2023-09-04, #78 2024-02-29, #231 2025-10-30). Only #126 is marked “since fixed”. Add the state and close date to every issue, and drop or downgrade the ones that are fixed. | medium |
| 1005 | “#103/#78: the formatter ‘requires explicit compilation…’” | #103 is about long module names, not the formatter. Cite #78 alone. | low |
| 141, 560, 853 | Spark.Dsl.Extension behaviour “is only 8 callbacks” including module_prefix/0, dsl_patches/0 |
7 callbacks: sections, module_imports, transformers, verifiers, persisters, explain (optional), add_extensions (spark/lib/spark/dsl/extension.ex:137-145). module_prefix/0 and dsl_patches/0 are generated functions, not callbacks (:470-475). |
medium |
| 239-243 | InfoGenerator: “three families”; “plain returns value | :error” |
Plain option getters return {:ok, value} | :error (spark/lib/spark/info_generator.ex:204-229). Boolean options also get a ? predicate (:175-190). There is a fourth family: entity-list functions per section (e.g. state_machine_transitions/1) (:95-115). |
medium |
| 392 | DslError has “exactly six attributes” (then lists five) |
Five: [:module, :message, :path, :stacktrace, :location] (spark/lib/spark/error/dsl_error.ex:7). |
low |
| 351-353 | “read-only” is enforced because “there are simply no write helpers in scope” | A verifier can still call Spark.Dsl.Transformer.set_option/4 fully qualified. Read-only holds because verify/1 returns only :ok | {:error,_} | {:warn,_} (spark/lib/spark/dsl/verifier.ex:54-57) and the result is discarded after compilation. Reword. |
low |
| 420, 437-440 | Table header “What it does (from its @moduledoc)”; #17 SetDefineFor “fills for on code interface definitions”; #18 SetEagerCheckWith “fills check_with for eager loading”; #19 “fills pre_check_with” |
AddPeriodAttribute, SetInterfaceExcludeInputs, SetDefineFor, SetEagerCheckWith, SetPreCheckWith all have @moduledoc false. SetDefineFor sets the [:code_interface] :domain option from the persisted domain (set_define_for.ex:9-17). SetEagerCheckWith/SetPreCheckWith set eager_check_with/pre_check_with on identities to the resource’s domain and raise if no domain is set (set_eager_check_with.ex:10-35, set_pre_check_with.ex:10-30). This has nothing to do with eager loading. |
medium |
| 434 | #13 SetPrimaryActions “validates the primary action configuration” |
It also adds the default actions (add_defaults/1, set_primary_actions.ex:27-28). PaperTrail’s comment quoted at 680 depends on this. |
low |
| 427 | #6 AddTemporalRelationshipFilters “compile-time wiring of temporal filters onto relationships” |
Also rejects accepting the period attribute as action input (add_temporal_relationship_filters.ex:6-20, 29). |
low |
| 486 | Ash.Reactor patches 14 entities into “Ash’s own [:reactor] section” |
13 entities (ash/lib/ash/reactor/reactor.ex:42-56), patched into the [:reactor] section of the Reactor library’s DSL, not Ash’s. |
low |
| 535, 551-552 | “26 builders”; add_new_* “is what makes extensions idempotent” |
23 defbuilders (the doc’s own list has 23) plus 18 build_* helpers that are not listed (ash/lib/ash/resource/builder.ex:23-703). add_change/add_preparation have no add_new_* form, so AshArchival’s and AshStateMachine’s add_preparation calls are not idempotent. |
low |
| 1086-1089 (open Q5) | “Whether [ReplaceEntity/DeleteEntity] landed under another name is unconfirmed” | Settled: the maintainer rejected it. “allowing extensions to overwrite entities by other extensions is a whole can of worms that we should avoid” (https://github.com/ash-project/spark/issues/83, closed 2024-03-30). Move it to facts: patches can only add. | medium |
| 1069-1077, 1039 | “five ecosystem modules” set after_compile? |
Seven in the clones: 6 in AshGraphql (ash_graphql/lib/resource/transformers/{validate_actions,validate_compatible_names,require_keyset_for_relay_queries}.ex:12, ash_graphql/lib/domain/transformers/{same three}.ex:12) plus ash_authentication/lib/ash_authentication/user_identity/verifier.ex:27. |
low |
| 56-58 | “between 3 and 8 extension points”; PaperTrail “is the ceiling” | Not supported. The doc’s own per-study lists have about 7 to 14 items, and AshOban (5,196 lines) is larger than PaperTrail (3,827). Restate or remove. | low |
| 604, 1056 | “an Ash.Check ValidNextState”; row 26 “Check | Ash.Check behaviour | runtime, on a field | pass/fail validation of one value” |
No Ash.Check module exists in ash/lib. AshStateMachine.Checks.ValidNextState is a policy check, use Ash.Policy.FilterCheck (ash_state_machine/lib/checks/valid_next_state.ex:5-9). The contract is Ash.Policy.Check / SimpleCheck / FilterCheck (ash/lib/ash/policy/{check,simple_check,filter_check}.ex), and it runs during authorization, not “on a field”. |
medium |
| 598 | EnsureStateSelected adds a runtime preparation “to every action” |
Global preparations apply to read actions (queries), not to every action. |
low |
| 708-709 | create_version_resource.ex:165-210 is the main generator; “a second Module.create at :367 for the temporal_inline variant” |
Reversed. :165 is inside create_inline_version_resource/1 (defined at :28). :367 is in create_version_resource/1 (defined at :227). |
low |
| 727 | PaperTrail uses “replace_entity on actions” (fine), “per-extension codegen-adjacent options” |
No codegen is involved. version_extensions just injects extensions into the generated resource. Remove “codegen-adjacent”. |
low |
| 765-766 | AshEvents.EventLog “itself creates a data-layer-backed resource” |
There is no Module.create in ash_events/lib. EventLog is an extension applied to a resource the user writes. Its transformers add actions and attributes to it (ash_events/lib/event_log/event_log.ex:152-155). |
low |
| 866 | “AshPaperTrail and AshOban ship their own extra tasks” |
ash_paper_trail/lib contains no mix tasks. Only AshOban does (install, upgrade, set_default_module_names). |
low |
| 1052 | Authorizer “14 callbacks” | 12 @callbacks (ash/lib/ash/authorizer.ex:17-84). §4 lists 12 correctly. |
low |
| 1058 | Row 28 contract “Ash.Resource.Aggregate, Ash.Type”, example Ash.Resource.Aggregate.Count |
Ash.Resource.Aggregate.Count does not exist (ash/lib/ash/resource/aggregate/ has only aggregate.ex, custom_aggregate.ex). Calculation contract: Ash.Resource.Calculation behaviour (resource/calculation/calculation.ex). Custom aggregate contract: Ash.Resource.Aggregate.CustomAggregate. |
medium |
| 1059 | Row 29 Interface: “runtime” | Code interfaces generate functions on the domain/resource at compile time. That cost is measurable: a forum thread shows 4148 ms vs 533 ms with and without code_interface (https://forum.elixirforum.com/t/73196). |
medium |
| 1141 | Sources: https://github.com/ash-project/spark/issues/2670 |
Wrong repo. Should be ash-project/ash/issues/2670. |
low |
| 368, 2.4 | “The docs say this explicitly (writing-extensions.md, ‘Verifiers’)” |
That section is in Spark’s spark/documentation/how_to/writing-extensions.md:88-91. Ash’s guide has no Verifiers section. Fix the path. |
low |
| 10-11 | spark mix.exs:9, ash mix.exs:9 |
spark/mix.exs:8, ash/mix.exs:13. |
low |
| 895 | no_depend_modules at entity.ex:70, section.ex:50 |
spark/lib/spark/dsl/entity.ex:87, spark/lib/spark/dsl/section.ex:44. |
low |
| 293-304 | Helper line numbers | Wrong-line-but-true: get_persisted is at :148 (doc :113), fetch_persisted :157 (:124), build_entity! :166 (:171), get_entities :301, fetch_option :310, get_option :342, remove_entity :290, get_opt_anno :332. |
low |
| 836, 848, 850-851 | AshPostgres codegen :648, setup :653, tear_down :670 |
:649, :654, :671. |
low |
| 643-648 | AshArchival ordering shown as one-line attributes | The real source spreads these over several lines (setup_archival.ex:10-17, 32-36). Content is the same. Rule 5 says to copy code, not reformat it. Also “does four things” lists three. |
low |
2. Recount results (check 2)
| List | Doc | Actual | Names/order match? |
|---|---|---|---|
Ash.Resource transformers |
20 in table, 21 in Summary/§8.3/§10 | 20 (dsl.ex:1807-1828) |
Yes, all 20 in order |
Ash.Resource persisters |
8 | 8 (:1830-1839) |
Yes |
Ash.Resource verifiers |
“28” (lists 27) | 27 (:1841-1869) |
Yes, 27 names in order |
Ash.Domain transformers |
2 | 2 (domain/dsl.ex:198-201) |
Yes |
Ash.Domain verifiers |
3 | 3 (:192-196) |
Yes |
| Resource sections | 14 | 14 (:1790-1805) |
Yes |
Also worth adding: there are 28 files in transformers/ because 20 transformers + 8 persisters = 28. The researcher’s “transformers/* (28)” is files, not transformers.
One-line descriptions checked against source (20 transformers + 4 persisters/verifiers = 24): 17 accurate. Five transformers have @moduledoc false (so “from its moduledoc” is false for them). SetDefineFor, SetEagerCheckWith and SetPreCheckWith are wrong. SetPrimaryActions and AddTemporalRelationshipFilters are incomplete.
3. Compile-pipeline claims (check 3)
| Claim | Result |
|---|---|
Transformers receive the dsl_state map and return :ok | {:ok,map} | {:error,_} | {:warn,map,w} | :halt |
Correct (spark/lib/spark/dsl/transformer.ex:60-68, extension.ex:747-812). Missing: :halt stops the whole remaining list, persisters included, because persisters are concatenated into the same reduce (extension.ex:693-711). |
| Persisters sorted separately, ordering against transformers ignored | Correct (extension.ex:693-702; Spark moduledoc :96-99). |
Verifiers in @after_verify |
Correct (dsl.ex:464). They receive the compile-time @spark_dsl_config map, not the module. |
| Verifiers raise on error | Wrong. They are caught and turned into warnings (see error table). |
after_compile? transformers before verifiers |
Wrong. They run after, and only if there were no errors. |
before?/after? via digraph |
Correct as far as it goes. The pairwise-conflict drop and the cycle placement are described wrongly (see error table). |
| “Cycle broken silently” | True (no error, no warning). But the order it produces is not “declaration order”. |
| Errors carry location | Correct: location + Spark.Dsl.Entity.anno/1 + __spark_metadata__. Missing: the extension module’s own @after_verify Spark.Dsl.Extension emits a deprecation warning at extension compile time for every entity target struct that lacks __spark_metadata__ (extension.ex:453, 2243-2262), not only at read time. Also, with a location, transformer DslErrors are emitted as an extra diagnostic warning and then re-raised with an empty stacktrace (extension.ex:713-722). |
4. Issues (check 4)
- ash#2670: title and date (opened 2026-04-09 by joshprice, closed 2026-04-09T17:18:51Z) are right, and so is the trace excerpt (trimmed). The description “init/1 called at compile time from
transform/1” is right. The “do it in the verifier” answer does not exist (see error table). - spark#290: title, author jechol, opened 2026-08-24T05:19:04Z, OPEN as of 2026-10-01, no comments. Both quotes are verbatim. The proposal summary (consumption-site ownership,
Macro.compile_apply/4, eventual removal ofno_depend_modules) is accurate. Confirmed. - spark#289: closed 2026-09-03. The doc says “closed”. OK.
5. AshArchival stale names (check 5)
Both halves confirmed. setup_archival.ex:10-17 lists Ash.Resource.Transformers.ValidatePrimaryActions (after) and …SetTypes (before). grep -rn "SetTypes\|ValidatePrimaryActions" ash/lib finds nothing. Note that the DefaultAccept entry in the same list is live. Under-reported: this is not the only dead ordering assertion. PaperTrail RelateVersionResource.before?(SetRelationshipSource) is dropped by the pairwise-conflict rule, and so is Ash core’s SetPrimaryActions↔DefaultAccept pair.
6. Callbacks (check 6)
- Duck-typed invocation with
function_exported?/3: true forcodegen/1,name/0(ash.codegen.ex:61-67),migrate/1,rollback/1,setup/1,tear_down/1,install/5. - “Rather than behaviour callbacks”: false.
Ash.Extensiondeclares all of them exceptname/0as optional callbacks (ash/lib/ash/extension.ex:21-44). - “No
migrate/1data-layer callback”: false (see error table).
7. Citation sample (check 10)
68 citations sampled across all sections.
| Judgement | Count | % |
|---|---|---|
| Supports | 43 | 63% |
| Wrong line, claim true | 14 | 21% |
| Does not support | 10 | 15% |
| Nonexistent | 1 | 1% |
Supports: spark/lib/spark/dsl.ex:620-740, :464, :11-63, transformer.ex:398-473, :88, :98, :132, :273, :372, :104-106, ash/lib/ash/resource/dsl.ex:1807-1868, domain/dsl.ex:192-205, resource.ex:18-36, domain/domain.ex:27-34, ash.codegen.ex:60-76, extension.ex:425-477, :458-462, :634-727, :270, :322, :385, :747-828, :757-766, :693-702, :742-745, :100-110, ash_state_machine/lib/info.ex:7, ash_events/lib/events/events.ex:86-93, patch/add_entity.ex:1-24, strategies/custom.ex:117, setup_archival.ex:9-16, ash_paper_trail/lib/resource/resource.ex:26, define_schedulers.ex:12, :38-44, helpers.ex:70-76, development-utilities.md:23, spark.formatter.ex:35, elixir_sense/plugin.ex:14-45, test.ex:341-357, spark#290 quotes, ash#2267 quote, Ash guide ordering quote, Spark tutorial quote.
Wrong line: transformer.ex :113 :124 :171 :300 :317 :335 :289, verifier.ex:47-50 (callback at 54), entity.ex:70, section.ex:50, data_layer.ex:648/653/670, create_version_resource.ex:165/367 (swapped), spark/mix.exs:9, ash/mix.exs:9.
Does not support: extension.ex:137-145 (8 callbacks), dsl_error.ex (six attrs), info_generator.ex:48-60 (return shape), reactor.ex:41-59 (14 / Ash’s section), builder.ex (26), ash.codegen.ex as proof that migrate/1 does not exist, ash#2670 for “do it in the verifier”, spark#86 for “resource, current pain”, dsl.ex:464-540 for the step order and the error outcome, transformer.ex:414-417 for the conflict condition.
Nonexistent: https://github.com/ash-project/spark/issues/2670.
8. Acceptance criteria (check 11)
| # | Criterion | Status | Missing / wrong |
|---|---|---|---|
| 1 | Spark DSL definition | partial | Extension behaviour has 7 callbacks; InfoGenerator return shape and the 4th (entity) family. Otherwise good. |
| 2 | Compile pipeline + full lists | partial | Counts (20/27); verifier errors become warnings; after_compile? order; conflict-drop and cycle-placement semantics; :halt skips persisters; 5 wrong/incomplete one-liners. |
| 3 | Extending other DSLs | complete (minor errors) | Reactor 13 / Reactor’s section; 23 builders + build_*; add the #83 rejection (patches can only add). |
| 4 | Kinds of extension | complete | Resource extensions use untyped extensions:, not single_extension_kinds (line 560). |
| 5 | Case studies | partial | See §9. Main gaps: dead ordering in PaperTrail, cross-extension writes in AddTemporalInlineAttributes, DefineSchedulers mutations and compile-time config read, ValidNextState is a policy check. |
| 6 | Free tooling | complete | Could add mix spark.cheat_sheets_in_search. Should note that the ElixirSense plugin does not work with Expert (forum, below). |
| 7 | Igniter and codegen | partial | Ash.Extension behaviour; migrate/rollback/reset; install/5 via ash.extend / ash.gen.resource --extend; full ash.gen.* list (base_resource, change, custom_expression, domain, enum, gettext, preparation, resource, validation) plus ash.install, ash.patch.extend. |
| 8 | Pain points with evidence | partial | Most cited issues are closed/fixed but presented as current; #86 resolved; no forum evidence (now supplied below); the #2670 analysis is misattributed. |
| 9 | Catalogue | partial | Rows 9, 22, 26, 28, 29 are wrong; missing points listed in §10. |
9. Check 7: case studies
AshStateMachine (1,184 lines, confirmed). Transformers and verifiers are confirmed (ash_state_machine/lib/ash_state_machine.ex:116-130), and so are the per-file line counts. Fixes:
FillInTransitionDefaultslives inlib/transformers/fill_in_event_defaults.ex. It also rewritesinitial_stateswithset_option(:28-33).ValidNextStateis anAsh.Policy.FilterCheck.- Missing pieces:
lib/charts.ex(76),lib/clarity/state_machine_diagram.ex(51),lib/mix/generate_flow_charts.ex(101). - Undeclared ordering dependency:
AddStatereadsdefault_initial_state, whichSetDefaultInitialStatesets, but neither declares ordering against the other.AddStatedeclares onlyafter?(FillInTransitionDefaults)andbefore?(DefaultAccept)(add_state.ex:11-15). The order depends on the digraph tie-break. Reviewer’s inference; this case-study example of the ordering problem is worth more than the AshArchival one.
AshArchival (430, confirmed). One transformer, no verifiers (resource.ex:78-80). Omitted: an Info module (resource/info.ex, 8 lines) and the bail-out on embedded? (it is mentioned in prose).
AshPaperTrail (3,827, confirmed; all six transformers, the verifier and their line counts confirmed).
AddTemporalInlineAttributes, read in full (188 lines). The doc says only “stamps version attributes on the resource itself”. It actually:- builds and adds
version_action_type, optionalversion_action_name,version_action_inputs,changes(sensitive?derived from the source attributes), the operation-id field, and everymetadataentity as attributes, withwritable?: false(:36-95); - adds a
belongs_torelationship perbelongs_to_actor, withtemporal_keysand a hand-setsource(:97-136); - writes a
referenceentity into another extension’s section, AshPostgres[:postgres, :references]or AshSqlite[:sqlite, :references], throughbuild_entity(extension, path, :reference, …), with the data-layer module names hard-coded (:138-175). This is cross-extension mutation withoutdsl_patches: a key extension point the catalogue does not mention; - declares
before?(BelongsToAttribute)andbefore?(SetRelationshipSource)so that core generates the FK attribute (:22-24); - returns a
DslErroron name conflicts with path[:paper_trail, :mode](:177-187).
- builds and adds
RelateVersionResource: itsbefore?(SetRelationshipSource)is dead because ofafter?(_) -> true.CreateVersionResource: theModule.createcitations are swapped (see error table).- The domain transformer
AllowResourceVersionscallsSpark.extensions(resource)andAshPaperTrail.Resource.Info.version_resource?(resource)on other compiled modules from inside a transformer (ash_paper_trail/lib/domain/transformers/allow_resource_versions.ex:16-20). That is a compile-time cross-module dependency of exactly the kind §8.1 discusses, and the doc should flag it.
AshOban (5,196, confirmed; transformer/verifier lists and line counts confirmed). DefineSchedulers, read in full (1,715 lines). Missed or wrong in the doc:
- It mutates the DSL:
replace_entity([:oban, :triggers], …)fillsscheduler:andworker:module names on every trigger (define_schedulers.ex:30-35).DefineActionWorkersdoes the same forscheduled_actions(define_action_workers.ex:26-29). The doc says only “generates a worker module”. - The worker is created synchronously with
Module.create. Only the scheduler goes throughasync_compile(:28, :36-43). The doc implies everything is async. - There are three generators, not two: the scheduler (
:57-294), a standard worker (:433-697), and an Oban Pro chunk worker whentrigger.chunksis set (:306-430). - It reads application config at compile time with
Application.get_env:AshOban.Info.pro?()(ash_oban/lib/info.ex:11-13, used atdefine_schedulers.ex:61, 435). Generated code switches betweenOban.Worker/performandOban.Pro.Worker/process. Because this is notcompile_env, changing:pro?does not trigger recompilation (reviewer’s inference from Elixir semantics). This is a real pain point. - It reads data-layer capabilities at compile time:
Ash.DataLayer.data_layer_can?(dsl, :transact)and{:lock, :for_update}decide whether to emit locking and transaction code (:446-491). - It validates inside the transformer (raises
DslErrorif theon_erroraction is missing,:448-457), not in a verifier. - The bulk of the file is generated error handling (
handle_error/6,:699-1008: on the final attempt, call theon_erroraction viabulk_update!/bulk_destroy!when atomic, otherwise re-read the record and run the action) andwork/8/work_chunk/7(:1026-1715), which run the trigger action, generic actions included (Ash.run_action!,:1068-1081). - The DSL also has a
scheduleentity underscheduled_actions(ash_oban.ex:549), which the doc omits.
10. Check 8: catalogue
Every row’s contract name was checked. Nonexistent names: row 26 Ash.Check, row 28 Ash.Resource.Aggregate.Count. Wrong timing: row 9 (it runs after verifiers) and row 29 (it is compile time). Wrong count: row 22 (12 callbacks). Row 1’s cited ash_state_machine.ex:71-107 is fine. Row 15 (dsl.ex:178-181) is fine. The other rows are correct.
Missing extension points (all exist in ash/lib/ash/ unless noted):
Ash.Extensionoptional callbacksmigrate/1,rollback/1,reset/1,install/5(mix-task time).install/5is invoked bymix ash.extend.Ash.Type/Ash.Type.NewType(custom types), with compile-timeinit/1on constraints.Ash.Resource.Calculationbehaviour (resource/calculation/calculation.ex), runtime.Ash.Resource.Aggregate.CustomAggregate(resource/aggregate/custom_aggregate.ex), runtime.- Manual actions:
Ash.Resource.ManualRead,ManualCreate,ManualUpdate,ManualDestroy(resource/manual_actions/), runtime. Ash.Resource.ManualRelationship(resource/manual_relationship/), runtime.- Generic action implementations:
Ash.Resource.Actions.Implementation(resource/actions/action/), runtime. - Policy checks:
Ash.Policy.Check,SimpleCheck,FilterCheck, run at authorization time. This should replace row 26. Ash.CustomExpression(custom_expression.ex), compile-time registration with run-time evaluation.Ash.Tracer(tracer/), runtime.- Cross-extension entity writes via
Transformer.build_entity(OtherExtension, path, …)+add_entity(PaperTrail → AshPostgres references): compile time. Spark.Dsl.Entityrecursive_as, andSpark.Dsl.Sectionafter_define(spark/lib/spark/dsl/entity.ex:72,section.ex:40): compile time.Spark.Dsl.Extensionexplain/1(optional callback that contributes to the generated@moduledoc): compile time.
11. Check 12: forum pain points (forum.elixirforum.com; the old host 302-redirects there, and its Discourse search.json / t/<id>.json endpoints work)
- Code interfaces dominate compile time. sezaru, 2025-11-05: a resource compiled in 4148 ms with
code_interfaceand 533 ms without; the domain took 3719 ms vs 368 ms. zachdaniel admitted “the code interface logic is not really the greatest macro code anyone ever wrote” and shipped improvements on 2025-11-15. https://forum.elixirforum.com/t/using-code-interface-makes-resource-slow-to-compile/73196 - Resources taking over 30 s to compile. sezaru, 2025-08-28. zachdaniel’s advice: “remove as many anonymous functions from your resources as possible”; whether fragments help is “heard … but I haven’t confirmed it”. https://forum.elixirforum.com/t/strategies-to-make-resources-compile-faster/72265
- Incremental recompiles growing to 8–10 s in an Ash/Phoenix app. joangavelan, 2025-08-15. The replies offer only generic
mix xrefcycle advice. https://forum.elixirforum.com/t/reducing-incremental-compilation-times-in-phoenix-ash-project/72113 - Editor support: no Spark DSL completion in Expert, the new official Elixir LSP. zachdaniel, 2026-02-27: “the spark extension that we wrote for ElixirLS/elixir_sense does not work with expert unfortunately”. katafrakt, 2026-03-01, explains why a fallback is not viable for completion. https://forum.elixirforum.com/t/expert-lsp-compatibility/74451. This updates §8.7: the single-vendor problem is now a live regression, not only issue #94 from 2024.
- An extension cannot add to core’s computed defaults; you must copy core transformer logic. zachdaniel, 2024-09-23, on adding a field to
default_accept: “the way it’s designed is not conducive to that change … copy the logic from our core transformer”. He also says to order withbefore?(Ash.Resource.Transformers.DefaultAccept). https://forum.elixirforum.com/t/how-do-i-set-default-accepts-to-a-resource-through-ash-extension-transformer/66257 - An extension cannot add an option to another extension’s entity (e.g.
cached? trueinsideread). zachdaniel, 2024-01-12: “won’t be possible with the current DSL extension options”; he recommends studying AshArchival as the template. https://forum.elixirforum.com/t/inputs-on-cache-layer-extension-for-read-actions/60933. This corroborates spark#83: patches can only add top-level entities. - Builder/transformer authoring friction and misleading errors. kamaroly, 2024-09-06: the maintainer-suggested
add_new_relationship(..., filter: expr(...))“was not working … the error message says it requires filter options even if it has been passed”. https://forum.elixirforum.com/t/how-to-extract-a-relationship-into-a-separate-module-for-reuse-in-ash/65890 - Learning Spark means reading other extensions. steele232, 2026-03-17: got a first DSL working only after “cloning down ash_graphql to take a closer look at that DSL extension and also the Ash DSL”. https://forum.elixirforum.com/t/trying-spark-for-the-first-time-and-dsl-doesnt-seem-to-be-recognized/74684. Also on error quality: At7heb, 2024-10-20, a duplicate
use Ash.Domainproduced “def can?/3 defines defaults multiple times” pointing at thedefmoduleline; zachdaniel agreed it should be detected. https://forum.elixirforum.com/t/misleading-error-message-def-can-3-defines-defaults-multiple-times/66875
Remove the “Not found: ElixirForum unreachable” note (line 1020) and open question 6.
12. Required fixes, in priority order
- Rewrite §7.3 and the Summary bullet on codegen: the
Ash.Extensionbehaviour, its optional callbacks,migrate/1exists,install/5viaash.extend/ash.gen.resource --extend. Update the status report too, which repeats the false claim. - §2.4 and catalogue row 9: verifier errors are caught and emitted as warnings;
after_compile?transformers run after verifiers and only when there are no errors. - §2.5, §8.4 and analysis #4: the pairwise-conflict drop (including the guide’s own documented pattern, PaperTrail’s dead
before?, core’s SetPrimaryActions/DefaultAccept pair), the tie-break by digraph order, and the cycle-break vertex placed at the front. - Analysis #1: remove “do it in the verifier”; restate #2670 as the maintainer’s caller-side fix, closed.
- Counts: 20/8/27 everywhere (Summary, §8.3, §10, report).
- State and close date for every cited issue; mark #86 as resolved.
- The remaining medium and low rows in §1, plus the §9 case-study additions and the §10 catalogue additions.
Round 2 re-verification
Re-checked on 2026-10-01 against the revised document (1,778 lines, 14,319 words, with a revision log). Line numbers below refer to the revised document. Paths are relative to scratch/ash-src/.
Verdict: ACCEPT-WITH-FIXES. All seven high findings are now fixed correctly in substance: I re-read the source for each one, not only the new text. The rewritten mechanism sections read the code correctly, and the “read from code, not executed” caveat appears where it should (§2.5 lines 484-486, §5.1 line 739, §5.4 lines 979-980, Open Q6). What is left is medium and low. The two things to take seriously:
- The revision log claims “fixed” for at least four items that were not changed in the text.
- The new catalogue rows name example modules that do not exist.
R2.1 High findings: status
| Round-1 finding | Status | Note |
|---|---|---|
Ash.Extension behaviour, migrate/1, duck-typed invocation |
Fixed, correct | §7.3 matches ash/lib/ash/extension.ex:21-44 and the task files. The callers table is right, and so is “reset/1 never invoked”. |
install/5 via ash.extend / ash.gen.resource --extend; Igniter callback is igniter/1 |
Fixed, correct | ash.extend.ex:178-186 verified, including the fallback to simple_add_extension. |
| Verifier errors become warnings; transformer errors abort | Fixed, correct | dsl.ex:558-574, extension.ex:704-722. The Spark doc quote is verbatim. |
after_compile? runs after verifiers, only on success |
Fixed, correct | dsl.ex:521-523. |
| Ordering: pairwise-conflict drop, tie-break, cycle placement | Fixed, reading correct, but one wrong consequence and wrong line numbers (R2.2 #1, #2) | |
| ash#2670 invented quote | Fixed; the analysis no longer depends on it. The quote is not fully verbatim (R2.2 #5). | |
| Counts 20/8/27 | Mostly fixed; one stray “21” left (R2.2 #3) |
R2.2 Still wrong (fix each as stated)
| # | Doc line | Problem | Correct statement / source | Sev |
|---|---|---|---|---|
| 1 | 446, 840 | “leaving X unordered with respect to everything”; RelateVersionResource “is unordered relative to everything” |
Only the edge to the one named transformer is dropped. Every other pair still gets an edge from after?(_) -> true, so X stays after all the others (spark/lib/spark/dsl/transformer.ex:418-433). Write: “unordered relative to SomeOtherTransformer (resp. SetRelationshipSource) only; still after every other transformer.” |
medium |
| 2 | 431-433, 447, 465-474 | Line citations in §2.5 are off by 3 to 5 | Conflict branch :418-419 (doc :415-416); “annoying” comment :415-417 (doc :412-414); walk_rest :444-473 (doc :439-471); reverse :447 (doc :442); cycle min_by :455-460 (doc :453-457); sink append :464 (doc :460-461); prepend :469 (doc :467-468). The reading is right. |
low |
| 3 | 1265-1266 | “Ash.Resource.Dsl … lists all 21 transformers and 27 verifiers” |
20 transformers (ash/lib/ash/resource/dsl.ex:1807-1828). |
medium |
| 4 | 156-157 | Leftover sentence “The behaviour itself … is only 8 callbacks”, which contradicts line 141 a few lines above | Delete it. The behaviour has 7 callbacks (spark/lib/spark/dsl/extension.ex:137-145). |
medium |
| 5 | 1240-1248 | Presented as “verbatim”, but it is not: somethign = %Something% is altered (original somethign = %Something{}), and the three code blocks are collapsed into prose without ellipsis markers. 1224: “29 minutes apart, by joshprice”: the issue was opened 16:48:51 and closed 17:18:51, which is 30 minutes, and joshprice only opened it. |
Copy the comment exactly from https://github.com/ash-project/ash/issues/2670, keeping the code as code, or drop the word “verbatim”. Say “opened by joshprice; closed 30 minutes later, at the same timestamp as zachdaniel’s comment”. | medium |
| 6 | 1474-1489 (catalogue) | Example users that do not exist (grep over all clones finds no defmodule): AshGraphql.ManualCreate (row 28c), AshGraphql.CustomExpression (37), AshGraphql.ManualRelationship (38), Ash.Money (36; the real module is AshMoney.Types.Money, ash_money/lib/ash_money/types/money.ex:5), Ash.Tracer.Telemetry (40; ash/lib/ash/tracer/ has only simple.ex and tracer.ex). Row 39 gives Ash.Reactor as an Ash.Resource.Actions.Implementation user: no reactor module uses it (grep matches only scope.ex, actions/action.ex and the implementation files). Row 42 “used in deeply nested aggregate schemas” and row 43 “used to inject shared helper macros” are made up. |
Replace them with real users or write “no example in the clones”. Real ones: row 36 Ash.Type.UUID (ash/lib/ash/type/uuid.ex) and AshMoney.Types.Money; row 28b AshPostgres.CustomAggregate (correct, keep it); row 40 Ash.Tracer.Simple (ash/lib/ash/tracer/simple.ex); row 42 recursive_as: :policies (ash/lib/ash/policy/authorizer/authorizer.ex:266) and recursive_as: :steps (ash/lib/ash/reactor/dsl/transaction.ex:57); row 43 Ash.TypedStruct after_define: {__MODULE__, :after_define} (ash/lib/ash/typed_struct.ex:146). Row 42’s citation is entity.ex:74, not :72. |
medium |
| 7 | 529-530, 549, 555-557 | “rows 1, 16, 17, 18, 19 all carry @moduledoc false” |
Wrong rows. RequireStringLengthCountConfig (row 1) has a moduledoc (require_string_length_count_config.ex:6-10). The five with @moduledoc false are rows 5 (AddPeriodAttribute, add_period_attribute.ex:8), 16, 17, 18, 19. Row 16 cites dsl.ex:1807-1828 as evidence for @moduledoc false; cite set_interface_exclude_inputs.ex:6. The column header at 532 still says “(from its @moduledoc)”. Rename it “What it does”. |
medium |
| 8 | 812-813 | “(The real module is RequireStringLengthCountConfig, and there is no SetTypes at all.)” |
No source, and implausible: validating primary actions is done by SetPrimaryActions (set_primary_actions.ex:6-10). Delete the parenthetical or cite a commit that shows the rename. |
medium |
| 9 | 1217-1218 | no_depend_modules at entity.ex:70, section.ex:50. The revision log (#24) says fixed. It is not. |
spark/lib/spark/dsl/entity.ex:87, spark/lib/spark/dsl/section.ex:44. |
low |
| 10 | 313-324 | Helper-table line numbers unchanged. The revision log (#24) claims “the eight transformer.ex helper lines” were corrected. They were not. |
get_entities :301, fetch_option :310, get_option :342, remove_entity :290, build_entity! :166, get_persisted :148, fetch_persisted :157, get_opt_anno :332. |
low |
| 11 | 1084, 1477-1478 | Still ash_postgres/lib/data_layer.ex:648-651, :653-670 (§7.2, catalogue rows 30-31). The log says moved to 649/654/671. Rows 30-31 still call these “duck-typed plain functions” with no mention of the behaviour. |
codegen :649-652, setup :654-670, tear_down :671-674. Rows 30-31: “optional Ash.Extension callbacks, invoked duck-typed”, or merge them into row 34. |
low |
| 12 | 779-780 | Still “which does four things” with three listed. The log (#28) says fixed. | “three things”. | low |
| 13 | 1572 | spark#104 “closed 2024-07-13” | Closed 2024-07-07T08:17:51Z (gh issue view 104 --repo ash-project/spark). |
low |
| 14 | 1022 | mix spark.cheat_sheets_in_search “now only emits a deprecation warning” |
It warns, then still runs: Mix.Task.run("compile") and the old logic follow (spark/lib/mix/tasks/spark.cheat_sheets_in_search.ex:11-26). Say “deprecated; warns, then still runs”. |
low |
| 15 | 254, 261 | “four families” over a five-row table. The example state_machine_temporal_inline?(r) does not exist (the state_machine section has no such option). |
Say “five kinds (options getter / bang / ? predicate / _options map / entity list)”. Use a real predicate, e.g. paper_trail_temporal_inline? if it exists in AshPaperTrail.Resource.Info, or archive_base_filter? (AshArchival.Resource.Info, called at ash_archival/lib/ash_archival/resource/preparations/filter_archived.ex:12). |
low |
| 16 | 858-860 | AddTemporalInlineAttributes citations: module-name matching “:164-166”, build/add “:138-162” |
Matching is at add_temporal_inline_attributes.ex:171-175; build_entity(extension, path, :reference, …) + add_entity at :140-169. The substance is correct (verified line by line). The §5.3 table cell at 841 says “four different things” but five are listed. |
low |
| 17 | 936 | PaperTrail “replace_entity on actions and triggers” |
Triggers belong to AshOban. PaperTrail only replaces actions (set_operation_id.ex:63). |
low |
| 18 | 83-84 | Summary: forum threads “document … dead ordering assertions” | No forum thread is about dead ordering assertions; that finding comes from source reading. Remove those words from the forum clause. | low |
| 19 | 1290 | “The three concrete casualties:” over a four-row table | “four”. | low |
| 20 | 733 | ensure_state_selected.ex:5 |
The comment is at :6. |
low |
R2.3 New round-2 material: verification
- AddTemporalInlineAttributes (§5.3, 846-872): every point is confirmed against
ash_paper_trail/lib/resource/transformers/add_temporal_inline_attributes.ex. That includes the cross-extensionreferencewrite viaTransformer.build_entity(extension, path, :reference, …)with hard-codedAshPostgres.DataLayer/AshSqlite.DataLayermatching and nodsl_patch. Only the line numbers are off (#16). - DefineSchedulers (§5.4, 962-989): all four points and every cited line range confirmed (
:28,:30-35,:36-43,:57-294,:306-430,:433-697,:61/:435,:206-213,:446,:471,:448-457,:699-1008,:1068-1081;ash_oban/lib/info.ex:11-13). Theget_envvscompile_envconsequence carries the caveat. - AllowResourceVersions compile-time cross-module calls: confirmed (
allow_resource_versions.ex:18-19). - Nine forum threads: all authors, dates and quoted text checked against
https://forum.elixirforum.com/t/<id>.json. All correct, including the two new quotes in thread 73196 (#7 DGollings 2025-11-16 “from ~32s to ~9s”; #8 sezaru 2025-11-17 “From 18 seconds in a resource to around 6s”). The spark#86 maintainer quotes (“sleeping in a genserver loop”, “pathologically strange”) are verbatim. - Issue states and close dates: #290 open; #289, #86, #239, #103, #259, #94, #193, #29, #57, #126, #78, #231, #38, #83, #54 all match
gh. Only #104 is wrong (#13).
R2.4 Catalogue
Ash.CheckandAsh.Resource.Aggregate.Countare removed as contracts. Row 28b mentions Count only to say it does not exist, which is fine.- Added and correct: rows 26 (policy check), 28/28b/28c, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, all with real contract paths.
- “When it runs” is now right for every row (9 → after verifiers; 29 → compile time; 36 → compile-time
initthen runtime; 37 → compile-time registration, runtime evaluation). - Still wrong: the example users in #6 above, and the row 30-31 wording in #11.
R2.5 Counts
20 / 8 / 27 is consistent in the Summary (50), §2.7 (527, 563), §4, Sources (1537) and analysis #10 (1715). The one exception is line 1265 (#3).
R2.6 Low findings, sampled (half of round 1’s low rows)
Fixed and correct: DslError 5 attributes; read-only wording; Reactor 13 / its own section; 23 builders + 18 build_* (lines 214/261 verified); after_compile? users = 7; untyped extensions:; authorizer 12; AshEvents has no Module.create; EnsureStateSelected affects read actions only; CreateVersionResource :165/:367 un-swapped; mix.exs:8/:13; PaperTrail ships no tasks; :halt skips persisters; __spark_metadata__ deprecation warning; empty-stacktrace re-raise.
Not fixed despite the log: helper lines (#10), no_depend_modules lines (#9), “four things” (#12), AshPostgres lines in §7.2 and rows 30-31 (#11).
R2.7 Fresh citation sample (30 citations not checked in round 1)
| Judgement | Count |
|---|---|
| Supports | 22 |
| Wrong line, claim true | 6 |
| Does not support | 2 |
| Nonexistent | 0 |
Supports: add_state.ex:25-30; fill_in_event_defaults.ex:28-33; setup_archival.ex:23-29; add_temporal_inline_attributes.ex:36-95; :97-136; allow_resource_versions.ex:16-20; define_schedulers.ex:30-35; :306-430; :206-213; :479-497; :699-1008; ash_oban/lib/info.ex:11-13; ash_oban.ex:549; spark/lib/spark/dsl/extension.ex:2243-2262; :713-722; spark/lib/spark/dsl.ex:557-574; test-spark-verifiers.md:9-12; warning.ex:35-54; ash/lib/mix/tasks/ash.extend.ex:178-186; igniter/lib/mix/task.ex:32-36; builder.ex:214/:261; info_generator.ex:204-229.
Wrong line: ensure_state_selected.ex:5 (→6); add_temporal_inline_attributes.ex:164-166 (→171-175); :138-162 (→140-169); entity.ex:72 (→74); dsl.ex:517-521 (→521-523); the transformer.ex walk lines in §2.5 (see #2).
Does not support: spark.cheat_sheets_in_search.ex:11-25 for “only emits a warning” (#14); spark#104 close date (#13).
Separately, the catalogue names 5 nonexistent example modules plus 3 made-up or unsupported usage claims (#6). They are not counted in the 30 above.
R2.8 Required before final acceptance
- Fix #1 (the ordering consequence is a mechanism claim), #6 (made-up catalogue examples), #7, #8, #3, #4, #5.
- Fix the four items the revision log marks as done but which are not (#9-#12), then correct the log.
- The rest are one-line fixes.
Round 3 final check
Checked on 2026-10-01 against the round-3 document (1,842 lines, 15,302 words; revision log “Round 2” amended, “Round 3” added). Line numbers below refer to this version. Paths are relative to scratch/ash-src/.
Final verdict: ACCEPT.
- The mechanism sections are now correct:
Ash.Extension, verifier errors becoming warnings, theafter_compile?timing, the pairwise-conflict rule and its scope, and cycle handling. They match source, and the “read from code, not executed” caveat appears where it applies (§2.5 lines 488-490, §5.1, §5.4, Open questions). - All 20 round-2 items are addressed. 18 are fixed correctly; 2 are fixed with a new or leftover defect (see R3.1).
- The amended round-2 log now honestly records the four claims that were false before.
- What remains is a short list of citation, naming and code-labelling errors. None changes a mechanism. Readers can rely on the document together with the residual-errors list below.
R3.1 Round-2 items
| # | Item | Status |
|---|---|---|
| 1 | Ordering consequence | Fixed correctly (lines 448-451, 844). Only the edge to the named transformer is dropped. X still runs after every other transformer (spark/lib/spark/dsl/transformer.ex:414-433). |
| 2 | §2.5 line citations | Fixed (:418-419, :415-417, :444-473, :447, :455-460, :464, :469 all verified). |
| 3 | stray “21” | Fixed (line 1290). |
| 4 | “only 8 callbacks” leftover | Fixed (deleted). |
| 5 | ash#2670 comment and timing | Fixed correctly. The quote matches the comment text exactly, code included, and the comment URL #issuecomment-4216132881 exists (gh api repos/ash-project/ash/issues/2670/comments). “30 minutes apart; opened by joshprice, closed at the same timestamp as zachdaniel’s comment” is accurate. The trace block above it (1234-1239) is still an abridged excerpt with no label (R3.3). |
| 6 | Catalogue example modules | Mostly fixed. Verified to exist: AshMoney.Types.Money, Ash.Test.Expressions.JaroDistance (uses Ash.CustomExpression), …PolicyComplex.User.Relationships.BestFriend (uses Ash.Resource.ManualRelationship), Ash.Resource.Action.ImplementationFunction (uses Ash.Resource.Actions.Implementation), Ash.Tracer.Simple, the recursive_as and after_define users. Fixed wrongly: row 28c names Ash.Resource.ManualCreateFunction, which does not exist (residual list). |
| 7 | @moduledoc false rows |
Fixed (rows 5, 16-19; row 1 has a moduledoc). |
| 8 | invented parenthetical | Fixed (deleted). |
| 9 | no_depend_modules lines (previously claimed but not done) |
Fixed (entity.ex:87, section.ex:44, line 1222). |
| 10 | helper-table lines (previously claimed but not done) | Fixed (lines 315-326 all verified). |
| 11 | AshPostgres lines and rows 30-31 (previously claimed but not done) | Fixed (:649-652, :654-670, :671-674). Row 30 now has a small new contradiction (residual list). |
| 12 | “four things” (previously claimed but not done) | Fixed (line 784). |
| 13 | spark#104 date | Fixed (2024-07-07). |
| 14 | cheat_sheets_in_search | Fixed. |
| 15 | InfoGenerator kinds and example | Fixed (archive_base_filter?/1 is real, called at filter_archived.ex:14). Small wording slip (residual list). |
| 16 | AddTemporalInlineAttributes lines | Fixed in §5.3, but catalogue row 41 still has the old citation (residual list). |
| 17-20 | triggers, forum summary, “four casualties”, ensure_state_selected.ex:6 |
Fixed. |
R3.2 Catalogue (all 44+ rows)
Every contract name exists in source: Spark.Dsl.Section/Entity/Transformer/Verifier/Fragment/Builder/InfoGenerator, Spark.Dsl.Patch.AddEntity, Ash.DataLayer, Ash.Authorizer, Ash.Notifier, Ash.Resource.Change/Preparation/Validation/Calculation, Ash.Resource.Aggregate.CustomAggregate, Ash.Resource.Manual{Read,Create,Update,Destroy}, Ash.Resource.Interface, Ash.Extension, Ash.Type/NewType, Ash.CustomExpression, Ash.Resource.ManualRelationship, Ash.Resource.Actions.Implementation, Ash.Tracer, Ash.Policy.Check/SimpleCheck/FilterCheck, Igniter.Mix.Task.
The two fixes the researcher reported are confirmed: Ash.Notifier.PubSub (ash/lib/ash/notifier/pub_sub/pub_sub.ex:5; §4 line 704 and row 23) and Mix.Tasks.AshOban.Install (ash_oban/lib/mix/tasks/ash_oban.install.ex:30).
“When it runs” is correct for every row.
Remaining name problems: row 28c (nonexistent module); row 28’s example is the contract itself; row 30 calls name/0 an Ash.Extension callback. All three are in the residual list.
R3.3 Code blocks
None is labelled “(abridged)”. Verbatim, or abridged with visible .../… placeholders: 148-154 (verifiers), 428-431, 774-781, 796-811 (verbatim), 891-914 (top verified verbatim against create_version_resource.ex:367-379, then …). Not verbatim and not labelled (residual list):
- 210-215 (map example: the
persist:line is not in the source guide); - 299-306 and 371-373 (callback specs reformatted);
- 351-358 (the
if transform? dowrapper is omitted); - 503-507 (DslError output constructed; the tutorial’s real output has no location);
- 636-648 (fragment example invented; the Spark guide uses a DataLayer/postgres fragment);
- 1075-1083 (codegen loop simplified);
- 1104-1123 (
@optional_callbackscollapsed onto one line); - 1234-1239 (trace with lines dropped).
126-136 is an illustrative composite. None of these changes a stated mechanism.
R3.4 Counts
20 / 8 / 27 appears consistently: lines 50, 531, 568, 1290-1291, 1562, 1740. No “21” or “28 verifiers” remains outside the revision log, which quotes the old values on purpose.
R3.5 Fresh citation sample (25, none checked in earlier rounds)
| Judgement | Count |
|---|---|
| Supports | 14 |
| Wrong line, claim true | 9 |
| Does not support | 1 |
| Nonexistent | 1 |
Supports: spark/lib/spark/info_generator.ex:48-60; ash_postgres/lib/data_layer.ex:438-455; spark/lib/spark/dsl.ex:702-741; dsl.ex:724-734; ash_state_machine/lib/ash_state_machine.ex:23-49; :129; ash_archival/.../filter_archived.ex:14; the AshArchival Info is an InfoGenerator over [:archive]; ash#2670 comment URL; ash_oban.install.ex:30-45; pub_sub.ex:5; implementation_function.ex:5-7; best_friend.ex:5; jaro_distance.ex:5.
Wrong line:
transformer.ex:52-56(callbacks at:60-68);transformer.ex:58-67(__using__at:70)section.ex:38-58(@fields:33-54);entity.ex:62-82(:70-93)ash/lib/ash/resource.ex:38-41(simple_notifiersat:34-37)ash_state_machine.ex:26(identifierat:27)ash/lib/ash/domain/dsl.ex:165-168({:spark, Ash.Resource}at:148)igniter/lib/igniter.ex:402-434(defstructat:33; 402-434 are theadd_issue/add_warning/add_notice/add_taskhelpers)igniter.ex:715-800(update_elixir_file:558,update_file:618,include_or_create_file:717)
Does not support: spark/lib/spark/dsl/entity.ex:388-397 as the place where target structs must declare __spark_metadata__. Those lines are maybe_apply_identifier. The metadata is set at entity.ex:311-313, and Spark.Dsl.Entity.Meta is defined in spark/lib/spark/dsl/entity/meta.ex:5.
Nonexistent: Ash.Resource.ManualCreateFunction (row 28c).
R3.6 Did round 3 lose or break content?
No content that was correct after round 2 was lost. Section structure, case studies, forum evidence, issue states and the analysis section are intact. The new defects introduced in round 3 are only three: row 28c’s module name, row 30’s name/0 wording, and catalogue row 41 keeping the old citation that §5.3 corrected.
Residual errors (for readers)
Every statement below is still wrong in the final document. Use the correct fact instead. Everything else in the document checked out or sits outside what was sampled.
| Doc line | Statement | Correct fact (source) |
|---|---|---|
| 1500 | Row 28c example “Ash.Resource.ManualCreateFunction” |
The module is Ash.Resource.ManualCreate.Function (ash/lib/ash/resource/manual_actions/manual_create_function.ex:5; use Ash.Resource.ManualCreate at :7). |
| 1498 | Row 28 example user is “Ash.Resource.Calculation” (that is the contract itself) |
A real implementer: Ash.Resource.Calculation.Concat (ash/lib/ash/resource/calculation/concat.ex:7, use Ash.Resource.Calculation). |
| 1502 | Row 30: “codegen/1 + name/0 … optional Ash.Extension callbacks” |
codegen/1 is an optional callback. name/0 is not in the behaviour; it is only probed with function_exported?/3 (ash/lib/ash/extension.ex:21-44; ash/lib/mix/tasks/ash.codegen.ex:63-67). Row 34 states this correctly. |
| 1513 | Row 41 cites add_temporal_inline_attributes.ex:138-166 |
:140-175 (build/add :140-169, module matching :171-175), as §5.3 line 863-864 says. |
| 263-264 | “AshArchival.Resource.Info is use Spark.InfoGenerator, sections: [:archive]” |
It is use Spark.InfoGenerator, extension: AshArchival.Resource, sections: [:archive] (ash_archival/lib/ash_archival/resource/info.ex). |
| 297 | Transformer behaviour at transformer.ex:52-56; the block is reformatted |
The callbacks are at spark/lib/spark/dsl/transformer.ex:60-68. Treat the block as abridged. |
| 309 | Defaults injected at transformer.ex:58-67 |
__using__ is at transformer.ex:70. |
| 96, 100 | section.ex:38-58, entity.ex:62-82 for the field lists |
spark/lib/spark/dsl/section.ex:33-54, spark/lib/spark/dsl/entity.ex:70-93. |
| 122 | {:spark, Ash.Resource} at ash/lib/ash/domain/dsl.ex:165-168 |
ash/lib/ash/domain/dsl.ex:148. |
| ~703 | simple_notifiers at ash/lib/ash/resource.ex:38-41 |
ash/lib/ash/resource.ex:34-37. |
| ~724, 1475 | identifier: {:auto, :unique_integer} at ash_state_machine.ex:26 |
ash_state_machine/lib/ash_state_machine.ex:27. |
| 511 | “__spark_metadata__ … (spark/lib/spark/dsl/entity.ex:388-397)” |
The metadata is written at entity.ex:311-313. Spark.Dsl.Entity.Meta is defined in spark/lib/spark/dsl/entity/meta.ex:5. Lines 388-397 are maybe_apply_identifier. |
| §7.1 (~1044, ~1050) | Igniter.t() struct at igniter.ex:402-434; update_elixir_file/4, update_file/4, include_or_create_file/4 at igniter.ex:715-800 |
defstruct at igniter/lib/igniter.ex:33. update_elixir_file :558, update_file :618, include_or_create_file :717. |
| 393 | cond at dsl.ex:518-556 |
The cond begins at spark/lib/spark/dsl.ex:521. |
| 210-215 | Map example presented as from the tutorial and the Ash guide | The Ash guide (ash/documentation/topics/advanced/writing-extensions.md:36-44) shows only the [:attributes] entry plus .... The persist: %{module: MyApp.Tweet, …} line is the researcher’s illustration. |
| 503-507 | DslError output with “defined in lib/my_app/bad_validator.ex:3:12:” |
Constructed from the message/1 code (dsl_error.ex:47-62). The tutorial’s actual output is fields -> required: with no location (spark/documentation/tutorials/get-started-with-spark.md:390-392). |
| 636-648 | Fragment example (AshGraphql.Resource, graphql do) |
Not in the source. The Spark guide’s example is a DataLayer fragment (spark/documentation/how_to/split-up-large-dsls.md, use Spark.Dsl.Fragment, of: Ash.Resource, data_layer: AshPostgres.DataLayer). The mechanics described are correct. |
| 1075-1083 | ash.codegen loop shows extension.codegen(argv ++ ["--name", name]) unconditionally |
Source appends --name only when "--name" is not already in argv (ash/lib/mix/tasks/ash.codegen.ex:69-76). |
| 351-358 | transformers_to_run block shown without its wrapper |
In source it sits inside if transform? do … else [] end (spark/lib/spark/dsl/extension.ex:693-702). |
| 1234-1239 | ash#2670 trace shown as a contiguous block | Abridged: the issue’s trace also contains Ash.BehaviourHelpers.call_and_validate_return/5, …ForbidUnless.__build__/5 and lib/my_app/some_resource.ex:10: (module) lines (https://github.com/ash-project/ash/issues/2670). |