From 56d2b063a26cdb6de031df021ecd53245b1ca3b7 Mon Sep 17 00:00:00 2001 From: piaro <7055807+piaro@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:24:36 +0900 Subject: [PATCH] =?UTF-8?q?=E6=97=A2=E5=AD=98Contract=E6=9D=A1=E9=A0=85?= =?UTF-8?q?=E3=81=AEEvidence=E7=99=BB=E9=8C=B2=E3=82=92=E5=8F=AF=E8=83=BD?= =?UTF-8?q?=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 + README.md | 18 +- docs/MCP-DESIGN.md | 2 + docs/concepts.ja.md | 11 + docs/implementation.md | 2 + schemas/v1/change.schema.json | 11 +- skill-src/adf-analyst/SKILL.md | 5 + skill-src/adf-builder/SKILL.md | 6 + src/kernel.rs | 264 ++++++++++++++++++ src/main.rs | 10 +- src/project_setup.rs | 6 +- src/schema.rs | 2 +- .../fixtures/cli-project/.adf/framework.lock | 4 +- testdata/fixtures/db-sqs/framework-lock.yaml | 4 +- testdata/golden/v1/application-initial.json | 6 +- testdata/golden/v1/framework-lock.json | 2 +- testdata/golden/v1/rule-compilation.json | 2 +- testdata/golden/v1/schema-validation.json | 35 ++- tests/cli.rs | 39 +++ tests/golden.rs | 4 +- 20 files changed, 421 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af92afd..0e0dbfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,12 @@ change, is written down in `COMPATIBILITY.md`. ### Added +- `change init --verify-clause ` can select existing Contract + clauses for first-time Evidence registration. Selected unverified clauses + enter the normal Builder Evidence workflow without declaring the existing + behavior as a new implementation impact or blocking on unrelated unverified + clauses. + - External runners can append execution start and completion events without asking ADF to launch an agent. The events retain failed and interrupted attempts, add cache-creation, cached-input, and reasoning-output token counts diff --git a/README.md b/README.md index da248b2..bd5c5ce 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,22 @@ adf change init change.first-feature \ adf next change.first-feature ``` +To verify behavior that already exists, select only the Contract clauses whose +Evidence you will register: + +```sh +adf change init change.verify-existing-orders \ + --title "Verify existing order behavior" \ + --intent "Register reproducible evidence for existing behavior" \ + --verify-clause contract.order-lifecycle#orders-source-of-truth +``` + +The selected clauses enter the normal Evidence workflow even when they have not +been verified before. Other unverified clauses do not block this Change. Existing +behavior is not reported as a new implementation impact merely to select it for +verification. Clauses whose `evidence_mode` is `review` remain human-review work +and cannot be selected with this option. + From here, `next` says what to do. Agents normally reach it over MCP: ```sh @@ -363,7 +379,7 @@ distribution is a later compatibility milestone. | `project observe` | list what the detectors can see, for review | | `project validate-bindings` | report missing bindings and what cannot be checked | | `project promote-bindings` | make a reviewed draft the observation of record | -| `change init` | start a change | +| `change init` | start a change, optionally selecting existing Contract clauses to verify | | `next` | issue the next action | | `explain` | say why the change is where it is | | `execution-log` | aggregate Context size and any execution metrics already reported | diff --git a/docs/MCP-DESIGN.md b/docs/MCP-DESIGN.md index 5285fc7..8c9a1e4 100644 --- a/docs/MCP-DESIGN.md +++ b/docs/MCP-DESIGN.md @@ -349,6 +349,8 @@ InputにはContract Recordと`expected_digest`を必須とし、条項単位で | `implement-change` | Repository file変更。MCP外のworkspace操作 | | `record-evidence` | Evidence | +Changeの`verification_scope`に指定した既存Contract条項は、未検証でも`record-evidence`の対象になる。この指定はImpact Assessmentの影響申告には変換しない。MCPは通常どおり`adf_next`でActionを発行し、`adf_add_evidence`と`adf_submit`で結果を受け付けるため、専用の書込みToolは追加しない。 + この対応はMCP handlerへ散在させず、Application protocolの固定tableとして一か所に置きます。 未知Actionは書込みなしとしてfail closedに扱います。 diff --git a/docs/concepts.ja.md b/docs/concepts.ja.md index a1d5e37..9c8c2a6 100644 --- a/docs/concepts.ja.md +++ b/docs/concepts.ja.md @@ -41,6 +41,8 @@ Contractは変更のたびに増えます。障害から学んだことはテス コードがないことを、影響がないこととは扱いません。新規プロジェクトでは、最初の変更依頼から実現しようとしている振る舞いを特定し、その変更に必要な最小限のContractを作ってから実装へ進みます。 +既存実装の移行では、検証するContract条項を`--verify-clause`で明示できます。指定した条項は未検証でもEvidence登録の対象になり、指定していない未検証条項は変更を止めません。既存の振る舞いを検証対象にするために、今回新たに生じる実装影響として申告する必要はありません。`evidence_mode: review`の条項は人のレビューを求めるため、この方法では選択できません。 + ```text adf change init │ @@ -162,6 +164,15 @@ adf change init change.first-feature \ adf next change.first-feature ``` +既存実装のEvidenceを登録する場合は、対象条項を指定して変更を作ります。 + +```sh +adf change init change.verify-existing-orders \ + --title "既存の注文処理を検証する" \ + --intent "既存の振る舞いに再現可能なEvidenceを登録する" \ + --verify-clause contract.order-lifecycle#orders-source-of-truth +``` + エージェントには、同じ操作をMCPサーバー経由で使わせます。 ```sh diff --git a/docs/implementation.md b/docs/implementation.md index ddadeae..17ce496 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -131,6 +131,8 @@ Contractへ状態を書き戻さず、未検証を合格扱いしません。 Applicationは、現在選択されたRequirementのsubjectと一致する`stale`/`failed`条項だけに、組込みの`contract-clause-revalidated` Requirementを追加します。これは`before-merge`の`evidence-backed`なBuilder作業です。Contextには対象条項とhealth findingを含め、無関係な条項と`unverified`条項はこの経路ではChangeを停止しません。現在入力に対する成功Evidenceを提出すると再検証は解決します。 +既存実装を初めて検証する場合は、Changeの`verification_scope`へ`contract.id#clause-id`を列挙します。指定した条項には、状態が`unverified`でも同じ`contract-clause-revalidated` Requirementを追加します。指定していない未検証条項は追加しません。存在しない条項と`evidence_mode: review`の条項は受理しません。この検証対象は変更影響とは別に扱うため、Evidence登録だけを目的とするChangeのImpact Assessmentは、実装への影響がなければ`no-impact`にできます。 + ```sh adf contract-health --project . --format text adf contract-health --project . --format json --require-clean diff --git a/schemas/v1/change.schema.json b/schemas/v1/change.schema.json index 140a797..6d41cd9 100644 --- a/schemas/v1/change.schema.json +++ b/schemas/v1/change.schema.json @@ -9,7 +9,16 @@ "title": {"type": "string", "minLength": 1}, "intent": {"type": "string", "minLength": 1}, "impact_assessment": {"const": "required"}, - "base_revision": {"type": "string", "minLength": 1} + "base_revision": {"type": "string", "minLength": 1}, + "verification_scope": { + "type": "array", + "items": { + "type": "string", + "pattern": "^contract\\.[A-Za-z0-9._-]+#[A-Za-z0-9][A-Za-z0-9._-]*$" + }, + "minItems": 1, + "uniqueItems": true + } }, "additionalProperties": false } diff --git a/skill-src/adf-analyst/SKILL.md b/skill-src/adf-analyst/SKILL.md index 3f85197..eaa2ef6 100644 --- a/skill-src/adf-analyst/SKILL.md +++ b/skill-src/adf-analyst/SKILL.md @@ -38,6 +38,11 @@ Assess what the Change is intended to affect before relying on detected source c action receives the Change, compact repository and governance indexes, the built-in Signal Catalog, and up to three prior Impact Assessments. +An explicit Contract verification scope selects existing behavior for Evidence work; it +does not by itself declare a new application impact. Assess the Change intent honestly. +For an Evidence-only Change, `no-impact` is valid after the same active check required for +any other Change. + For each intended effect, choose a Signal from `context.payload.signal_catalog` and copy its required binding names exactly. Record: diff --git a/skill-src/adf-builder/SKILL.md b/skill-src/adf-builder/SKILL.md index 3d1ff92..36f9e74 100644 --- a/skill-src/adf-builder/SKILL.md +++ b/skill-src/adf-builder/SKILL.md @@ -54,6 +54,12 @@ Show that each requirement instance in the action actually holds. For each one, - `summary`: what the evidence shows - `basis_refs`: the tests, probes, and artifacts you actually ran or read +A Change may explicitly select existing Contract clauses for verification without +changing product code. In that case, run or inspect a reproducible check for the exact +selected clauses and register what it establishes. Do not invent implementation output +or treat the absence of a code change as Evidence. A generic passing test is insufficient +unless it exercises the behavior named by the clause. + Evidence is traceable to a contract clause or it is not evidence. A mock does not demonstrate that a platform behaves as assumed, and a passing test that never exercised the path proves nothing about it. Where you could not establish the requirement, report diff --git a/src/kernel.rs b/src/kernel.rs index 42581bc..d5d0e95 100644 --- a/src/kernel.rs +++ b/src/kernel.rs @@ -257,6 +257,30 @@ impl ThinKernel { instances.extend(applicability_reviews); if let Some(contract_health) = contract_health { instances.extend(contract_revalidation_instances(contract_health, &instances)); + let explicit = + match explicit_contract_verification_instances(contract_health, &snapshot.change) { + Ok(instances) => instances, + Err(error) => return decision("invalid", None, instances, vec![error]), + }; + for explicit_instance in explicit { + if let Some(existing) = instances + .iter_mut() + .find(|instance| instance.instance_key == explicit_instance.instance_key) + { + existing.selected_by.extend(explicit_instance.selected_by); + existing.selected_by.sort(); + existing.selected_by.dedup(); + } else { + instances.push(explicit_instance); + } + } + } else if !nested_array(&snapshot.change, &["verification_scope"]).is_empty() { + return decision( + "invalid", + None, + instances, + vec!["verification_scope requires Contract health evaluation".to_owned()], + ); } instances.sort_by(|left, right| left.instance_key.cmp(&right.instance_key)); @@ -730,6 +754,33 @@ fn contract_revalidation_instances( .collect() } +fn explicit_contract_verification_instances( + contract_health: &ContractHealthReport, + change: &Value, +) -> Result, String> { + nested_array(change, &["verification_scope"]) + .iter() + .filter_map(Value::as_str) + .map(|clause_ref| { + let clause = contract_health + .clauses + .iter() + .find(|clause| clause.clause_ref == clause_ref) + .ok_or_else(|| { + format!("verification_scope references unknown Contract clause: {clause_ref}") + })?; + if clause.evidence_mode == "review" { + return Err(format!( + "verification_scope cannot request Evidence for review-only Contract clause: {clause_ref}" + )); + } + let mut instance = contract_revalidation_instance(clause); + instance.selected_by = vec!["change.explicit-contract-verification-v1".to_owned()]; + Ok(instance) + }) + .collect() +} + fn contract_revalidation_instance(clause: &ClauseHealth) -> RequirementInstance { let definition = json!({ "id": CONTRACT_CLAUSE_REVALIDATION_REQUIREMENT, @@ -2300,4 +2351,217 @@ mod tests { ["contract.orders#persistence", "data.orders"] ); } + + #[test] + fn explicit_verification_selects_only_requested_unverified_clauses() { + let clause = |clause_ref: &str, target: &str| ClauseHealth { + contract_id: clause_ref.split('#').next().unwrap().to_owned(), + clause_id: clause_ref.split('#').nth(1).unwrap().to_owned(), + clause_ref: clause_ref.to_owned(), + text: "test clause".to_owned(), + applies_to: vec![target.to_owned()], + authority_ref: None, + evidence_mode: "direct".to_owned(), + status: "unverified".to_owned(), + evidence_refs: Vec::new(), + verification_result_ids: Vec::new(), + stale_refs: Vec::new(), + }; + let health = ContractHealthReport { + schema_version: "1".to_owned(), + repository_revision: Some("revision.test".to_owned()), + summary: ContractHealthSummary { + total: 2, + verified: 0, + stale: 0, + unverified: 2, + failed: 0, + review: 0, + }, + clauses: vec![ + clause("contract.orders#persistence", "data.orders"), + clause("contract.orders#unrelated", "data.orders"), + ], + }; + let change = json!({ + "id": "change.verify-orders", + "verification_scope": ["contract.orders#persistence"] + }); + + let instances = explicit_contract_verification_instances(&health, &change).unwrap(); + + assert_eq!(instances.len(), 1); + assert_eq!( + instances[0].instance_key, + "contract-clause-revalidated|contract.orders#persistence" + ); + assert_eq!( + instances[0].selected_by, + ["change.explicit-contract-verification-v1"] + ); + } + + #[test] + fn explicit_verification_rejects_unknown_clause_refs() { + let health = ContractHealthReport { + schema_version: "1".to_owned(), + repository_revision: Some("revision.test".to_owned()), + summary: ContractHealthSummary { + total: 0, + verified: 0, + stale: 0, + unverified: 0, + failed: 0, + review: 0, + }, + clauses: Vec::new(), + }; + let change = json!({ + "id": "change.verify-orders", + "verification_scope": ["contract.orders#missing"] + }); + + let error = explicit_contract_verification_instances(&health, &change).unwrap_err(); + + assert_eq!( + error, + "verification_scope references unknown Contract clause: contract.orders#missing" + ); + } + + #[test] + fn explicit_verification_rejects_review_only_clauses() { + let health = ContractHealthReport { + schema_version: "1".to_owned(), + repository_revision: Some("revision.test".to_owned()), + summary: ContractHealthSummary { + total: 1, + verified: 0, + stale: 0, + unverified: 0, + failed: 0, + review: 1, + }, + clauses: vec![ClauseHealth { + contract_id: "contract.orders".to_owned(), + clause_id: "approval".to_owned(), + clause_ref: "contract.orders#approval".to_owned(), + text: "orders receive human approval".to_owned(), + applies_to: vec!["data.orders".to_owned()], + authority_ref: None, + evidence_mode: "review".to_owned(), + status: "review".to_owned(), + evidence_refs: Vec::new(), + verification_result_ids: Vec::new(), + stale_refs: Vec::new(), + }], + }; + let change = json!({ + "id": "change.verify-orders", + "verification_scope": ["contract.orders#approval"] + }); + + let error = explicit_contract_verification_instances(&health, &change).unwrap_err(); + + assert_eq!( + error, + "verification_scope cannot request Evidence for review-only Contract clause: contract.orders#approval" + ); + } + + #[test] + fn explicit_verification_issues_evidence_action_for_post_build_repository() { + let change_digest = format!("sha256:{}", "1".repeat(64)); + let snapshot = ProjectSnapshot { + change_id: "change.verify-orders".to_owned(), + change: json!({ + "id": "change.verify-orders", + "impact_assessment": "required", + "verification_scope": ["contract.orders#persistence"] + }), + contracts: vec![json!({ + "id": "contract.orders", + "applies_to": ["data.orders"], + "clauses": [{ + "id": "persistence", + "text": "accepted orders persist" + }] + })], + decisions: Vec::new(), + results: vec![json!({ + "id": "result.no-impact", + "result_schema": "result.impact-assessment", + "role": "Analyst", + "input_refs": { + "change.verify-orders": change_digest + }, + "payload": { + "status": "no-impact", + "impacts": [], + "unknowns": [] + } + })], + evidence: Vec::new(), + repository: json!({"phase": "post-build"}), + artifact_digests: BTreeMap::from([( + "change.verify-orders".to_owned(), + format!("sha256:{}", "1".repeat(64)), + )]), + digest: String::new(), + }; + let health = ContractHealthReport { + schema_version: "1".to_owned(), + repository_revision: Some("revision.test".to_owned()), + summary: ContractHealthSummary { + total: 1, + verified: 0, + stale: 0, + unverified: 1, + failed: 0, + review: 0, + }, + clauses: vec![ClauseHealth { + contract_id: "contract.orders".to_owned(), + clause_id: "persistence".to_owned(), + clause_ref: "contract.orders#persistence".to_owned(), + text: "accepted orders persist".to_owned(), + applies_to: vec!["data.orders".to_owned()], + authority_ref: None, + evidence_mode: "direct".to_owned(), + status: "unverified".to_owned(), + evidence_refs: Vec::new(), + verification_result_ids: Vec::new(), + stale_refs: Vec::new(), + }], + }; + let rule_index = RuleIndex { + requirements: BTreeMap::new(), + rules: Vec::new(), + digest: String::new(), + }; + let detection = DetectionReport { + change_id: "change.verify-orders".to_owned(), + coverage: DetectionCoverage { + status: "complete".to_owned(), + scope: "declared-artifacts".to_owned(), + analyzed_refs: Vec::new(), + gaps: Vec::new(), + }, + candidates: Vec::new(), + digest: String::new(), + }; + + let decision = + ThinKernel.evaluate_with_health(&snapshot, &rule_index, &detection, Some(&health)); + + assert_eq!(decision.state, "needs-evidence"); + let action = decision.action.unwrap(); + assert_eq!(action.role, "Builder"); + assert_eq!(action.action, "record-evidence"); + assert_eq!(action.requirement_instances.len(), 1); + assert_eq!( + action.requirement_instances[0].subject_refs, + ["contract.orders#persistence", "data.orders"] + ); + } } diff --git a/src/main.rs b/src/main.rs index d60943d..082a67f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1353,6 +1353,7 @@ struct ChangeManagementCommand { change_id: String, title: String, intent: String, + verification_scope: Vec, } fn parse_project_management_command( @@ -1680,6 +1681,7 @@ fn parse_change_management_command( let mut project_root = PathBuf::from("."); let mut title = None; let mut intent = None; + let mut verification_scope = Vec::new(); let mut index = 2; while index < arguments.len() { match arguments[index].as_str() { @@ -1692,6 +1694,10 @@ fn parse_change_management_command( "--intent" => { intent = Some(flag_value(arguments, &mut index, "--intent")?.to_owned()); } + "--verify-clause" => { + verification_scope + .push(flag_value(arguments, &mut index, "--verify-clause")?.to_owned()); + } other => return Err(format!("unknown change init argument: {other}")), } index += 1; @@ -1701,6 +1707,7 @@ fn parse_change_management_command( change_id, title: title.ok_or_else(|| "change init requires --title ".to_owned())?, intent: intent.ok_or_else(|| "change init requires --intent <intent>".to_owned())?, + verification_scope, }) } @@ -1710,6 +1717,7 @@ fn run_change_management_command(options: &ChangeManagementCommand) -> Result<St &options.change_id, &options.title, &options.intent, + &options.verification_scope, ) .map_err(|error| error.to_string())?; Ok(format!( @@ -2381,7 +2389,7 @@ usage:\n\ adf project observe [--project <root>] [--analysis-root <path>]... [--format <yaml|json>] [--output <path>]\n\ adf project validate-bindings [--project <root>] [--draft <path>] [--format <text|json>] [--require-clean]\n\ adf project promote-bindings --draft <path> [--project <root>]\n\ - adf change init <change-id> --title <title> --intent <intent> [--project <root>]\n\ + adf change init <change-id> --title <title> --intent <intent> [--verify-clause <contract#clause>]... [--project <root>]\n\ adf <next|explain|execution-log> <change-id> [--project <root>] [--release <root>] [--format <text|json>] [--require-clean]\n\ adf execution <begin|complete> <id> [options]\n\ adf contract-health [--project <root>] [--release <root>] [--policy <path>] [--format <text|json>] [--require-clean]\n\ diff --git a/src/project_setup.rs b/src/project_setup.rs index 7ea20be..24c8016 100644 --- a/src/project_setup.rs +++ b/src/project_setup.rs @@ -284,6 +284,7 @@ pub fn initialize_change( change_id: &str, title: &str, intent: &str, + verification_scope: &[String], ) -> Result<PathBuf, ProjectSetupError> { if !safe_prefixed_id(change_id, "change.") { return Err(setup_error( @@ -315,13 +316,16 @@ pub fn initialize_change( .join(".adf/changes") .join(change_id) .join("change.yaml"); - let value = json!({ + let mut value = json!({ "schema_version": "1", "id": change_id, "title": title, "intent": intent, "impact_assessment": "required", }); + if !verification_scope.is_empty() { + value["verification_scope"] = json!(verification_scope); + } let yaml = serde_yaml::to_string(&value).map_err(|error| setup_error(error.to_string()))?; write_new(&path, yaml.as_bytes())?; Ok(path) diff --git a/src/schema.rs b/src/schema.rs index f539b18..2c52f4c 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -12,7 +12,7 @@ use std::fmt; use std::fs; use std::path::Path; -pub const SCHEMA_BUNDLE_VERSION: &str = "4"; +pub const SCHEMA_BUNDLE_VERSION: &str = "5"; const RECORD_KINDS: [&str; 5] = ["change", "contract", "decision", "evidence", "result"]; const RESULT_SCHEMA_IDS: [&str; 7] = [ "result.analysis", diff --git a/testdata/fixtures/cli-project/.adf/framework.lock b/testdata/fixtures/cli-project/.adf/framework.lock index d8c0355..6d5402a 100644 --- a/testdata/fixtures/cli-project/.adf/framework.lock +++ b/testdata/fixtures/cli-project/.adf/framework.lock @@ -12,8 +12,8 @@ protocols: detectors: typed-repository-fact: "3" schema_bundle: - version: "4" - digest: sha256:500fe6b6b0cadaa3706a515fa3e0d58f3dc047796c92fd7d22dbc4c626cdac81 + version: "5" + digest: sha256:4c2428d1abc3aaed2ef4ab13fd2fcfa4e746c5fefac136608b0aab14ebd85f2f rule_set: source_digest: sha256:a9dc2e8901db7e4553be042ffce47388f1e8971ebb26feb2ecc377e2396c2f07 index_digest: sha256:58adc0315e3f1460cb03931e0022a866a94f8718b0a071ca2863f6a0c7bec9c1 diff --git a/testdata/fixtures/db-sqs/framework-lock.yaml b/testdata/fixtures/db-sqs/framework-lock.yaml index d8c0355..6d5402a 100644 --- a/testdata/fixtures/db-sqs/framework-lock.yaml +++ b/testdata/fixtures/db-sqs/framework-lock.yaml @@ -12,8 +12,8 @@ protocols: detectors: typed-repository-fact: "3" schema_bundle: - version: "4" - digest: sha256:500fe6b6b0cadaa3706a515fa3e0d58f3dc047796c92fd7d22dbc4c626cdac81 + version: "5" + digest: sha256:4c2428d1abc3aaed2ef4ab13fd2fcfa4e746c5fefac136608b0aab14ebd85f2f rule_set: source_digest: sha256:a9dc2e8901db7e4553be042ffce47388f1e8971ebb26feb2ecc377e2396c2f07 index_digest: sha256:58adc0315e3f1460cb03931e0022a866a94f8718b0a071ca2863f6a0c7bec9c1 diff --git a/testdata/golden/v1/application-initial.json b/testdata/golden/v1/application-initial.json index 8cd23ce..2f977df 100644 --- a/testdata/golden/v1/application-initial.json +++ b/testdata/golden/v1/application-initial.json @@ -256,8 +256,8 @@ "typed-repository-fact": "3" }, "schema_bundle": { - "version": "4", - "digest": "sha256:500fe6b6b0cadaa3706a515fa3e0d58f3dc047796c92fd7d22dbc4c626cdac81" + "version": "5", + "digest": "sha256:4c2428d1abc3aaed2ef4ab13fd2fcfa4e746c5fefac136608b0aab14ebd85f2f" }, "rule_set": { "source_digest": "sha256:6fe7e6252e94c44901203395e6ac00ead20173dc857e0e630fe986c878af5855", @@ -268,7 +268,7 @@ "expected": { "snapshot_digest": "sha256:3ae18b6dc5493c557b0737acc55d0cddb85e2f45d098ef053a68a85f445d3360", "rule_index_digest": "sha256:175099cb33658a88160e59f31a600157f0d5b64cb046199efbae4b3a117adcbe", - "framework_lock_digest": "sha256:e3354d9da1d28a7d6b8afe05657288330475fd4def7ef9883b39508bcebef6db", + "framework_lock_digest": "sha256:147a693a6cd5fdef03c001de5f17ad8376c1f76adfe2ff988d21e20729914108", "decision": { "state": "needs-analysis", "action": { diff --git a/testdata/golden/v1/framework-lock.json b/testdata/golden/v1/framework-lock.json index c4234bf..8cf83af 100644 --- a/testdata/golden/v1/framework-lock.json +++ b/testdata/golden/v1/framework-lock.json @@ -3,7 +3,7 @@ "framework_lock_schema_version": "1", "schema_root": "../../../schemas/v1", "base_case": "application-initial.json", - "expected_digest": "sha256:e3354d9da1d28a7d6b8afe05657288330475fd4def7ef9883b39508bcebef6db", + "expected_digest": "sha256:147a693a6cd5fdef03c001de5f17ad8376c1f76adfe2ff988d21e20729914108", "invalid_cases": [ { "case_id": "detector-version-mismatch", diff --git a/testdata/golden/v1/rule-compilation.json b/testdata/golden/v1/rule-compilation.json index 63338f2..478fc5f 100644 --- a/testdata/golden/v1/rule-compilation.json +++ b/testdata/golden/v1/rule-compilation.json @@ -2,7 +2,7 @@ "schema_version": "1", "compiler_version": "1", "schema_root": "../../../schemas/v1", - "schema_bundle_digest": "sha256:500fe6b6b0cadaa3706a515fa3e0d58f3dc047796c92fd7d22dbc4c626cdac81", + "schema_bundle_digest": "sha256:4c2428d1abc3aaed2ef4ab13fd2fcfa4e746c5fefac136608b0aab14ebd85f2f", "cases": [ { "case_id": "normalized-index", diff --git a/testdata/golden/v1/schema-validation.json b/testdata/golden/v1/schema-validation.json index f448138..e787f54 100644 --- a/testdata/golden/v1/schema-validation.json +++ b/testdata/golden/v1/schema-validation.json @@ -1,9 +1,40 @@ { "schema_version": "1", - "schema_bundle_version": "4", - "schema_bundle_digest": "sha256:500fe6b6b0cadaa3706a515fa3e0d58f3dc047796c92fd7d22dbc4c626cdac81", + "schema_bundle_version": "5", + "schema_bundle_digest": "sha256:4c2428d1abc3aaed2ef4ab13fd2fcfa4e746c5fefac136608b0aab14ebd85f2f", "schema_root": "../../../schemas/v1", "cases": [ + { + "case_id": "valid-change-with-verification-scope", + "record_kind": "change", + "record": { + "schema_version": "1", + "id": "change.verify-existing-orders", + "title": "Verify existing order behavior", + "intent": "Register reproducible evidence for existing behavior", + "impact_assessment": "required", + "verification_scope": [ + "contract.order-lifecycle#orders-source-of-truth" + ] + }, + "valid": true + }, + { + "case_id": "invalid-change-verification-scope-reference", + "record_kind": "change", + "record": { + "schema_version": "1", + "id": "change.verify-existing-orders", + "title": "Verify existing order behavior", + "intent": "Register reproducible evidence for existing behavior", + "impact_assessment": "required", + "verification_scope": [ + "order-lifecycle#orders-source-of-truth" + ] + }, + "valid": false, + "error_path": "$.verification_scope[0]" + }, { "case_id": "valid-contract", "record_kind": "contract", diff --git a/tests/cli.rs b/tests/cli.rs index b1a665c..b8f5597 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -3840,6 +3840,45 @@ fn source_artifact_without_a_binding_record_blocks_the_project() { assert!(stdout.contains("Binding candidates are never applied automatically")); } +#[test] +fn change_init_records_explicit_contract_verification_scope() { + let project = TestProject::new(); + + let output = project.run(&[ + "change", + "init", + "change.verify-orders", + "--title", + "Verify existing order behavior", + "--intent", + "Register reproducible evidence for existing behavior", + "--verify-clause", + "contract.order-lifecycle#orders-source-of-truth", + "--verify-clause", + "contract.order-lifecycle#order-created-once", + ]); + + assert_success(&output); + let change = read_yaml( + &project + .root + .join(".adf/changes/change.verify-orders/change.yaml"), + ); + assert_eq!( + change["verification_scope"], + json!([ + "contract.order-lifecycle#orders-source-of-truth", + "contract.order-lifecycle#order-created-once" + ]) + ); + let schema: Value = serde_json::from_slice( + &fs::read(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("schemas/v1/change.schema.json")) + .unwrap(), + ) + .unwrap(); + validate_json_document(&change, &schema).unwrap(); +} + #[test] fn java_artifact_runs_through_the_real_project_loader() { assert_language_artifact_runs_through_real_loader( diff --git a/tests/golden.rs b/tests/golden.rs index 27b8212..8b3b3d6 100644 --- a/tests/golden.rs +++ b/tests/golden.rs @@ -19,8 +19,8 @@ fn shared_canonicalization_golden_matches() { fn shared_schema_validation_golden_matches() { let golden_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata/golden/v1"); let report = verify_schema_suite(golden_root).unwrap(); - assert_eq!(report.valid_cases, 6); - assert_eq!(report.invalid_cases, 6); + assert_eq!(report.valid_cases, 7); + assert_eq!(report.invalid_cases, 7); } #[test]