Complete memory graph and document value workplans

This commit is contained in:
2026-05-15 13:30:50 +02:00
parent f49ebb563b
commit 6cc44da628
25 changed files with 1546 additions and 168 deletions

View File

@@ -8,7 +8,12 @@ from markitect_tool.document_function import (
DocumentFunctionDescriptor,
DocumentFunctionParameter,
DocumentFunctionRegistry,
DocumentValue,
MAX_FUNCTION_PIPELINE_DEPTH,
coerce_document_value,
default_document_function_registry,
document_value_to_json,
format_document_value,
parse_document_function_calls,
render_document_functions,
validate_document_functions,
@@ -51,6 +56,20 @@ Decision
assert "### Decision" in result.content
assert len(result.calls) == 2
assert result.provenance[0].operation == "document_function.text.upper"
assert result.calls[0].value.kind == "string"
assert result.calls[1].value.kind == "markdown"
def test_document_values_coerce_and_map_to_markdown():
table = coerce_document_value(
[{"name": "Ada", "role": "Architect"}, {"name": "Grace", "role": "Reviewer"}],
declared_kind="table",
)
assert document_value_to_json(table)["kind"] == "table"
assert format_document_value(DocumentValue(kind="boolean", value=True), inline=True) == "true"
assert "| name | role |" in format_document_value(table, inline=False)
assert "| Ada | Architect |" in format_document_value(table, inline=False)
def test_pipeline_passes_previous_output_to_next_function():
@@ -67,6 +86,17 @@ def test_pipeline_separator_inside_quotes_is_literal():
assert result.content == "a/b"
def test_pipeline_depth_limit_reports_syntax_error():
expression = " | ".join(["text.trim value"] * (MAX_FUNCTION_PIPELINE_DEPTH + 1))
try:
parse_document_function_calls("{{mkt:" + expression + "}}")
except Exception as exc:
assert "maximum depth" in str(exc)
else:
raise AssertionError("Expected pipeline depth diagnostic")
def test_context_variables_can_be_used_in_function_arguments():
context = ProcessingContext(variables={"title": "Architecture Decision"})
@@ -75,6 +105,16 @@ def test_context_variables_can_be_used_in_function_arguments():
assert result.content == "## Architecture Decision"
def test_dynamic_context_values_render_through_typed_mapper():
context = ProcessingContext(variables={"items": ["alpha", "beta"]})
result = render_document_functions("{{mkt:data.get items}}", context=context)
assert result.valid
assert result.content == "alpha, beta"
assert result.calls[0].value.kind == "list"
def test_validate_document_functions_reports_forbidden_calls():
result = validate_document_functions("{{mkt:text.upper draft}}", forbidden=["text.upper"])
@@ -106,6 +146,41 @@ def test_registry_can_expose_custom_function_without_core_rewrite():
assert result.content == "[ok]"
def test_descriptor_output_type_mismatch_is_diagnostic():
registry = DocumentFunctionRegistry()
registry.register(
DocumentFunctionDescriptor(
id="demo.count",
summary="Return a count.",
output_type="number",
implementation=lambda: "not-a-number",
)
)
result = render_document_functions("{{mkt:demo.count}}", registry=registry)
assert not result.valid
assert result.content == "{{mkt:demo.count}}"
assert result.diagnostics[0].code == "function.output_type_mismatch"
def test_reference_values_require_provenance():
registry = DocumentFunctionRegistry()
registry.register(
DocumentFunctionDescriptor(
id="demo.reference",
summary="Return a reference.",
output_type="reference",
implementation=lambda: DocumentValue(kind="reference", value="std:clause.md#payment"),
)
)
result = render_document_functions("{{mkt:demo.reference}}", registry=registry)
assert not result.valid
assert result.diagnostics[0].code == "function.provenance_missing"
def test_unknown_function_is_left_in_place_with_diagnostic():
result = render_document_functions("{{mkt:nope.missing value}}")
@@ -133,6 +208,17 @@ def test_mkt_function_render_outputs_expanded_markdown(tmp_path: Path):
assert "**Important**" in result.output
def test_mkt_function_render_json_includes_typed_values(tmp_path: Path):
file = tmp_path / "functions.md"
file.write_text("{{mkt:text.upper draft}}\n", encoding="utf-8")
result = CliRunner().invoke(main, ["function", "render", str(file), "--format", "json"])
data = json.loads(result.output)
assert result.exit_code == 0
assert data["calls"][0]["value"] == {"kind": "string", "value": "DRAFT"}
def test_mkt_function_check_can_restrict_allowed_functions(tmp_path: Path):
file = tmp_path / "functions.md"
file.write_text("{{mkt:text.upper draft}}\n", encoding="utf-8")
@@ -148,3 +234,4 @@ def test_default_registry_serializes_without_implementations():
assert data["count"] >= 1
assert "implementation" not in data["functions"][0]
assert {function["id"]: function["output_type"] for function in data["functions"]}["data.get"] == "dynamic"