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
data/lib/textus/boot.rb
DELETED
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
# Read-only "what's in this store and how do I use it" envelope.
|
|
3
|
-
# Boot is side-effect-free. Reads from pre-computed artifacts and
|
|
4
|
-
# the store catalog rather than computing inline.
|
|
5
|
-
module Boot
|
|
6
|
-
PROTOCOL_ID = PROTOCOL
|
|
7
|
-
|
|
8
|
-
# Human-readable name(s) for the live lane(s) of a given kind, or `fallback`
|
|
9
|
-
# when the manifest declares none. Lets write-flow guidance name the live
|
|
10
|
-
# lane by kind instead of a hardcoded instance name (ADR 0034).
|
|
11
|
-
def self.lane_label(manifest, kind, fallback)
|
|
12
|
-
lanes = manifest.policy.lanes_of_kind(kind)
|
|
13
|
-
lanes.empty? ? fallback : lanes.join(", ")
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# Static, store-independent parts of the agent-facing protocol. The
|
|
17
|
-
# `recipes` and `role_resolution` blocks are derived per-manifest in
|
|
18
|
-
# agent_protocol(...) because lane and role names are user-configurable.
|
|
19
|
-
AGENT_PROTOCOL_TEMPLATE = {
|
|
20
|
-
"envelope_shape" => {
|
|
21
|
-
"summary" => "every read/write payload is a JSON envelope with _meta, body, uid, and etag",
|
|
22
|
-
"fields" => {
|
|
23
|
-
"_meta" => "hash of structured frontmatter; schema-validated per entry family",
|
|
24
|
-
"body" => "string payload (markdown/text) or nil for json/yaml formats where body lives in _meta",
|
|
25
|
-
"uid" => "stable 16-char hex identifier; preserved across writes and key renames",
|
|
26
|
-
"etag" => "content hash; pass back on writes to detect concurrent edits",
|
|
27
|
-
},
|
|
28
|
-
"ref" => "SPEC.md §8",
|
|
29
|
-
},
|
|
30
|
-
}.freeze
|
|
31
|
-
|
|
32
|
-
# Curated agent-facing verb catalog. This declares which verbs the operator
|
|
33
|
-
# CLI surfaces and in what order — the editorial presentation. The summary of
|
|
34
|
-
# each verb is a fact, not presentation: it is derived from `contract.summary`
|
|
35
|
-
# at load time (ADR 0039). A literal "summary" survives here only for grouped
|
|
36
|
-
# CLI tokens (schema/key/rule/hook) that aggregate several sub-contracts and so
|
|
37
|
-
# have no single contract to derive from. CLI_VERBS itself is assigned in
|
|
38
|
-
# textus.rb after Zeitwerk eager_load so all contract files are present.
|
|
39
|
-
CURATED_CLI_VERBS = [
|
|
40
|
-
{ "name" => "boot" },
|
|
41
|
-
{ "name" => "list" },
|
|
42
|
-
{ "name" => "get" },
|
|
43
|
-
{ "name" => "ingest" },
|
|
44
|
-
{ "name" => "where" },
|
|
45
|
-
{ "name" => "schema", "summary" => "schema operations: 'schema show KEY', 'schema diff', 'schema init', 'schema migrate'" },
|
|
46
|
-
{ "name" => "put" },
|
|
47
|
-
{ "name" => "propose" },
|
|
48
|
-
{ "name" => "accept" },
|
|
49
|
-
{ "name" => "enqueue" },
|
|
50
|
-
{ "name" => "key", "summary" => "key operations: 'key delete', 'key mv', 'key uid'" },
|
|
51
|
-
{ "name" => "drain" },
|
|
52
|
-
{ "name" => "audit" },
|
|
53
|
-
{ "name" => "blame" },
|
|
54
|
-
{ "name" => "rule", "summary" => "inspect effective rules: 'rule list', 'rule explain KEY'" },
|
|
55
|
-
{ "name" => "doctor" },
|
|
56
|
-
{ "name" => "jobs" },
|
|
57
|
-
{ "name" => "pulse" },
|
|
58
|
-
].freeze
|
|
59
|
-
|
|
60
|
-
# verb token => contract.summary, for every Dispatcher verb that carries a
|
|
61
|
-
# contract. The single source for a verb's one-line summary (ADR 0039).
|
|
62
|
-
def self.contract_summaries
|
|
63
|
-
Textus::Action::VERBS.values
|
|
64
|
-
.select { |k| k.respond_to?(:contract?) && k.contract? }
|
|
65
|
-
.to_h { |k| [k.contract.verb.to_s, k.contract.summary] }
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Build the CLI verb catalog: each summary is derived from its contract when
|
|
69
|
-
# one exists, falling back to the curated editorial string for grouped tokens
|
|
70
|
-
# (schema/key/rule/hook). Called once from textus.rb after eager_load.
|
|
71
|
-
def self.build_cli_verbs
|
|
72
|
-
summaries = contract_summaries
|
|
73
|
-
CURATED_CLI_VERBS.map do |entry|
|
|
74
|
-
derived = summaries[entry["name"]]
|
|
75
|
-
derived ? entry.merge("summary" => derived) : entry
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def self.agent_quickstart(manifest, audit_log)
|
|
80
|
-
agent_role = manifest.policy.proposer_role
|
|
81
|
-
|
|
82
|
-
writable_lanes = manifest.data.declared_lane_kinds.keys.each_with_object([]) do |lane_name, acc|
|
|
83
|
-
next unless agent_role
|
|
84
|
-
|
|
85
|
-
verb = manifest.policy.verb_for_lane(lane_name)
|
|
86
|
-
writers = manifest.policy.roles_with_capability(verb)
|
|
87
|
-
acc << lane_name if writers.include?(agent_role)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
propose_lane = manifest.policy.propose_lane_for(agent_role)
|
|
91
|
-
|
|
92
|
-
{
|
|
93
|
-
"read_verbs" => Textus::Surface::MCP::Catalog.read_verbs,
|
|
94
|
-
"write_verbs" => agent_role ? Textus::Surface::MCP::Catalog.write_verbs : [],
|
|
95
|
-
"writable_lanes" => writable_lanes,
|
|
96
|
-
"propose_lane" => propose_lane,
|
|
97
|
-
"latest_seq" => audit_log.latest_seq,
|
|
98
|
-
}
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def self.recipes(manifest)
|
|
102
|
-
queue = manifest.policy.queue_lane
|
|
103
|
-
feeds = lane_label(manifest, :machine, "the machine lane")
|
|
104
|
-
{
|
|
105
|
-
"read" => {
|
|
106
|
-
"purpose" => "find and read an entry",
|
|
107
|
-
"steps" => [
|
|
108
|
-
"list (lane:, prefix:) — discover keys without reading bodies",
|
|
109
|
-
"get KEY — returns the entry envelope",
|
|
110
|
-
],
|
|
111
|
-
},
|
|
112
|
-
"write" => {
|
|
113
|
-
"purpose" => "create or update an entry",
|
|
114
|
-
"steps" => [
|
|
115
|
-
"schema KEY — learn the _meta field shape (required, optional, field types) before writing",
|
|
116
|
-
"assemble an envelope: { _meta: {…}, body: \"…\" }",
|
|
117
|
-
"put KEY — persist it (role-gated); pass if_etag to guard a concurrent edit",
|
|
118
|
-
],
|
|
119
|
-
},
|
|
120
|
-
"propose" => {
|
|
121
|
-
"purpose" => "agent suggests a change for human review",
|
|
122
|
-
"agent_steps" => [
|
|
123
|
-
"propose KEY — writes the change into the #{queue} lane for review",
|
|
124
|
-
],
|
|
125
|
-
"human_steps" => [
|
|
126
|
-
"accept #{queue}.KEY — promotes the proposal into its target lane",
|
|
127
|
-
],
|
|
128
|
-
},
|
|
129
|
-
"drain" => {
|
|
130
|
-
"purpose" => "keep the machine-maintained lanes fresh — re-pull stale intake entries from their declared source",
|
|
131
|
-
"steps" => [
|
|
132
|
-
"pulse — its `stale` list names entries past their ttl",
|
|
133
|
-
"drain (lane: #{feeds}) — re-pull the stale entries",
|
|
134
|
-
],
|
|
135
|
-
},
|
|
136
|
-
}
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def self.agent_protocol(manifest)
|
|
140
|
-
AGENT_PROTOCOL_TEMPLATE.merge(
|
|
141
|
-
"recipes" => recipes(manifest),
|
|
142
|
-
"role_resolution" => {
|
|
143
|
-
"summary" => "write role is resolved in order: --as flag, TEXTUS_ROLE env var, .textus/role file, " \
|
|
144
|
-
"then a transport default ('human' for CLI, 'agent' for MCP)",
|
|
145
|
-
"roles" => manifest.data.role_caps.keys,
|
|
146
|
-
"ref" => "SPEC.md §5",
|
|
147
|
-
},
|
|
148
|
-
)
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def self.build(container:)
|
|
152
|
-
manifest = container.manifest
|
|
153
|
-
etag = Textus::Value::Etag.for_contract(container.root)
|
|
154
|
-
|
|
155
|
-
{
|
|
156
|
-
"protocol" => PROTOCOL_ID,
|
|
157
|
-
"store_root" => container.root,
|
|
158
|
-
"contract_etag" => etag,
|
|
159
|
-
"lanes" => lanes_for(manifest),
|
|
160
|
-
"agent_quickstart" => agent_quickstart(manifest, container.audit_log),
|
|
161
|
-
"orientation" => read_artifact_content(container, "artifacts.config.orientation"),
|
|
162
|
-
"context" => read_boot_context(container),
|
|
163
|
-
"agent_protocol" => agent_protocol(manifest),
|
|
164
|
-
}.compact
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def self.read_artifact_content(container, key)
|
|
168
|
-
res = container.manifest.resolver.resolve(key)
|
|
169
|
-
return nil unless res.path && File.exist?(res.path)
|
|
170
|
-
|
|
171
|
-
call = Textus::Value::Call.build(role: Textus::Value::Role::DEFAULT)
|
|
172
|
-
env = Textus::Action::Get.call(container: container, call: call, key: key)
|
|
173
|
-
env&.content
|
|
174
|
-
rescue Textus::Error
|
|
175
|
-
nil
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def self.read_boot_context(container)
|
|
179
|
-
res = container.manifest.resolver.resolve("knowledge.boot")
|
|
180
|
-
return nil unless res.path && File.exist?(res.path)
|
|
181
|
-
|
|
182
|
-
call = Textus::Value::Call.build(role: Textus::Value::Role::DEFAULT)
|
|
183
|
-
env = Textus::Action::Get.call(container: container, call: call, key: "knowledge.boot")
|
|
184
|
-
body = env&.body&.strip
|
|
185
|
-
body.nil? || body.empty? ? nil : body
|
|
186
|
-
rescue Textus::Error
|
|
187
|
-
nil
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def self.lanes_for(manifest)
|
|
191
|
-
manifest.data.declared_lane_kinds.keys.map do |name|
|
|
192
|
-
verb = manifest.policy.verb_for_lane(name)
|
|
193
|
-
row = { "name" => name, "writers" => manifest.policy.roles_with_capability(verb) }
|
|
194
|
-
kind = manifest.policy.declared_kind(name)
|
|
195
|
-
row["kind"] = kind.to_s if kind
|
|
196
|
-
purpose = manifest.data.lane_descs[name]
|
|
197
|
-
row["purpose"] = purpose if purpose && !purpose.empty?
|
|
198
|
-
row
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
end
|
data/lib/textus/contract/arg.rb
DELETED
data/lib/textus/contract/dsl.rb
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Contract
|
|
3
|
-
module DSL
|
|
4
|
-
def verb(name = nil)
|
|
5
|
-
if name
|
|
6
|
-
raise "contract already built; declare verb before reading .contract" if defined?(@contract) && @contract
|
|
7
|
-
|
|
8
|
-
@__verb = name
|
|
9
|
-
else
|
|
10
|
-
@__verb
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def summary(text = nil)
|
|
15
|
-
if text
|
|
16
|
-
raise "contract already built; declare summary before reading .contract" if defined?(@contract) && @contract
|
|
17
|
-
|
|
18
|
-
@__summary = text
|
|
19
|
-
else
|
|
20
|
-
@__summary
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def surfaces(*list)
|
|
25
|
-
if list.empty?
|
|
26
|
-
@__surfaces ||= []
|
|
27
|
-
else
|
|
28
|
-
raise "contract already built; declare surfaces before reading .contract" if defined?(@contract) && @contract
|
|
29
|
-
|
|
30
|
-
@__surfaces = list
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def cli(path = nil)
|
|
35
|
-
if path
|
|
36
|
-
raise "contract already built; declare cli before reading .contract" if defined?(@contract) && @contract
|
|
37
|
-
|
|
38
|
-
@__cli = path.to_s
|
|
39
|
-
else
|
|
40
|
-
@__cli
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def arg(name, type, required: false, positional: false, session_default: nil, description: nil, wire_name: nil, default: nil, source: nil, coerce: nil, cli_default: :__unset) # rubocop:disable Metrics/ParameterLists,Layout/LineLength
|
|
45
|
-
raise "contract already built; declare args before reading .contract" if defined?(@contract) && @contract
|
|
46
|
-
|
|
47
|
-
(@__args ||= []) << Arg.new(
|
|
48
|
-
name: name, type: type, required: required,
|
|
49
|
-
positional: positional, session_default: session_default,
|
|
50
|
-
description: description, wire_name: wire_name, default: default,
|
|
51
|
-
source: source, coerce: coerce, cli_default: cli_default
|
|
52
|
-
)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def cli_stdin(mode = :__read)
|
|
56
|
-
return @__cli_stdin if mode == :__read
|
|
57
|
-
|
|
58
|
-
raise "contract already built; declare cli_stdin before reading .contract" if defined?(@contract) && @contract
|
|
59
|
-
|
|
60
|
-
@__cli_stdin = mode
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def view(surface = :default, &blk)
|
|
64
|
-
return (@__views ||= {})[surface] unless blk
|
|
65
|
-
|
|
66
|
-
raise "contract already built; declare view before reading .contract" if defined?(@contract) && @contract
|
|
67
|
-
|
|
68
|
-
(@__views ||= {})[surface] = blk
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def contract?
|
|
72
|
-
!@__verb.nil?
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def contract
|
|
76
|
-
@contract ||= Spec.new(
|
|
77
|
-
verb: @__verb,
|
|
78
|
-
summary: @__summary,
|
|
79
|
-
args: (@__args || []).freeze,
|
|
80
|
-
surfaces: (@__surfaces || []).freeze,
|
|
81
|
-
views: ((@__views ||= {})[:default] ||= ->(v, _i) { v }) && @__views,
|
|
82
|
-
cli: @__cli,
|
|
83
|
-
cli_stdin: @__cli_stdin,
|
|
84
|
-
)
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
data/lib/textus/contract/spec.rb
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Contract
|
|
3
|
-
Spec = Data.define(:verb, :summary, :args, :surfaces, :views, :cli, :cli_stdin) do
|
|
4
|
-
def mcp? = surfaces.include?(:mcp)
|
|
5
|
-
def cli? = surfaces.include?(:cli)
|
|
6
|
-
|
|
7
|
-
def view(surface = :default) = views[surface] || views.fetch(:default)
|
|
8
|
-
def cli_path = cli || verb.to_s
|
|
9
|
-
def cli_words = cli_path.split
|
|
10
|
-
def cli_group = cli_words.size > 1 ? cli_words.first : nil
|
|
11
|
-
def cli_leaf = cli_words.last
|
|
12
|
-
|
|
13
|
-
def required_args = args.select(&:required)
|
|
14
|
-
|
|
15
|
-
def input_schema
|
|
16
|
-
props = args.to_h do |a|
|
|
17
|
-
h = { "type" => Contract.json_type(a.type) }
|
|
18
|
-
h["description"] = a.description if a.description
|
|
19
|
-
[a.wire.to_s, h]
|
|
20
|
-
end
|
|
21
|
-
{ type: "object", properties: props, required: required_args.map { |a| a.wire.to_s } }
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
data/lib/textus/contract.rb
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Contract
|
|
3
|
-
JSON_TYPES = {
|
|
4
|
-
String => "string", Integer => "integer", Hash => "object",
|
|
5
|
-
Array => "array", :boolean => "boolean"
|
|
6
|
-
}.freeze
|
|
7
|
-
|
|
8
|
-
def self.json_type(type)
|
|
9
|
-
JSON_TYPES.fetch(type) { raise ArgumentError.new("no JSON type mapping for #{type.inspect}") }
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
end
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
require "time"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Core
|
|
5
|
-
module Freshness
|
|
6
|
-
# The single currency evaluator (ADR 0099). Answers "is the stored data
|
|
7
|
-
# stale relative to its retention rule?" and detects generator drift for
|
|
8
|
-
# external entries.
|
|
9
|
-
# - retention rule TTL -> AGE signal: now - file_basis > ttl_seconds
|
|
10
|
-
# - external -> DRIFT signal: a source changed since generated.at
|
|
11
|
-
# (surfaced by the doctor generator_drift check).
|
|
12
|
-
class Evaluator
|
|
13
|
-
def initialize(manifest:, file_stat:, clock:)
|
|
14
|
-
@manifest = manifest
|
|
15
|
-
@file_stat = file_stat
|
|
16
|
-
@clock = clock
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# Per-entry currency Verdict driven by the retention rule TTL (if any).
|
|
20
|
-
def verdict(mentry)
|
|
21
|
-
ttl = @manifest.rules.for(mentry.key).retention&.ttl_seconds
|
|
22
|
-
return fresh if ttl.nil?
|
|
23
|
-
|
|
24
|
-
stale = age_stale?(file_basis(mentry), ttl)
|
|
25
|
-
Verdict.build(stale: stale, reason: stale ? "ttl exceeded" : nil, fetching: false)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Keys of entries past their retention rule TTL — the refresh produce scope.
|
|
29
|
-
def stale_keys(prefix: nil, lane: nil)
|
|
30
|
-
@manifest.data.entries.select { |m| due?(m, prefix: prefix, lane: lane) }.map(&:key)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
alias stale_intake_keys stale_keys
|
|
34
|
-
|
|
35
|
-
# File basis as a Time (or nil): file mtime when present, else nil.
|
|
36
|
-
def file_basis(mentry)
|
|
37
|
-
path = @manifest.resolver.resolve(mentry.key).path
|
|
38
|
-
return nil unless @file_stat.exists?(path)
|
|
39
|
-
|
|
40
|
-
@file_stat.mtime(path)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# Generator-drift rows for one entry (replaces Staleness::GeneratorCheck#
|
|
44
|
-
# rows_for) — consumed by the doctor generator_drift check.
|
|
45
|
-
def drift_rows(mentry)
|
|
46
|
-
return [] unless drift_applicable?(mentry)
|
|
47
|
-
|
|
48
|
-
path = Textus::Key::Path.resolve(@manifest.data, mentry)
|
|
49
|
-
reason = drift_reason(mentry, path)
|
|
50
|
-
reason ? [drift_row(mentry, path, reason)] : []
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
private
|
|
54
|
-
|
|
55
|
-
def fresh = Verdict.build(stale: false, reason: nil, fetching: false)
|
|
56
|
-
|
|
57
|
-
def due?(mentry, prefix:, lane:)
|
|
58
|
-
return false if lane && mentry.lane != lane
|
|
59
|
-
return false if prefix && !mentry.key.start_with?(prefix)
|
|
60
|
-
|
|
61
|
-
ttl = @manifest.rules.for(mentry.key).retention&.ttl_seconds
|
|
62
|
-
return false if ttl.nil?
|
|
63
|
-
|
|
64
|
-
path = @manifest.resolver.resolve(mentry.key).path
|
|
65
|
-
return true unless @file_stat.exists?(path)
|
|
66
|
-
|
|
67
|
-
age_stale?(file_basis(mentry), ttl)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# The one age comparison. A never-recorded entry (nil basis) is stale.
|
|
71
|
-
def age_stale?(basis, ttl)
|
|
72
|
-
return true if basis.nil?
|
|
73
|
-
|
|
74
|
-
(@clock.now - basis).to_i > ttl
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# --- generator drift (lifted from Staleness::GeneratorCheck) ---
|
|
78
|
-
|
|
79
|
-
def drift_applicable?(mentry) = mentry.external?
|
|
80
|
-
|
|
81
|
-
def drift_reason(mentry, path)
|
|
82
|
-
return "derived entry has never been generated" unless @file_stat.exists?(path)
|
|
83
|
-
|
|
84
|
-
generated_at = generated_at_of(mentry, path)
|
|
85
|
-
return "missing generated.at frontmatter" unless generated_at
|
|
86
|
-
|
|
87
|
-
gen_time = parse_time(generated_at)
|
|
88
|
-
return "unparseable generated.at: #{generated_at.inspect}" unless gen_time
|
|
89
|
-
|
|
90
|
-
offender = newest_source_after(mentry.source, gen_time)
|
|
91
|
-
"source '#{offender}' modified after generated.at" if offender
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def generated_at_of(mentry, path)
|
|
95
|
-
Format.for(mentry.format).parse(@file_stat.read(path), path: path)["_meta"].dig("generated", "at")
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def parse_time(str)
|
|
99
|
-
Time.parse(str.to_s)
|
|
100
|
-
rescue StandardError
|
|
101
|
-
nil
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def newest_source_after(external_src, gen_time)
|
|
105
|
-
Array(external_src.sources).each do |src|
|
|
106
|
-
offender = check_source(src, gen_time)
|
|
107
|
-
return offender if offender
|
|
108
|
-
end
|
|
109
|
-
nil
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def check_source(src, gen_time)
|
|
113
|
-
if src.match?(/\A[a-z0-9.][a-z0-9._-]*\z/) && !src.include?("/")
|
|
114
|
-
@manifest.resolver.enumerate(prefix: src).each do |row|
|
|
115
|
-
return src if @file_stat.mtime(row[:path]) > gen_time
|
|
116
|
-
end
|
|
117
|
-
nil
|
|
118
|
-
else
|
|
119
|
-
check_filesystem_source(src, gen_time)
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def check_filesystem_source(src, gen_time)
|
|
124
|
-
abs = absolutize_source(src)
|
|
125
|
-
if @file_stat.directory?(abs)
|
|
126
|
-
dir_has_newer_file?(abs, gen_time) ? src : nil
|
|
127
|
-
elsif @file_stat.exists?(abs) && @file_stat.mtime(abs) > gen_time
|
|
128
|
-
src
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def absolutize_source(src)
|
|
133
|
-
File.absolute_path?(src) ? src : File.join(File.dirname(@manifest.data.root), src)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def dir_has_newer_file?(abs, gen_time)
|
|
137
|
-
@file_stat.glob(File.join(abs, "**", "*")).any? do |fpath|
|
|
138
|
-
file?(fpath) && @file_stat.mtime(fpath) > gen_time
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def file?(fpath) = !@file_stat.directory?(fpath) && @file_stat.exists?(fpath)
|
|
143
|
-
|
|
144
|
-
def drift_row(mentry, path, reason)
|
|
145
|
-
{ "key" => mentry.key, "path" => path, "generator" => mentry.source.command, "reason" => reason }
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
end
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Core
|
|
5
|
-
module Freshness
|
|
6
|
-
# Value object describing the freshness annotation attached to an Envelope
|
|
7
|
-
# after a currency evaluation (ADR 0099 — was Core::Freshness).
|
|
8
|
-
#
|
|
9
|
-
# Note on wire format: `#to_h_for_wire` is intentionally narrower than the
|
|
10
|
-
# full field set. It emits the legacy keys ("stale", "stale_reason",
|
|
11
|
-
# "fetching", and "fetch_error" when present) so the CLI JSON wire stays
|
|
12
|
-
# byte-identical with textus/4. The gem-side fields `checked_at` and
|
|
13
|
-
# `ttl_remaining_ms` are NOT emitted on the wire.
|
|
14
|
-
Verdict = Data.define(
|
|
15
|
-
:stale, :fetching, :reason, :fetch_error, :checked_at, :ttl_remaining_ms
|
|
16
|
-
) do
|
|
17
|
-
def self.build(stale:, fetching: false, reason: nil, fetch_error: nil,
|
|
18
|
-
checked_at: nil, ttl_remaining_ms: nil)
|
|
19
|
-
new(
|
|
20
|
-
stale: stale, fetching: fetching, reason: reason,
|
|
21
|
-
fetch_error: fetch_error, checked_at: checked_at,
|
|
22
|
-
ttl_remaining_ms: ttl_remaining_ms
|
|
23
|
-
)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def to_h_for_wire
|
|
27
|
-
h = { "stale" => stale, "stale_reason" => reason, "fetching" => fetching }
|
|
28
|
-
h["fetch_error"] = fetch_error unless fetch_error.nil?
|
|
29
|
-
h
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Core
|
|
5
|
-
# Currency — "is the stored data stale relative to its source?" (ADR 0099).
|
|
6
|
-
# The home of the single Freshness evaluator and its Verdict value object.
|
|
7
|
-
# Distinct from Core::Retention (GC dueness, Q2).
|
|
8
|
-
module Freshness
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
end
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
require "time"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Core
|
|
5
|
-
module Retention
|
|
6
|
-
# Retention sweep reporter (ADR 0093/0099). Which entries are past their
|
|
7
|
-
# `retention:` ttl and the destructive action that applies. Age basis: file
|
|
8
|
-
# mtime. Only drop/archive. Renamed off the Core::Retention vs
|
|
9
|
-
# Manifest::Policy::Retention collision (ADR 0099).
|
|
10
|
-
class Sweep
|
|
11
|
-
def self.expired?(ttl_seconds:, mtime:, now:)
|
|
12
|
-
return false if ttl_seconds.nil? || mtime.nil?
|
|
13
|
-
|
|
14
|
-
(now - mtime).to_i > ttl_seconds
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def initialize(manifest:, file_stat:, clock:)
|
|
18
|
-
@manifest = manifest
|
|
19
|
-
@file_stat = file_stat
|
|
20
|
-
@clock = clock
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def call(prefix: nil, lane: nil)
|
|
24
|
-
@manifest.data.entries
|
|
25
|
-
.select { |m| matches?(m, prefix: prefix, lane: lane) }
|
|
26
|
-
.flat_map { |m| rows_for(m) }
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
def matches?(mentry, prefix:, lane:)
|
|
32
|
-
return false if lane && mentry.lane != lane
|
|
33
|
-
return false if prefix && !Textus::Key::Matching.matches_prefix?(
|
|
34
|
-
mentry.key, prefix, nested: mentry.is_a?(Textus::Manifest::Entry::Nested)
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
true
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def rows_for(mentry)
|
|
41
|
-
policy = @manifest.rules.for(mentry.key).retention
|
|
42
|
-
return [] if policy.nil?
|
|
43
|
-
|
|
44
|
-
@manifest.resolver.enumerate(prefix: mentry.key).filter_map do |row|
|
|
45
|
-
path = row[:path]
|
|
46
|
-
next unless @file_stat.exists?(path)
|
|
47
|
-
next unless self.class.expired?(
|
|
48
|
-
ttl_seconds: policy.ttl_seconds, mtime: @file_stat.mtime(path), now: @clock.now,
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
{ "key" => row[:key], "path" => path, "action" => policy.action.to_s }
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Core
|
|
5
|
-
# Retention — "is the entry old enough to retire?" (Q2, ADR 0093/0099).
|
|
6
|
-
# GC dueness, orthogonal to Freshness (content currency). The reporter is
|
|
7
|
-
# Core::Retention::Sweep; the manifest rule policy is Manifest::Policy::Retention.
|
|
8
|
-
module Retention
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
end
|
data/lib/textus/core/sentinel.rb
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
require "digest"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Core
|
|
5
|
-
# Pure value object representing a published-file sentinel. Holds the
|
|
6
|
-
# recorded target path, source path, sha256 checksum, and publish mode.
|
|
7
|
-
# Has no filesystem I/O — path layout and persistence live in
|
|
8
|
-
# Ports::SentinelStore; predicate methods accept a FileStat port for
|
|
9
|
-
# existence and content checks.
|
|
10
|
-
class Sentinel
|
|
11
|
-
attr_reader :target, :source, :sha256, :mode
|
|
12
|
-
|
|
13
|
-
def initialize(target:, source:, sha256:, mode:)
|
|
14
|
-
@target = target
|
|
15
|
-
@source = source
|
|
16
|
-
@sha256 = sha256
|
|
17
|
-
@mode = mode
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def orphan?(file_stat) = @target.nil? || !file_stat.exists?(@target)
|
|
21
|
-
|
|
22
|
-
def drift?(file_stat)
|
|
23
|
-
return false if orphan?(file_stat)
|
|
24
|
-
return false if @sha256.nil?
|
|
25
|
-
|
|
26
|
-
Digest::SHA256.hexdigest(file_stat.read(@target)) != @sha256
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
class AuditLog < Check
|
|
5
|
-
def call
|
|
6
|
-
path = Textus::Store::Geometry.new(root).audit_log_path
|
|
7
|
-
Textus::Port::AuditLog.new(root).verify_integrity.map do |v|
|
|
8
|
-
{
|
|
9
|
-
"code" => "audit.parse_error",
|
|
10
|
-
"level" => "warning",
|
|
11
|
-
"subject" => "#{path}:#{v["lineno"]}",
|
|
12
|
-
"message" => violation_message(v),
|
|
13
|
-
"fix" => "inspect #{path} at line #{v["lineno"]} and remove the corrupted row",
|
|
14
|
-
}
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
private
|
|
19
|
-
|
|
20
|
-
def violation_message(v)
|
|
21
|
-
case v["reason"]
|
|
22
|
-
when "invalid_json"
|
|
23
|
-
"audit log line #{v["lineno"]} is invalid JSON: #{v["detail"]}"
|
|
24
|
-
when "short_tsv"
|
|
25
|
-
"audit log line #{v["lineno"]} #{v["detail"]} " \
|
|
26
|
-
"(consider migrating to NDJSON)"
|
|
27
|
-
else
|
|
28
|
-
v["detail"]
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|