textus 0.55.1 → 0.56.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 +175 -2768
- data/README.md +17 -18
- data/lib/textus/artifact.rb +8 -0
- data/lib/textus/errors.rb +2 -39
- data/lib/textus/human.rb +7 -0
- data/lib/textus/infra/adapters/concurrency_adapter.rb +15 -0
- data/lib/textus/infra/clock.rb +7 -0
- data/lib/textus/infra/database.rb +127 -0
- data/lib/textus/infra/file_system.rb +54 -0
- data/lib/textus/infra/locks.rb +50 -0
- data/lib/textus/infra/port/clock.rb +18 -0
- data/lib/textus/infra/port/publisher.rb +71 -0
- data/lib/textus/infra/port/sentinel.rb +25 -0
- data/lib/textus/infra/port/sentinel_store.rb +93 -0
- data/lib/textus/infra/store/audit_log.rb +42 -0
- data/lib/textus/infra/store/base.rb +25 -0
- data/lib/textus/infra/store/cursor_store.rb +27 -0
- data/lib/textus/infra/store/entry_index.rb +31 -0
- data/lib/textus/infra/store/envelope/meta.rb +63 -0
- data/lib/textus/infra/store/file_store.rb +23 -0
- data/lib/textus/infra/store/index/builder.rb +28 -0
- data/lib/textus/infra/store/index/lookup.rb +23 -0
- data/lib/textus/infra/store/link_store.rb +43 -0
- data/lib/textus/infra/store/workflow_queue.rb +54 -0
- data/lib/textus/infra/store.rb +29 -0
- data/lib/textus/ingest.rb +8 -0
- data/lib/textus/lanes/artifact/cache.rb +35 -0
- data/lib/textus/lanes/artifact/handlers.rb +29 -0
- data/lib/textus/lanes/artifact/lifecycle.rb +23 -0
- data/lib/textus/lanes/artifact/verbs.rb +18 -0
- data/lib/textus/lanes/ingest/dedup.rb +17 -0
- data/lib/textus/lanes/ingest/entry_types/asset.rb +45 -0
- data/lib/textus/lanes/ingest/entry_types/link.rb +16 -0
- data/lib/textus/lanes/ingest/entry_types/text.rb +17 -0
- data/lib/textus/lanes/ingest/entry_types.rb +25 -0
- data/lib/textus/lanes/ingest/handlers.rb +54 -0
- data/lib/textus/lanes/ingest/index_rebuilder.rb +13 -0
- data/lib/textus/lanes/ingest/key_builder.rb +33 -0
- data/lib/textus/lanes/ingest/resolver.rb +21 -0
- data/lib/textus/lanes/knowledge/freeform.rb +15 -0
- data/lib/textus/lanes/knowledge/handlers.rb +53 -0
- data/lib/textus/lanes/knowledge/runner.rb +45 -0
- data/lib/textus/lanes/knowledge/verbs.rb +16 -0
- data/lib/textus/lanes/scratchpad/handlers.rb +164 -0
- data/lib/textus/lanes/scratchpad/verbs.rb +73 -0
- data/lib/textus/{gate → protocol}/binder.rb +2 -10
- data/lib/textus/protocol/boot/catalog.rb +34 -0
- data/lib/textus/protocol/boot/context.rb +124 -0
- data/lib/textus/protocol/boot/protocol.rb +33 -0
- data/lib/textus/protocol/boot.rb +42 -0
- data/lib/textus/protocol/builder/domain.rb +17 -0
- data/lib/textus/protocol/builder/infrastructure.rb +17 -0
- data/lib/textus/protocol/builder/runtime.rb +46 -0
- data/lib/textus/protocol/builder.rb +11 -0
- data/lib/textus/protocol/container.rb +22 -0
- data/lib/textus/protocol/diff.rb +170 -0
- data/lib/textus/protocol/entry_constraint.rb +54 -0
- data/lib/textus/protocol/envelope.rb +70 -0
- data/lib/textus/protocol/format/base.rb +80 -0
- data/lib/textus/protocol/format/bash.rb +10 -0
- data/lib/textus/protocol/format/javascript.rb +10 -0
- data/lib/textus/protocol/format/json.rb +82 -0
- data/lib/textus/protocol/format/markdown.rb +53 -0
- data/lib/textus/protocol/format/python.rb +10 -0
- data/lib/textus/protocol/format/ruby.rb +10 -0
- data/lib/textus/protocol/format/script.rb +55 -0
- data/lib/textus/protocol/format/text.rb +48 -0
- data/lib/textus/protocol/format/yaml.rb +105 -0
- data/lib/textus/protocol/format.rb +73 -0
- data/lib/textus/protocol/freshness/ttl_evaluator.rb +61 -0
- data/lib/textus/protocol/gate.rb +61 -0
- data/lib/textus/protocol/handlers/command_context.rb +19 -0
- data/lib/textus/protocol/handlers/concern.rb +15 -0
- data/lib/textus/protocol/handlers/ops/audit_filter.rb +37 -0
- data/lib/textus/protocol/handlers/ops/bulk_planner.rb +38 -0
- data/lib/textus/protocol/handlers/ops/data_mv.rb +49 -0
- data/lib/textus/protocol/handlers/ops/git_blame.rb +65 -0
- data/lib/textus/protocol/handlers/ops/pulse_coordinator.rb +37 -0
- data/lib/textus/protocol/handlers/ops/source_expander.rb +41 -0
- data/lib/textus/protocol/handlers/query_context.rb +41 -0
- data/lib/textus/protocol/handlers/read.rb +67 -0
- data/lib/textus/protocol/handlers/system.rb +93 -0
- data/lib/textus/protocol/handlers/write.rb +57 -0
- data/lib/textus/protocol/init.rb +143 -0
- data/lib/textus/protocol/key/grammar.rb +35 -0
- data/lib/textus/protocol/key/matching.rb +23 -0
- data/lib/textus/protocol/key/path.rb +23 -0
- data/lib/textus/protocol/key.rb +25 -0
- data/lib/textus/protocol/lane.rb +12 -0
- data/lib/textus/{store/geometry.rb → protocol/layout.rb} +32 -22
- data/lib/textus/protocol/links/resolver.rb +33 -0
- data/lib/textus/protocol/links/uri_rewriter.rb +30 -0
- data/lib/textus/protocol/links.rb +10 -0
- data/lib/textus/protocol/manifest/data.rb +90 -0
- data/lib/textus/protocol/manifest/entry/base.rb +95 -0
- data/lib/textus/protocol/manifest/entry/leaf.rb +19 -0
- data/lib/textus/protocol/manifest/entry/nested.rb +19 -0
- data/lib/textus/protocol/manifest/entry/parser.rb +71 -0
- data/lib/textus/protocol/manifest/entry/publish.rb +209 -0
- data/lib/textus/protocol/manifest/entry.rb +10 -0
- data/lib/textus/protocol/manifest/policy/publish_target.rb +36 -0
- data/lib/textus/protocol/manifest/policy.rb +69 -0
- data/lib/textus/protocol/manifest/resolver.rb +156 -0
- data/lib/textus/protocol/manifest/schema/contract.rb +53 -0
- data/lib/textus/protocol/manifest/schema/validator.rb +52 -0
- data/lib/textus/protocol/manifest/schema.rb +40 -0
- data/lib/textus/protocol/manifest.rb +62 -0
- data/lib/textus/protocol/produce/render/context.rb +43 -0
- data/lib/textus/protocol/produce/render.rb +44 -0
- data/lib/textus/protocol/schema/registry.rb +44 -0
- data/lib/textus/protocol/schema/tools.rb +104 -0
- data/lib/textus/protocol/schema.rb +117 -0
- data/lib/textus/protocol/session/bridge.rb +11 -0
- data/lib/textus/protocol/session/context.rb +5 -0
- data/lib/textus/protocol/session.rb +87 -0
- data/lib/textus/protocol/store_engine/delete.rb +37 -0
- data/lib/textus/protocol/store_engine/move.rb +109 -0
- data/lib/textus/protocol/store_engine/pipeline.rb +61 -0
- data/lib/textus/protocol/store_engine/put.rb +104 -0
- data/lib/textus/protocol/store_engine.rb +86 -0
- data/lib/textus/protocol/v4.rb +25 -0
- data/lib/textus/protocol/verb_registry/arg_spec.rb +19 -0
- data/lib/textus/protocol/verb_registry/core_verbs.rb +164 -0
- data/lib/textus/protocol/verb_registry/verb_spec.rb +42 -0
- data/lib/textus/protocol/verb_registry.rb +42 -0
- data/lib/textus/surface/cli/group/{rule.rb → web.rb} +2 -2
- data/lib/textus/surface/cli/runner.rb +25 -25
- data/lib/textus/surface/cli/sources.rb +1 -3
- data/lib/textus/surface/cli/verb/get.rb +3 -6
- data/lib/textus/surface/cli/verb/init.rb +1 -1
- data/lib/textus/surface/cli/verb/put.rb +8 -6
- data/lib/textus/surface/cli/verb/web_serve.rb +22 -0
- data/lib/textus/surface/cli.rb +25 -6
- data/lib/textus/surface/dispatch.rb +17 -0
- data/lib/textus/surface/mcp/adapters/mcp_adapter.rb +28 -0
- data/lib/textus/surface/mcp/catalog.rb +26 -62
- data/lib/textus/surface/mcp/cursor_manager.rb +22 -0
- data/lib/textus/surface/mcp/drift_annotation.rb +19 -0
- data/lib/textus/surface/mcp/errors.rb +0 -10
- data/lib/textus/surface/mcp/server.rb +46 -46
- data/lib/textus/surface/watcher.rb +18 -12
- data/lib/textus/surface/web.rb +50 -0
- data/lib/textus/value/call.rb +1 -1
- data/lib/textus/{core → value}/duration.rb +1 -4
- data/lib/textus/value/etag.rb +7 -7
- data/lib/textus/value/payload.rb +7 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/consumer.rb +39 -0
- data/lib/textus/workflow/dsl/lifecycle.rb +32 -0
- data/lib/textus/workflow/dsl/pattern_matcher.rb +20 -0
- data/lib/textus/workflow/dsl/step_builder.rb +56 -0
- data/lib/textus/workflow/dsl.rb +111 -21
- data/lib/textus/workflow/event_emitter.rb +21 -0
- data/lib/textus/workflow/publisher.rb +59 -0
- data/lib/textus/workflow/registry.rb +31 -8
- data/lib/textus/workflow/runner.rb +25 -61
- data/lib/textus/workflow/scheduler.rb +45 -0
- data/lib/textus/workflow/step_executor.rb +78 -0
- data/lib/textus/workflow/step_helpers.rb +102 -0
- data/lib/textus/workflow.rb +2 -5
- data/lib/textus.rb +16 -80
- metadata +167 -183
- data/SPEC.md +0 -908
- data/docs/architecture/README.md +0 -273
- data/docs/reference/conventions.md +0 -138
- data/lib/textus/action/accept.rb +0 -46
- data/lib/textus/action/audit.rb +0 -94
- data/lib/textus/action/base.rb +0 -42
- data/lib/textus/action/blame.rb +0 -79
- data/lib/textus/action/boot.rb +0 -15
- data/lib/textus/action/data_mv.rb +0 -58
- data/lib/textus/action/deps.rb +0 -19
- data/lib/textus/action/doctor.rb +0 -17
- data/lib/textus/action/drain.rb +0 -31
- data/lib/textus/action/enqueue.rb +0 -37
- data/lib/textus/action/get.rb +0 -34
- data/lib/textus/action/ingest.rb +0 -199
- data/lib/textus/action/jobs.rb +0 -27
- data/lib/textus/action/key_delete.rb +0 -26
- data/lib/textus/action/key_delete_prefix.rb +0 -35
- data/lib/textus/action/key_mv.rb +0 -122
- data/lib/textus/action/key_mv_prefix.rb +0 -48
- data/lib/textus/action/list.rb +0 -28
- data/lib/textus/action/propose.rb +0 -42
- data/lib/textus/action/published.rb +0 -22
- data/lib/textus/action/pulse.rb +0 -49
- data/lib/textus/action/put.rb +0 -38
- data/lib/textus/action/rdeps.rb +0 -24
- data/lib/textus/action/reject.rb +0 -28
- data/lib/textus/action/rule_explain.rb +0 -81
- data/lib/textus/action/rule_lint.rb +0 -62
- data/lib/textus/action/rule_list.rb +0 -38
- data/lib/textus/action/schema_envelope.rb +0 -22
- data/lib/textus/action/uid.rb +0 -19
- data/lib/textus/action/where.rb +0 -21
- data/lib/textus/boot.rb +0 -202
- data/lib/textus/contract/arg.rb +0 -10
- data/lib/textus/contract/dsl.rb +0 -88
- data/lib/textus/contract/spec.rb +0 -25
- data/lib/textus/contract.rb +0 -12
- data/lib/textus/core/freshness/evaluator.rb +0 -150
- data/lib/textus/core/freshness/verdict.rb +0 -34
- data/lib/textus/core/freshness.rb +0 -11
- data/lib/textus/core/retention/sweep.rb +0 -57
- data/lib/textus/core/retention.rb +0 -11
- data/lib/textus/core/sentinel.rb +0 -30
- data/lib/textus/doctor/check/audit_log.rb +0 -34
- data/lib/textus/doctor/check/generator_drift.rb +0 -29
- data/lib/textus/doctor/check/illegal_keys.rb +0 -60
- data/lib/textus/doctor/check/manifest_files.rb +0 -25
- data/lib/textus/doctor/check/notebook_sources.rb +0 -50
- data/lib/textus/doctor/check/orphaned_publish_targets.rb +0 -35
- data/lib/textus/doctor/check/proposal_targets.rb +0 -45
- data/lib/textus/doctor/check/protocol_version.rb +0 -47
- data/lib/textus/doctor/check/publish_tree_index_overlap.rb +0 -48
- data/lib/textus/doctor/check/raw_asset_paths.rb +0 -50
- data/lib/textus/doctor/check/rule_ambiguity.rb +0 -49
- data/lib/textus/doctor/check/schema_parse_error.rb +0 -28
- data/lib/textus/doctor/check/schema_violations.rb +0 -28
- data/lib/textus/doctor/check/schemas.rb +0 -26
- data/lib/textus/doctor/check/sentinels.rb +0 -60
- data/lib/textus/doctor/check/stale_reviewed_stamp.rb +0 -54
- data/lib/textus/doctor/check/templates.rb +0 -29
- data/lib/textus/doctor/check/unowned_schema_fields.rb +0 -40
- data/lib/textus/doctor/check.rb +0 -41
- data/lib/textus/doctor/validator.rb +0 -93
- data/lib/textus/doctor.rb +0 -54
- data/lib/textus/format/base.rb +0 -49
- data/lib/textus/format/json.rb +0 -102
- data/lib/textus/format/markdown.rb +0 -80
- data/lib/textus/format/shared.rb +0 -17
- data/lib/textus/format/text.rb +0 -55
- data/lib/textus/format/yaml.rb +0 -125
- data/lib/textus/format.rb +0 -46
- data/lib/textus/gate/auth.rb +0 -212
- data/lib/textus/gate.rb +0 -92
- data/lib/textus/init.rb +0 -157
- data/lib/textus/jobs.rb +0 -9
- data/lib/textus/key/distance.rb +0 -55
- data/lib/textus/key/grammar.rb +0 -33
- data/lib/textus/key/matching.rb +0 -24
- data/lib/textus/key/path.rb +0 -28
- data/lib/textus/manifest/capabilities.rb +0 -29
- data/lib/textus/manifest/data.rb +0 -102
- data/lib/textus/manifest/entry/base.rb +0 -105
- data/lib/textus/manifest/entry/ignore_matcher.rb +0 -46
- data/lib/textus/manifest/entry/leaf.rb +0 -17
- data/lib/textus/manifest/entry/nested.rb +0 -37
- data/lib/textus/manifest/entry/parser.rb +0 -99
- data/lib/textus/manifest/entry/produced.rb +0 -36
- data/lib/textus/manifest/entry/publish/mode.rb +0 -45
- data/lib/textus/manifest/entry/publish/none.rb +0 -14
- data/lib/textus/manifest/entry/publish/subtree_mirror.rb +0 -73
- data/lib/textus/manifest/entry/publish/template.rb +0 -16
- data/lib/textus/manifest/entry/publish/to_paths.rb +0 -76
- data/lib/textus/manifest/entry/publish/tree.rb +0 -50
- data/lib/textus/manifest/entry/publish.rb +0 -45
- data/lib/textus/manifest/entry/validators/format_matrix.rb +0 -21
- data/lib/textus/manifest/entry/validators/ignore.rb +0 -28
- data/lib/textus/manifest/entry/validators/publish.rb +0 -30
- data/lib/textus/manifest/entry/validators.rb +0 -18
- data/lib/textus/manifest/entry.rb +0 -8
- data/lib/textus/manifest/policy/matcher.rb +0 -51
- data/lib/textus/manifest/policy/publish_target.rb +0 -34
- data/lib/textus/manifest/policy/react.rb +0 -30
- data/lib/textus/manifest/policy/retention.rb +0 -29
- data/lib/textus/manifest/policy/source.rb +0 -30
- data/lib/textus/manifest/policy.rb +0 -95
- data/lib/textus/manifest/resolver.rb +0 -118
- data/lib/textus/manifest/rules.rb +0 -86
- data/lib/textus/manifest/schema/contract.rb +0 -61
- data/lib/textus/manifest/schema/keys.rb +0 -90
- data/lib/textus/manifest/schema/semantics/cross_field.rb +0 -53
- data/lib/textus/manifest/schema/semantics/invariants.rb +0 -125
- data/lib/textus/manifest/schema/semantics/migration.rb +0 -83
- data/lib/textus/manifest/schema/semantics.rb +0 -27
- data/lib/textus/manifest/schema/validator.rb +0 -48
- data/lib/textus/manifest/schema/vocabulary.rb +0 -25
- data/lib/textus/manifest/schema.rb +0 -30
- data/lib/textus/manifest.rb +0 -65
- data/lib/textus/meta.rb +0 -54
- data/lib/textus/port/audit_log.rb +0 -268
- data/lib/textus/port/build_lock.rb +0 -64
- data/lib/textus/port/clock.rb +0 -10
- data/lib/textus/port/publisher.rb +0 -60
- data/lib/textus/port/sentinel_store.rb +0 -83
- data/lib/textus/port/storage/file_stat.rb +0 -19
- data/lib/textus/port/storage/file_store.rb +0 -26
- data/lib/textus/port/store.rb +0 -93
- data/lib/textus/port/watcher_lock.rb +0 -48
- data/lib/textus/produce/engine.rb +0 -50
- data/lib/textus/produce/render.rb +0 -20
- data/lib/textus/schema/tools.rb +0 -115
- data/lib/textus/schema.rb +0 -100
- data/lib/textus/schemas.rb +0 -54
- data/lib/textus/store/compositor.rb +0 -34
- data/lib/textus/store/container.rb +0 -43
- data/lib/textus/store/cursor.rb +0 -26
- data/lib/textus/store/envelope/reader.rb +0 -43
- data/lib/textus/store/envelope/writer.rb +0 -195
- data/lib/textus/store/index/builder.rb +0 -74
- data/lib/textus/store/index/lookup.rb +0 -60
- data/lib/textus/store/jobs/base.rb +0 -13
- data/lib/textus/store/jobs/index.rb +0 -15
- data/lib/textus/store/jobs/materialize.rb +0 -15
- data/lib/textus/store/jobs/plan.rb +0 -11
- data/lib/textus/store/jobs/planner.rb +0 -104
- data/lib/textus/store/jobs/queue.rb +0 -154
- data/lib/textus/store/jobs/registry.rb +0 -19
- data/lib/textus/store/jobs/retention.rb +0 -50
- data/lib/textus/store/jobs/sweep.rb +0 -21
- data/lib/textus/store/jobs/worker.rb +0 -64
- data/lib/textus/store/session.rb +0 -37
- data/lib/textus/store.rb +0 -99
- data/lib/textus/surface/cli/verb/doctor.rb +0 -19
- data/lib/textus/surface/cli/verb/schema_diff.rb +0 -17
- data/lib/textus/surface/cli/verb/schema_init.rb +0 -21
- data/lib/textus/surface/cli/verb/schema_migrate.rb +0 -21
- data/lib/textus/surface/mcp.rb +0 -8
- data/lib/textus/surface/projector.rb +0 -27
- data/lib/textus/surface/role_scope.rb +0 -34
- data/lib/textus/value/command.rb +0 -16
- data/lib/textus/value/envelope.rb +0 -89
- data/lib/textus/value/result.rb +0 -26
- data/lib/textus/workflow/collector.rb +0 -27
- data/lib/textus/workflow/context.rb +0 -5
- data/lib/textus/workflow/loader.rb +0 -17
- data/lib/textus/workflow/pattern.rb +0 -18
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
# ADR 0079: generator/build drift — a derived/external entry whose sources
|
|
5
|
-
# changed since its generated.at. Dependency-based (not age-based), so it
|
|
6
|
-
# stays OUT of the lifecycle/freshness unification and lives here as a
|
|
7
|
-
# health signal. This is the surviving home for what the removed `stale`
|
|
8
|
-
# verb reported.
|
|
9
|
-
class GeneratorDrift < Check
|
|
10
|
-
def call
|
|
11
|
-
gen = Textus::Core::Freshness::Evaluator.new(
|
|
12
|
-
manifest: manifest,
|
|
13
|
-
file_stat: Textus::Port::Storage::FileStat.new,
|
|
14
|
-
clock: Textus::Port::Clock.new,
|
|
15
|
-
)
|
|
16
|
-
manifest.data.entries.flat_map { |m| gen.drift_rows(m) }.map do |row|
|
|
17
|
-
{
|
|
18
|
-
"code" => "generator_drift",
|
|
19
|
-
"level" => "warning",
|
|
20
|
-
"subject" => row["key"],
|
|
21
|
-
"message" => row["reason"],
|
|
22
|
-
"fix" => "rematerialize the entry: `textus drain`",
|
|
23
|
-
}
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
class IllegalKeys < Check
|
|
5
|
-
def call
|
|
6
|
-
out = []
|
|
7
|
-
manifest.data.entries.each do |entry|
|
|
8
|
-
next unless entry.nested?
|
|
9
|
-
next if entry.publish_mode.keyless? # publish_tree files are opaque payload, never keys (ADR 0047)
|
|
10
|
-
|
|
11
|
-
base = File.join(root, Textus::Key::Path.normalize_relative_path(entry.path))
|
|
12
|
-
next unless File.directory?(base)
|
|
13
|
-
|
|
14
|
-
check_all_paths(entry, base, out)
|
|
15
|
-
end
|
|
16
|
-
out
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
def check_all_paths(entry, base, out)
|
|
22
|
-
walk_nested(base) do |abs_path, is_dir|
|
|
23
|
-
rel = abs_path.sub(%r{\A#{Regexp.escape(base)}/?}, "")
|
|
24
|
-
next if entry.ignored?(rel)
|
|
25
|
-
|
|
26
|
-
basename = File.basename(abs_path)
|
|
27
|
-
stem = is_dir ? basename : basename.sub(/#{Regexp.escape(File.extname(basename))}\z/, "")
|
|
28
|
-
next if stem.match?(Key::Grammar::SEGMENT)
|
|
29
|
-
|
|
30
|
-
out << issue(abs_path, stem)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def issue(abs_path, stem)
|
|
35
|
-
{
|
|
36
|
-
"code" => "key.illegal",
|
|
37
|
-
"level" => "error",
|
|
38
|
-
"subject" => abs_path,
|
|
39
|
-
"path" => abs_path,
|
|
40
|
-
"message" => "illegal key segment '#{stem}' at #{abs_path}",
|
|
41
|
-
"fix" => "rename the file/directory so each segment matches [a-z0-9][a-z0-9-]* " \
|
|
42
|
-
"(lowercase, digits, hyphens)",
|
|
43
|
-
}
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def walk_nested(root, &block)
|
|
47
|
-
Dir.each_child(root) do |name|
|
|
48
|
-
abs = File.join(root, name)
|
|
49
|
-
if File.directory?(abs)
|
|
50
|
-
walk_nested(abs, &block)
|
|
51
|
-
yield abs, true
|
|
52
|
-
else
|
|
53
|
-
yield abs, false
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
class ManifestFiles < Check
|
|
5
|
-
def call
|
|
6
|
-
manifest.data.entries.each_with_object([]) do |entry, out|
|
|
7
|
-
next if entry.nested?
|
|
8
|
-
|
|
9
|
-
path = Textus::Key::Path.resolve(manifest.data, entry)
|
|
10
|
-
next if File.exist?(path)
|
|
11
|
-
|
|
12
|
-
out << {
|
|
13
|
-
"code" => "manifest.missing_file",
|
|
14
|
-
"level" => "info",
|
|
15
|
-
"subject" => entry.key,
|
|
16
|
-
"message" => "declared entry has no file on disk at #{path}",
|
|
17
|
-
"fix" => "create the entry with 'textus put #{entry.key} --stdin --as=<role>' " \
|
|
18
|
-
"(or leave empty if not yet authored)",
|
|
19
|
-
}
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
class NotebookSources < Check
|
|
5
|
-
def call
|
|
6
|
-
issues = []
|
|
7
|
-
manifest.resolver.enumerate.each do |row|
|
|
8
|
-
next unless row[:key].start_with?("notebook.notes.")
|
|
9
|
-
next unless row[:path] && File.exist?(row[:path])
|
|
10
|
-
|
|
11
|
-
sources = parse_sources(row[:path])
|
|
12
|
-
sources.each do |raw_key|
|
|
13
|
-
next if raw_entry_exists?(raw_key)
|
|
14
|
-
|
|
15
|
-
issues << {
|
|
16
|
-
"code" => "notebook.source_missing",
|
|
17
|
-
"level" => "warning",
|
|
18
|
-
"subject" => row[:key],
|
|
19
|
-
"message" => "notebook entry '#{row[:key]}' references raw key '#{raw_key}' " \
|
|
20
|
-
"which does not exist in the store",
|
|
21
|
-
"fix" => "re-ingest the source: textus ingest ..., or remove the stale sources: entry",
|
|
22
|
-
}
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
issues
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def parse_sources(path)
|
|
31
|
-
content = File.read(path)
|
|
32
|
-
match = content.match(/\A---\n(.*?)\n---/m)
|
|
33
|
-
return [] unless match
|
|
34
|
-
|
|
35
|
-
front = YAML.safe_load(match[1])
|
|
36
|
-
Array(front&.dig("sources"))
|
|
37
|
-
rescue StandardError
|
|
38
|
-
[]
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def raw_entry_exists?(key)
|
|
42
|
-
path = manifest.resolver.resolve(key).path
|
|
43
|
-
File.exist?(path)
|
|
44
|
-
rescue Textus::UnknownKey, Textus::Error
|
|
45
|
-
false
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
# Flags published files whose recorded source no longer exists in the
|
|
5
|
-
# store. Per-leaf prune (ADR 0046) reconciles within a still-present
|
|
6
|
-
# leaf; a renamed or removed *whole* leaf orphans its entire target
|
|
7
|
-
# directory, which a per-entry build won't revisit. This check catches
|
|
8
|
-
# that drift without making `build` scan globally.
|
|
9
|
-
class OrphanedPublishTargets < Check
|
|
10
|
-
def call
|
|
11
|
-
sdir = Textus::Store::Geometry.new(root).sentinels_root
|
|
12
|
-
return [] unless File.directory?(sdir)
|
|
13
|
-
|
|
14
|
-
repo_root = File.dirname(root)
|
|
15
|
-
store = Textus::Port::SentinelStore.new
|
|
16
|
-
glob = File.join(sdir, "**", "*#{Textus::Port::SentinelStore::SUFFIX}")
|
|
17
|
-
Dir.glob(glob).filter_map do |spath|
|
|
18
|
-
sentinel = store.load(spath, repo_root)
|
|
19
|
-
next nil if sentinel.nil? || sentinel.source.nil?
|
|
20
|
-
next nil if File.exist?(sentinel.source)
|
|
21
|
-
|
|
22
|
-
{
|
|
23
|
-
"code" => "publish.orphaned_target",
|
|
24
|
-
"level" => "warning",
|
|
25
|
-
"subject" => sentinel.target,
|
|
26
|
-
"message" => "published file #{sentinel.target} has no source in the store " \
|
|
27
|
-
"(recorded source #{sentinel.source} is gone) — likely a renamed or removed leaf",
|
|
28
|
-
"fix" => "remove the stale copy and its sentinel: rm '#{sentinel.target}' '#{spath}'",
|
|
29
|
-
}
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
# Flags pending proposals whose `proposal.target_key` cannot ever be
|
|
5
|
-
# accepted: it points at a non-canon zone or resolves to no declared
|
|
6
|
-
# entry (ADR 0035). Reads the live queue zone; silent when there is no
|
|
7
|
-
# queue zone. Warnings, not errors — they are stale junk, not store
|
|
8
|
-
# corruption (the accept gate already refuses them).
|
|
9
|
-
class ProposalTargets < Check
|
|
10
|
-
def call
|
|
11
|
-
queue = manifest.policy.queue_lane
|
|
12
|
-
return [] unless queue
|
|
13
|
-
|
|
14
|
-
dispatch(:list, lane: queue).filter_map { |row| issue_for(row["key"]) }
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
private
|
|
18
|
-
|
|
19
|
-
def issue_for(key)
|
|
20
|
-
target = dispatch(:get, key).meta&.dig("proposal", "target_key")
|
|
21
|
-
return nil if target.nil? # not a proposal entry — skip
|
|
22
|
-
|
|
23
|
-
zone = manifest.resolver.resolve(target).entry.lane
|
|
24
|
-
return nil if manifest.policy.declared_kind(zone.to_s) == :canon
|
|
25
|
-
|
|
26
|
-
{
|
|
27
|
-
"code" => "proposal.target_not_canon",
|
|
28
|
-
"level" => "warning",
|
|
29
|
-
"subject" => key,
|
|
30
|
-
"message" => "proposal '#{key}' targets '#{target}' in zone '#{zone}' (not canon); it can never be accepted",
|
|
31
|
-
"fix" => "delete the proposal, or repoint target_key at a canon zone",
|
|
32
|
-
}
|
|
33
|
-
rescue Textus::UnknownKey
|
|
34
|
-
{
|
|
35
|
-
"code" => "proposal.target_unresolved",
|
|
36
|
-
"level" => "warning",
|
|
37
|
-
"subject" => key,
|
|
38
|
-
"message" => "proposal '#{key}' targets '#{target}', which resolves to no declared entry",
|
|
39
|
-
"fix" => "delete the proposal, or fix target_key",
|
|
40
|
-
}
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
require "yaml"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Doctor
|
|
5
|
-
class Check
|
|
6
|
-
# Runs as a standalone module (Check::ProtocolVersion.run(root:)) and also
|
|
7
|
-
# as a class-based doctor check (ProtocolVersion.new(store).call).
|
|
8
|
-
class ProtocolVersion < Check
|
|
9
|
-
# Standalone interface: root is the project root (parent of .textus/).
|
|
10
|
-
def self.run(root:)
|
|
11
|
-
path = File.join(root, ".textus", "manifest.yaml")
|
|
12
|
-
return [] unless File.exist?(path)
|
|
13
|
-
|
|
14
|
-
doc = YAML.safe_load_file(path, aliases: false) || {}
|
|
15
|
-
version = doc["version"]
|
|
16
|
-
return [] if version == "textus/4"
|
|
17
|
-
|
|
18
|
-
[{
|
|
19
|
-
"code" => "protocol_mismatch",
|
|
20
|
-
"severity" => "error",
|
|
21
|
-
"message" => "Store reports version=#{version.inspect}; this gem expects textus/4.",
|
|
22
|
-
"hint" => "Upgrade the store's manifest version to textus/4 (see CHANGELOG for breaking changes).",
|
|
23
|
-
}]
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Doctor check interface: root is the .textus/ directory itself,
|
|
27
|
-
# so manifest.yaml lives directly inside it.
|
|
28
|
-
def call
|
|
29
|
-
path = geometry.manifest_path
|
|
30
|
-
return [] unless File.exist?(path)
|
|
31
|
-
|
|
32
|
-
doc = YAML.safe_load_file(path, aliases: false) || {}
|
|
33
|
-
version = doc["version"]
|
|
34
|
-
return [] if version == "textus/4"
|
|
35
|
-
|
|
36
|
-
[{
|
|
37
|
-
"code" => "protocol_mismatch",
|
|
38
|
-
"level" => "error",
|
|
39
|
-
"subject" => path,
|
|
40
|
-
"message" => "Store reports version=#{version.inspect}; this gem expects textus/4.",
|
|
41
|
-
"fix" => "Upgrade the store's manifest version to textus/4 (see CHANGELOG for breaking changes).",
|
|
42
|
-
}]
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
# ADR 0047 Decision 4. A publish_tree entry prunes its WHOLE target dir on
|
|
5
|
-
# every build. If a derived entry's publish_to writes a file into that same
|
|
6
|
-
# dir, the tree's prune will delete it unless the tree `ignore`s that
|
|
7
|
-
# filename. Warn so the author adds the ignore before prune eats the index.
|
|
8
|
-
class PublishTreeIndexOverlap < Check
|
|
9
|
-
def call
|
|
10
|
-
entries = manifest.data.entries
|
|
11
|
-
trees = entries.select { |e| e.nested? && e.publish_tree }
|
|
12
|
-
return [] if trees.empty?
|
|
13
|
-
|
|
14
|
-
derived_targets = entries.flat_map do |e|
|
|
15
|
-
Array(e.publish_to).map { |rel| [e, rel] }
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
trees.flat_map do |tree|
|
|
19
|
-
target_prefix = "#{tree.publish_tree.chomp("/")}/"
|
|
20
|
-
derived_targets.filter_map do |(derived, rel)|
|
|
21
|
-
next nil unless rel.start_with?(target_prefix)
|
|
22
|
-
|
|
23
|
-
rel_to_target = rel.delete_prefix(target_prefix)
|
|
24
|
-
next nil if tree.ignored?(rel_to_target)
|
|
25
|
-
|
|
26
|
-
issue(tree, derived, rel, rel_to_target)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
private
|
|
32
|
-
|
|
33
|
-
def issue(tree, derived, rel, rel_to_target)
|
|
34
|
-
basename = File.basename(rel_to_target)
|
|
35
|
-
{
|
|
36
|
-
"code" => "publish.tree_index_overlap",
|
|
37
|
-
"level" => "warning",
|
|
38
|
-
"subject" => tree.key,
|
|
39
|
-
"message" => "publish_tree '#{tree.publish_tree}' overlaps derived entry " \
|
|
40
|
-
"'#{derived.key}' publish_to '#{rel}'; the tree's prune will delete it on rebuild",
|
|
41
|
-
"fix" => "add a glob covering '#{rel_to_target}' to entry '#{tree.key}' ignore " \
|
|
42
|
-
"(e.g. ignore: [\"**/#{basename}\"])",
|
|
43
|
-
}
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
class RawAssetPaths < Check
|
|
5
|
-
def call
|
|
6
|
-
raw_lane = manifest.policy.lanes_of_kind(:raw).first
|
|
7
|
-
return [] unless raw_lane
|
|
8
|
-
|
|
9
|
-
issues = []
|
|
10
|
-
manifest.resolver.enumerate.each do |row|
|
|
11
|
-
next unless row[:key].start_with?("raw.")
|
|
12
|
-
next unless row[:path] && File.exist?(row[:path])
|
|
13
|
-
|
|
14
|
-
raw_content = load_content(row[:path])
|
|
15
|
-
next unless raw_content.is_a?(Hash)
|
|
16
|
-
|
|
17
|
-
asset = raw_content["asset"]
|
|
18
|
-
next unless asset.is_a?(String) && !asset.empty?
|
|
19
|
-
|
|
20
|
-
asset_path = find_asset_path(asset)
|
|
21
|
-
next if File.exist?(asset_path)
|
|
22
|
-
|
|
23
|
-
issues << {
|
|
24
|
-
"code" => "raw_asset.missing_file",
|
|
25
|
-
"level" => "error",
|
|
26
|
-
"subject" => row[:key],
|
|
27
|
-
"message" => "raw entry '#{row[:key]}' references asset '#{asset}' " \
|
|
28
|
-
"which does not exist at #{asset_path}",
|
|
29
|
-
"fix" => "re-ingest the asset: textus key-delete #{row[:key]}, then textus ingest",
|
|
30
|
-
}
|
|
31
|
-
end
|
|
32
|
-
issues
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
def load_content(path)
|
|
38
|
-
require "yaml"
|
|
39
|
-
YAML.safe_load_file(path)
|
|
40
|
-
rescue StandardError
|
|
41
|
-
nil
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def find_asset_path(asset_rel)
|
|
45
|
-
File.join(geometry.root, "assets", asset_rel)
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
# Flags entries whose key is matched by two or more rule blocks of the
|
|
5
|
-
# SAME specificity in the same slot (lifecycle / handler_permit /
|
|
6
|
-
# guard / materialize). Ties are non-deterministic in the parser's pick step, so
|
|
7
|
-
# they're a configuration smell — surface them.
|
|
8
|
-
class RuleAmbiguity < Check
|
|
9
|
-
SLOTS = Textus::Manifest::Schema::FIELD_REGISTRY.select { |_, m| m[:in_ambiguity] }.keys.freeze
|
|
10
|
-
|
|
11
|
-
def call
|
|
12
|
-
out = []
|
|
13
|
-
rules = manifest.rules
|
|
14
|
-
manifest.data.entries.each do |mentry|
|
|
15
|
-
matches = rules.explain(mentry.key)
|
|
16
|
-
next if matches.length < 2
|
|
17
|
-
|
|
18
|
-
SLOTS.each { |slot| out.concat(ambiguities_for(mentry, slot, matches)) }
|
|
19
|
-
end
|
|
20
|
-
out
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
def ambiguities_for(mentry, slot, matches)
|
|
26
|
-
carriers = matches.select { |b| b.public_send(slot) }
|
|
27
|
-
return [] if carriers.length < 2
|
|
28
|
-
|
|
29
|
-
by_specificity = carriers.group_by { |b| Textus::Manifest::Policy::Matcher.specificity(b.match) }
|
|
30
|
-
tied = by_specificity.values.select { |group| group.length > 1 }
|
|
31
|
-
tied.map { |group| issue_for(mentry, slot, group) }
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def issue_for(mentry, slot, group)
|
|
35
|
-
globs = group.map(&:match).sort
|
|
36
|
-
{
|
|
37
|
-
"code" => "rule.ambiguity",
|
|
38
|
-
"level" => "warning",
|
|
39
|
-
"subject" => mentry.key,
|
|
40
|
-
"message" => "entry '#{mentry.key}' matches #{group.length} rule blocks at the same " \
|
|
41
|
-
"specificity for #{slot}: #{globs.join(", ")}",
|
|
42
|
-
"fix" => "narrow one of the conflicting match: globs in .textus/manifest.yaml so a single " \
|
|
43
|
-
"block wins for this key",
|
|
44
|
-
}
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
# Surfaces YAML parse failures for files in <store>/schemas/. Without
|
|
5
|
-
# this check, malformed schemas are silently skipped by other doctor
|
|
6
|
-
# checks (UnownedSchemaFields rescues, Schemas only checks filenames),
|
|
7
|
-
# leaving the operator with no signal that a schema is broken.
|
|
8
|
-
class SchemaParseError < Check
|
|
9
|
-
def call
|
|
10
|
-
dir = geometry.schemas_dir
|
|
11
|
-
return [] unless File.directory?(dir)
|
|
12
|
-
|
|
13
|
-
Dir.glob(File.join(dir, "*.yaml")).each_with_object([]) do |path, out|
|
|
14
|
-
Schema.load(path)
|
|
15
|
-
rescue StandardError => e
|
|
16
|
-
out << {
|
|
17
|
-
"code" => "schema.parse_error",
|
|
18
|
-
"level" => "error",
|
|
19
|
-
"subject" => path,
|
|
20
|
-
"message" => "schema failed to parse: #{e.class}: #{e.message}",
|
|
21
|
-
"fix" => "fix the YAML at #{path} (check indentation, quoted scalars, and aliases)",
|
|
22
|
-
}
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
class SchemaViolations < Check
|
|
5
|
-
def call
|
|
6
|
-
result = Textus::Doctor::Validator.new(
|
|
7
|
-
reader: ->(key, ctnr, c) { Value::Result.unwrap(Textus::Action::Get.call(container: ctnr, call: c, key: key)) },
|
|
8
|
-
manifest: @container.manifest,
|
|
9
|
-
audit_log: @container.audit_log,
|
|
10
|
-
schema_for: ->(name) { @container.schemas.fetch_or_nil(name) },
|
|
11
|
-
).call(container: @container, call: Textus::Value::Call.build(role: Textus::Value::Role::DEFAULT))
|
|
12
|
-
|
|
13
|
-
result["violations"].map do |v|
|
|
14
|
-
fix = v["expected"] &&
|
|
15
|
-
"field '#{v["field"]}' should be written by '#{v["expected"]}' (last writer: #{v["last_writer"]})"
|
|
16
|
-
{
|
|
17
|
-
"code" => v["code"],
|
|
18
|
-
"level" => "error",
|
|
19
|
-
"subject" => v["key"],
|
|
20
|
-
"message" => v["message"] || "#{v["code"]} on #{v["key"]}",
|
|
21
|
-
"fix" => fix,
|
|
22
|
-
}.compact
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
class Schemas < Check
|
|
5
|
-
def call
|
|
6
|
-
out = []
|
|
7
|
-
manifest.data.entries.each do |entry|
|
|
8
|
-
next if entry.schema.nil?
|
|
9
|
-
|
|
10
|
-
sp = geometry.schema_path(entry.schema)
|
|
11
|
-
next if File.exist?(sp)
|
|
12
|
-
|
|
13
|
-
out << {
|
|
14
|
-
"code" => "schema.missing",
|
|
15
|
-
"level" => "error",
|
|
16
|
-
"subject" => entry.key,
|
|
17
|
-
"message" => "schema '#{entry.schema}' not found at #{sp}",
|
|
18
|
-
"fix" => "create the schema file or run 'textus schema init #{entry.schema} --from=<key>'",
|
|
19
|
-
}
|
|
20
|
-
end
|
|
21
|
-
out
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
class Sentinels < Check
|
|
5
|
-
def call
|
|
6
|
-
store = Textus::Port::SentinelStore.new
|
|
7
|
-
file_stat = Textus::Port::Storage::FileStat.new
|
|
8
|
-
dir = Textus::Store::Geometry.new(root).sentinels_root
|
|
9
|
-
return [] unless file_stat.directory?(dir)
|
|
10
|
-
|
|
11
|
-
repo_root = File.dirname(root)
|
|
12
|
-
file_stat.glob(File.join(dir, "**", "*#{Textus::Port::SentinelStore::SUFFIX}")).flat_map do |sentinel_path|
|
|
13
|
-
inspect_sentinel(sentinel_path, repo_root, store, file_stat)
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
private
|
|
18
|
-
|
|
19
|
-
def inspect_sentinel(sentinel_path, repo_root, store, file_stat)
|
|
20
|
-
sentinel = store.load(sentinel_path, repo_root)
|
|
21
|
-
return [parse_error_issue(sentinel_path)] if sentinel.nil?
|
|
22
|
-
return [orphan_issue(sentinel_path, sentinel)] if sentinel.orphan?(file_stat)
|
|
23
|
-
return [drift_issue(sentinel)] if sentinel.drift?(file_stat)
|
|
24
|
-
|
|
25
|
-
[]
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def parse_error_issue(sentinel_path)
|
|
29
|
-
{
|
|
30
|
-
"code" => "sentinel.parse_error",
|
|
31
|
-
"level" => "warning",
|
|
32
|
-
"subject" => sentinel_path,
|
|
33
|
-
"message" => "sentinel is not valid JSON",
|
|
34
|
-
"fix" => "delete #{sentinel_path} and re-run 'textus drain' to regenerate",
|
|
35
|
-
}
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def orphan_issue(sentinel_path, sentinel)
|
|
39
|
-
{
|
|
40
|
-
"code" => "sentinel.orphan",
|
|
41
|
-
"level" => "warning",
|
|
42
|
-
"subject" => sentinel_path,
|
|
43
|
-
"message" => "sentinel target #{sentinel.target.inspect} no longer exists",
|
|
44
|
-
"fix" => "delete #{sentinel_path} (the published file is gone) or restore the target",
|
|
45
|
-
}
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def drift_issue(sentinel)
|
|
49
|
-
{
|
|
50
|
-
"code" => "sentinel.drift",
|
|
51
|
-
"level" => "warning",
|
|
52
|
-
"subject" => sentinel.target,
|
|
53
|
-
"message" => "published file at #{sentinel.target} was modified out-of-band",
|
|
54
|
-
"fix" => "re-run 'textus drain' to overwrite, or copy the manual edit back into the store source",
|
|
55
|
-
}
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
# Warns when a knowledge-lane doc's `**reviewed** YYYY-MM (vX.Y)` stamp
|
|
5
|
-
# is more than MINOR_THRESHOLD minor versions behind Textus::VERSION.
|
|
6
|
-
# The stamp is the human staleness signal declared in docs conventions
|
|
7
|
-
# (knowledge.docs-index); this check makes it machine-enforced.
|
|
8
|
-
class StaleReviewedStamp < Check
|
|
9
|
-
STAMP_RE = /\*\*reviewed\*\*\s+\d{4}-\d{2}\s+\(v(\d+\.\d+(?:\.\d+)?)\)/
|
|
10
|
-
MINOR_THRESHOLD = 5
|
|
11
|
-
|
|
12
|
-
def call
|
|
13
|
-
current_minor = parse_minor(Textus::VERSION)
|
|
14
|
-
issues = []
|
|
15
|
-
|
|
16
|
-
manifest.resolver.enumerate.each do |row|
|
|
17
|
-
next unless row[:key].to_s.start_with?("knowledge.")
|
|
18
|
-
next unless row[:path] && File.file?(row[:path])
|
|
19
|
-
|
|
20
|
-
body = File.read(row[:path])
|
|
21
|
-
m = body.match(STAMP_RE)
|
|
22
|
-
next unless m
|
|
23
|
-
|
|
24
|
-
reviewed_minor = parse_minor(m[1])
|
|
25
|
-
behind = current_minor - reviewed_minor
|
|
26
|
-
next unless behind > MINOR_THRESHOLD
|
|
27
|
-
|
|
28
|
-
issues << stale_issue(row[:key], m[1], behind)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
issues
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def parse_minor(version_str)
|
|
37
|
-
version_str.sub(/\Av/, "").split(".").map(&:to_i)[1] || 0
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def stale_issue(key, stamp_version, behind)
|
|
41
|
-
current_short = Textus::VERSION[/\A\d+\.\d+/]
|
|
42
|
-
{
|
|
43
|
-
"code" => "stale_reviewed_stamp",
|
|
44
|
-
"level" => "warning",
|
|
45
|
-
"subject" => key.to_s,
|
|
46
|
-
"message" => "reviewed at v#{stamp_version}; current is v#{Textus::VERSION} " \
|
|
47
|
-
"(#{behind} minor versions behind, threshold is #{MINOR_THRESHOLD})",
|
|
48
|
-
"fix" => "review the doc and update the stamp to: **reviewed** YYYY-MM (v#{current_short})",
|
|
49
|
-
}
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
class Templates < Check
|
|
5
|
-
def call
|
|
6
|
-
out = []
|
|
7
|
-
manifest.data.entries.each do |entry|
|
|
8
|
-
entry.publish_targets.each do |target|
|
|
9
|
-
template = target.template
|
|
10
|
-
next if template.nil?
|
|
11
|
-
|
|
12
|
-
tp = geometry.template_path(template)
|
|
13
|
-
next if File.exist?(tp)
|
|
14
|
-
|
|
15
|
-
out << {
|
|
16
|
-
"code" => "template.missing",
|
|
17
|
-
"level" => "error",
|
|
18
|
-
"subject" => entry.key,
|
|
19
|
-
"message" => "template '#{template}' not found at #{tp}",
|
|
20
|
-
"fix" => "create the file at #{tp} or update the publish target's template: field",
|
|
21
|
-
}
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
out
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|