generated from coulomb/repo-seed
markitect-tool integration
This commit is contained in:
@@ -18,6 +18,7 @@ Start with:
|
|||||||
- `docs/evaluation-and-inspection.md`
|
- `docs/evaluation-and-inspection.md`
|
||||||
- `docs/reference-pilot-decision.md`
|
- `docs/reference-pilot-decision.md`
|
||||||
- `docs/markitect-main-scope-assessment.md`
|
- `docs/markitect-main-scope-assessment.md`
|
||||||
|
- `docs/markitect-tool-adapter.md`
|
||||||
- `docs/orthogonal-successor-roadmap.md`
|
- `docs/orthogonal-successor-roadmap.md`
|
||||||
- `docs/legacy-infospace-feature-inventory.md`
|
- `docs/legacy-infospace-feature-inventory.md`
|
||||||
- `docs/successor-boundary-interface-map.md`
|
- `docs/successor-boundary-interface-map.md`
|
||||||
|
|||||||
68
docs/markitect-tool-adapter.md
Normal file
68
docs/markitect-tool-adapter.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# Markitect Tool Adapter
|
||||||
|
|
||||||
|
Date: 2026-05-14
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
`infospace-bench` delegates Markdown syntax work to `markitect-tool` through a
|
||||||
|
single narrow adapter:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/infospace_bench/markdown_adapter.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This keeps the repos orthogonal:
|
||||||
|
|
||||||
|
- `markitect-tool` parses and validates Markdown.
|
||||||
|
- `infospace-bench` interprets parsed/validated artifacts as parts of concrete
|
||||||
|
infospaces.
|
||||||
|
|
||||||
|
## Current Adapter Surface
|
||||||
|
|
||||||
|
- `parse_markdown_artifact(path)`
|
||||||
|
- `extract_section_text(parsed, heading)`
|
||||||
|
- `validate_markdown_artifact(artifact_id, path, contract_path)`
|
||||||
|
- `validate_infospace_artifacts(root)`
|
||||||
|
|
||||||
|
The adapter normalizes `markitect-tool` diagnostics into
|
||||||
|
`MarkdownDiagnostic`, preserving severity, code, message, source, contract,
|
||||||
|
rule, guidance, and details.
|
||||||
|
|
||||||
|
## Contract Selection
|
||||||
|
|
||||||
|
`validate_infospace_artifacts()` reads `infospace.yaml` and uses the `schemas`
|
||||||
|
mapping as artifact-kind contract references:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
schemas:
|
||||||
|
source: contracts/source.contract.md
|
||||||
|
generated: contracts/generated.contract.md
|
||||||
|
```
|
||||||
|
|
||||||
|
If no contract is configured for an artifact kind, validation skips that
|
||||||
|
artifact. If a configured contract path is missing, validation raises a
|
||||||
|
structured `InfospaceError`.
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m infospace_bench validate infospaces/bootstrap-pilot
|
||||||
|
```
|
||||||
|
|
||||||
|
The command emits JSON:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"valid": true,
|
||||||
|
"results": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It exits with `0` when all checked artifacts are valid and `1` when any checked
|
||||||
|
artifact fails validation.
|
||||||
|
|
||||||
|
## Boundary Rule
|
||||||
|
|
||||||
|
Only `src/infospace_bench/markdown_adapter.py` should import `markitect_tool`.
|
||||||
|
Other modules should consume adapter results so `infospace-bench` remains an
|
||||||
|
application layer, not a Markdown toolkit.
|
||||||
@@ -3,11 +3,14 @@ name = "infospace-bench"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Application-layer workspace for concrete structured knowledge spaces."
|
description = "Application-layer workspace for concrete structured knowledge spaces."
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = ["PyYAML>=6"]
|
dependencies = [
|
||||||
|
"PyYAML>=6",
|
||||||
|
"markitect-tool @ file:///home/worsch/markitect-tool",
|
||||||
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
infospace-bench = "infospace_bench.cli:main"
|
infospace-bench = "infospace_bench.cli:main"
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
pythonpath = ["src"]
|
pythonpath = ["src", "../markitect-tool/src"]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from .errors import InfospaceError
|
from .errors import InfospaceError
|
||||||
from .lifecycle import add_artifact, create_infospace, load_infospace
|
from .lifecycle import add_artifact, create_infospace, load_infospace
|
||||||
|
from .markdown_adapter import validate_infospace_artifacts
|
||||||
|
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
@@ -31,6 +32,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
export = sub.add_parser("export", help="Print the infospace representation")
|
export = sub.add_parser("export", help="Print the infospace representation")
|
||||||
export.add_argument("root")
|
export.add_argument("root")
|
||||||
|
|
||||||
|
validate = sub.add_parser("validate", help="Validate infospace artifacts")
|
||||||
|
validate.add_argument("root")
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
@@ -58,6 +62,16 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
_write_json({"artifact": artifact.to_dict()})
|
_write_json({"artifact": artifact.to_dict()})
|
||||||
elif args.command == "export":
|
elif args.command == "export":
|
||||||
_write_json(load_infospace(Path(args.root)).to_dict())
|
_write_json(load_infospace(Path(args.root)).to_dict())
|
||||||
|
elif args.command == "validate":
|
||||||
|
results = validate_infospace_artifacts(Path(args.root))
|
||||||
|
valid = all(result.valid for result in results)
|
||||||
|
_write_json(
|
||||||
|
{
|
||||||
|
"valid": valid,
|
||||||
|
"results": [result.to_dict() for result in results],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return 0 if valid else 1
|
||||||
else:
|
else:
|
||||||
parser.error(f"Unhandled command: {args.command}")
|
parser.error(f"Unhandled command: {args.command}")
|
||||||
except InfospaceError as exc:
|
except InfospaceError as exc:
|
||||||
|
|||||||
169
src/infospace_bench/markdown_adapter.py
Normal file
169
src/infospace_bench/markdown_adapter.py
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from markitect_tool import Heading, Section, parse_markdown_file
|
||||||
|
from markitect_tool.contract import check_markdown_file
|
||||||
|
|
||||||
|
from .errors import InfospaceError
|
||||||
|
from .lifecycle import load_infospace
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MarkdownDiagnostic:
|
||||||
|
severity: str
|
||||||
|
code: str
|
||||||
|
message: str
|
||||||
|
source: dict[str, Any] | None = None
|
||||||
|
contract: dict[str, Any] | None = None
|
||||||
|
rule_id: str | None = None
|
||||||
|
guidance: str | None = None
|
||||||
|
details: dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_markitect(cls, diagnostic: Any) -> "MarkdownDiagnostic":
|
||||||
|
data = diagnostic.to_dict()
|
||||||
|
return cls(
|
||||||
|
severity=str(data.get("severity") or ""),
|
||||||
|
code=str(data.get("code") or ""),
|
||||||
|
message=str(data.get("message") or ""),
|
||||||
|
source=data.get("source"),
|
||||||
|
contract=data.get("contract"),
|
||||||
|
rule_id=data.get("rule_id"),
|
||||||
|
guidance=data.get("guidance"),
|
||||||
|
details=dict(data.get("details") or {}),
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
data: dict[str, Any] = {
|
||||||
|
"severity": self.severity,
|
||||||
|
"code": self.code,
|
||||||
|
"message": self.message,
|
||||||
|
}
|
||||||
|
if self.source:
|
||||||
|
data["source"] = self.source
|
||||||
|
if self.contract:
|
||||||
|
data["contract"] = self.contract
|
||||||
|
if self.rule_id:
|
||||||
|
data["rule_id"] = self.rule_id
|
||||||
|
if self.guidance:
|
||||||
|
data["guidance"] = self.guidance
|
||||||
|
if self.details:
|
||||||
|
data["details"] = self.details
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ParsedMarkdownArtifact:
|
||||||
|
path: Path
|
||||||
|
frontmatter: dict[str, Any]
|
||||||
|
headings: list[Heading]
|
||||||
|
sections: list[Section]
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"path": str(self.path),
|
||||||
|
"frontmatter": self.frontmatter,
|
||||||
|
"headings": [heading.to_dict() for heading in self.headings],
|
||||||
|
"sections": [section.to_dict() for section in self.sections],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ArtifactValidationResult:
|
||||||
|
artifact_id: str
|
||||||
|
path: str
|
||||||
|
contract_path: str
|
||||||
|
valid: bool
|
||||||
|
diagnostics: list[MarkdownDiagnostic] = field(default_factory=list)
|
||||||
|
metrics: dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"artifact_id": self.artifact_id,
|
||||||
|
"path": self.path,
|
||||||
|
"contract_path": self.contract_path,
|
||||||
|
"valid": self.valid,
|
||||||
|
"diagnostics": [diagnostic.to_dict() for diagnostic in self.diagnostics],
|
||||||
|
"metrics": self.metrics,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_markdown_artifact(path: str | Path) -> ParsedMarkdownArtifact:
|
||||||
|
artifact_path = Path(path)
|
||||||
|
document = parse_markdown_file(artifact_path)
|
||||||
|
return ParsedMarkdownArtifact(
|
||||||
|
path=artifact_path,
|
||||||
|
frontmatter=document.frontmatter,
|
||||||
|
headings=document.headings,
|
||||||
|
sections=document.sections,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_section_text(
|
||||||
|
parsed: ParsedMarkdownArtifact,
|
||||||
|
heading: str,
|
||||||
|
) -> str:
|
||||||
|
expected = _normalize_heading(heading)
|
||||||
|
for section in parsed.sections:
|
||||||
|
if _normalize_heading(section.heading.text) == expected:
|
||||||
|
return "\n".join(block.text for block in section.blocks if block.text).strip()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def validate_markdown_artifact(
|
||||||
|
artifact_id: str,
|
||||||
|
path: str | Path,
|
||||||
|
contract_path: str | Path,
|
||||||
|
) -> ArtifactValidationResult:
|
||||||
|
artifact_path = Path(path)
|
||||||
|
contract = Path(contract_path)
|
||||||
|
result = check_markdown_file(artifact_path, contract)
|
||||||
|
return ArtifactValidationResult(
|
||||||
|
artifact_id=artifact_id,
|
||||||
|
path=str(artifact_path),
|
||||||
|
contract_path=str(contract),
|
||||||
|
valid=result.valid,
|
||||||
|
diagnostics=[
|
||||||
|
MarkdownDiagnostic.from_markitect(diagnostic)
|
||||||
|
for diagnostic in result.diagnostics
|
||||||
|
],
|
||||||
|
metrics=result.metrics,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_infospace_artifacts(root: str | Path) -> list[ArtifactValidationResult]:
|
||||||
|
infospace = load_infospace(root)
|
||||||
|
results: list[ArtifactValidationResult] = []
|
||||||
|
for artifact in infospace.artifacts:
|
||||||
|
contract_ref = (
|
||||||
|
infospace.config.schemas.get(artifact.kind)
|
||||||
|
or infospace.config.schemas.get("artifact")
|
||||||
|
)
|
||||||
|
if not contract_ref:
|
||||||
|
continue
|
||||||
|
artifact_path = infospace.root / artifact.path
|
||||||
|
contract_path = infospace.root / contract_ref
|
||||||
|
if not contract_path.is_file():
|
||||||
|
raise InfospaceError(
|
||||||
|
"missing_contract",
|
||||||
|
f"Configured contract does not exist: {contract_path}",
|
||||||
|
{
|
||||||
|
"artifact_id": artifact.id,
|
||||||
|
"contract_path": str(contract_path),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
results.append(
|
||||||
|
validate_markdown_artifact(
|
||||||
|
artifact.id,
|
||||||
|
artifact_path,
|
||||||
|
contract_path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_heading(value: str) -> str:
|
||||||
|
return " ".join(value.strip().lower().split())
|
||||||
@@ -7,7 +7,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
def run_cli(*args: str) -> subprocess.CompletedProcess[str]:
|
def run_cli(*args: str) -> subprocess.CompletedProcess[str]:
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["PYTHONPATH"] = "src"
|
env["PYTHONPATH"] = "src:/home/worsch/markitect-tool/src"
|
||||||
return subprocess.run(
|
return subprocess.run(
|
||||||
[sys.executable, "-m", "infospace_bench", *args],
|
[sys.executable, "-m", "infospace_bench", *args],
|
||||||
check=False,
|
check=False,
|
||||||
|
|||||||
150
tests/test_markdown_adapter.py
Normal file
150
tests/test_markdown_adapter.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from infospace_bench import add_artifact, create_infospace
|
||||||
|
from infospace_bench.markdown_adapter import (
|
||||||
|
extract_section_text,
|
||||||
|
parse_markdown_artifact,
|
||||||
|
validate_infospace_artifacts,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
CONTRACT = """# Source Artifact Contract
|
||||||
|
|
||||||
|
```yaml contract
|
||||||
|
id: source-artifact-v1
|
||||||
|
document:
|
||||||
|
type: source-artifact
|
||||||
|
sections:
|
||||||
|
- id: summary
|
||||||
|
title: Summary
|
||||||
|
presence: required
|
||||||
|
level: 2
|
||||||
|
- id: evidence
|
||||||
|
title: Evidence
|
||||||
|
presence: required
|
||||||
|
level: 2
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
VALID_SOURCE = """---
|
||||||
|
document_type: source-artifact
|
||||||
|
status: draft
|
||||||
|
---
|
||||||
|
|
||||||
|
# Source A
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This source describes the first artifact.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
The required evidence section is present.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
INVALID_SOURCE = """---
|
||||||
|
document_type: source-artifact
|
||||||
|
---
|
||||||
|
|
||||||
|
# Source B
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This source is missing evidence.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def cli_env() -> dict[str, str]:
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["PYTHONPATH"] = "src:/home/worsch/markitect-tool/src"
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_markdown_artifact_uses_markitect_tool_sections(tmp_path: Path) -> None:
|
||||||
|
source = tmp_path / "source.md"
|
||||||
|
source.write_text(VALID_SOURCE, encoding="utf-8")
|
||||||
|
|
||||||
|
parsed = parse_markdown_artifact(source)
|
||||||
|
|
||||||
|
assert parsed.frontmatter["document_type"] == "source-artifact"
|
||||||
|
assert [heading.text for heading in parsed.headings] == [
|
||||||
|
"Source A",
|
||||||
|
"Summary",
|
||||||
|
"Evidence",
|
||||||
|
]
|
||||||
|
assert extract_section_text(parsed, "Summary") == (
|
||||||
|
"This source describes the first artifact."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_infospace_artifacts_uses_configured_contract(tmp_path: Path) -> None:
|
||||||
|
infospace = create_infospace(tmp_path, "pilot", name="Pilot")
|
||||||
|
contract = infospace.root / "contracts" / "source.contract.md"
|
||||||
|
contract.parent.mkdir()
|
||||||
|
contract.write_text(CONTRACT, encoding="utf-8")
|
||||||
|
|
||||||
|
config = infospace.root / "infospace.yaml"
|
||||||
|
config.write_text(
|
||||||
|
config.read_text(encoding="utf-8").replace(
|
||||||
|
"schemas: {}\n", "schemas:\n source: contracts/source.contract.md\n"
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
valid = tmp_path / "valid.md"
|
||||||
|
valid.write_text(VALID_SOURCE, encoding="utf-8")
|
||||||
|
invalid = tmp_path / "invalid.md"
|
||||||
|
invalid.write_text(INVALID_SOURCE, encoding="utf-8")
|
||||||
|
add_artifact(infospace.root, valid, kind="source", title="Valid")
|
||||||
|
add_artifact(infospace.root, invalid, kind="source", title="Invalid")
|
||||||
|
|
||||||
|
results = validate_infospace_artifacts(infospace.root)
|
||||||
|
|
||||||
|
by_id = {result.artifact_id: result for result in results}
|
||||||
|
assert by_id["source/valid.md"].valid is True
|
||||||
|
assert by_id["source/invalid.md"].valid is False
|
||||||
|
assert by_id["source/invalid.md"].diagnostics[0].code == "contract.section.missing"
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_validate_outputs_json_results(tmp_path: Path) -> None:
|
||||||
|
infospace = create_infospace(tmp_path, "pilot", name="Pilot")
|
||||||
|
contract = infospace.root / "contracts" / "source.contract.md"
|
||||||
|
contract.parent.mkdir()
|
||||||
|
contract.write_text(CONTRACT, encoding="utf-8")
|
||||||
|
config = infospace.root / "infospace.yaml"
|
||||||
|
config.write_text(
|
||||||
|
config.read_text(encoding="utf-8").replace(
|
||||||
|
"schemas: {}\n", "schemas:\n source: contracts/source.contract.md\n"
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
source = tmp_path / "invalid.md"
|
||||||
|
source.write_text(INVALID_SOURCE, encoding="utf-8")
|
||||||
|
add_artifact(infospace.root, source, kind="source", title="Invalid")
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
"-m",
|
||||||
|
"infospace_bench",
|
||||||
|
"validate",
|
||||||
|
str(infospace.root),
|
||||||
|
],
|
||||||
|
check=False,
|
||||||
|
env=cli_env(),
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.returncode == 1
|
||||||
|
payload = json.loads(result.stdout)
|
||||||
|
assert payload["valid"] is False
|
||||||
|
assert payload["results"][0]["diagnostics"][0]["code"] == (
|
||||||
|
"contract.section.missing"
|
||||||
|
)
|
||||||
@@ -4,7 +4,7 @@ type: workplan
|
|||||||
title: "Markitect Tool Adapter And Markdown Artifact Validation"
|
title: "Markitect Tool Adapter And Markdown Artifact Validation"
|
||||||
domain: markitect
|
domain: markitect
|
||||||
repo: infospace-bench
|
repo: infospace-bench
|
||||||
status: planned
|
status: done
|
||||||
owner: markitect
|
owner: markitect
|
||||||
topic_slug: markitect
|
topic_slug: markitect
|
||||||
created: "2026-05-14"
|
created: "2026-05-14"
|
||||||
@@ -26,7 +26,7 @@ a thin application adapter.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: IB-WP-0006-T01
|
id: IB-WP-0006-T01
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "b438be61-6d2a-44f9-b235-2b415e9f4bca"
|
state_hub_task_id: "b438be61-6d2a-44f9-b235-2b415e9f4bca"
|
||||||
```
|
```
|
||||||
@@ -40,7 +40,7 @@ state_hub_task_id: "b438be61-6d2a-44f9-b235-2b415e9f4bca"
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: IB-WP-0006-T02
|
id: IB-WP-0006-T02
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "d37d6be3-ea04-4279-a113-086ab4fb617d"
|
state_hub_task_id: "d37d6be3-ea04-4279-a113-086ab4fb617d"
|
||||||
```
|
```
|
||||||
@@ -54,7 +54,7 @@ state_hub_task_id: "d37d6be3-ea04-4279-a113-086ab4fb617d"
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: IB-WP-0006-T03
|
id: IB-WP-0006-T03
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "74a78679-4ef2-4b6b-a80c-b40119d9f90e"
|
state_hub_task_id: "74a78679-4ef2-4b6b-a80c-b40119d9f90e"
|
||||||
```
|
```
|
||||||
@@ -67,7 +67,7 @@ state_hub_task_id: "74a78679-4ef2-4b6b-a80c-b40119d9f90e"
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: IB-WP-0006-T04
|
id: IB-WP-0006-T04
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "99b4e2ca-d9be-40a5-8b10-0b0f3c18fb19"
|
state_hub_task_id: "99b4e2ca-d9be-40a5-8b10-0b0f3c18fb19"
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user