seshat-bi 0.2.0__py3-none-any.whl
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.
- retail/__init__.py +3 -0
- retail/cli.py +6 -0
- seshat/__init__.py +1 -0
- seshat/agent_next.py +457 -0
- seshat/approval_inbox.py +240 -0
- seshat/approver_view.py +343 -0
- seshat/artifact_identity.py +49 -0
- seshat/benchmark/__init__.py +7 -0
- seshat/benchmark/model.py +118 -0
- seshat/benchmark/reference.py +42 -0
- seshat/benchmark/render.py +85 -0
- seshat/benchmark/runner.py +291 -0
- seshat/blocker_explainer.py +184 -0
- seshat/blueprint_preview.py +368 -0
- seshat/capability_feeders.py +230 -0
- seshat/capability_inventory.py +289 -0
- seshat/cli/__init__.py +319 -0
- seshat/cli/__main__.py +15 -0
- seshat/cli/commands/__init__.py +7 -0
- seshat/cli/commands/approvals.py +38 -0
- seshat/cli/commands/approver_view.py +21 -0
- seshat/cli/commands/benchmark.py +86 -0
- seshat/cli/commands/blockers.py +34 -0
- seshat/cli/commands/dashboard_planner.py +53 -0
- seshat/cli/commands/drift.py +219 -0
- seshat/cli/commands/evidence_pack.py +36 -0
- seshat/cli/commands/explorer.py +47 -0
- seshat/cli/commands/gap_detector.py +25 -0
- seshat/cli/commands/generate.py +100 -0
- seshat/cli/commands/init.py +45 -0
- seshat/cli/commands/init_project.py +44 -0
- seshat/cli/commands/kit_lint.py +33 -0
- seshat/cli/commands/manifest.py +17 -0
- seshat/cli/commands/next.py +106 -0
- seshat/cli/commands/pack.py +86 -0
- seshat/cli/commands/passport.py +108 -0
- seshat/cli/commands/pii_notice.py +31 -0
- seshat/cli/commands/scaffold.py +60 -0
- seshat/cli/commands/semantic.py +95 -0
- seshat/cli/commands/severity_posture.py +17 -0
- seshat/cli/commands/status.py +52 -0
- seshat/cli/commands/validate.py +122 -0
- seshat/cli/commands/value_check.py +315 -0
- seshat/cli/guards.py +28 -0
- seshat/cli/parser.py +1003 -0
- seshat/cli/parser_ecosystem.py +256 -0
- seshat/color.py +127 -0
- seshat/compass_project.py +165 -0
- seshat/core.py +92 -0
- seshat/coverage_status.py +56 -0
- seshat/dashboard_coordinator.py +457 -0
- seshat/dashboard_planner.py +628 -0
- seshat/dax_gen.py +327 -0
- seshat/decision_gate.py +308 -0
- seshat/decision_store.py +413 -0
- seshat/demo/__init__.py +69 -0
- seshat/demo/assets/demo.css +75 -0
- seshat/demo/assets/demo.js +10 -0
- seshat/demo/fixtures.py +101 -0
- seshat/demo/html_report.py +144 -0
- seshat/demo/init.py +30 -0
- seshat/demo/live.py +62 -0
- seshat/demo/load.py +78 -0
- seshat/demo/report.py +133 -0
- seshat/demo/resources/demo_sample_orders/assumptions.md +25 -0
- seshat/demo/resources/demo_sample_orders/readiness-status.yaml +99 -0
- seshat/demo/resources/demo_sample_orders/source-map.yaml +27 -0
- seshat/demo/resources/demo_sample_orders/source-profile.md +33 -0
- seshat/demo/resources/demo_sample_orders/unresolved-questions.md +26 -0
- seshat/demo/resources/demo_sample_orders.csv +25 -0
- seshat/demo/resources/seshat-seven-star.svg +1 -0
- seshat/demo/run.py +119 -0
- seshat/dialect.py +580 -0
- seshat/disclosure.py +113 -0
- seshat/doctor.py +98 -0
- seshat/drift.py +385 -0
- seshat/drift_semantics.py +69 -0
- seshat/ecosystem_contracts.py +187 -0
- seshat/evidence_pack.py +306 -0
- seshat/explorer/__init__.py +7 -0
- seshat/explorer/assets/explorer.css +84 -0
- seshat/explorer/assets/explorer.js +19 -0
- seshat/explorer/build.py +407 -0
- seshat/fence.py +133 -0
- seshat/file_profile.py +438 -0
- seshat/gap_detector.py +533 -0
- seshat/gitutil.py +75 -0
- seshat/governor/__init__.py +5 -0
- seshat/governor/mcp_server.py +84 -0
- seshat/governor/service.py +232 -0
- seshat/identifiers.py +102 -0
- seshat/interview_review.py +148 -0
- seshat/kit_init.py +139 -0
- seshat/kit_lint.py +138 -0
- seshat/kpi_answerability.py +301 -0
- seshat/kpi_contracts.py +483 -0
- seshat/manifest.py +65 -0
- seshat/metric_drift.py +473 -0
- seshat/packs/__init__.py +10 -0
- seshat/packs/loader.py +53 -0
- seshat/packs/model.py +112 -0
- seshat/packs/scaffold.py +183 -0
- seshat/packs/validator.py +413 -0
- seshat/passport.py +327 -0
- seshat/pbir_compile.py +488 -0
- seshat/pbir_geometry.py +238 -0
- seshat/pbir_page_background.py +264 -0
- seshat/pbir_theme_apply.py +162 -0
- seshat/pbir_validate_blueprint.py +519 -0
- seshat/pbir_visual_format.py +267 -0
- seshat/pii_notice.py +287 -0
- seshat/portfolio_enumerate.py +105 -0
- seshat/profile.py +169 -0
- seshat/readiness_classify.py +90 -0
- seshat/readiness_evidence.py +136 -0
- seshat/readiness_projection.py +108 -0
- seshat/registry.py +24 -0
- seshat/release_evidence.py +688 -0
- seshat/report_intent.py +129 -0
- seshat/review_integration.py +127 -0
- seshat/review_pack_export.py +250 -0
- seshat/rules/__init__.py +110 -0
- seshat/rules/additivity_consistency.py +195 -0
- seshat/rules/answerability_reconciler.py +242 -0
- seshat/rules/assumption_coherence.py +166 -0
- seshat/rules/assumptions.py +122 -0
- seshat/rules/comparison_baseline.py +188 -0
- seshat/rules/conformed_dimension.py +319 -0
- seshat/rules/currency_unit.py +309 -0
- seshat/rules/date_spine.py +426 -0
- seshat/rules/dax.py +606 -0
- seshat/rules/decision_store.py +583 -0
- seshat/rules/design_background.py +348 -0
- seshat/rules/design_categorical_distinctness.py +136 -0
- seshat/rules/design_contrast.py +150 -0
- seshat/rules/design_grid_closure.py +183 -0
- seshat/rules/design_ramp_deltae.py +110 -0
- seshat/rules/design_review_evidence.py +189 -0
- seshat/rules/design_routes.py +147 -0
- seshat/rules/design_theme.py +169 -0
- seshat/rules/design_theme_fidelity.py +421 -0
- seshat/rules/design_visual_selfcheck.py +144 -0
- seshat/rules/formatting_plan.py +287 -0
- seshat/rules/g6.py +77 -0
- seshat/rules/git_meta.py +776 -0
- seshat/rules/live_surface_boundary.py +94 -0
- seshat/rules/never_execute.py +190 -0
- seshat/rules/parked_on.py +149 -0
- seshat/rules/pbir.py +199 -0
- seshat/rules/publish_pack.py +162 -0
- seshat/rules/readiness_status.py +464 -0
- seshat/rules/reload_idempotency.py +172 -0
- seshat/rules/rename_impact_guard.py +405 -0
- seshat/rules/report_intent.py +217 -0
- seshat/rules/rls_access.py +457 -0
- seshat/rules/routes.py +152 -0
- seshat/rules/routes_coverage.py +198 -0
- seshat/rules/rule_ap1.py +227 -0
- seshat/rules/rule_count_claims.py +202 -0
- seshat/rules/rule_kp1.py +174 -0
- seshat/rules/rule_kr1.py +367 -0
- seshat/rules/rule_sf1.py +183 -0
- seshat/rules/scorecard.py +246 -0
- seshat/rules/snapshot_time_additivity.py +216 -0
- seshat/rules/source_data_contract.py +214 -0
- seshat/rules/source_freshness.py +228 -0
- seshat/rules/sql.py +674 -0
- seshat/rules/status_claims.py +161 -0
- seshat/run_next.py +444 -0
- seshat/runner.py +227 -0
- seshat/sarif.py +84 -0
- seshat/scaffold.py +542 -0
- seshat/semantic.py +75 -0
- seshat/semantic_audit.py +516 -0
- seshat/severity_posture.py +644 -0
- seshat/source_profile_reader.py +167 -0
- seshat/sql.py +456 -0
- seshat/status_surface.py +138 -0
- seshat/theme_compile.py +369 -0
- seshat/theme_gen.py +780 -0
- seshat/tmdl.py +731 -0
- seshat/validate.py +380 -0
- seshat/validate_targets.py +143 -0
- seshat/value_proxy.py +332 -0
- seshat/workspace_init.py +221 -0
- seshat_bi-0.2.0.dist-info/METADATA +458 -0
- seshat_bi-0.2.0.dist-info/RECORD +190 -0
- seshat_bi-0.2.0.dist-info/WHEEL +4 -0
- seshat_bi-0.2.0.dist-info/entry_points.txt +3 -0
- seshat_bi-0.2.0.dist-info/licenses/LICENSE +201 -0
retail/__init__.py
ADDED
retail/cli.py
ADDED
seshat/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Seshat BI static governance checker."""
|
seshat/agent_next.py
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
"""Agent-facing next-action document (Seshat Agent-Driven v0.1).
|
|
2
|
+
|
|
3
|
+
``retail next --format agent`` / repo-level ``retail next --format json`` answer
|
|
4
|
+
the guarded-agent questions in ONE stable document: what stage is this project
|
|
5
|
+
in, what evidence exists, what is blocked, what is the next allowed action, what
|
|
6
|
+
is forbidden, and where must the agent stop.
|
|
7
|
+
|
|
8
|
+
This module is a COMPOSITION, not a second source of truth:
|
|
9
|
+
|
|
10
|
+
- the per-table decision (next action / blocked / approval required /
|
|
11
|
+
terminal pass / input defect) is ``seshat.run_next.build_run_next_response``
|
|
12
|
+
(spec 080), reused verbatim;
|
|
13
|
+
- the recorded evidence/status projection is
|
|
14
|
+
``seshat.status_surface.build_status_projection`` (spec 109), reused
|
|
15
|
+
verbatim;
|
|
16
|
+
- the gate ordering is the seven-stage spine already fixed in
|
|
17
|
+
``run_next._STAGE_ORDER``.
|
|
18
|
+
|
|
19
|
+
Contract (same posture as both parents): read-only -- no writes, no DB, no
|
|
20
|
+
network; deterministic -- same committed state, byte-identical document; never
|
|
21
|
+
a numeric readiness value -- only the four categorical statuses plus named
|
|
22
|
+
evidence/blocker strings (hard rule #9, Principle V). When evidence is missing
|
|
23
|
+
the document degrades to the conservative evidence-first action (start at
|
|
24
|
+
Source Ready), never a fabricated stage.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from seshat.run_next import (
|
|
33
|
+
_STAGE_ORDER,
|
|
34
|
+
build_run_next_response,
|
|
35
|
+
)
|
|
36
|
+
from seshat.status_surface import build_status_projection
|
|
37
|
+
|
|
38
|
+
_STAGE_TITLES: dict[str, str] = {
|
|
39
|
+
"source_ready": "Source Ready (Stage 1)",
|
|
40
|
+
"mapping_ready": "Mapping Ready (Stage 2)",
|
|
41
|
+
"silver_ready": "Silver Ready (Stage 3)",
|
|
42
|
+
"gold_ready": "Gold Ready (Stage 4)",
|
|
43
|
+
"semantic_model_ready": "Semantic Model Ready (Stage 5)",
|
|
44
|
+
"dashboard_ready": "Dashboard Ready (Stage 6)",
|
|
45
|
+
"publish_ready": "Publish Ready (Stage 7)",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# One sentence per gate, in spine order: work forbidden until that gate passes.
|
|
49
|
+
_GATE_RULES: tuple[tuple[str, str], ...] = (
|
|
50
|
+
(
|
|
51
|
+
"mapping_ready",
|
|
52
|
+
"No silver work (no silver.* SQL) before Mapping Ready passes.",
|
|
53
|
+
),
|
|
54
|
+
(
|
|
55
|
+
"silver_ready",
|
|
56
|
+
"No gold work (no gold star/mart SQL) before Silver Ready passes.",
|
|
57
|
+
),
|
|
58
|
+
(
|
|
59
|
+
"gold_ready",
|
|
60
|
+
"No semantic-model work before Gold Ready (live-validated) passes.",
|
|
61
|
+
),
|
|
62
|
+
(
|
|
63
|
+
"semantic_model_ready",
|
|
64
|
+
"No dashboard work before Semantic Model Ready "
|
|
65
|
+
"(approved metric contracts) passes.",
|
|
66
|
+
),
|
|
67
|
+
(
|
|
68
|
+
"dashboard_ready",
|
|
69
|
+
"No publish/handoff work before Dashboard Ready passes.",
|
|
70
|
+
),
|
|
71
|
+
(
|
|
72
|
+
"publish_ready",
|
|
73
|
+
"No live publish before Publish Ready passes; publish execution is "
|
|
74
|
+
"the deferred F016 adapter.",
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Invariants that hold at every stage, including terminal pass.
|
|
79
|
+
_ALWAYS_FORBIDDEN: tuple[str, ...] = (
|
|
80
|
+
"Never self-grant an approval; approvals are named-human actions.",
|
|
81
|
+
"Never fabricate readiness; readiness is status + evidence + blockers, "
|
|
82
|
+
"never a number.",
|
|
83
|
+
"Never run the Power BI execution adapter (F016) from this surface.",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
_BASE_VALIDATION_COMMANDS: tuple[str, ...] = (
|
|
87
|
+
"python -m seshat.cli check --repo .",
|
|
88
|
+
"python -m seshat.cli status --repo . --format json",
|
|
89
|
+
"python -m seshat.cli next --repo . --format json",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
_VALIDATION_EXTRAS_BY_STAGE: dict[str, tuple[str, ...]] = {
|
|
93
|
+
"gold_ready": (
|
|
94
|
+
"python -m seshat.cli validate --source-map "
|
|
95
|
+
"mappings/<table>/source-map.yaml # needs the db extra + a DSN; "
|
|
96
|
+
"without them, report the deferred state -- never fake a pass",
|
|
97
|
+
),
|
|
98
|
+
"semantic_model_ready": ("python -m seshat.cli semantic-check --repo .",),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_STOP_POINT_BY_STAGE: dict[str, str] = {
|
|
102
|
+
"source_ready": (
|
|
103
|
+
"Stop once the read-only source profile and readiness-status.yaml are "
|
|
104
|
+
"recorded; mapping review is a human gate."
|
|
105
|
+
),
|
|
106
|
+
"mapping_ready": (
|
|
107
|
+
"Stop at the mapping gate: source-map.yaml must be reviewed and "
|
|
108
|
+
"approved by a named human before any silver SQL."
|
|
109
|
+
),
|
|
110
|
+
"silver_ready": (
|
|
111
|
+
"Stop after authoring the silver migration SQL; do not execute it and "
|
|
112
|
+
"do not begin gold work before Silver Ready passes."
|
|
113
|
+
),
|
|
114
|
+
"gold_ready": (
|
|
115
|
+
"Stop after authoring the gold SQL and preparing live-validate "
|
|
116
|
+
"evidence; Gold Ready passes only on live validation."
|
|
117
|
+
),
|
|
118
|
+
"semantic_model_ready": (
|
|
119
|
+
"Stop at the metric-contract gate: the metric owner approves the "
|
|
120
|
+
"contracts before any dashboard work."
|
|
121
|
+
),
|
|
122
|
+
"dashboard_ready": (
|
|
123
|
+
"Stop at the design-review gate: governance approves the dashboard "
|
|
124
|
+
"design before publish preparation."
|
|
125
|
+
),
|
|
126
|
+
"publish_ready": (
|
|
127
|
+
"Stop before any publish: assemble the handoff pack only; live "
|
|
128
|
+
"publish is the deferred F016 execution adapter."
|
|
129
|
+
),
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
_TERMINAL_STOP_POINT = (
|
|
133
|
+
"All seven stages pass. Live publish stays with the deferred F016 "
|
|
134
|
+
"execution adapter; nothing further from this surface."
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
_FRESH_NEXT_ACTION = (
|
|
138
|
+
"No readiness evidence found under mappings/. Begin at Source Ready: "
|
|
139
|
+
"onboard one table with a read-only source profile and record "
|
|
140
|
+
"mappings/<table>/readiness-status.yaml before any warehouse or "
|
|
141
|
+
"dashboard work."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _stage_index(stage: str | None) -> int:
|
|
146
|
+
"""Spine position for ranking; terminal (``None``) sorts last."""
|
|
147
|
+
if stage is None:
|
|
148
|
+
return len(_STAGE_ORDER)
|
|
149
|
+
return _STAGE_ORDER.index(stage)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _forbidden_scope(stage: str | None, outcome: str) -> list[str]:
|
|
153
|
+
"""Every gate at or after the current stage is still closed; the
|
|
154
|
+
invariants hold always. Deterministic given (stage, outcome)."""
|
|
155
|
+
if outcome == "terminal_pass" or stage is None:
|
|
156
|
+
gates: list[str] = []
|
|
157
|
+
else:
|
|
158
|
+
current = _stage_index(stage)
|
|
159
|
+
gates = [
|
|
160
|
+
sentence
|
|
161
|
+
for gate_stage, sentence in _GATE_RULES
|
|
162
|
+
if _stage_index(gate_stage) >= current
|
|
163
|
+
]
|
|
164
|
+
return gates + list(_ALWAYS_FORBIDDEN)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _validation_commands(stage: str | None) -> list[str]:
|
|
168
|
+
commands = list(_BASE_VALIDATION_COMMANDS)
|
|
169
|
+
commands.extend(_VALIDATION_EXTRAS_BY_STAGE.get(stage or "", ()))
|
|
170
|
+
return commands
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _stop_point(response: dict[str, Any]) -> str:
|
|
174
|
+
outcome = response["outcome"]
|
|
175
|
+
stage = response["stage"]
|
|
176
|
+
if outcome == "terminal_pass":
|
|
177
|
+
return _TERMINAL_STOP_POINT
|
|
178
|
+
if outcome == "stop_blocked":
|
|
179
|
+
return (
|
|
180
|
+
"Stopped now: resolve or escalate the recorded blocking_reasons; "
|
|
181
|
+
"do not work around the block."
|
|
182
|
+
)
|
|
183
|
+
if outcome == "approval_required":
|
|
184
|
+
authority = response.get("required_authority") or "named human"
|
|
185
|
+
return (
|
|
186
|
+
f"Stopped now: a named-human approval ({authority}) for stage "
|
|
187
|
+
f"{stage!r} is required before any further stage work."
|
|
188
|
+
)
|
|
189
|
+
if outcome == "input_defect":
|
|
190
|
+
return (
|
|
191
|
+
"Stopped now: repair the malformed readiness-status.yaml before "
|
|
192
|
+
"any pipeline work."
|
|
193
|
+
)
|
|
194
|
+
return _STOP_POINT_BY_STAGE.get(stage or "", _STOP_POINT_BY_STAGE["source_ready"])
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _next_allowed_action(response: dict[str, Any]) -> str:
|
|
198
|
+
outcome = response["outcome"]
|
|
199
|
+
stage = response["stage"]
|
|
200
|
+
if outcome == "next_action":
|
|
201
|
+
return str(response.get("action_text") or "")
|
|
202
|
+
if outcome == "stop_blocked":
|
|
203
|
+
return (
|
|
204
|
+
f"STOP -- stage {stage!r} is blocked; resolve the recorded "
|
|
205
|
+
"blocking_reasons before any other pipeline work."
|
|
206
|
+
)
|
|
207
|
+
if outcome == "approval_required":
|
|
208
|
+
authority = response.get("required_authority") or "named human"
|
|
209
|
+
return (
|
|
210
|
+
f"STOP -- obtain the named-human approval ({authority}) for "
|
|
211
|
+
f"stage {stage!r}; never self-grant it."
|
|
212
|
+
)
|
|
213
|
+
if outcome == "terminal_pass":
|
|
214
|
+
return "No pipeline action: all seven readiness stages pass for this table."
|
|
215
|
+
return "Repair the readiness-status.yaml input defect before any pipeline work."
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _readiness_state(
|
|
219
|
+
response: dict[str, Any], entry: dict[str, Any] | None
|
|
220
|
+
) -> str | None:
|
|
221
|
+
"""The RECORDED four-status of the current stage, read from the same
|
|
222
|
+
committed projection -- never derived. ``input_defect`` has no honest
|
|
223
|
+
state, so it projects as ``None``."""
|
|
224
|
+
outcome = response["outcome"]
|
|
225
|
+
if outcome == "terminal_pass":
|
|
226
|
+
return "pass"
|
|
227
|
+
if outcome == "input_defect":
|
|
228
|
+
return None
|
|
229
|
+
stage = response["stage"]
|
|
230
|
+
if entry is not None and stage is not None:
|
|
231
|
+
block = entry.get("stages", {}).get(stage)
|
|
232
|
+
if isinstance(block, dict) and isinstance(block.get("status"), str):
|
|
233
|
+
return block["status"]
|
|
234
|
+
# No readiness file (or stage block unreadable): the conservative,
|
|
235
|
+
# non-fabricated state is the journey's start.
|
|
236
|
+
return "blocked" if outcome == "stop_blocked" else "not_started"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _evidence(entry: dict[str, Any] | None) -> list[dict[str, Any]]:
|
|
240
|
+
"""Every recorded stage, verbatim from the committed projection, in spine
|
|
241
|
+
order -- items are the readiness file's own evidence strings."""
|
|
242
|
+
if entry is None:
|
|
243
|
+
return []
|
|
244
|
+
stages = entry.get("stages", {})
|
|
245
|
+
return [
|
|
246
|
+
{
|
|
247
|
+
"stage": name,
|
|
248
|
+
"status": stages[name]["status"],
|
|
249
|
+
"items": list(stages[name]["evidence"]),
|
|
250
|
+
}
|
|
251
|
+
for name in _STAGE_ORDER
|
|
252
|
+
if name in stages
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _summaries(
|
|
257
|
+
triples: list[tuple[dict[str, Any] | None, dict[str, Any], str]],
|
|
258
|
+
) -> list[dict]:
|
|
259
|
+
return [
|
|
260
|
+
{
|
|
261
|
+
"table": response["table"],
|
|
262
|
+
"source_path": source_path,
|
|
263
|
+
"outcome": response["outcome"],
|
|
264
|
+
"stage": response["stage"],
|
|
265
|
+
}
|
|
266
|
+
for _entry, response, source_path in triples
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _rank(response: dict[str, Any]) -> int:
|
|
271
|
+
"""Focus ranking: a malformed file is the most urgent, then the earliest
|
|
272
|
+
incomplete stage; a fully-passed table sorts last."""
|
|
273
|
+
if response["outcome"] == "input_defect":
|
|
274
|
+
return -1
|
|
275
|
+
return _stage_index(response["stage"])
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _compose(
|
|
279
|
+
response: dict[str, Any],
|
|
280
|
+
entry: dict[str, Any] | None,
|
|
281
|
+
summaries: list[dict],
|
|
282
|
+
) -> dict[str, Any]:
|
|
283
|
+
stage = response["stage"]
|
|
284
|
+
outcome = response["outcome"]
|
|
285
|
+
return {
|
|
286
|
+
"current_stage": stage,
|
|
287
|
+
"readiness_state": _readiness_state(response, entry),
|
|
288
|
+
"evidence": _evidence(entry),
|
|
289
|
+
"blocking_reasons": list(response.get("blocking_reasons", [])),
|
|
290
|
+
"next_allowed_action": _next_allowed_action(response),
|
|
291
|
+
"forbidden_scope": _forbidden_scope(stage, outcome),
|
|
292
|
+
"validation_commands": _validation_commands(stage),
|
|
293
|
+
"stop_point": _stop_point(response),
|
|
294
|
+
"table": response["table"],
|
|
295
|
+
"outcome": outcome,
|
|
296
|
+
"required_authority": response.get("required_authority"),
|
|
297
|
+
"caveats": list(response.get("caveats", [])),
|
|
298
|
+
"tables": summaries,
|
|
299
|
+
"read_only_proof": True,
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _fresh_repo_document() -> dict[str, Any]:
|
|
304
|
+
"""No committed readiness evidence at all: the conservative,
|
|
305
|
+
evidence-first answer -- never a fabricated stage or state."""
|
|
306
|
+
return {
|
|
307
|
+
"current_stage": "source_ready",
|
|
308
|
+
"readiness_state": "not_started",
|
|
309
|
+
"evidence": [],
|
|
310
|
+
"blocking_reasons": [],
|
|
311
|
+
"next_allowed_action": _FRESH_NEXT_ACTION,
|
|
312
|
+
"forbidden_scope": _forbidden_scope("source_ready", "next_action"),
|
|
313
|
+
"validation_commands": _validation_commands("source_ready"),
|
|
314
|
+
"stop_point": _STOP_POINT_BY_STAGE["source_ready"],
|
|
315
|
+
"table": None,
|
|
316
|
+
"outcome": "next_action",
|
|
317
|
+
"required_authority": None,
|
|
318
|
+
"caveats": [],
|
|
319
|
+
"tables": [],
|
|
320
|
+
"read_only_proof": True,
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _dir_name(source_path: str) -> str:
|
|
325
|
+
"""The ``mappings/<dir>/`` directory name -- the identity
|
|
326
|
+
``build_run_next_response`` always resolves via its direct candidate path,
|
|
327
|
+
even when the file records no string ``table`` field."""
|
|
328
|
+
return source_path.rsplit("/", 2)[-2]
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _unprojected_status_paths(root: Path, entries: list[dict[str, Any]]) -> list[str]:
|
|
332
|
+
"""Committed readiness-status files the best-effort projection SKIPPED
|
|
333
|
+
(unreadable / unparseable / non-mapping). They must still surface -- as
|
|
334
|
+
``input_defect``, never as an absent table -- or a broken committed file
|
|
335
|
+
would silently read as a fresh journey."""
|
|
336
|
+
projected = {entry["source_path"] for entry in entries}
|
|
337
|
+
mappings_dir = root / "mappings"
|
|
338
|
+
if not mappings_dir.is_dir():
|
|
339
|
+
return []
|
|
340
|
+
return [
|
|
341
|
+
path.relative_to(root).as_posix()
|
|
342
|
+
for path in sorted(mappings_dir.glob("*/readiness-status.yaml"))
|
|
343
|
+
if path.relative_to(root).as_posix() not in projected
|
|
344
|
+
]
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _all_triples(
|
|
348
|
+
root: Path, entries: list[dict[str, Any]]
|
|
349
|
+
) -> list[tuple[dict[str, Any] | None, dict[str, Any], str]]:
|
|
350
|
+
"""One ``(projection entry, run-next response, source path)`` triple per
|
|
351
|
+
committed readiness-status file, including files the projection skipped
|
|
352
|
+
(entry ``None``; their run-next outcome is ``input_defect``)."""
|
|
353
|
+
triples: list[tuple[dict[str, Any] | None, dict[str, Any], str]] = [
|
|
354
|
+
(
|
|
355
|
+
entry,
|
|
356
|
+
build_run_next_response(root, _dir_name(entry["source_path"])),
|
|
357
|
+
entry["source_path"],
|
|
358
|
+
)
|
|
359
|
+
for entry in entries
|
|
360
|
+
]
|
|
361
|
+
for source_path in _unprojected_status_paths(root, entries):
|
|
362
|
+
triples.append(
|
|
363
|
+
(
|
|
364
|
+
None,
|
|
365
|
+
build_run_next_response(root, _dir_name(source_path)),
|
|
366
|
+
source_path,
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
return triples
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _resolved_source_path(root: Path, table: str) -> str | None:
|
|
373
|
+
"""The repo-relative path of the readiness file run-next itself resolves
|
|
374
|
+
for ``table`` (its ``_find_status_data`` matches dir name / recorded
|
|
375
|
+
table / source_id -- reused, not re-derived)."""
|
|
376
|
+
from seshat.run_next import _find_status_data
|
|
377
|
+
|
|
378
|
+
status_path, _data, _error = _find_status_data(root, table)
|
|
379
|
+
if status_path is None:
|
|
380
|
+
return None
|
|
381
|
+
return status_path.relative_to(root).as_posix()
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _entry_by_source_path(
|
|
385
|
+
entries: list[dict[str, Any]], source_path: str | None
|
|
386
|
+
) -> dict[str, Any] | None:
|
|
387
|
+
if source_path is None:
|
|
388
|
+
return None
|
|
389
|
+
return next((e for e in entries if e["source_path"] == source_path), None)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _entry_by_name(
|
|
393
|
+
entries: list[dict[str, Any]], names: set[str | None]
|
|
394
|
+
) -> dict[str, Any] | None:
|
|
395
|
+
return next(
|
|
396
|
+
(e for e in entries if {e.get("table"), _dir_name(e["source_path"])} & names),
|
|
397
|
+
None,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _entry_matching(
|
|
402
|
+
root: Path,
|
|
403
|
+
entries: list[dict[str, Any]],
|
|
404
|
+
table: str,
|
|
405
|
+
response: dict[str, Any],
|
|
406
|
+
) -> dict[str, Any] | None:
|
|
407
|
+
"""Find the projection entry behind a --table response: authoritatively by
|
|
408
|
+
the source path of the file run-next resolved, else by name."""
|
|
409
|
+
by_path = _entry_by_source_path(entries, _resolved_source_path(root, table))
|
|
410
|
+
if by_path is not None:
|
|
411
|
+
return by_path
|
|
412
|
+
return _entry_by_name(entries, {response.get("table"), table})
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def build_table_next_document(repo_root: Path | str, table: str) -> dict[str, Any]:
|
|
416
|
+
"""Single-table next-action document WITHOUT the portfolio summaries.
|
|
417
|
+
|
|
418
|
+
Same composed shape as :func:`build_agent_next_document`, but it reads
|
|
419
|
+
only this table's readiness file (one run-next response) instead of
|
|
420
|
+
re-projecting every table -- O(1) file reads, which keeps portfolio-wide
|
|
421
|
+
consumers (the shared readiness projection, spec 120) linear instead of
|
|
422
|
+
quadratic. ``tables`` is empty and the entry-derived fields
|
|
423
|
+
(``readiness_state``/``evidence``) degrade conservatively; callers that
|
|
424
|
+
need those use the full document."""
|
|
425
|
+
return _compose(build_run_next_response(Path(repo_root), table), None, [])
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def build_agent_next_document(
|
|
429
|
+
repo_root: Path | str, table: str | None = None
|
|
430
|
+
) -> dict[str, Any]:
|
|
431
|
+
"""Build the agent-facing next-action document for ``repo_root``.
|
|
432
|
+
|
|
433
|
+
With ``table``, the document focuses that table (missing file degrades to
|
|
434
|
+
the conservative Source Ready start, exactly as ``build_run_next_response``
|
|
435
|
+
does). Without it, the focus is the table with the most urgent run-next
|
|
436
|
+
outcome -- a malformed committed readiness file first, then the earliest
|
|
437
|
+
incomplete stage (ties broken by source path, so the answer is
|
|
438
|
+
deterministic); a repo with no readiness files at all produces the
|
|
439
|
+
conservative evidence-first document. Read-only in every path.
|
|
440
|
+
"""
|
|
441
|
+
root = Path(repo_root)
|
|
442
|
+
projection = build_status_projection(root)
|
|
443
|
+
entries: list[dict[str, Any]] = projection["tables"]
|
|
444
|
+
triples = _all_triples(root, entries)
|
|
445
|
+
|
|
446
|
+
if table is not None:
|
|
447
|
+
response = build_run_next_response(root, table)
|
|
448
|
+
entry = _entry_matching(root, entries, table, response)
|
|
449
|
+
return _compose(response, entry, _summaries(triples))
|
|
450
|
+
|
|
451
|
+
if not triples:
|
|
452
|
+
return _fresh_repo_document()
|
|
453
|
+
|
|
454
|
+
focus_entry, focus_response, _ = min(
|
|
455
|
+
triples, key=lambda triple: (_rank(triple[1]), triple[2])
|
|
456
|
+
)
|
|
457
|
+
return _compose(focus_response, focus_entry, _summaries(triples))
|