generated from coulomb/repo-seed
Markitect schema-validation integration use case and fixture for Markdown proxy documents
This commit is contained in:
@@ -206,6 +206,11 @@ Adapter rules:
|
|||||||
checks, and context-package interoperability. Engine domain code must not
|
checks, and context-package interoperability. Engine domain code must not
|
||||||
import it directly; adapter code should persist serializable Markitect
|
import it directly; adapter code should persist serializable Markitect
|
||||||
outputs as adapter provenance or representation metadata.
|
outputs as adapter provenance or representation metadata.
|
||||||
|
- Markdown proxy documents are allowed as adapter projections for managed
|
||||||
|
assets. They can make every asset inspectable and contract-checkable through
|
||||||
|
Markitect where useful, but they are not the canonical engine identity or
|
||||||
|
storage model. The canonical layer remains asset, representation, metadata,
|
||||||
|
lifecycle, policy, lineage, and audit state.
|
||||||
- `llm-connect` or equivalent is an adapter for LLM providers.
|
- `llm-connect` or equivalent is an adapter for LLM providers.
|
||||||
- `phase-memory` is an adjacent memory runtime; this engine may exchange opaque
|
- `phase-memory` is an adjacent memory runtime; this engine may exchange opaque
|
||||||
memory references or context packages but should not implement memory phases.
|
memory references or context packages but should not implement memory phases.
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ and SQLite repositories are adapters behind those ports.
|
|||||||
- `MetadataRecord` persistence with inferred/confirmed semantics preserved.
|
- `MetadataRecord` persistence with inferred/confirmed semantics preserved.
|
||||||
- Custom metadata schema primitives with structured validation issues.
|
- Custom metadata schema primitives with structured validation issues.
|
||||||
- Metadata schema validation before asset create and metadata update writes.
|
- Metadata schema validation before asset create and metadata update writes.
|
||||||
|
- Durable metadata schema registry and assignment rules for policy-selected
|
||||||
|
validation.
|
||||||
- Actor and `OperationContext` required for material mutations.
|
- Actor and `OperationContext` required for material mutations.
|
||||||
- Policy gateway authorization before asset mutations.
|
- Policy gateway authorization before asset mutations.
|
||||||
- Fail-closed policy denial through `AuthorizationError`.
|
- Fail-closed policy denial through `AuthorizationError`.
|
||||||
@@ -59,6 +61,8 @@ and SQLite repositories are adapters behind those ports.
|
|||||||
- `assets`
|
- `assets`
|
||||||
- `representations`
|
- `representations`
|
||||||
- `metadata_records`
|
- `metadata_records`
|
||||||
|
- `metadata_schemas`
|
||||||
|
- `metadata_schema_assignments`
|
||||||
- `context_entities`
|
- `context_entities`
|
||||||
- `core_relationships`
|
- `core_relationships`
|
||||||
- `asset_versions`
|
- `asset_versions`
|
||||||
@@ -72,7 +76,6 @@ idempotency key.
|
|||||||
|
|
||||||
## Not Yet Implemented
|
## Not Yet Implemented
|
||||||
|
|
||||||
- Schema registry persistence and policy-assigned schema selection.
|
|
||||||
- Standard metadata filtering beyond lifecycle and asset type.
|
- Standard metadata filtering beyond lifecycle and asset type.
|
||||||
- Policy assignment storage and enterprise policy adapters.
|
- Policy assignment storage and enterprise policy adapters.
|
||||||
- Conflict detection beyond version-sequence uniqueness.
|
- Conflict detection beyond version-sequence uniqueness.
|
||||||
@@ -90,9 +93,10 @@ These remain in scope for later `KONT-WP-0005` tasks or adjacent workplans.
|
|||||||
- lifecycle denial with fail-closed policy and denied audit event,
|
- lifecycle denial with fail-closed policy and denied audit event,
|
||||||
- SQLite reload preserving asset lifecycle, representation, metadata, versions,
|
- SQLite reload preserving asset lifecycle, representation, metadata, versions,
|
||||||
and audit history,
|
and audit history,
|
||||||
- SQLite referential integrity for representation asset references.
|
- SQLite referential integrity for representation asset references,
|
||||||
- idempotent asset creation and conflicting idempotency-key reuse,
|
- idempotent asset creation and conflicting idempotency-key reuse,
|
||||||
- relationship creation with source-asset versioning and audit,
|
- relationship creation with source-asset versioning and audit,
|
||||||
- SQLite reload preserving context entities, relationships, and idempotency
|
- SQLite reload preserving context entities, relationships, and idempotency
|
||||||
records,
|
records,
|
||||||
- custom metadata schema validation before registry writes.
|
- custom metadata schema validation before registry writes,
|
||||||
|
- persistent metadata schema registry and assignment reload behavior.
|
||||||
|
|||||||
@@ -218,6 +218,34 @@ Engine expectation:
|
|||||||
- The engine owns workflow templates, run state, retries, review gates,
|
- The engine owns workflow templates, run state, retries, review gates,
|
||||||
exceptions, audit, and derived artifacts.
|
exceptions, audit, and derived artifacts.
|
||||||
|
|
||||||
|
## Use Case 7: Markdown Proxy Schema Validation
|
||||||
|
|
||||||
|
Intent: validate Markdown source or proxy documents through Markitect document
|
||||||
|
schemas instead of adding a second Markdown schema validator to the engine.
|
||||||
|
|
||||||
|
Expected Markitect APIs:
|
||||||
|
|
||||||
|
- `load_schema_file(...)`
|
||||||
|
- `validate_schema(...)`
|
||||||
|
- `validate_document(...)`
|
||||||
|
- `validate_markdown_file(...)`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from markitect_tool import validate_markdown_file
|
||||||
|
|
||||||
|
result = validate_markdown_file("asset-proxy.md", "asset-proxy.schema.md")
|
||||||
|
```
|
||||||
|
|
||||||
|
Engine expectation:
|
||||||
|
|
||||||
|
- Markdown proxy documents are adapter representations of governed assets.
|
||||||
|
- Markitect owns Markdown document schema validation for those proxies.
|
||||||
|
- Engine metadata schema validation remains registry-owned because it governs
|
||||||
|
asset metadata records, confirmation state, policy assignment, write
|
||||||
|
rejection, and audit behavior.
|
||||||
|
|
||||||
## Integration Test Matrix
|
## Integration Test Matrix
|
||||||
|
|
||||||
| Test area | Boundary protected |
|
| Test area | Boundary protected |
|
||||||
@@ -228,6 +256,7 @@ Engine expectation:
|
|||||||
| Snapshot identity | Engine stores Markitect snapshot metadata without owning the algorithm. |
|
| Snapshot identity | Engine stores Markitect snapshot metadata without owning the algorithm. |
|
||||||
| Context package policy filtering | Agent context can reuse Markitect packages and local label policy. |
|
| Context package policy filtering | Agent context can reuse Markitect packages and local label policy. |
|
||||||
| Document contracts | Markdown validation can call Markitect contracts without moving contract semantics into the engine. |
|
| Document contracts | Markdown validation can call Markitect contracts without moving contract semantics into the engine. |
|
||||||
|
| Markdown document schemas | Markdown source/proxy validation uses Markitect schema APIs instead of duplicating them. |
|
||||||
| Capacity sentinels | Larger generated examples expose likely parser, selector, include, context-package, and snapshot bottlenecks. |
|
| Capacity sentinels | Larger generated examples expose likely parser, selector, include, context-package, and snapshot bottlenecks. |
|
||||||
|
|
||||||
These tests are intentionally small but example-backed. They are not a
|
These tests are intentionally small but example-backed. They are not a
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ state should persist serializable envelopes, source references, digests,
|
|||||||
lineage, policy decisions, and audit events rather than storing Markitect
|
lineage, policy decisions, and audit events rather than storing Markitect
|
||||||
runtime objects as canonical engine entities.
|
runtime objects as canonical engine entities.
|
||||||
|
|
||||||
|
Markdown proxy documents are a supported adapter pattern. The engine may create
|
||||||
|
or store Markdown representations that proxy non-Markdown assets so Markitect
|
||||||
|
selectors, contracts, document schemas, functions, and workflows can operate on
|
||||||
|
them. Those proxies are representations of governed assets, not replacements
|
||||||
|
for engine-owned asset identity, metadata, lifecycle, policy, lineage, or audit
|
||||||
|
state.
|
||||||
|
|
||||||
Required integration behavior is captured in
|
Required integration behavior is captured in
|
||||||
`docs/markitect-tool-integration-usecases.md` and exercised by
|
`docs/markitect-tool-integration-usecases.md` and exercised by
|
||||||
`tests/test_markitect_tool_contract.py`. These tests are allowed to skip when
|
`tests/test_markitect_tool_contract.py`. These tests are allowed to skip when
|
||||||
@@ -35,6 +42,7 @@ stability checks for the boundary when the `markdown` extra is installed.
|
|||||||
| Document-level selectors and extraction | `markitect_tool.query`, `docs/query-extraction.md` | Use for markdown source extraction and context package creation. Engine query should operate over persisted artifacts and relationships. |
|
| Document-level selectors and extraction | `markitect_tool.query`, `docs/query-extraction.md` | Use for markdown source extraction and context package creation. Engine query should operate over persisted artifacts and relationships. |
|
||||||
| Deterministic transforms, composition, and includes | `markitect_tool.ops.engine`, `docs/transform-compose-include.md` | Treat as external operations invoked by workflows. Store operation provenance and derived artifacts in the engine. |
|
| Deterministic transforms, composition, and includes | `markitect_tool.ops.engine`, `docs/transform-compose-include.md` | Treat as external operations invoked by workflows. Store operation provenance and derived artifacts in the engine. |
|
||||||
| Contract checks, runtime context, forms, and assessments | `markitect_tool.contract.*`, `markitect_tool.runtime.*`, `docs/runtime-context-forms-assessments.md` | Use as validation/assessment step adapters. Engine owns run state and audit trail. |
|
| Contract checks, runtime context, forms, and assessments | `markitect_tool.contract.*`, `markitect_tool.runtime.*`, `docs/runtime-context-forms-assessments.md` | Use as validation/assessment step adapters. Engine owns run state and audit trail. |
|
||||||
|
| Markdown document schema validation | `markitect_tool.schema.*` | Use for Markdown document/proxy validation. Engine-owned asset metadata validation stays in the registry layer. |
|
||||||
| Backend manifests, local snapshots, FTS, and query adapters | `markitect_tool.backend.*`, `docs/backend-fabric.md` | Reuse snapshot identity and local index concepts. Engine storage remains separate and cross-format. |
|
| Backend manifests, local snapshots, FTS, and query adapters | `markitect_tool.backend.*`, `docs/backend-fabric.md` | Reuse snapshot identity and local index concepts. Engine storage remains separate and cross-format. |
|
||||||
| Agent working memory context packages | `markitect_tool.memory.engine`, `docs/agent-working-memory.md` | Reuse as a portable context-package format for markdown-backed context. Engine should provide durable context registries across formats. |
|
| Agent working memory context packages | `markitect_tool.memory.engine`, `docs/agent-working-memory.md` | Reuse as a portable context-package format for markdown-backed context. Engine should provide durable context registries across formats. |
|
||||||
| Workflow definition syntax and markdown-centered step kinds | `markitect_tool.workflow.*`, `docs/workflow-definition-standard.md` | Reuse where workflows consume markdown inputs. Engine workflows should generalize to artifact collections, external tools, and service operations. |
|
| Workflow definition syntax and markdown-centered step kinds | `markitect_tool.workflow.*`, `docs/workflow-definition-standard.md` | Reuse where workflows consume markdown inputs. Engine workflows should generalize to artifact collections, external tools, and service operations. |
|
||||||
@@ -44,7 +52,8 @@ stability checks for the boundary when the `markdown` extra is installed.
|
|||||||
## Adapter Ownership Rules
|
## Adapter Ownership Rules
|
||||||
|
|
||||||
- Markdown ingestion adapters may call `parse_markdown`, `parse_markdown_file`,
|
- Markdown ingestion adapters may call `parse_markdown`, `parse_markdown_file`,
|
||||||
`query_document`, `extract_document`, and `snapshot_identity_for_file`.
|
`query_document`, `extract_document`, `validate_document`,
|
||||||
|
`validate_markdown_file`, and `snapshot_identity_for_file`.
|
||||||
- Markdown transformation adapters may call `transform_markdown`,
|
- Markdown transformation adapters may call `transform_markdown`,
|
||||||
`compose_files`, `resolve_includes`, Markitect contract checks, document
|
`compose_files`, `resolve_includes`, Markitect contract checks, document
|
||||||
functions, templates, and workflow helpers.
|
functions, templates, and workflow helpers.
|
||||||
|
|||||||
38
examples/markitect-tool-contract/schemas/adr-proxy.schema.md
Normal file
38
examples/markitect-tool-contract/schemas/adr-proxy.schema.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
schema-id: "kontextual-engine.markdown-proxy.adr.v1"
|
||||||
|
version: "1.0.0"
|
||||||
|
status: "example"
|
||||||
|
---
|
||||||
|
|
||||||
|
# ADR Proxy Document Schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"title": "ADR Proxy Document",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["frontmatter", "headings"],
|
||||||
|
"properties": {
|
||||||
|
"frontmatter": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["document_type", "status", "owner"],
|
||||||
|
"properties": {
|
||||||
|
"document_type": {"const": "adr"},
|
||||||
|
"status": {"enum": ["proposed", "accepted", "deprecated", "superseded"]},
|
||||||
|
"owner": {"type": "string", "minLength": 1}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"headings": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 3,
|
||||||
|
"contains": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["text"],
|
||||||
|
"properties": {
|
||||||
|
"text": {"const": "Decision"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -39,6 +39,7 @@ from .core import (
|
|||||||
MetadataFieldDefinition,
|
MetadataFieldDefinition,
|
||||||
MetadataRecord,
|
MetadataRecord,
|
||||||
MetadataSchema,
|
MetadataSchema,
|
||||||
|
MetadataSchemaAssignment,
|
||||||
MetadataValidationIssue,
|
MetadataValidationIssue,
|
||||||
MetadataValueType,
|
MetadataValueType,
|
||||||
NormalizedDocument,
|
NormalizedDocument,
|
||||||
@@ -142,6 +143,7 @@ __all__ = [
|
|||||||
"MetadataFieldDefinition",
|
"MetadataFieldDefinition",
|
||||||
"MetadataRecord",
|
"MetadataRecord",
|
||||||
"MetadataSchema",
|
"MetadataSchema",
|
||||||
|
"MetadataSchemaAssignment",
|
||||||
"MetadataValidationIssue",
|
"MetadataValidationIssue",
|
||||||
"MetadataValueType",
|
"MetadataValueType",
|
||||||
"NormalizedDocument",
|
"NormalizedDocument",
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ from kontextual_engine.core import (
|
|||||||
KnowledgeAsset,
|
KnowledgeAsset,
|
||||||
LifecycleState,
|
LifecycleState,
|
||||||
MetadataRecord,
|
MetadataRecord,
|
||||||
|
MetadataSchema,
|
||||||
|
MetadataSchemaAssignment,
|
||||||
RepresentationKind,
|
RepresentationKind,
|
||||||
)
|
)
|
||||||
from kontextual_engine.errors import NotFoundError, ValidationError
|
from kontextual_engine.errors import NotFoundError, ValidationError
|
||||||
@@ -29,6 +31,8 @@ class InMemoryAssetRegistryRepository:
|
|||||||
assets: dict[str, KnowledgeAsset] = field(default_factory=dict)
|
assets: dict[str, KnowledgeAsset] = field(default_factory=dict)
|
||||||
representations: dict[str, AssetRepresentation] = field(default_factory=dict)
|
representations: dict[str, AssetRepresentation] = field(default_factory=dict)
|
||||||
metadata_records: dict[str, list[MetadataRecord]] = field(default_factory=dict)
|
metadata_records: dict[str, list[MetadataRecord]] = field(default_factory=dict)
|
||||||
|
metadata_schemas: dict[str, MetadataSchema] = field(default_factory=dict)
|
||||||
|
metadata_schema_assignments: dict[str, MetadataSchemaAssignment] = field(default_factory=dict)
|
||||||
context_entities: dict[str, ContextEntity] = field(default_factory=dict)
|
context_entities: dict[str, ContextEntity] = field(default_factory=dict)
|
||||||
relationships: dict[str, CoreRelationship] = field(default_factory=dict)
|
relationships: dict[str, CoreRelationship] = field(default_factory=dict)
|
||||||
versions: dict[str, list[AssetVersion]] = field(default_factory=dict)
|
versions: dict[str, list[AssetVersion]] = field(default_factory=dict)
|
||||||
@@ -105,6 +109,42 @@ class InMemoryAssetRegistryRepository:
|
|||||||
self.get_asset(asset_id)
|
self.get_asset(asset_id)
|
||||||
return list(self.metadata_records.get(asset_id, []))
|
return list(self.metadata_records.get(asset_id, []))
|
||||||
|
|
||||||
|
def save_metadata_schema(self, schema: MetadataSchema) -> MetadataSchema:
|
||||||
|
self.metadata_schemas[schema.schema_id] = schema
|
||||||
|
return schema
|
||||||
|
|
||||||
|
def get_metadata_schema(self, schema_id: str) -> MetadataSchema:
|
||||||
|
try:
|
||||||
|
return self.metadata_schemas[schema_id]
|
||||||
|
except KeyError as exc:
|
||||||
|
raise NotFoundError("Metadata schema not found", details={"schema_id": schema_id}) from exc
|
||||||
|
|
||||||
|
def list_metadata_schemas(self) -> list[MetadataSchema]:
|
||||||
|
return sorted(self.metadata_schemas.values(), key=lambda schema: (schema.name, schema.schema_id))
|
||||||
|
|
||||||
|
def save_metadata_schema_assignment(
|
||||||
|
self,
|
||||||
|
assignment: MetadataSchemaAssignment,
|
||||||
|
) -> MetadataSchemaAssignment:
|
||||||
|
self.get_metadata_schema(assignment.schema_id)
|
||||||
|
self.metadata_schema_assignments[assignment.assignment_id] = assignment
|
||||||
|
return assignment
|
||||||
|
|
||||||
|
def get_metadata_schema_assignment(self, assignment_id: str) -> MetadataSchemaAssignment:
|
||||||
|
try:
|
||||||
|
return self.metadata_schema_assignments[assignment_id]
|
||||||
|
except KeyError as exc:
|
||||||
|
raise NotFoundError(
|
||||||
|
"Metadata schema assignment not found",
|
||||||
|
details={"assignment_id": assignment_id},
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
def list_metadata_schema_assignments(self) -> list[MetadataSchemaAssignment]:
|
||||||
|
return sorted(
|
||||||
|
self.metadata_schema_assignments.values(),
|
||||||
|
key=lambda assignment: (assignment.priority, assignment.schema_id, assignment.assignment_id),
|
||||||
|
)
|
||||||
|
|
||||||
def save_context_entity(self, entity: ContextEntity) -> ContextEntity:
|
def save_context_entity(self, entity: ContextEntity) -> ContextEntity:
|
||||||
self.context_entities[entity.entity_id] = entity
|
self.context_entities[entity.entity_id] = entity
|
||||||
return entity
|
return entity
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ from kontextual_engine.core import (
|
|||||||
KnowledgeAsset,
|
KnowledgeAsset,
|
||||||
LifecycleState,
|
LifecycleState,
|
||||||
MetadataRecord,
|
MetadataRecord,
|
||||||
|
MetadataSchema,
|
||||||
|
MetadataSchemaAssignment,
|
||||||
RepresentationKind,
|
RepresentationKind,
|
||||||
RelationshipTargetKind,
|
RelationshipTargetKind,
|
||||||
)
|
)
|
||||||
@@ -189,6 +191,74 @@ class SQLiteAssetRegistryRepository:
|
|||||||
self.get_asset(asset_id)
|
self.get_asset(asset_id)
|
||||||
return [MetadataRecord.from_dict(_loads(row["payload"])) for row in rows]
|
return [MetadataRecord.from_dict(_loads(row["payload"])) for row in rows]
|
||||||
|
|
||||||
|
def save_metadata_schema(self, schema: MetadataSchema) -> MetadataSchema:
|
||||||
|
with self._connect() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
insert into metadata_schemas (id, name, version, payload)
|
||||||
|
values (?, ?, ?, ?)
|
||||||
|
on conflict(id) do update set
|
||||||
|
name=excluded.name,
|
||||||
|
version=excluded.version,
|
||||||
|
payload=excluded.payload
|
||||||
|
""",
|
||||||
|
(schema.schema_id, schema.name, schema.version, _json(schema.to_dict())),
|
||||||
|
)
|
||||||
|
return schema
|
||||||
|
|
||||||
|
def get_metadata_schema(self, schema_id: str) -> MetadataSchema:
|
||||||
|
row = self._one("select payload from metadata_schemas where id = ?", (schema_id,))
|
||||||
|
if row is None:
|
||||||
|
raise NotFoundError("Metadata schema not found", details={"schema_id": schema_id})
|
||||||
|
return MetadataSchema.from_dict(_loads(row["payload"]))
|
||||||
|
|
||||||
|
def list_metadata_schemas(self) -> list[MetadataSchema]:
|
||||||
|
rows = self._all("select payload from metadata_schemas order by name, id", ())
|
||||||
|
return [MetadataSchema.from_dict(_loads(row["payload"])) for row in rows]
|
||||||
|
|
||||||
|
def save_metadata_schema_assignment(
|
||||||
|
self,
|
||||||
|
assignment: MetadataSchemaAssignment,
|
||||||
|
) -> MetadataSchemaAssignment:
|
||||||
|
self.get_metadata_schema(assignment.schema_id)
|
||||||
|
with self._connect() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
insert into metadata_schema_assignments (id, schema_id, priority, payload)
|
||||||
|
values (?, ?, ?, ?)
|
||||||
|
on conflict(id) do update set
|
||||||
|
schema_id=excluded.schema_id,
|
||||||
|
priority=excluded.priority,
|
||||||
|
payload=excluded.payload
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
assignment.assignment_id,
|
||||||
|
assignment.schema_id,
|
||||||
|
assignment.priority,
|
||||||
|
_json(assignment.to_dict()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return assignment
|
||||||
|
|
||||||
|
def get_metadata_schema_assignment(self, assignment_id: str) -> MetadataSchemaAssignment:
|
||||||
|
row = self._one(
|
||||||
|
"select payload from metadata_schema_assignments where id = ?",
|
||||||
|
(assignment_id,),
|
||||||
|
)
|
||||||
|
if row is None:
|
||||||
|
raise NotFoundError(
|
||||||
|
"Metadata schema assignment not found",
|
||||||
|
details={"assignment_id": assignment_id},
|
||||||
|
)
|
||||||
|
return MetadataSchemaAssignment.from_dict(_loads(row["payload"]))
|
||||||
|
|
||||||
|
def list_metadata_schema_assignments(self) -> list[MetadataSchemaAssignment]:
|
||||||
|
rows = self._all(
|
||||||
|
"select payload from metadata_schema_assignments order by priority, schema_id, id",
|
||||||
|
(),
|
||||||
|
)
|
||||||
|
return [MetadataSchemaAssignment.from_dict(_loads(row["payload"])) for row in rows]
|
||||||
|
|
||||||
def save_context_entity(self, entity: ContextEntity) -> ContextEntity:
|
def save_context_entity(self, entity: ContextEntity) -> ContextEntity:
|
||||||
with self._connect() as conn:
|
with self._connect() as conn:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -457,6 +527,18 @@ class SQLiteAssetRegistryRepository:
|
|||||||
key text not null,
|
key text not null,
|
||||||
payload text not null
|
payload text not null
|
||||||
);
|
);
|
||||||
|
create table if not exists metadata_schemas (
|
||||||
|
id text primary key,
|
||||||
|
name text not null,
|
||||||
|
version text not null,
|
||||||
|
payload text not null
|
||||||
|
);
|
||||||
|
create table if not exists metadata_schema_assignments (
|
||||||
|
id text primary key,
|
||||||
|
schema_id text not null references metadata_schemas(id) on delete cascade,
|
||||||
|
priority integer not null,
|
||||||
|
payload text not null
|
||||||
|
);
|
||||||
create table if not exists context_entities (
|
create table if not exists context_entities (
|
||||||
id text primary key,
|
id text primary key,
|
||||||
entity_type text not null,
|
entity_type text not null,
|
||||||
@@ -508,6 +590,7 @@ class SQLiteAssetRegistryRepository:
|
|||||||
create index if not exists idx_assets_lifecycle on assets(lifecycle);
|
create index if not exists idx_assets_lifecycle on assets(lifecycle);
|
||||||
create index if not exists idx_representations_asset on representations(asset_id);
|
create index if not exists idx_representations_asset on representations(asset_id);
|
||||||
create index if not exists idx_metadata_asset on metadata_records(asset_id);
|
create index if not exists idx_metadata_asset on metadata_records(asset_id);
|
||||||
|
create index if not exists idx_schema_assignments_schema on metadata_schema_assignments(schema_id);
|
||||||
create index if not exists idx_entities_type on context_entities(entity_type);
|
create index if not exists idx_entities_type on context_entities(entity_type);
|
||||||
create index if not exists idx_relationships_source on core_relationships(source_id);
|
create index if not exists idx_relationships_source on core_relationships(source_id);
|
||||||
create index if not exists idx_relationships_target on core_relationships(target_id);
|
create index if not exists idx_relationships_target on core_relationships(target_id);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from .metadata import (
|
|||||||
MetadataFieldDefinition,
|
MetadataFieldDefinition,
|
||||||
MetadataRecord,
|
MetadataRecord,
|
||||||
MetadataSchema,
|
MetadataSchema,
|
||||||
|
MetadataSchemaAssignment,
|
||||||
MetadataValidationIssue,
|
MetadataValidationIssue,
|
||||||
MetadataValueType,
|
MetadataValueType,
|
||||||
Sensitivity,
|
Sensitivity,
|
||||||
@@ -64,6 +65,7 @@ __all__ = [
|
|||||||
"MetadataFieldDefinition",
|
"MetadataFieldDefinition",
|
||||||
"MetadataRecord",
|
"MetadataRecord",
|
||||||
"MetadataSchema",
|
"MetadataSchema",
|
||||||
|
"MetadataSchemaAssignment",
|
||||||
"MetadataValidationIssue",
|
"MetadataValidationIssue",
|
||||||
"MetadataValueType",
|
"MetadataValueType",
|
||||||
"NormalizedDocument",
|
"NormalizedDocument",
|
||||||
|
|||||||
@@ -290,6 +290,63 @@ class MetadataSchema:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MetadataSchemaAssignment:
|
||||||
|
schema_id: str
|
||||||
|
asset_types: tuple[str, ...] = ()
|
||||||
|
sensitivities: tuple[Sensitivity | str, ...] = ()
|
||||||
|
lifecycle_states: tuple[LifecycleState | str, ...] = ()
|
||||||
|
policy_ref: str | None = None
|
||||||
|
priority: int = 100
|
||||||
|
metadata: dict[str, Any] = field(default_factory=dict)
|
||||||
|
assignment_id: str = field(default_factory=lambda: new_id("metadata_schema_assignment"))
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
object.__setattr__(self, "asset_types", tuple(self.asset_types))
|
||||||
|
object.__setattr__(self, "sensitivities", tuple(Sensitivity(item) for item in self.sensitivities))
|
||||||
|
object.__setattr__(
|
||||||
|
self,
|
||||||
|
"lifecycle_states",
|
||||||
|
tuple(LifecycleState(item) for item in self.lifecycle_states),
|
||||||
|
)
|
||||||
|
|
||||||
|
def applies_to(self, classification: "Classification") -> bool:
|
||||||
|
if self.asset_types and classification.asset_type not in self.asset_types:
|
||||||
|
return False
|
||||||
|
if self.sensitivities and classification.sensitivity not in self.sensitivities:
|
||||||
|
return False
|
||||||
|
if self.lifecycle_states and classification.lifecycle not in self.lifecycle_states:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return compact_dict(
|
||||||
|
{
|
||||||
|
"assignment_id": self.assignment_id,
|
||||||
|
"schema_id": self.schema_id,
|
||||||
|
"asset_types": list(self.asset_types),
|
||||||
|
"sensitivities": [item.value for item in self.sensitivities],
|
||||||
|
"lifecycle_states": [item.value for item in self.lifecycle_states],
|
||||||
|
"policy_ref": self.policy_ref,
|
||||||
|
"priority": self.priority,
|
||||||
|
"metadata": dict(self.metadata),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict[str, Any]) -> "MetadataSchemaAssignment":
|
||||||
|
return cls(
|
||||||
|
assignment_id=data["assignment_id"],
|
||||||
|
schema_id=data["schema_id"],
|
||||||
|
asset_types=tuple(data.get("asset_types", [])),
|
||||||
|
sensitivities=tuple(Sensitivity(item) for item in data.get("sensitivities", [])),
|
||||||
|
lifecycle_states=tuple(LifecycleState(item) for item in data.get("lifecycle_states", [])),
|
||||||
|
policy_ref=data.get("policy_ref"),
|
||||||
|
priority=int(data.get("priority", 100)),
|
||||||
|
metadata=dict(data.get("metadata", {})),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Classification:
|
class Classification:
|
||||||
asset_type: str
|
asset_type: str
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ from kontextual_engine.core import (
|
|||||||
KnowledgeAsset,
|
KnowledgeAsset,
|
||||||
LifecycleState,
|
LifecycleState,
|
||||||
MetadataRecord,
|
MetadataRecord,
|
||||||
|
MetadataSchema,
|
||||||
|
MetadataSchemaAssignment,
|
||||||
RepresentationKind,
|
RepresentationKind,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,6 +48,16 @@ class AssetRegistryRepository(Protocol):
|
|||||||
def save_metadata_record(self, asset_id: str, record: MetadataRecord) -> MetadataRecord: ...
|
def save_metadata_record(self, asset_id: str, record: MetadataRecord) -> MetadataRecord: ...
|
||||||
def list_metadata_records(self, asset_id: str) -> list[MetadataRecord]: ...
|
def list_metadata_records(self, asset_id: str) -> list[MetadataRecord]: ...
|
||||||
|
|
||||||
|
def save_metadata_schema(self, schema: MetadataSchema) -> MetadataSchema: ...
|
||||||
|
def get_metadata_schema(self, schema_id: str) -> MetadataSchema: ...
|
||||||
|
def list_metadata_schemas(self) -> list[MetadataSchema]: ...
|
||||||
|
def save_metadata_schema_assignment(
|
||||||
|
self,
|
||||||
|
assignment: MetadataSchemaAssignment,
|
||||||
|
) -> MetadataSchemaAssignment: ...
|
||||||
|
def get_metadata_schema_assignment(self, assignment_id: str) -> MetadataSchemaAssignment: ...
|
||||||
|
def list_metadata_schema_assignments(self) -> list[MetadataSchemaAssignment]: ...
|
||||||
|
|
||||||
def save_context_entity(self, entity: ContextEntity) -> ContextEntity: ...
|
def save_context_entity(self, entity: ContextEntity) -> ContextEntity: ...
|
||||||
def get_context_entity(self, entity_id: str) -> ContextEntity: ...
|
def get_context_entity(self, entity_id: str) -> ContextEntity: ...
|
||||||
def list_context_entities(self) -> list[ContextEntity]: ...
|
def list_context_entities(self) -> list[ContextEntity]: ...
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from kontextual_engine.core import (
|
|||||||
mapping_digest,
|
mapping_digest,
|
||||||
MetadataRecord,
|
MetadataRecord,
|
||||||
MetadataSchema,
|
MetadataSchema,
|
||||||
|
MetadataSchemaAssignment,
|
||||||
OperationContext,
|
OperationContext,
|
||||||
PolicyDecision,
|
PolicyDecision,
|
||||||
RelationshipTargetKind,
|
RelationshipTargetKind,
|
||||||
@@ -176,6 +177,57 @@ class AssetRegistryService:
|
|||||||
)
|
)
|
||||||
return AssetChangeResult(asset, version, event, decision)
|
return AssetChangeResult(asset, version, event, decision)
|
||||||
|
|
||||||
|
def register_metadata_schema(
|
||||||
|
self,
|
||||||
|
schema: MetadataSchema,
|
||||||
|
context: OperationContext,
|
||||||
|
) -> MetadataSchema:
|
||||||
|
decision = self._authorize(
|
||||||
|
context,
|
||||||
|
"metadata_schema.register",
|
||||||
|
f"metadata_schema:{schema.schema_id}",
|
||||||
|
resource_metadata={"schema_id": schema.schema_id, "version": schema.version},
|
||||||
|
)
|
||||||
|
saved = self.repository.save_metadata_schema(schema)
|
||||||
|
self._audit(
|
||||||
|
"metadata_schema.register",
|
||||||
|
f"metadata_schema:{schema.schema_id}",
|
||||||
|
AuditOutcome.SUCCESS,
|
||||||
|
context,
|
||||||
|
decision,
|
||||||
|
details={"schema_id": schema.schema_id, "version": schema.version},
|
||||||
|
)
|
||||||
|
return saved
|
||||||
|
|
||||||
|
def assign_metadata_schema(
|
||||||
|
self,
|
||||||
|
assignment: MetadataSchemaAssignment,
|
||||||
|
context: OperationContext,
|
||||||
|
) -> MetadataSchemaAssignment:
|
||||||
|
self.repository.get_metadata_schema(assignment.schema_id)
|
||||||
|
decision = self._authorize(
|
||||||
|
context,
|
||||||
|
"metadata_schema.assign",
|
||||||
|
f"metadata_schema_assignment:{assignment.assignment_id}",
|
||||||
|
resource_metadata={"schema_id": assignment.schema_id},
|
||||||
|
)
|
||||||
|
saved = self.repository.save_metadata_schema_assignment(assignment)
|
||||||
|
self._audit(
|
||||||
|
"metadata_schema.assign",
|
||||||
|
f"metadata_schema_assignment:{assignment.assignment_id}",
|
||||||
|
AuditOutcome.SUCCESS,
|
||||||
|
context,
|
||||||
|
decision,
|
||||||
|
details={"schema_id": assignment.schema_id, "assignment_id": assignment.assignment_id},
|
||||||
|
)
|
||||||
|
return saved
|
||||||
|
|
||||||
|
def list_metadata_schemas(self) -> list[MetadataSchema]:
|
||||||
|
return self.repository.list_metadata_schemas()
|
||||||
|
|
||||||
|
def list_metadata_schema_assignments(self) -> list[MetadataSchemaAssignment]:
|
||||||
|
return self.repository.list_metadata_schema_assignments()
|
||||||
|
|
||||||
def add_representation(
|
def add_representation(
|
||||||
self,
|
self,
|
||||||
asset_id: str,
|
asset_id: str,
|
||||||
@@ -417,9 +469,23 @@ class AssetRegistryService:
|
|||||||
classification: Classification,
|
classification: Classification,
|
||||||
records: list[MetadataRecord],
|
records: list[MetadataRecord],
|
||||||
) -> None:
|
) -> None:
|
||||||
for schema in self.metadata_schemas:
|
for schema in self._metadata_schemas_for(classification):
|
||||||
if schema.applies_to(classification):
|
schema.validate_or_raise(records)
|
||||||
schema.validate_or_raise(records)
|
|
||||||
|
def _metadata_schemas_for(self, classification: Classification) -> tuple[MetadataSchema, ...]:
|
||||||
|
selected: list[MetadataSchema] = [
|
||||||
|
schema for schema in self.metadata_schemas if schema.applies_to(classification)
|
||||||
|
]
|
||||||
|
seen = {schema.schema_id for schema in selected}
|
||||||
|
for assignment in self.repository.list_metadata_schema_assignments():
|
||||||
|
if not assignment.applies_to(classification):
|
||||||
|
continue
|
||||||
|
schema = self.repository.get_metadata_schema(assignment.schema_id)
|
||||||
|
if schema.schema_id in seen or not schema.applies_to(classification):
|
||||||
|
continue
|
||||||
|
selected.append(schema)
|
||||||
|
seen.add(schema.schema_id)
|
||||||
|
return tuple(selected)
|
||||||
|
|
||||||
def _idempotent_lookup(
|
def _idempotent_lookup(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from kontextual_engine import (
|
|||||||
MetadataFieldDefinition,
|
MetadataFieldDefinition,
|
||||||
MetadataRecord,
|
MetadataRecord,
|
||||||
MetadataSchema,
|
MetadataSchema,
|
||||||
|
MetadataSchemaAssignment,
|
||||||
MetadataValueType,
|
MetadataValueType,
|
||||||
OperationContext,
|
OperationContext,
|
||||||
PolicyDecision,
|
PolicyDecision,
|
||||||
@@ -224,6 +225,60 @@ def test_asset_registry_validates_metadata_schema_before_writes() -> None:
|
|||||||
assert [record.key for record in repo.list_metadata_records(created.asset.id)] == ["owner"]
|
assert [record.key for record in repo.list_metadata_records(created.asset.id)] == ["owner"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_asset_registry_applies_persisted_metadata_schema_assignments() -> None:
|
||||||
|
repo = InMemoryAssetRegistryRepository()
|
||||||
|
service = AssetRegistryService(repo)
|
||||||
|
context = operation_context()
|
||||||
|
schema = MetadataSchema(
|
||||||
|
schema_id="schema-policy-note-v1",
|
||||||
|
name="Policy Note Metadata",
|
||||||
|
allow_unknown=False,
|
||||||
|
fields=(
|
||||||
|
MetadataFieldDefinition("owner", MetadataValueType.STRING, required=True, require_confirmed=True),
|
||||||
|
MetadataFieldDefinition("state", MetadataValueType.STRING, allowed_values=("draft", "approved")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assignment = MetadataSchemaAssignment(
|
||||||
|
assignment_id="assignment-policy-note",
|
||||||
|
schema_id=schema.schema_id,
|
||||||
|
asset_types=("policy-note",),
|
||||||
|
sensitivities=(Sensitivity.INTERNAL,),
|
||||||
|
policy_ref="local://metadata-policy/policy-note",
|
||||||
|
)
|
||||||
|
|
||||||
|
service.register_metadata_schema(schema, context)
|
||||||
|
service.assign_metadata_schema(assignment, context)
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError) as exc_info:
|
||||||
|
service.create_asset(
|
||||||
|
"Policy Note",
|
||||||
|
Classification(asset_type="policy-note", sensitivity=Sensitivity.INTERNAL),
|
||||||
|
context,
|
||||||
|
asset_id="asset-policy-note-invalid",
|
||||||
|
metadata_records=[MetadataRecord("state", "published")],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {issue["code"] for issue in exc_info.value.details["issues"]} == {
|
||||||
|
"metadata.required_missing",
|
||||||
|
"metadata.value_not_allowed",
|
||||||
|
}
|
||||||
|
assert repo.list_assets() == []
|
||||||
|
|
||||||
|
created = service.create_asset(
|
||||||
|
"Policy Note",
|
||||||
|
Classification(asset_type="policy-note", sensitivity=Sensitivity.INTERNAL),
|
||||||
|
context,
|
||||||
|
asset_id="asset-policy-note",
|
||||||
|
metadata_records=[
|
||||||
|
MetadataRecord("owner", "Platform Knowledge", confirmed=True),
|
||||||
|
MetadataRecord("state", "approved", confirmed=True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert created.asset.id == "asset-policy-note"
|
||||||
|
assert service.list_metadata_schema_assignments()[0].policy_ref == "local://metadata-policy/policy-note"
|
||||||
|
|
||||||
|
|
||||||
def test_sqlite_asset_registry_survives_reinstantiation(tmp_path: Path) -> None:
|
def test_sqlite_asset_registry_survives_reinstantiation(tmp_path: Path) -> None:
|
||||||
db_path = tmp_path / "registry.sqlite"
|
db_path = tmp_path / "registry.sqlite"
|
||||||
repo = SQLiteAssetRegistryRepository(db_path)
|
repo = SQLiteAssetRegistryRepository(db_path)
|
||||||
@@ -305,6 +360,67 @@ def test_sqlite_registry_persists_context_entities_relationships_and_idempotency
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_sqlite_registry_persists_metadata_schemas_and_assignments(tmp_path: Path) -> None:
|
||||||
|
db_path = tmp_path / "registry.sqlite"
|
||||||
|
repo = SQLiteAssetRegistryRepository(db_path)
|
||||||
|
service = AssetRegistryService(repo)
|
||||||
|
context = operation_context()
|
||||||
|
schema = MetadataSchema(
|
||||||
|
schema_id="schema-review-v1",
|
||||||
|
name="Review Metadata",
|
||||||
|
allow_unknown=False,
|
||||||
|
fields=(
|
||||||
|
MetadataFieldDefinition("reviewer", MetadataValueType.STRING, required=True, require_confirmed=True),
|
||||||
|
MetadataFieldDefinition("score", MetadataValueType.NUMBER, min_value=0, max_value=1),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
service.register_metadata_schema(schema, context)
|
||||||
|
service.assign_metadata_schema(
|
||||||
|
MetadataSchemaAssignment(
|
||||||
|
assignment_id="assignment-review-documents",
|
||||||
|
schema_id=schema.schema_id,
|
||||||
|
asset_types=("review",),
|
||||||
|
),
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
|
||||||
|
reloaded_service = AssetRegistryService(SQLiteAssetRegistryRepository(db_path))
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError) as exc_info:
|
||||||
|
reloaded_service.create_asset(
|
||||||
|
"Review",
|
||||||
|
Classification(asset_type="review", sensitivity=Sensitivity.INTERNAL),
|
||||||
|
context,
|
||||||
|
asset_id="asset-review-invalid",
|
||||||
|
metadata_records=[
|
||||||
|
MetadataRecord("reviewer", "Ada", confirmed=False),
|
||||||
|
MetadataRecord("score", 1.7),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {issue["code"] for issue in exc_info.value.details["issues"]} == {
|
||||||
|
"metadata.confirmation_required",
|
||||||
|
"metadata.value_too_large",
|
||||||
|
}
|
||||||
|
|
||||||
|
created = reloaded_service.create_asset(
|
||||||
|
"Review",
|
||||||
|
Classification(asset_type="review", sensitivity=Sensitivity.INTERNAL),
|
||||||
|
context,
|
||||||
|
asset_id="asset-review",
|
||||||
|
metadata_records=[
|
||||||
|
MetadataRecord("reviewer", "Ada", confirmed=True),
|
||||||
|
MetadataRecord("score", 0.92),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
reloaded_repo = SQLiteAssetRegistryRepository(db_path)
|
||||||
|
assert created.asset.id == "asset-review"
|
||||||
|
assert reloaded_repo.get_metadata_schema("schema-review-v1").name == "Review Metadata"
|
||||||
|
assert reloaded_repo.get_metadata_schema_assignment("assignment-review-documents").schema_id == "schema-review-v1"
|
||||||
|
|
||||||
|
|
||||||
def test_sqlite_registry_enforces_representation_asset_reference(tmp_path: Path) -> None:
|
def test_sqlite_registry_enforces_representation_asset_reference(tmp_path: Path) -> None:
|
||||||
repo = SQLiteAssetRegistryRepository(tmp_path / "registry.sqlite")
|
repo = SQLiteAssetRegistryRepository(tmp_path / "registry.sqlite")
|
||||||
representation = AssetRepresentation.from_content(
|
representation = AssetRepresentation.from_content(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from kontextual_engine.core import (
|
|||||||
MetadataFieldDefinition,
|
MetadataFieldDefinition,
|
||||||
MetadataRecord,
|
MetadataRecord,
|
||||||
MetadataSchema,
|
MetadataSchema,
|
||||||
|
MetadataSchemaAssignment,
|
||||||
MetadataValueType,
|
MetadataValueType,
|
||||||
OperationContext,
|
OperationContext,
|
||||||
PolicyDecision,
|
PolicyDecision,
|
||||||
@@ -210,3 +211,25 @@ def test_metadata_schema_reports_structured_validation_issues() -> None:
|
|||||||
assert schema.applies_to(Classification(asset_type="document")) is True
|
assert schema.applies_to(Classification(asset_type="document")) is True
|
||||||
assert schema.applies_to(Classification(asset_type="dataset")) is False
|
assert schema.applies_to(Classification(asset_type="dataset")) is False
|
||||||
assert MetadataSchema.from_dict(schema.to_dict()).fields[0].value_type == MetadataValueType.STRING
|
assert MetadataSchema.from_dict(schema.to_dict()).fields[0].value_type == MetadataValueType.STRING
|
||||||
|
|
||||||
|
|
||||||
|
def test_metadata_schema_assignment_matches_classification_and_roundtrips() -> None:
|
||||||
|
assignment = MetadataSchemaAssignment(
|
||||||
|
assignment_id="assignment-documents",
|
||||||
|
schema_id="schema-document-v1",
|
||||||
|
asset_types=("document",),
|
||||||
|
sensitivities=(Sensitivity.INTERNAL,),
|
||||||
|
lifecycle_states=(LifecycleState.ACTIVE,),
|
||||||
|
policy_ref="local://policy/document-metadata",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert assignment.applies_to(
|
||||||
|
Classification(asset_type="document", sensitivity=Sensitivity.INTERNAL)
|
||||||
|
) is True
|
||||||
|
assert assignment.applies_to(
|
||||||
|
Classification(asset_type="document", sensitivity=Sensitivity.CONFIDENTIAL)
|
||||||
|
) is False
|
||||||
|
assert (
|
||||||
|
MetadataSchemaAssignment.from_dict(assignment.to_dict()).policy_ref
|
||||||
|
== "local://policy/document-metadata"
|
||||||
|
)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ INTERNAL = EXAMPLE_ROOT / "corpus" / "internal-risk-note.md"
|
|||||||
BUNDLE = EXAMPLE_ROOT / "composition" / "context-bundle.md"
|
BUNDLE = EXAMPLE_ROOT / "composition" / "context-bundle.md"
|
||||||
MANIFEST = EXAMPLE_ROOT / "manifests" / "agent-context.yaml"
|
MANIFEST = EXAMPLE_ROOT / "manifests" / "agent-context.yaml"
|
||||||
CONTRACT = EXAMPLE_ROOT / "contracts" / "decision-record.contract.md"
|
CONTRACT = EXAMPLE_ROOT / "contracts" / "decision-record.contract.md"
|
||||||
|
SCHEMA = EXAMPLE_ROOT / "schemas" / "adr-proxy.schema.md"
|
||||||
|
|
||||||
|
|
||||||
def test_markitect_parser_returns_structured_markdown_document() -> None:
|
def test_markitect_parser_returns_structured_markdown_document() -> None:
|
||||||
@@ -170,3 +171,16 @@ def test_markitect_document_contracts_accept_valid_and_report_invalid_documents(
|
|||||||
assert invalid.valid is False
|
assert invalid.valid is False
|
||||||
assert "contract.section.missing" in invalid_codes
|
assert "contract.section.missing" in invalid_codes
|
||||||
assert "contract.section.forbidden" in invalid_codes
|
assert "contract.section.forbidden" in invalid_codes
|
||||||
|
|
||||||
|
|
||||||
|
def test_markitect_schema_validation_accepts_markdown_proxy_documents() -> None:
|
||||||
|
loaded_schema = mkt.load_schema_file(SCHEMA)
|
||||||
|
schema_check = mkt.validate_schema(loaded_schema.schema)
|
||||||
|
valid = mkt.validate_markdown_file(ADR, SCHEMA)
|
||||||
|
invalid = mkt.validate_markdown_file(INVALID_ADR, SCHEMA)
|
||||||
|
|
||||||
|
assert loaded_schema.metadata["schema-id"] == "kontextual-engine.markdown-proxy.adr.v1"
|
||||||
|
assert schema_check.valid is True
|
||||||
|
assert valid.valid is True
|
||||||
|
assert invalid.valid is False
|
||||||
|
assert any("Decision" in violation.message for violation in invalid.violations)
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ adapter metadata on representations or versions. It must not make Markitect
|
|||||||
document classes canonical engine entities, and asset identity must remain
|
document classes canonical engine entities, and asset identity must remain
|
||||||
independent of Markitect snapshot identity.
|
independent of Markitect snapshot identity.
|
||||||
|
|
||||||
|
Markdown proxy documents are valid source, normalized, or derived
|
||||||
|
representations for assets when Markitect selectors, contracts, document
|
||||||
|
schemas, or workflows are useful. They remain adapter representations under
|
||||||
|
engine governance; the registry still owns identity, metadata, lifecycle,
|
||||||
|
policy, lineage, and audit.
|
||||||
|
|
||||||
## Implementation Note
|
## Implementation Note
|
||||||
|
|
||||||
The first registry slice is recorded in
|
The first registry slice is recorded in
|
||||||
@@ -59,11 +65,11 @@ As of 2026-05-06, the registry core has a working asset service, in-memory and
|
|||||||
SQLite repositories, policy gateway boundary, audit events, versions,
|
SQLite repositories, policy gateway boundary, audit events, versions,
|
||||||
representations, metadata records, context entities, asset/context
|
representations, metadata records, context entities, asset/context
|
||||||
relationships, idempotent asset creation, and custom metadata schema
|
relationships, idempotent asset creation, and custom metadata schema
|
||||||
validation before registry writes. Remaining work in this workplan is
|
validation before registry writes. It now also includes a durable metadata
|
||||||
concentrated on schema registry/policy assignment, standard metadata filtering
|
schema registry and assignment rules for policy-selected validation. Remaining
|
||||||
beyond lifecycle and asset type, restore/supersession operations, conflict
|
work in this workplan is concentrated on standard metadata filtering beyond
|
||||||
semantics beyond sequence/idempotency checks, and batch partial-failure
|
lifecycle and asset type, restore/supersession operations, conflict semantics
|
||||||
envelopes.
|
beyond sequence/idempotency checks, and batch partial-failure envelopes.
|
||||||
|
|
||||||
## G5.1 - Implement stable asset identity and source references
|
## G5.1 - Implement stable asset identity and source references
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user