From 4a2525b221460425ede3b4652fe18b504dc367ec Mon Sep 17 00:00:00 2001 From: callumalpass Date: Mon, 3 Aug 2026 09:32:39 +1000 Subject: [PATCH] Define transactional type-pack evolution --- 02-collection-layout.md | 3 + 05-data-contracts.md | 125 +++++++++++++++--- .../tasknotes-migration/v0.3/mdbase-pack.yaml | 2 + schemas/v0.3/README.md | 1 + schemas/v0.3/type-pack-lock.schema.json | 64 +++++++++ schemas/v0.3/type-pack.schema.json | 5 +- scripts/build_runtime_pack.mjs | 1 + scripts/check_v03_tests.py | 99 +++++++++++--- .../mdbase-runtime/0.2.0/mdbase-pack.yaml | 43 ++++++ tests/v0.3/README.md | 3 +- tests/v0.3/manifest.yaml | 6 +- tests/v0.3/type-packs/type-packs.yaml | 73 +++++++++- 12 files changed, 385 insertions(+), 40 deletions(-) create mode 100644 schemas/v0.3/type-pack-lock.schema.json diff --git a/02-collection-layout.md b/02-collection-layout.md index 38d6ae5..25ef42e 100644 --- a/02-collection-layout.md +++ b/02-collection-layout.md @@ -17,6 +17,7 @@ directory that has its own `mdbase.yaml`. ```text collection/ mdbase.yaml + mdbase.lock.yaml _types/ meta.md task.md @@ -49,6 +50,7 @@ record scan unless explicitly excluded. The following paths are reserved by default: - `mdbase.yaml` +- `mdbase.lock.yaml`, when managed type packs are installed - the configured types folder, default `_types/` - the configured contracts folder, default `_contracts/` - `.mdbase/` for derived implementation state @@ -75,6 +77,7 @@ Tools MUST: - skip the configured types folder - skip the configured contracts folder - skip `.mdbase/` +- skip `mdbase.lock.yaml` - stop scanning at nested collection roots - ignore non-record extensions unless configured otherwise diff --git a/05-data-contracts.md b/05-data-contracts.md index 1f0ed0a..8bb9513 100644 --- a/05-data-contracts.md +++ b/05-data-contracts.md @@ -27,8 +27,8 @@ The complete collection contract model has three intentionally small parts: interface using JSON Schema 2020-12. 2. For a `record` contract, a type's `implements` entry maps that interface to the type and supplies contract-specific binding data. -3. An optional `mdbase.type-pack` manifest groups contracts, types, and their - referenced schemas for transactional installation. +3. An optional managed `mdbase.type-pack` groups contracts, types, and their + referenced schemas for transactional installation and evolution. Event sources and action providers make runtime declarations because they are executable, instance-specific implementations rather than record types. @@ -335,15 +335,23 @@ version: 1.0.0 name: Example task types resources: - kind: contract + mode: managed source: contracts/example.task.md target: _contracts/example.task.md digest: sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef - kind: type + mode: managed source: types/task.md target: _types/task.md digest: sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 ``` +`mode` is required. A `managed` resource remains owned by the pack and may be +updated or retired by a later pack version only while its live bytes still +match the installed digest. A `seed` resource is created only when its target +is absent and becomes user-owned immediately; later pack versions neither +replace nor delete it. + Resource digests are SHA-256 over the exact resource bytes. Source and target paths are relative, forward-slash paths without traversal. A directory, archive, repository, or package that distributes a pack MUST @@ -356,19 +364,100 @@ Type packs are installation units, not record types and not permission grants. A pack may include several contracts, several implementing or auxiliary types, and local JSON Schemas referenced by those artifacts. -## Transactional Pack Installation - -A pack-aware installer MUST: +## Pack Identity And Portable Provenance + +The pack digest is SHA-256 over RFC 8785 JSON Canonicalization Scheme bytes for +the complete validated manifest, prefixed with `sha256:`. Because every +resource digest is part of the manifest, the pack digest identifies the exact +manifest and exact resource bytes without incorporating their distribution +location. + +Managed pack state is stored in `mdbase.lock.yaml` at the collection root and +validates against `schemas/v0.3/type-pack-lock.schema.json`. The lock is +portable collection source, not derived cache state. It records each installed +pack's exact ID, version, pack digest, stable installer identity, and the kind, +mode, canonical source, resolved target, and installed digest of every resource. Tools write it +deterministically and users MAY inspect or version it. Tools MUST NOT infer +ownership from filenames, application names, or `x-*` metadata. + +Full collection snapshots, authority transfers, and unscoped synchronization +MUST carry `mdbase.lock.yaml` as a `lock` resource when it exists. A scoped +application projection MAY omit it to avoid disclosing unrelated pack metadata. +The lock is connector-generated and protocol-bounded; a hosted provider MAY +count its bytes toward aggregate storage, but MUST NOT reject it against a +user-authored per-document size quota that the collection owner cannot +remediate. + +An absent lock means no resource is pack-managed, including resources created +by older one-shot installers. Adopting existing files into managed ownership is +a separate explicit operation and MUST verify their exact digests. + +## Assessment And Transactional Apply + +Pack evolution has two public phases: `assess_type_pack` is read-only and +`apply_type_pack` consumes a reviewed assessment. A caller supplies the desired +manifest, exact resources, and a stable reverse-domain `installed_by` +application or tool identity. Optional `target_overrides` map canonical manifest +targets to collection-specific paths without changing the publisher-owned pack +identity. Overrides are validated as safe collection paths, are included in the +assessment digest, and the resolved targets are written to the receipt. + +Assessment reports one of `current`, `install`, `upgrade`, `downgrade`, +`reconfigure`, or `conflict`, the current and desired pack identities, and every resource in a +stable exact diff. Resource actions are `create`, `update`, `delete`, +`adopt`, `unchanged`, `preserve`, or `conflict`. `adopt` records an existing +byte-identical managed resource without rewriting it. `preserve` is used for seed resources +and for seed resources retired from a newer pack. A conflicting assessment is +not applicable. The assessment also reports the planned `mdbase.lock.yaml` +action (`create`, `update`, or `unchanged`) and its resulting digest, so storage +adapters can persist the complete authority transaction without inferring +hidden engine writes. + +`reconfigure` means the exact same immutable pack is being resolved to different +collection targets. Managed resources are relocated only when their installed +bytes are unchanged; modified resources conflict. Seed resources are never +moved or deleted automatically. A caller may name seed targets in +`preserve_seed_targets` to record an intentional omission, such as when a user +maps the contract to an existing type instead of accepting the starter. Unknown +or non-seed preservation targets MUST be rejected. + +When guided setup maps a provided contract to an existing user-owned type, the +reviewed `contract_setups` choices are part of the same assessment and apply. +The assessment reports their exact type-resource diff and binds the selected +type revisions, field mappings, and bindings into `assessment_digest`. Apply +MUST stage those edits together with the managed resources and lock, validate +the combined collection, and commit them in the same transaction. A stale type +revision therefore leaves both the user-owned type and every pack resource +unchanged. A connector MUST NOT install the pack first and apply the reviewed +mapping as a later independent mutation. + +The assessment includes a deterministic `assessment_digest` over the desired +pack identity, current lock entry, and every relevant live target digest. Apply +MUST recompute it inside the collection mutation boundary and fail with +`concurrent_modification` before writing when it differs. Downgrades require an +explicit `allow_downgrade` decision. + +An unmanaged target with different bytes is a conflict unless the assessment +request contains an explicit adoption decision naming that target and its exact +current digest. An accepted adoption may plan `update`, records the adopted +digest in the diff, and is covered by `assessment_digest`. This is the safe +upgrade bridge for collections created before pack receipts existed: tools can +show the files that will become package-managed, require informed approval, and +remain race-safe. Unknown targets, seed targets, and stale adoption digests MUST +be rejected. Adoption is never inferred from application access or install +intent. + +A pack-aware apply implementation MUST: 1. validate the manifest, safe paths, source bytes, and resource digests -2. stage every resource without changing the live collection +2. stage every resource and reviewed existing-type setup without changing the live collection 3. resolve the complete staged contract and type registries 4. validate every contract, implementation, type, reference, and affected existing record -5. compute and present the exact create, replace, and unchanged diff -6. acquire its collection mutation boundary and recheck overwritten hashes -7. commit all resources as one recoverable transaction -8. reopen the collection and verify the committed registry +5. compute and present the exact resource diff and assessment digest +6. acquire its collection mutation boundary and recheck the assessment +7. commit all resource changes and the lock as one recoverable transaction +8. reopen the collection and verify the committed registry and lock An invalid resource aborts before any live write. A conflict or concurrent change aborts with the live collection unchanged. Implementations may use an @@ -380,12 +469,12 @@ Revoking an application's access does not uninstall its type pack. Uninstall is a separate, explicitly requested operation because records may still depend on the installed types. -A pack install or dry-run result reports the pack `id`, exact `version`, and -every resource in manifest order as `{ target, action, digest }`, where -`action` is `create`, `replace`, or `unchanged`. It also reports -`cleanup_deferred` when committed state is valid but transaction-journal cleanup -must be retried. Reinstalling identical bytes is valid and reports every -resource as `unchanged`; it MUST NOT create a new logical collection revision. +Applying a current pack is valid and reports every managed resource as +`unchanged`; it MUST NOT create a new logical collection revision. Applying an +upgrade deletes a retired managed target only when its live digest still +matches the lock. User-modified managed resources produce `conflict` and no +live write. Successful apply reports the committed receipt, exact resource +diff, and `cleanup_deferred` when transaction-journal cleanup must be retried. ## Diagnostics @@ -400,8 +489,8 @@ Data-contract-aware tools use these codes: | `data_contract_binding_invalid` | implementation binding fails its binding schema | | `data_contract_field_invalid` | a mapped contract or record field is missing or incompatible | | `data_contract_record_invalid` | a projected contract view fails the contract schema | -| `invalid_type_pack` | a pack manifest, resource, path, or digest is invalid | -| `type_pack_conflict` | a target differs from live state and replacement was not approved | +| `invalid_type_pack` | a pack manifest, lock, resource, path, or digest is invalid | +| `type_pack_conflict` | ownership, live bytes, or an untracked target prevents a safe apply | | `type_pack_apply_failed` | transactional commit or recovery did not complete normally | Diagnostics use the canonical shape from Chapter 16 and identify the contract diff --git a/examples/v0.3/tasknotes-migration/v0.3/mdbase-pack.yaml b/examples/v0.3/tasknotes-migration/v0.3/mdbase-pack.yaml index 45bb1b2..3446bb4 100644 --- a/examples/v0.3/tasknotes-migration/v0.3/mdbase-pack.yaml +++ b/examples/v0.3/tasknotes-migration/v0.3/mdbase-pack.yaml @@ -5,10 +5,12 @@ name: TaskNotes task support description: The TaskNotes data contract and its canonical task type. resources: - kind: contract + mode: managed source: _contracts/tasknotes.task.md target: _contracts/tasknotes.task.md digest: sha256:22a1e45daf5d78e5f6ee8c4c678b37a63d76c8190cfbf8e3cecc84481e5076f3 - kind: type + mode: managed source: _types/task.md target: _types/task.md digest: sha256:da2e044d3448c53dd2197e0ad1926fd6c797eb5810bfc896e3eda81203b94b74 diff --git a/schemas/v0.3/README.md b/schemas/v0.3/README.md index 337ac40..e2e9f49 100644 --- a/schemas/v0.3/README.md +++ b/schemas/v0.3/README.md @@ -12,6 +12,7 @@ yet published package artifacts. | `type-file.schema.json` | frontmatter of `_types/*.md` v0.3 type files | | `data-contract.schema.json` | first-class record, event, and action contract frontmatter in `_contracts/*.md` | | `type-pack.schema.json` | transactional type-pack manifests | +| `type-pack-lock.schema.json` | portable managed type-pack provenance and ownership | | `query.schema.json` | portable query input objects | | `query-result.schema.json` | query results plus optional context, view, grouping, and summary metadata | | `record-document.schema.json` | complete authoritative record documents returned by read and successful mutations | diff --git a/schemas/v0.3/type-pack-lock.schema.json b/schemas/v0.3/type-pack-lock.schema.json new file mode 100644 index 0000000..0d5be20 --- /dev/null +++ b/schemas/v0.3/type-pack-lock.schema.json @@ -0,0 +1,64 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mdbase.dev/schemas/v0.3/type-pack-lock.schema.json", + "title": "mdbase v0.3 managed type-pack lock", + "type": "object", + "required": ["kind", "lock_version", "packs"], + "properties": { + "kind": { "const": "mdbase.type-pack-lock" }, + "lock_version": { "const": 1 }, + "packs": { + "type": "array", + "items": { "$ref": "#/$defs/receipt" } + } + }, + "additionalProperties": false, + "$defs": { + "identifier": { + "type": "string", + "minLength": 3, + "maxLength": 150, + "pattern": "^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)+$" + }, + "semanticVersion": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$" + }, + "safeRelativePath": { + "type": "string", + "minLength": 1, + "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$" + }, + "digest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + }, + "receipt": { + "type": "object", + "required": ["id", "version", "digest", "installed_by", "resources"], + "properties": { + "id": { "$ref": "#/$defs/identifier" }, + "version": { "$ref": "#/$defs/semanticVersion" }, + "digest": { "$ref": "#/$defs/digest" }, + "installed_by": { "$ref": "#/$defs/identifier" }, + "resources": { + "type": "array", + "items": { "$ref": "#/$defs/resource" } + } + }, + "additionalProperties": false + }, + "resource": { + "type": "object", + "required": ["kind", "mode", "source", "target", "digest"], + "properties": { + "kind": { "enum": ["contract", "type", "schema"] }, + "mode": { "enum": ["managed", "seed"] }, + "source": { "$ref": "#/$defs/safeRelativePath" }, + "target": { "$ref": "#/$defs/safeRelativePath" }, + "digest": { "$ref": "#/$defs/digest" } + }, + "additionalProperties": false + } + } +} diff --git a/schemas/v0.3/type-pack.schema.json b/schemas/v0.3/type-pack.schema.json index 9f370d4..d4b1f4c 100644 --- a/schemas/v0.3/type-pack.schema.json +++ b/schemas/v0.3/type-pack.schema.json @@ -55,11 +55,14 @@ }, "resource": { "type": "object", - "required": ["kind", "source", "target", "digest"], + "required": ["kind", "mode", "source", "target", "digest"], "properties": { "kind": { "enum": ["contract", "type", "schema"] }, + "mode": { + "enum": ["managed", "seed"] + }, "source": { "$ref": "#/$defs/safeRelativePath" }, diff --git a/scripts/build_runtime_pack.mjs b/scripts/build_runtime_pack.mjs index 2cad6e2..ec2c39e 100644 --- a/scripts/build_runtime_pack.mjs +++ b/scripts/build_runtime_pack.mjs @@ -769,6 +769,7 @@ function packManifest(items) { for (const item of items) { lines.push( ` - kind: ${item.kind}`, + " mode: managed", ` source: ${item.source}`, ` target: ${item.target}`, ` digest: ${item.digest}`, diff --git a/scripts/check_v03_tests.py b/scripts/check_v03_tests.py index 657c1b6..7762f4c 100755 --- a/scripts/check_v03_tests.py +++ b/scripts/check_v03_tests.py @@ -52,7 +52,8 @@ "inspect_yaml", "migrate_type", "type_pack_resources_validate", - "install_type_pack", + "assess_type_pack", + "apply_type_pack", "data_contract_implementation_validate", "data_contract_digest", "data_contract_implementation_digest", @@ -395,8 +396,12 @@ def run_executable_test(test: dict[str, Any], setup: dict[str, Any] | None = Non run_type_pack_resources_test(input_data, expect) return - if operation == "install_type_pack": - run_install_type_pack_test(input_data, expect, setup) + if operation == "assess_type_pack": + run_assess_type_pack_test(input_data, expect, setup) + return + + if operation == "apply_type_pack": + run_apply_type_pack_test(input_data, expect, setup) return if operation == "data_contract_implementation_validate": @@ -518,7 +523,7 @@ def run_type_pack_resources_test(input_data: dict[str, Any], expect: dict[str, A assert_expected_validation_result(failures, expect) -def run_install_type_pack_test( +def run_apply_type_pack_test( input_data: dict[str, Any], expect: dict[str, Any], setup: dict[str, Any] ) -> None: """Simulate the normative preflight/diff/atomicity rules without an engine.""" @@ -530,10 +535,12 @@ def run_install_type_pack_test( for target, content in (setup.get("files") or {}).items() } corrupt_digest = input_data.get("corrupt_digest") is True + adopt_conflicts = input_data.get("adopt_conflicts") is True + installed: dict[str, str] = {} runs: list[dict[str, Any]] = [] for _ in range(int(input_data.get("repeat", 1))): - planned: list[tuple[str, bytes, str]] = [] + planned: list[tuple[str, bytes, str, str]] = [] error: dict[str, str] | None = None seen_sources: set[str] = set() seen_targets: set[str] = set() @@ -566,30 +573,45 @@ def run_install_type_pack_test( if actual_digest != declared_digest: error = {"code": "invalid_type_pack", "message": "digest mismatch"} break - action = ( - "create" - if target not in live - else "unchanged" - if live[target] == document - else "replace" + current_digest = ( + "sha256:" + hashlib.sha256(live[target]).hexdigest() + if target in live + else None ) - planned.append((target, document, action)) + if target not in live: + action = "create" + elif live[target] == document: + action = "unchanged" if target in installed else "adopt" + elif target not in installed and adopt_conflicts: + action = "update" + else: + action = "conflict" + planned.append((target, document, action, current_digest or "")) if error is None and len(planned) != len(resources): error = {"code": "invalid_type_pack", "message": "incomplete pack"} - if error is None and any(action == "replace" for _, _, action in planned): + if error is None and any(action == "conflict" for _, _, action, _ in planned): error = {"code": "type_pack_conflict", "message": "target conflict"} + if error is None and input_data.get("mutate_after_assess"): + error = { + "code": "concurrent_modification", + "message": "collection changed after assessment", + } + if error is not None: runs.append({"valid": False, "actions": [], "error": error}) break - for target, document, _ in planned: + status = "current" if installed else "install" + for target, document, _, _ in planned: live[target] = document + installed[target] = "sha256:" + hashlib.sha256(document).hexdigest() runs.append( { "valid": True, - "actions": [action for _, _, action in planned], + "status": status, + "actions": [action for _, _, action, _ in planned], } ) @@ -613,6 +635,7 @@ def run_install_type_pack_test( "valid": last["valid"], "runs": runs, "implementations": implementation_count, + "lock_exists": bool(installed), "targets_exist": [ resource.get("target") in live for resource in resources ], @@ -622,6 +645,52 @@ def run_install_type_pack_test( assert_subset(actual, expect) +def run_assess_type_pack_test( + input_data: dict[str, Any], expect: dict[str, Any], setup: dict[str, Any] +) -> None: + """Exercise the structured managed-resource conflict shape used before apply.""" + manifest_path = resolve(input_data["pack"]) + manifest = load_yaml(manifest_path) + resources = manifest.get("resources", []) or [] + live: dict[str, bytes] = {} + installed: dict[str, str] = {} + + for resource in resources: + source = str(resource["source"]) + target = str(resource["target"]) + document = (manifest_path.parent / source).read_bytes() + live[target] = document + installed[target] = "sha256:" + hashlib.sha256(document).hexdigest() + + modified_target = input_data.get("install_then_modify") + if isinstance(modified_target, str): + live[modified_target] = b"User-modified managed bytes." + + actions: list[str] = [] + for resource in resources: + target = str(resource["target"]) + document = (manifest_path.parent / str(resource["source"])).read_bytes() + if target not in live: + actions.append("create") + elif live[target] == document: + actions.append("unchanged") + elif target in installed: + actions.append("conflict") + else: + actions.append("conflict") + + conflicted = "conflict" in actions + assert_subset( + { + "valid": True, + "status": "conflict" if conflicted else "current", + "applicable": not conflicted, + "actions": actions, + }, + expect, + ) + + def run_data_contract_implementation_test( input_data: dict[str, Any], expect: dict[str, Any] ) -> None: diff --git a/standard-packs/mdbase-runtime/0.2.0/mdbase-pack.yaml b/standard-packs/mdbase-runtime/0.2.0/mdbase-pack.yaml index a3a4a50..145e0d0 100644 --- a/standard-packs/mdbase-runtime/0.2.0/mdbase-pack.yaml +++ b/standard-packs/mdbase-runtime/0.2.0/mdbase-pack.yaml @@ -5,174 +5,217 @@ name: mdbase durable runtime standard library description: Runtime record implementations and inspectable built-in event/action contracts. resources: - kind: schema + mode: managed source: schemas/mdbase.runtime.workflow/1.0.0.schema.json target: schemas/mdbase.runtime.workflow/1.0.0.schema.json digest: sha256:e8f413663a2911be5a51b1f7aefae80e9e42ea5c729f04d6ccfd0bc4eb1965ea - kind: contract + mode: managed source: _contracts/mdbase.runtime.workflow/1.0.0.md target: _contracts/mdbase.runtime.workflow/1.0.0.md digest: sha256:7cf8ed016bf6232d9c1abc8d8bb9bb25fd2af1ffa6c6094317aa65baf3c31049 - kind: type + mode: managed source: _types/workflow.md target: _types/workflow.md digest: sha256:f0a848daa96fe0f92459e5157bb6cdee41c0679d49a4f581d585846a339c11c9 - kind: schema + mode: managed source: schemas/mdbase.runtime.policy/1.0.0.schema.json target: schemas/mdbase.runtime.policy/1.0.0.schema.json digest: sha256:4d1d2cb59df9991dcc47e269b21dab9e71c1628b99f71e5d9ff5948ba6f7bc45 - kind: contract + mode: managed source: _contracts/mdbase.runtime.policy/1.0.0.md target: _contracts/mdbase.runtime.policy/1.0.0.md digest: sha256:319a9bd4ca62a246871ee52e0ef0250a82c240cb14a95f88e4a505ad04450300 - kind: type + mode: managed source: _types/policy.md target: _types/policy.md digest: sha256:0f14852150c7ad48c6f5457167333ff4d50ce272822fa4feca34aae09c539aa4 - kind: schema + mode: managed source: schemas/mdbase.runtime.provider-registration/1.0.0.schema.json target: schemas/mdbase.runtime.provider-registration/1.0.0.schema.json digest: sha256:4cfd9649863835163baee7542b601991c18b21b75c63b48324383da674a1f797 - kind: contract + mode: managed source: _contracts/mdbase.runtime.provider-registration/1.0.0.md target: _contracts/mdbase.runtime.provider-registration/1.0.0.md digest: sha256:97d157efaeb311745fc84e2cbe01e8147aaf44109384090c154527b68838d766 - kind: type + mode: managed source: _types/provider-registration.md target: _types/provider-registration.md digest: sha256:3f8f1334b345ebc63236f499b7c1fadf09884bd3ef60c5e0df2216020e50f388 - kind: schema + mode: managed source: schemas/mdbase.runtime.capability-grant/1.0.0.schema.json target: schemas/mdbase.runtime.capability-grant/1.0.0.schema.json digest: sha256:14fcd3ad1fbe3d3c360024e869d8a52ec047aa8b2dd37755c63e2412535f50ca - kind: contract + mode: managed source: _contracts/mdbase.runtime.capability-grant/1.0.0.md target: _contracts/mdbase.runtime.capability-grant/1.0.0.md digest: sha256:1cd3a2ff5fdc9294f766e538c371ed278ad4c54452480ba6fcc2870be08b47a1 - kind: type + mode: managed source: _types/capability-grant.md target: _types/capability-grant.md digest: sha256:bb2060851c08e28a0ee096da870398b519c90621e3bb7206082f59df85169340 - kind: schema + mode: managed source: schemas/mdbase.runtime.run/1.0.0.schema.json target: schemas/mdbase.runtime.run/1.0.0.schema.json digest: sha256:0add08b601ebd15b80dc14812fc7fbf9c1b5bd8415daed7b7d566e33741d91c3 - kind: contract + mode: managed source: _contracts/mdbase.runtime.run/1.0.0.md target: _contracts/mdbase.runtime.run/1.0.0.md digest: sha256:7e46270c951e6634fe25519dff10523652365f8adf30b034f01e29e1ca48cadb - kind: type + mode: managed source: _types/run.md target: _types/run.md digest: sha256:03a1236cfb4fd097904cf6ad3ff04c821ff3c485b6aea8bfec0568a3b1baec2e - kind: schema + mode: managed source: schemas/mdbase.runtime.action-attempt/1.0.0.schema.json target: schemas/mdbase.runtime.action-attempt/1.0.0.schema.json digest: sha256:71c8cb274daa63ed8b428f474ec07d0053e65e06f42354cb7f36072cb136506c - kind: contract + mode: managed source: _contracts/mdbase.runtime.action-attempt/1.0.0.md target: _contracts/mdbase.runtime.action-attempt/1.0.0.md digest: sha256:04c1bf55580d0d280d2be645c56177da25c2d0129f8fa71def7d5f96cf0ea629 - kind: type + mode: managed source: _types/action-attempt.md target: _types/action-attempt.md digest: sha256:96ed9f48adcc7e81118895351823a4db7eeba5d7e1fd35454394f072f0488b0e - kind: schema + mode: managed source: schemas/mdbase.runtime.checkpoint/1.0.0.schema.json target: schemas/mdbase.runtime.checkpoint/1.0.0.schema.json digest: sha256:0c4a550356b974814690bb2e5db1902c6c3cff13be1ea69a139e2e43781ecf2c - kind: contract + mode: managed source: _contracts/mdbase.runtime.checkpoint/1.0.0.md target: _contracts/mdbase.runtime.checkpoint/1.0.0.md digest: sha256:6413676d7482a71b31cbe4abf8c5a543a54a530ef62f669f8ef8e21bcc6bc237 - kind: type + mode: managed source: _types/checkpoint.md target: _types/checkpoint.md digest: sha256:da22fafe5975c53b1d3adab345194e74e8f03ea816386de04e91cbbc4ddbd0e3 - kind: schema + mode: managed source: schemas/mdbase.runtime.timer/1.0.0.schema.json target: schemas/mdbase.runtime.timer/1.0.0.schema.json digest: sha256:fd6fa1b8fa7a3e782160d1b0e1399f15392230d1560393e840e7869c7d407aac - kind: contract + mode: managed source: _contracts/mdbase.runtime.timer/1.0.0.md target: _contracts/mdbase.runtime.timer/1.0.0.md digest: sha256:0500a421e81eef09c37bc4ef559e3805843e87a6e1b2cf2a393da4d76d4384fc - kind: type + mode: managed source: _types/timer.md target: _types/timer.md digest: sha256:4cfe2766837203bd277812e855aaa250efa613c465b2b9e2aec470b66d258851 - kind: schema + mode: managed source: schemas/mdbase.runtime.diagnostic/1.0.0.schema.json target: schemas/mdbase.runtime.diagnostic/1.0.0.schema.json digest: sha256:b4d78f97b4feadc6c1e79b214fac20f0c305609e20f152f878d23d5d4e370935 - kind: contract + mode: managed source: _contracts/mdbase.runtime.diagnostic/1.0.0.md target: _contracts/mdbase.runtime.diagnostic/1.0.0.md digest: sha256:2e7379de9ceefa6435e9c66a71f4292e4805e8038ed6496c83dad946900267f6 - kind: type + mode: managed source: _types/diagnostic.md target: _types/diagnostic.md digest: sha256:5bca79bacfcfeae8bc573216bd09d00e6ec8807ffae7422571834350bf6e241a - kind: schema + mode: managed source: schemas/mdbase.runtime.dead-letter/1.0.0.schema.json target: schemas/mdbase.runtime.dead-letter/1.0.0.schema.json digest: sha256:1f866b1dfca061571a8dc3a87748fd9deb6268a6674fe5eaf764e8701d32fee9 - kind: contract + mode: managed source: _contracts/mdbase.runtime.dead-letter/1.0.0.md target: _contracts/mdbase.runtime.dead-letter/1.0.0.md digest: sha256:737d08bad6662e087c3393f6f17d24374f258d60b41112a3f864cb5138af8edd - kind: type + mode: managed source: _types/dead-letter.md target: _types/dead-letter.md digest: sha256:b0d60190734983889d63400b2c9aa1c0b5fa668e37920ab5969575cea43aa1f3 - kind: schema + mode: managed source: schemas/mdbase.record.created/1.0.0.schema.json target: schemas/mdbase.record.created/1.0.0.schema.json digest: sha256:8d28a2a32083ab9a55f53ad1a619da47022fa3ef291191c6c2e5a9848ce59fcc - kind: contract + mode: managed source: _contracts/mdbase.record.created/1.0.0.md target: _contracts/mdbase.record.created/1.0.0.md digest: sha256:4c157f94e63d1f4fb1a74da640e03264ccb6c78cfa9f88ddb71b09c8bd6d1d44 - kind: schema + mode: managed source: schemas/mdbase.record.modified/1.0.0.schema.json target: schemas/mdbase.record.modified/1.0.0.schema.json digest: sha256:1ecda771bdeb4b6cea32b81ee344257005b49c4dcf71cdf95a1577a7459b74b0 - kind: contract + mode: managed source: _contracts/mdbase.record.modified/1.0.0.md target: _contracts/mdbase.record.modified/1.0.0.md digest: sha256:2f670ef18bf7330fbba73861f41889ee946b33644074ca7366e88913f6e4d88a - kind: schema + mode: managed source: schemas/mdbase.record.deleted/1.0.0.schema.json target: schemas/mdbase.record.deleted/1.0.0.schema.json digest: sha256:b5a1fa20b6526ea8d857e03a2eebfc8b2ed126b1c4e7ca2d9735c59ddbeb7206 - kind: contract + mode: managed source: _contracts/mdbase.record.deleted/1.0.0.md target: _contracts/mdbase.record.deleted/1.0.0.md digest: sha256:cb4f4a09f9f9b1f7f5e2207de0074ce02fd89008c08386626fd964a29299f956 - kind: schema + mode: managed source: schemas/mdbase.record.renamed/1.0.0.schema.json target: schemas/mdbase.record.renamed/1.0.0.schema.json digest: sha256:11546c9497c9700ed48b2f634be8001a009b7cea92f19584ed8b8aafc783e990 - kind: contract + mode: managed source: _contracts/mdbase.record.renamed/1.0.0.md target: _contracts/mdbase.record.renamed/1.0.0.md digest: sha256:b6c507198379ba48408c9b48161a1c22d46dad2d291691af449167618484bb4c - kind: schema + mode: managed source: schemas/mdbase.runtime.timer.fired/1.0.0.schema.json target: schemas/mdbase.runtime.timer.fired/1.0.0.schema.json digest: sha256:986cdde8c88a6ff363062d056101141c436271608e565d2a02f250cd74853f74 - kind: contract + mode: managed source: _contracts/mdbase.runtime.timer.fired/1.0.0.md target: _contracts/mdbase.runtime.timer.fired/1.0.0.md digest: sha256:9381a2c6778a3268758ac235e3c3b66991a769fa8e19f7303d4404b1e7e62bc6 - kind: schema + mode: managed source: schemas/mdbase.runtime.run.cancel/1.0.0.output.schema.json target: schemas/mdbase.runtime.run.cancel/1.0.0.output.schema.json digest: sha256:88cbccbfe0e4d087b67159a2cc36cc6ec92b1ea7e849a5c6a82e074f06b270bb - kind: schema + mode: managed source: schemas/mdbase.runtime.run.cancel/1.0.0.schema.json target: schemas/mdbase.runtime.run.cancel/1.0.0.schema.json digest: sha256:21dce4d30fd9848e43610c26990fd208476cdb27c5b489439f632384b4a76675 - kind: contract + mode: managed source: _contracts/mdbase.runtime.run.cancel/1.0.0.md target: _contracts/mdbase.runtime.run.cancel/1.0.0.md digest: sha256:949f039334cfc3f096beb870419baa08c3b40679b184e9849f9330a34a6f524d diff --git a/tests/v0.3/README.md b/tests/v0.3/README.md index 07932d3..ee03a02 100644 --- a/tests/v0.3/README.md +++ b/tests/v0.3/README.md @@ -101,7 +101,8 @@ Future v0.3 adapters should support these operations: - `runtime_validate_action_input` - `runtime_validate_action_output` - `migrate_type` -- `install_type_pack` +- `assess_type_pack` +- `apply_type_pack` The repository also includes `scripts/check_v03_tests.py`, which validates the suite structure and executes local artifact checks that do not require a full diff --git a/tests/v0.3/manifest.yaml b/tests/v0.3/manifest.yaml index aa46ab0..b44a843 100644 --- a/tests/v0.3/manifest.yaml +++ b/tests/v0.3/manifest.yaml @@ -84,8 +84,12 @@ claim_profiles: requires: [collection_semantics] requirements: - transactional_type_pack_preflight + - managed_type_pack_provenance + - managed_type_pack_assessment + - managed_type_pack_concurrency + - managed_type_pack_adoption - exact_type_pack_diff - - idempotent_type_pack_install + - idempotent_type_pack_apply - type_pack_atomicity - id: lifecycle status: draft diff --git a/tests/v0.3/type-packs/type-packs.yaml b/tests/v0.3/type-packs/type-packs.yaml index b620298..935542f 100644 --- a/tests/v0.3/type-packs/type-packs.yaml +++ b/tests/v0.3/type-packs/type-packs.yaml @@ -14,7 +14,7 @@ groups: tests: - id: type-pack-install-idempotent name: "a complete pack installs as one exact diff and identical reinstall is unchanged" - operation: install_type_pack + operation: apply_type_pack input: pack: "examples/v0.3/tasknotes-migration/v0.3/mdbase-pack.yaml" repeat: 2 @@ -22,14 +22,19 @@ groups: valid: true runs: - valid: true + status: install actions: [create, create] - valid: true + status: current actions: [unchanged, unchanged] + lock_exists: true implementations: 1 covers: - core_write.transactional_type_pack_preflight - core_write.exact_type_pack_diff - - core_write.idempotent_type_pack_install + - core_write.idempotent_type_pack_apply + - core_write.managed_type_pack_provenance + - core_write.managed_type_pack_assessment - name: "preflight failure is atomic" setup: @@ -40,7 +45,7 @@ groups: tests: - id: type-pack-digest-failure-atomic name: "a digest mismatch writes no live target" - operation: install_type_pack + operation: apply_type_pack input: pack: "examples/v0.3/tasknotes-migration/v0.3/mdbase-pack.yaml" corrupt_digest: true @@ -64,7 +69,7 @@ groups: tests: - id: type-pack-target-conflict-atomic name: "a differing target rejects the whole pack without creating later resources" - operation: install_type_pack + operation: apply_type_pack input: pack: "examples/v0.3/tasknotes-migration/v0.3/mdbase-pack.yaml" expect: @@ -74,3 +79,63 @@ groups: targets_exist: [true, false] covers: - core_write.type_pack_atomicity + + - name: "managed upgrade conflict and concurrency" + setup: + config: | + spec_version: "0.3.0" + settings: + validation: error + tests: + - id: type-pack-managed-resource-conflict + name: "a user-modified managed target produces a structured non-applicable assessment" + operation: assess_type_pack + input: + pack: "examples/v0.3/tasknotes-migration/v0.3/mdbase-pack.yaml" + install_then_modify: "_contracts/tasknotes.task.md" + expect: + valid: true + status: conflict + applicable: false + actions: [conflict, unchanged] + covers: + - core_write.managed_type_pack_assessment + - id: type-pack-stale-assessment + name: "apply rejects a target changed after assessment without writing" + operation: apply_type_pack + input: + pack: "examples/v0.3/tasknotes-migration/v0.3/mdbase-pack.yaml" + mutate_after_assess: "_contracts/tasknotes.task.md" + expect: + valid: false + error: + code: concurrent_modification + covers: + - core_write.managed_type_pack_concurrency + + - name: "explicit adoption of legacy definitions" + setup: + config: | + spec_version: "0.3.0" + settings: + validation: error + files: + _contracts/tasknotes.task.md: | + Older unmanaged definition. + tests: + - id: type-pack-explicit-adoption + name: "a digest-pinned adoption decision can replace reviewed unmanaged bytes" + operation: apply_type_pack + input: + pack: "examples/v0.3/tasknotes-migration/v0.3/mdbase-pack.yaml" + adopt_conflicts: true + expect: + valid: true + runs: + - valid: true + status: install + actions: [update, create] + lock_exists: true + implementations: 1 + covers: + - core_write.managed_type_pack_adoption