textus 0.50.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/README.md +41 -43
- data/SPEC.md +176 -176
- data/docs/architecture/README.md +46 -42
- data/docs/reference/conventions.md +31 -26
- data/lib/textus/boot.rb +15 -17
- data/lib/textus/call.rb +1 -1
- data/lib/textus/cli/runner.rb +15 -10
- data/lib/textus/cli/verb/get.rb +1 -3
- data/lib/textus/cli/verb/hook_run.rb +1 -1
- data/lib/textus/cli/verb/put.rb +4 -20
- data/lib/textus/cli/verb/serve.rb +19 -0
- data/lib/textus/cli.rb +1 -3
- data/lib/textus/dispatcher.rb +3 -3
- data/lib/textus/doctor/check/generator_drift.rb +4 -3
- data/lib/textus/doctor/check/handler_allowlist.rb +1 -1
- data/lib/textus/doctor/check/intake_registration.rb +5 -5
- data/lib/textus/doctor/check/rule_ambiguity.rb +3 -3
- data/lib/textus/doctor/check/sentinels.rb +2 -2
- data/lib/textus/doctor/check/templates.rb +13 -11
- data/lib/textus/doctor.rb +0 -2
- data/lib/textus/domain/freshness/evaluator.rb +150 -14
- data/lib/textus/domain/freshness/verdict.rb +28 -6
- data/lib/textus/domain/freshness.rb +4 -33
- data/lib/textus/domain/jobs/job.rb +58 -0
- data/lib/textus/domain/jobs/registry.rb +37 -0
- data/lib/textus/domain/policy/base_guards.rb +1 -1
- data/lib/textus/domain/policy/predicates/fresh_within.rb +1 -1
- data/lib/textus/domain/policy/publish_target.rb +34 -0
- data/lib/textus/domain/policy/retention.rb +29 -0
- data/lib/textus/domain/policy/source.rb +73 -0
- data/lib/textus/domain/retention/sweep.rb +57 -0
- data/lib/textus/domain/retention.rb +11 -0
- data/lib/textus/errors.rb +4 -4
- data/lib/textus/hooks/builtin.rb +5 -5
- data/lib/textus/hooks/catalog.rb +7 -7
- data/lib/textus/hooks/context.rb +5 -10
- data/lib/textus/init/templates/machine_intake.rb +4 -4
- data/lib/textus/init.rb +47 -47
- data/lib/textus/jobs/handlers.rb +62 -0
- data/lib/textus/jobs/scheduler.rb +36 -0
- data/lib/textus/jobs/seeder.rb +57 -0
- data/lib/textus/key/matching.rb +24 -0
- data/lib/textus/layout.rb +8 -0
- data/lib/textus/maintenance/drain.rb +42 -0
- data/lib/textus/maintenance/retention/apply.rb +52 -0
- data/lib/textus/maintenance/serve.rb +30 -0
- data/lib/textus/maintenance/worker.rb +74 -0
- data/lib/textus/manifest/capabilities.rb +1 -1
- data/lib/textus/manifest/data.rb +18 -3
- data/lib/textus/manifest/entry/base.rb +28 -9
- data/lib/textus/manifest/entry/nested.rb +3 -4
- data/lib/textus/manifest/entry/parser.rb +25 -21
- data/lib/textus/manifest/entry/produced.rb +56 -0
- data/lib/textus/manifest/entry/publish/subtree_mirror.rb +7 -6
- data/lib/textus/manifest/entry/publish/to_paths.rb +62 -11
- data/lib/textus/manifest/entry/validators/format_matrix.rb +3 -11
- data/lib/textus/manifest/entry/validators/publish.rb +3 -1
- data/lib/textus/manifest/entry/validators.rb +0 -1
- data/lib/textus/manifest/policy.rb +16 -4
- data/lib/textus/manifest/resolver.rb +10 -4
- data/lib/textus/manifest/rules.rb +37 -36
- data/lib/textus/manifest/schema/keys.rb +98 -0
- data/lib/textus/manifest/schema/validator.rb +324 -0
- data/lib/textus/manifest/schema/vocabulary.rb +24 -0
- data/lib/textus/manifest/schema.rb +27 -247
- data/lib/textus/manifest.rb +5 -3
- data/lib/textus/mcp/server.rb +1 -1
- data/lib/textus/ports/audit_log.rb +6 -0
- data/lib/textus/ports/build_lock.rb +6 -0
- data/lib/textus/ports/clock.rb +4 -3
- data/lib/textus/ports/produce_on_write_subscriber.rb +73 -0
- data/lib/textus/ports/publisher.rb +11 -7
- data/lib/textus/ports/queue.rb +130 -0
- data/lib/textus/produce/acquire/handler.rb +29 -0
- data/lib/textus/produce/acquire/intake.rb +130 -0
- data/lib/textus/produce/acquire/projection.rb +127 -0
- data/lib/textus/produce/acquire/serializer/json.rb +31 -0
- data/lib/textus/produce/acquire/serializer/text.rb +16 -0
- data/lib/textus/produce/acquire/serializer/yaml.rb +31 -0
- data/lib/textus/produce/acquire/serializer.rb +17 -0
- data/lib/textus/produce/engine.rb +95 -0
- data/lib/textus/produce/events.rb +36 -0
- data/lib/textus/produce/render.rb +23 -0
- data/lib/textus/projection.rb +17 -6
- data/lib/textus/read/deps.rb +3 -3
- data/lib/textus/read/freshness.rb +61 -31
- data/lib/textus/read/get.rb +20 -102
- data/lib/textus/read/jobs.rb +31 -0
- data/lib/textus/read/rdeps.rb +3 -3
- data/lib/textus/read/rule_explain.rb +41 -23
- data/lib/textus/read/rule_list.rb +25 -8
- data/lib/textus/read/validate_all.rb +14 -0
- data/lib/textus/role.rb +2 -1
- data/lib/textus/schemas.rb +8 -0
- data/lib/textus/store.rb +1 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/write/enqueue.rb +50 -0
- data/lib/textus/write/put.rb +1 -1
- metadata +35 -30
- data/lib/textus/builder/pipeline.rb +0 -88
- data/lib/textus/builder/renderer/json.rb +0 -45
- data/lib/textus/builder/renderer/markdown.rb +0 -24
- data/lib/textus/builder/renderer/text.rb +0 -14
- data/lib/textus/builder/renderer/yaml.rb +0 -45
- data/lib/textus/builder/renderer.rb +0 -17
- data/lib/textus/cli/verb/boot.rb +0 -14
- data/lib/textus/cli/verb/build.rb +0 -15
- data/lib/textus/doctor/check/fetch_locks.rb +0 -49
- data/lib/textus/doctor/check/lifecycle_action_invalid.rb +0 -39
- data/lib/textus/domain/freshness/policy.rb +0 -18
- data/lib/textus/domain/lifecycle.rb +0 -83
- data/lib/textus/domain/outcome.rb +0 -10
- data/lib/textus/domain/policy/lifecycle.rb +0 -35
- data/lib/textus/domain/staleness/generator_check.rb +0 -109
- data/lib/textus/domain/staleness.rb +0 -29
- data/lib/textus/maintenance/tend.rb +0 -110
- data/lib/textus/manifest/entry/derived.rb +0 -67
- data/lib/textus/manifest/entry/intake.rb +0 -31
- data/lib/textus/manifest/entry/validators/inject_boot.rb +0 -21
- data/lib/textus/mcp/tools.rb +0 -14
- data/lib/textus/ports/fetch/detached.rb +0 -52
- data/lib/textus/ports/fetch/lock.rb +0 -44
- data/lib/textus/write/build.rb +0 -90
- data/lib/textus/write/fetch_events.rb +0 -42
- data/lib/textus/write/fetch_orchestrator.rb +0 -101
- data/lib/textus/write/fetch_worker.rb +0 -127
- data/lib/textus/write/intake_fetch.rb +0 -25
- data/lib/textus/write/materializer.rb +0 -51
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: textus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.52.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -111,12 +111,6 @@ files:
|
|
|
111
111
|
- exe/textus
|
|
112
112
|
- lib/textus.rb
|
|
113
113
|
- lib/textus/boot.rb
|
|
114
|
-
- lib/textus/builder/pipeline.rb
|
|
115
|
-
- lib/textus/builder/renderer.rb
|
|
116
|
-
- lib/textus/builder/renderer/json.rb
|
|
117
|
-
- lib/textus/builder/renderer/markdown.rb
|
|
118
|
-
- lib/textus/builder/renderer/text.rb
|
|
119
|
-
- lib/textus/builder/renderer/yaml.rb
|
|
120
114
|
- lib/textus/call.rb
|
|
121
115
|
- lib/textus/cli.rb
|
|
122
116
|
- lib/textus/cli/group.rb
|
|
@@ -128,8 +122,6 @@ files:
|
|
|
128
122
|
- lib/textus/cli/group/zone.rb
|
|
129
123
|
- lib/textus/cli/runner.rb
|
|
130
124
|
- lib/textus/cli/verb.rb
|
|
131
|
-
- lib/textus/cli/verb/boot.rb
|
|
132
|
-
- lib/textus/cli/verb/build.rb
|
|
133
125
|
- lib/textus/cli/verb/doctor.rb
|
|
134
126
|
- lib/textus/cli/verb/get.rb
|
|
135
127
|
- lib/textus/cli/verb/hook_run.rb
|
|
@@ -140,6 +132,7 @@ files:
|
|
|
140
132
|
- lib/textus/cli/verb/schema_diff.rb
|
|
141
133
|
- lib/textus/cli/verb/schema_init.rb
|
|
142
134
|
- lib/textus/cli/verb/schema_migrate.rb
|
|
135
|
+
- lib/textus/cli/verb/serve.rb
|
|
143
136
|
- lib/textus/container.rb
|
|
144
137
|
- lib/textus/contract.rb
|
|
145
138
|
- lib/textus/contract/around.rb
|
|
@@ -153,13 +146,11 @@ files:
|
|
|
153
146
|
- lib/textus/doctor.rb
|
|
154
147
|
- lib/textus/doctor/check.rb
|
|
155
148
|
- lib/textus/doctor/check/audit_log.rb
|
|
156
|
-
- lib/textus/doctor/check/fetch_locks.rb
|
|
157
149
|
- lib/textus/doctor/check/generator_drift.rb
|
|
158
150
|
- lib/textus/doctor/check/handler_allowlist.rb
|
|
159
151
|
- lib/textus/doctor/check/hooks.rb
|
|
160
152
|
- lib/textus/doctor/check/illegal_keys.rb
|
|
161
153
|
- lib/textus/doctor/check/intake_registration.rb
|
|
162
|
-
- lib/textus/doctor/check/lifecycle_action_invalid.rb
|
|
163
154
|
- lib/textus/doctor/check/manifest_files.rb
|
|
164
155
|
- lib/textus/doctor/check/orphaned_publish_targets.rb
|
|
165
156
|
- lib/textus/doctor/check/proposal_targets.rb
|
|
@@ -176,17 +167,15 @@ files:
|
|
|
176
167
|
- lib/textus/domain/duration.rb
|
|
177
168
|
- lib/textus/domain/freshness.rb
|
|
178
169
|
- lib/textus/domain/freshness/evaluator.rb
|
|
179
|
-
- lib/textus/domain/freshness/policy.rb
|
|
180
170
|
- lib/textus/domain/freshness/verdict.rb
|
|
181
|
-
- lib/textus/domain/
|
|
182
|
-
- lib/textus/domain/
|
|
171
|
+
- lib/textus/domain/jobs/job.rb
|
|
172
|
+
- lib/textus/domain/jobs/registry.rb
|
|
183
173
|
- lib/textus/domain/permission.rb
|
|
184
174
|
- lib/textus/domain/policy/base_guards.rb
|
|
185
175
|
- lib/textus/domain/policy/evaluation.rb
|
|
186
176
|
- lib/textus/domain/policy/guard.rb
|
|
187
177
|
- lib/textus/domain/policy/guard_factory.rb
|
|
188
178
|
- lib/textus/domain/policy/handler_allowlist.rb
|
|
189
|
-
- lib/textus/domain/policy/lifecycle.rb
|
|
190
179
|
- lib/textus/domain/policy/matcher.rb
|
|
191
180
|
- lib/textus/domain/policy/predicates/author_held.rb
|
|
192
181
|
- lib/textus/domain/policy/predicates/etag_match.rb
|
|
@@ -195,9 +184,12 @@ files:
|
|
|
195
184
|
- lib/textus/domain/policy/predicates/schema_valid.rb
|
|
196
185
|
- lib/textus/domain/policy/predicates/target_is_canon.rb
|
|
197
186
|
- lib/textus/domain/policy/predicates/zone_writable_by.rb
|
|
187
|
+
- lib/textus/domain/policy/publish_target.rb
|
|
188
|
+
- lib/textus/domain/policy/retention.rb
|
|
189
|
+
- lib/textus/domain/policy/source.rb
|
|
190
|
+
- lib/textus/domain/retention.rb
|
|
191
|
+
- lib/textus/domain/retention/sweep.rb
|
|
198
192
|
- lib/textus/domain/sentinel.rb
|
|
199
|
-
- lib/textus/domain/staleness.rb
|
|
200
|
-
- lib/textus/domain/staleness/generator_check.rb
|
|
201
193
|
- lib/textus/entry.rb
|
|
202
194
|
- lib/textus/entry/base.rb
|
|
203
195
|
- lib/textus/entry/json.rb
|
|
@@ -221,27 +213,33 @@ files:
|
|
|
221
213
|
- lib/textus/init.rb
|
|
222
214
|
- lib/textus/init/templates/machine_intake.rb
|
|
223
215
|
- lib/textus/init/templates/orientation_reducer.rb
|
|
216
|
+
- lib/textus/jobs/handlers.rb
|
|
217
|
+
- lib/textus/jobs/scheduler.rb
|
|
218
|
+
- lib/textus/jobs/seeder.rb
|
|
224
219
|
- lib/textus/key/distance.rb
|
|
225
220
|
- lib/textus/key/grammar.rb
|
|
221
|
+
- lib/textus/key/matching.rb
|
|
226
222
|
- lib/textus/key/path.rb
|
|
227
223
|
- lib/textus/layout.rb
|
|
228
224
|
- lib/textus/maintenance.rb
|
|
225
|
+
- lib/textus/maintenance/drain.rb
|
|
229
226
|
- lib/textus/maintenance/key_delete_prefix.rb
|
|
230
227
|
- lib/textus/maintenance/key_mv_prefix.rb
|
|
228
|
+
- lib/textus/maintenance/retention/apply.rb
|
|
231
229
|
- lib/textus/maintenance/rule_lint.rb
|
|
232
|
-
- lib/textus/maintenance/
|
|
230
|
+
- lib/textus/maintenance/serve.rb
|
|
231
|
+
- lib/textus/maintenance/worker.rb
|
|
233
232
|
- lib/textus/maintenance/zone_mv.rb
|
|
234
233
|
- lib/textus/manifest.rb
|
|
235
234
|
- lib/textus/manifest/capabilities.rb
|
|
236
235
|
- lib/textus/manifest/data.rb
|
|
237
236
|
- lib/textus/manifest/entry.rb
|
|
238
237
|
- lib/textus/manifest/entry/base.rb
|
|
239
|
-
- lib/textus/manifest/entry/derived.rb
|
|
240
238
|
- lib/textus/manifest/entry/ignore_matcher.rb
|
|
241
|
-
- lib/textus/manifest/entry/intake.rb
|
|
242
239
|
- lib/textus/manifest/entry/leaf.rb
|
|
243
240
|
- lib/textus/manifest/entry/nested.rb
|
|
244
241
|
- lib/textus/manifest/entry/parser.rb
|
|
242
|
+
- lib/textus/manifest/entry/produced.rb
|
|
245
243
|
- lib/textus/manifest/entry/publish.rb
|
|
246
244
|
- lib/textus/manifest/entry/publish/mode.rb
|
|
247
245
|
- lib/textus/manifest/entry/publish/none.rb
|
|
@@ -253,30 +251,41 @@ files:
|
|
|
253
251
|
- lib/textus/manifest/entry/validators/events.rb
|
|
254
252
|
- lib/textus/manifest/entry/validators/format_matrix.rb
|
|
255
253
|
- lib/textus/manifest/entry/validators/ignore.rb
|
|
256
|
-
- lib/textus/manifest/entry/validators/inject_boot.rb
|
|
257
254
|
- lib/textus/manifest/entry/validators/publish.rb
|
|
258
255
|
- lib/textus/manifest/policy.rb
|
|
259
256
|
- lib/textus/manifest/resolver.rb
|
|
260
257
|
- lib/textus/manifest/rules.rb
|
|
261
258
|
- lib/textus/manifest/schema.rb
|
|
259
|
+
- lib/textus/manifest/schema/keys.rb
|
|
260
|
+
- lib/textus/manifest/schema/validator.rb
|
|
261
|
+
- lib/textus/manifest/schema/vocabulary.rb
|
|
262
262
|
- lib/textus/mcp.rb
|
|
263
263
|
- lib/textus/mcp/catalog.rb
|
|
264
264
|
- lib/textus/mcp/errors.rb
|
|
265
265
|
- lib/textus/mcp/server.rb
|
|
266
266
|
- lib/textus/mcp/session.rb
|
|
267
267
|
- lib/textus/mcp/tool_schemas.rb
|
|
268
|
-
- lib/textus/mcp/tools.rb
|
|
269
268
|
- lib/textus/mustache.rb
|
|
270
269
|
- lib/textus/ports/audit_log.rb
|
|
271
270
|
- lib/textus/ports/audit_subscriber.rb
|
|
272
271
|
- lib/textus/ports/build_lock.rb
|
|
273
272
|
- lib/textus/ports/clock.rb
|
|
274
|
-
- lib/textus/ports/
|
|
275
|
-
- lib/textus/ports/fetch/lock.rb
|
|
273
|
+
- lib/textus/ports/produce_on_write_subscriber.rb
|
|
276
274
|
- lib/textus/ports/publisher.rb
|
|
275
|
+
- lib/textus/ports/queue.rb
|
|
277
276
|
- lib/textus/ports/sentinel_store.rb
|
|
278
277
|
- lib/textus/ports/storage/file_stat.rb
|
|
279
278
|
- lib/textus/ports/storage/file_store.rb
|
|
279
|
+
- lib/textus/produce/acquire/handler.rb
|
|
280
|
+
- lib/textus/produce/acquire/intake.rb
|
|
281
|
+
- lib/textus/produce/acquire/projection.rb
|
|
282
|
+
- lib/textus/produce/acquire/serializer.rb
|
|
283
|
+
- lib/textus/produce/acquire/serializer/json.rb
|
|
284
|
+
- lib/textus/produce/acquire/serializer/text.rb
|
|
285
|
+
- lib/textus/produce/acquire/serializer/yaml.rb
|
|
286
|
+
- lib/textus/produce/engine.rb
|
|
287
|
+
- lib/textus/produce/events.rb
|
|
288
|
+
- lib/textus/produce/render.rb
|
|
280
289
|
- lib/textus/projection.rb
|
|
281
290
|
- lib/textus/read/audit.rb
|
|
282
291
|
- lib/textus/read/blame.rb
|
|
@@ -286,6 +295,7 @@ files:
|
|
|
286
295
|
- lib/textus/read/doctor.rb
|
|
287
296
|
- lib/textus/read/freshness.rb
|
|
288
297
|
- lib/textus/read/get.rb
|
|
298
|
+
- lib/textus/read/jobs.rb
|
|
289
299
|
- lib/textus/read/list.rb
|
|
290
300
|
- lib/textus/read/published.rb
|
|
291
301
|
- lib/textus/read/pulse.rb
|
|
@@ -307,14 +317,9 @@ files:
|
|
|
307
317
|
- lib/textus/uid.rb
|
|
308
318
|
- lib/textus/version.rb
|
|
309
319
|
- lib/textus/write/accept.rb
|
|
310
|
-
- lib/textus/write/
|
|
311
|
-
- lib/textus/write/fetch_events.rb
|
|
312
|
-
- lib/textus/write/fetch_orchestrator.rb
|
|
313
|
-
- lib/textus/write/fetch_worker.rb
|
|
314
|
-
- lib/textus/write/intake_fetch.rb
|
|
320
|
+
- lib/textus/write/enqueue.rb
|
|
315
321
|
- lib/textus/write/key_delete.rb
|
|
316
322
|
- lib/textus/write/key_mv.rb
|
|
317
|
-
- lib/textus/write/materializer.rb
|
|
318
323
|
- lib/textus/write/propose.rb
|
|
319
324
|
- lib/textus/write/put.rb
|
|
320
325
|
- lib/textus/write/reject.rb
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Builder
|
|
5
|
-
module InjectMeta
|
|
6
|
-
# Returns a new hash with _meta as the first key, per SPEC §6 ordering.
|
|
7
|
-
# Carries only deterministic provenance (`from`/`reduce`/`template`) — the
|
|
8
|
-
# volatile `generated_at` is deliberately NOT stamped, so the built
|
|
9
|
-
# artifact is content-addressed and a rebuild is a byte-for-byte no-op
|
|
10
|
-
# (ADR 0070). Build time lives out of the tracked artifact.
|
|
11
|
-
def self.call(content_hash, mentry)
|
|
12
|
-
meta = {}
|
|
13
|
-
if mentry.is_a?(Textus::Manifest::Entry::Derived)
|
|
14
|
-
src = mentry.source
|
|
15
|
-
if src.is_a?(Textus::Manifest::Entry::Derived::Projection)
|
|
16
|
-
from = Array(src.select).compact
|
|
17
|
-
meta["from"] = from unless from.empty?
|
|
18
|
-
meta["reduce"] = src.transform if src.transform
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
meta["template"] = mentry.template if mentry.template
|
|
22
|
-
|
|
23
|
-
out = { "_meta" => meta }
|
|
24
|
-
content_hash.each { |k, v| out[k] = v unless k == "_meta" }
|
|
25
|
-
out
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
module Pipeline
|
|
30
|
-
Deps = Data.define(
|
|
31
|
-
:manifest, :reader, :lister, :rpc, :template_loader, :transform_context, :inject_boot
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
def self.renderers
|
|
35
|
-
@renderers ||= {
|
|
36
|
-
"markdown" => Renderer::Markdown,
|
|
37
|
-
"text" => Renderer::Text,
|
|
38
|
-
"json" => Renderer::Json,
|
|
39
|
-
"yaml" => Renderer::Yaml,
|
|
40
|
-
}
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def self.run(mentry:, deps:)
|
|
44
|
-
# 1. Load sources + project + reduce. Only projection-derived entries are
|
|
45
|
-
# buildable in-process; External entries are generated out-of-band and are
|
|
46
|
-
# filtered out upstream (Derived#publish_via), so reaching here with a
|
|
47
|
-
# non-projection source is a wiring bug — fail loudly rather than emit an
|
|
48
|
-
# empty payload (and never re-stamp the volatile generated_at, ADR 0070).
|
|
49
|
-
unless mentry.is_a?(Textus::Manifest::Entry::Derived) && mentry.projection?
|
|
50
|
-
raise UsageError.new(
|
|
51
|
-
"builder: '#{mentry.key}' is not a projection-derived entry; only projections are buildable",
|
|
52
|
-
)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
data =
|
|
56
|
-
Textus::Projection.new(
|
|
57
|
-
reader: deps.reader,
|
|
58
|
-
spec: mentry.source.to_h.transform_keys(&:to_s),
|
|
59
|
-
lister: deps.lister,
|
|
60
|
-
rpc: deps.rpc,
|
|
61
|
-
transform_context: deps.transform_context,
|
|
62
|
-
).run
|
|
63
|
-
data = data.merge("boot" => deps.inject_boot.call) if mentry.inject_boot && deps.inject_boot
|
|
64
|
-
|
|
65
|
-
# 2. Render
|
|
66
|
-
klass = renderers[mentry.format] or
|
|
67
|
-
raise UsageError.new("builder: unsupported format #{mentry.format.inspect} for '#{mentry.key}'")
|
|
68
|
-
bytes = klass.new(template_loader: deps.template_loader).call(mentry: mentry, data: data)
|
|
69
|
-
|
|
70
|
-
# 3. Write (idempotent: skip if only generated_at would differ)
|
|
71
|
-
target_path = Key::Path.resolve(deps.manifest.data, mentry)
|
|
72
|
-
FileUtils.mkdir_p(File.dirname(target_path))
|
|
73
|
-
write_if_changed(target_path, bytes, mentry.format)
|
|
74
|
-
|
|
75
|
-
target_path
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Built artifacts are content-addressed (no volatile timestamp, ADR 0070),
|
|
79
|
-
# so identity is plain byte-equality: skip the write when nothing changed.
|
|
80
|
-
# `format` is retained for signature stability across renderers.
|
|
81
|
-
def self.write_if_changed(target_path, bytes, _format)
|
|
82
|
-
return if File.exist?(target_path) && File.binread(target_path) == bytes
|
|
83
|
-
|
|
84
|
-
File.binwrite(target_path, bytes)
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Builder
|
|
5
|
-
class Renderer
|
|
6
|
-
class Json < Renderer
|
|
7
|
-
def call(mentry:, data:)
|
|
8
|
-
content = mentry.template ? parse_rendered_template!(mentry, data) : default_shape(mentry, data)
|
|
9
|
-
final = mentry.provenance ? InjectMeta.call(content, mentry) : content
|
|
10
|
-
Entry.for_format("json").serialize(meta: {}, body: "", content: final)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
private
|
|
14
|
-
|
|
15
|
-
def parse_rendered_template!(mentry, data)
|
|
16
|
-
rendered = Mustache.render(@template_loader.call(mentry.template), data)
|
|
17
|
-
begin
|
|
18
|
-
parsed = ::JSON.parse(rendered)
|
|
19
|
-
rescue ::JSON::ParserError => e
|
|
20
|
-
raise BadRender.new("entry '#{mentry.key}': template did not render valid json: #{e.message}", format: "json")
|
|
21
|
-
end
|
|
22
|
-
unless parsed.is_a?(Hash)
|
|
23
|
-
raise BadRender.new("entry '#{mentry.key}': template must render a top-level object/mapping",
|
|
24
|
-
format: "json")
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
parsed
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def default_shape(mentry, data)
|
|
31
|
-
has_transform = mentry.is_a?(Textus::Manifest::Entry::Derived) &&
|
|
32
|
-
mentry.source.is_a?(Textus::Manifest::Entry::Derived::Projection) &&
|
|
33
|
-
mentry.source.transform
|
|
34
|
-
if has_transform && data.is_a?(Hash) && !data.key?("entries")
|
|
35
|
-
data
|
|
36
|
-
elsif data.is_a?(Hash) && data["entries"].is_a?(Array)
|
|
37
|
-
{ "entries" => data["entries"] }
|
|
38
|
-
else
|
|
39
|
-
data.is_a?(Hash) ? data : { "entries" => Array(data) }
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Builder
|
|
3
|
-
class Renderer
|
|
4
|
-
class Markdown < Renderer
|
|
5
|
-
def call(mentry:, data:)
|
|
6
|
-
raise TemplateError.new("entry '#{mentry.key}': markdown build requires a template") unless mentry.template
|
|
7
|
-
|
|
8
|
-
body = Mustache.render(@template_loader.call(mentry.template), data)
|
|
9
|
-
from = if mentry.is_a?(Textus::Manifest::Entry::Derived) &&
|
|
10
|
-
mentry.source.is_a?(Textus::Manifest::Entry::Derived::Projection)
|
|
11
|
-
Array(mentry.source.select).compact
|
|
12
|
-
else
|
|
13
|
-
[]
|
|
14
|
-
end
|
|
15
|
-
# Deterministic frontmatter only — `from` (the source keys), never a
|
|
16
|
-
# volatile `generated.at` (ADR 0070): the artifact is content-addressed
|
|
17
|
-
# so a rebuild is a byte-for-byte no-op and a revert never drifts.
|
|
18
|
-
frontmatter = { "generated" => { "from" => from } }
|
|
19
|
-
Entry.for_format("markdown").serialize(meta: frontmatter, body: body)
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Builder
|
|
3
|
-
class Renderer
|
|
4
|
-
class Text < Renderer
|
|
5
|
-
def call(mentry:, data:)
|
|
6
|
-
raise TemplateError.new("entry '#{mentry.key}': text build requires a template") unless mentry.template
|
|
7
|
-
|
|
8
|
-
body = Mustache.render(@template_loader.call(mentry.template), data)
|
|
9
|
-
Entry.for_format("text").serialize(meta: {}, body: body)
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
require "yaml"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Builder
|
|
5
|
-
class Renderer
|
|
6
|
-
class Yaml < Renderer
|
|
7
|
-
def call(mentry:, data:)
|
|
8
|
-
content = mentry.template ? parse_rendered_template!(mentry, data) : default_shape(mentry, data)
|
|
9
|
-
final = InjectMeta.call(content, mentry)
|
|
10
|
-
Entry.for_format("yaml").serialize(meta: {}, body: "", content: final)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
private
|
|
14
|
-
|
|
15
|
-
def parse_rendered_template!(mentry, data)
|
|
16
|
-
rendered = Mustache.render(@template_loader.call(mentry.template), data)
|
|
17
|
-
begin
|
|
18
|
-
parsed = ::YAML.safe_load(rendered, permitted_classes: [Date, Time], aliases: false)
|
|
19
|
-
rescue Psych::SyntaxError, Psych::DisallowedClass, Psych::AliasesNotEnabled => e
|
|
20
|
-
raise BadRender.new("entry '#{mentry.key}': template did not render valid yaml: #{e.message}", format: "yaml")
|
|
21
|
-
end
|
|
22
|
-
unless parsed.is_a?(Hash)
|
|
23
|
-
raise BadRender.new("entry '#{mentry.key}': template must render a top-level object/mapping",
|
|
24
|
-
format: "yaml")
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
parsed
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def default_shape(mentry, data)
|
|
31
|
-
has_transform = mentry.is_a?(Textus::Manifest::Entry::Derived) &&
|
|
32
|
-
mentry.source.is_a?(Textus::Manifest::Entry::Derived::Projection) &&
|
|
33
|
-
mentry.source.transform
|
|
34
|
-
if has_transform && data.is_a?(Hash) && !data.key?("entries")
|
|
35
|
-
data
|
|
36
|
-
elsif data.is_a?(Hash) && data["entries"].is_a?(Array)
|
|
37
|
-
{ "entries" => data["entries"] }
|
|
38
|
-
else
|
|
39
|
-
data.is_a?(Hash) ? data : { "entries" => Array(data) }
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Builder
|
|
3
|
-
# Abstract base for output renderers. Each concrete renderer owns
|
|
4
|
-
# producing the bytes for one manifest format (markdown/json/yaml/text).
|
|
5
|
-
class Renderer
|
|
6
|
-
def initialize(template_loader:)
|
|
7
|
-
@template_loader = template_loader
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def call(mentry:, data:)
|
|
11
|
-
_ = mentry
|
|
12
|
-
_ = data
|
|
13
|
-
raise NotImplementedError.new("#{self.class.name}#call not implemented")
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
data/lib/textus/cli/verb/boot.rb
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class CLI
|
|
3
|
-
class Verb
|
|
4
|
-
class Build < Runner::Base
|
|
5
|
-
self.spec = Textus::Write::Build.contract
|
|
6
|
-
|
|
7
|
-
option :prefix, "--prefix=K"
|
|
8
|
-
|
|
9
|
-
def invoke(store)
|
|
10
|
-
emit(store.as(resolved_role(store)).build(prefix: prefix))
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
# Lists per-key fetch lock files under <root>/.run/locks/ whose
|
|
5
|
-
# recorded PID is no longer running. These are forensic artifacts only:
|
|
6
|
-
# Fetch::Lock uses flock(2), which the kernel releases on process
|
|
7
|
-
# death, so stale files do not block subsequent acquires. The check
|
|
8
|
-
# exists to let users clean up clutter and notice unexpected accumulation
|
|
9
|
-
# (e.g. a fetch path that crashes repeatedly).
|
|
10
|
-
class FetchLocks < Check
|
|
11
|
-
def call
|
|
12
|
-
dir = Textus::Layout.locks(root)
|
|
13
|
-
return [] unless File.directory?(dir)
|
|
14
|
-
|
|
15
|
-
Dir.glob(File.join(dir, "*.lock")).filter_map { |path| inspect_lock(path) }
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
private
|
|
19
|
-
|
|
20
|
-
def inspect_lock(path)
|
|
21
|
-
pid = File.read(path).strip.to_i
|
|
22
|
-
return nil if pid.zero?
|
|
23
|
-
return nil if pid_alive?(pid)
|
|
24
|
-
|
|
25
|
-
{
|
|
26
|
-
"code" => "fetch_lock.stale",
|
|
27
|
-
"level" => "info",
|
|
28
|
-
"subject" => path,
|
|
29
|
-
"message" => "fetch lock file at #{path} records dead PID #{pid} " \
|
|
30
|
-
"(does not block fetch; flock is kernel-released on exit)",
|
|
31
|
-
"fix" => "safe to delete: rm #{path}",
|
|
32
|
-
}
|
|
33
|
-
rescue Errno::ENOENT
|
|
34
|
-
nil
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def pid_alive?(pid)
|
|
38
|
-
Process.kill(0, pid)
|
|
39
|
-
true
|
|
40
|
-
rescue Errno::ESRCH
|
|
41
|
-
false
|
|
42
|
-
rescue Errno::EPERM
|
|
43
|
-
# Process exists but owned by another user — treat as alive.
|
|
44
|
-
true
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
# ADR 0079: refresh is valid only for intake entries; drop/archive are
|
|
5
|
-
# invalid for intake entries (they would re-fetch, not prune).
|
|
6
|
-
class LifecycleActionInvalid < Check
|
|
7
|
-
def call
|
|
8
|
-
manifest.data.entries.filter_map do |mentry|
|
|
9
|
-
policy = manifest.rules.for(mentry.key).lifecycle
|
|
10
|
-
next if policy.nil?
|
|
11
|
-
|
|
12
|
-
intake = mentry.is_a?(Textus::Manifest::Entry::Intake)
|
|
13
|
-
bad = (policy.on_expire == :refresh && !intake) || (policy.destructive? && intake)
|
|
14
|
-
next unless bad
|
|
15
|
-
|
|
16
|
-
issue_for(mentry, policy, intake)
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
def issue_for(mentry, policy, intake)
|
|
23
|
-
{
|
|
24
|
-
"code" => "lifecycle.action_invalid",
|
|
25
|
-
"level" => "error",
|
|
26
|
-
"subject" => mentry.key,
|
|
27
|
-
"message" => "on_expire: #{policy.on_expire} is not valid for a " \
|
|
28
|
-
"#{intake ? "intake" : "stored"} entry",
|
|
29
|
-
"fix" => if intake
|
|
30
|
-
"use on_expire: refresh|warn for intake entries"
|
|
31
|
-
else
|
|
32
|
-
"use on_expire: drop|archive|warn for stored entries"
|
|
33
|
-
end,
|
|
34
|
-
}
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Domain
|
|
3
|
-
class Freshness
|
|
4
|
-
Policy = Data.define(:ttl_seconds, :on_stale, :sync_budget_ms) do
|
|
5
|
-
def decide(verdict)
|
|
6
|
-
return Action::Return.new if verdict.fresh?
|
|
7
|
-
|
|
8
|
-
case on_stale
|
|
9
|
-
when :warn then Action::Return.new
|
|
10
|
-
when :sync then Action::FetchSync.new
|
|
11
|
-
when :timed_sync then Action::FetchTimed.new(budget_ms: sync_budget_ms)
|
|
12
|
-
else Action::Return.new
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
require "time"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Domain
|
|
5
|
-
# Unified lifecycle reporter (ADR 0079): which entries are past their ttl,
|
|
6
|
-
# and the on_expire action that applies. Replaces both Staleness::IntakeCheck
|
|
7
|
-
# and Retention. Age basis: _meta.last_fetched_at (intake) when present, else
|
|
8
|
-
# file mtime (stored). `self.verdict` is the pure per-entry decision that BOTH
|
|
9
|
-
# this reporter and `Read::Get` (Plan 2) call, so the basis logic lives once.
|
|
10
|
-
class Lifecycle
|
|
11
|
-
# Pure: is the entry past its ttl? -> [expired(bool), reason(String|nil)].
|
|
12
|
-
def self.verdict(policy:, last_fetched_at:, mtime:, now:)
|
|
13
|
-
ttl = policy.ttl_seconds
|
|
14
|
-
return [false, nil] if ttl.nil?
|
|
15
|
-
|
|
16
|
-
basis = parse_time(last_fetched_at) || mtime
|
|
17
|
-
return [true, "never recorded"] if basis.nil?
|
|
18
|
-
|
|
19
|
-
age = (now - basis).to_i
|
|
20
|
-
age > ttl ? [true, "ttl exceeded (age=#{age}s, ttl=#{ttl}s)"] : [false, nil]
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def self.parse_time(str)
|
|
24
|
-
return nil if str.nil?
|
|
25
|
-
|
|
26
|
-
Time.parse(str.to_s)
|
|
27
|
-
rescue ArgumentError, TypeError
|
|
28
|
-
nil
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def initialize(manifest:, file_stat:, clock:)
|
|
32
|
-
@manifest = manifest
|
|
33
|
-
@file_stat = file_stat
|
|
34
|
-
@clock = clock
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def call(prefix: nil, zone: nil)
|
|
38
|
-
@manifest.data.entries
|
|
39
|
-
.select { |m| entry_matches?(m, prefix: prefix, zone: zone) }
|
|
40
|
-
.flat_map { |m| rows_for(m) }
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
def entry_matches?(mentry, prefix:, zone:)
|
|
46
|
-
return false if zone && mentry.zone != zone
|
|
47
|
-
return false if prefix && !(mentry.key == prefix || mentry.key.start_with?("#{prefix}."))
|
|
48
|
-
|
|
49
|
-
true
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def rows_for(mentry)
|
|
53
|
-
policy = @manifest.rules.for(mentry.key).lifecycle
|
|
54
|
-
return [] if policy.nil?
|
|
55
|
-
|
|
56
|
-
@manifest.resolver.enumerate(prefix: mentry.key).filter_map do |row|
|
|
57
|
-
path = row[:path]
|
|
58
|
-
next unless @file_stat.exists?(path)
|
|
59
|
-
|
|
60
|
-
expired, _reason = self.class.verdict(
|
|
61
|
-
policy: policy,
|
|
62
|
-
last_fetched_at: last_fetched_at_of(mentry, path),
|
|
63
|
-
mtime: @file_stat.mtime(path),
|
|
64
|
-
now: @clock.now,
|
|
65
|
-
)
|
|
66
|
-
next unless expired
|
|
67
|
-
|
|
68
|
-
{
|
|
69
|
-
"key" => row[:key], "path" => path,
|
|
70
|
-
"action" => policy.on_expire.to_s, "expired" => true
|
|
71
|
-
}
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Reads _meta.last_fetched_at from the on-disk envelope (intake basis).
|
|
76
|
-
def last_fetched_at_of(mentry, path)
|
|
77
|
-
Entry.for_format(mentry.format).parse(@file_stat.read(path), path: path)["_meta"]["last_fetched_at"]
|
|
78
|
-
rescue StandardError
|
|
79
|
-
nil
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|