1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
//! What kind of thing a declared asset name refers to.
/// Set once, at the point an asset is declared — an operator's own config, SQL
/// introspection, or a manifest author's explicit `produces:`/`depends_on:` — and
/// carried alongside its case-preserved spelling from then on, rather than
/// reconstructed later by inspecting the name or the filesystem. Every earlier
/// attempt to answer "how do I check this asset's bytes for staleness" from the
/// string alone got a different case wrong — `contains('/')`, a directory scan, an
/// extension allowlist, `is_dir()` used to opt *out* of hashing instead of choosing
/// *how*. The kind is knowable exactly where the asset is declared; nothing after
/// that point should have to guess it back.
///
/// One declaration site has no parser and no operator to consult: a `produces:`,
/// `depends_on:` or `assets:` string a human wrote. [`default_kind_for_declared_name`]
/// is that site, and it reads only what such a string carries syntactically — a glob
/// metacharacter — rather than enumerating extensions, splitting on a path separator,
/// or asking the filesystem.
/// The default for a `produces:`, `depends_on:` or `assets:` name with no operator
/// or SQL parser behind it to consult — a plain manifest string a human wrote, with
/// no type syntax available.
///
/// Decided on the two things such a string carries syntactically, and nothing else.
/// A glob metacharacter means it can never resolve to one path, so the name is a
/// [`AssetKind::Pattern`]. A TRAILING path separator means it can never be a regular
/// file — `build/parts/` and `build/parts` name the same place, and only the first
/// spelling says which of the two it is — so the name is a [`AssetKind::Directory`].
/// Everything else is a file. Both rules read the string itself; neither enumerates
/// extensions and neither asks the filesystem.
///
/// The trailing separator used to fall through to [`AssetKind::File`], and the cost
/// was silent: `fs::read` on a directory errors, `runner::produced_artifact_hash`
/// withholds the hash, and the step re-runs on every run — for a `depends_on:`
/// entry with nothing on stderr, because `runner::missing_declared_produces`
/// reports the `produces:` side only.
///
/// **A separator-free `produces:` token is a path too, and for the ordering-edge
/// shape `examples/code-lists/arcform.yaml` writes as `produces: [raw_tables]` that
/// is a deliberate, loud cost.** There is no file at `<protocol dir>/raw_tables`, so
/// `fs::read` fails, `runner::produced_artifact_hash` returns `None`, the step is
/// stale on every run, and `runner::missing_declared_produces` names it on stderr
/// each time.
///
/// The alternative — reading a separator-free `produces:` name as [`AssetKind::Table`]
/// and dropping it out of the staleness path entirely — was built, driven and
/// withdrawn. It fabricates a false "unchanged": what the code can test is whether
/// arcform's introspection recorded the name, not whether the step's own work wrote
/// bytes under it, and those differ wherever a step writes through a path
/// introspection does not model — `ATTACH 'side.db'` inside a `sql:` step, or an
/// `assets:` override, which exists precisely for an asset arcform cannot discover.
/// A `side.db` declared that way could be truncated or deleted and the step still
/// reported `[skip: hash_clean]` at exit 0. A step that re-runs forever is expensive
/// and visible; a step certifying an artifact it can no longer read is neither.
/// `runner::tests::test_bare_produces_ordering_token_reruns_rather_than_settling`
/// and `tests/bare_produces_ordering_token.rs` pin that trade from both ends.
///
/// The failure mode when this lands on a name that is really a directory: `fs::read`
/// on a directory errors, which forces staleness exactly as a missing file does, so it
/// does not fabricate a false "unchanged" — the step re-runs on every run instead.
/// That is safe and it is expensive, and on the `depends_on:` side it is also
/// **silent**: nothing prints, because `missing_declared_produces` reports the
/// `produces:` side only.