Merged
feat(governance): 接入兼容旧知识的可选 Scope 基础契约 #171
feat(governance): 接入兼容旧知识的可选 Scope 基础契约 #171
Merged
汪超Add on 13 days ago
Total: 19 files changed+1031-12
@@ -37,6 +37,8 @@ python3 <skill-dir>/scripts/knowledge_ingest.py --knowledge-root <repo> create \
37 37 
38审阅 dry-run 后移除 `--dry-run`。`create` 子命令以排他原子写入创建候选卡,不覆盖已有文件,也不更新 index 或日志。默认 `draft`;只有来源、平台、正文与 Profile 门禁均满足后才显式使用 `--status stable`。38审阅 dry-run 后移除 `--dry-run`。`create` 子命令以排他原子写入创建候选卡,不覆盖已有文件,也不更新 index 或日志。默认 `draft`;只有来源、平台、正文与 Profile 门禁均满足后才显式使用 `--status stable`。
39 39 
40+候选卡可在 Profile 允许时声明 `scope`,由同版本 Contract 校验。只提取来源支持且有过滤价值的条件,不要求每张卡填写或补齐维度;按领域选取注册 Schema。字段及准入边界见[路径与 Frontmatter Profile](references/frontmatter-profiles.md),不把声明存在当成 Query 已启用过滤。
41+ 
40## 收尾门禁42## 收尾门禁
41 43 
42- Ingest 的验证由全库确定性命令和仓库 `evals/` 承担,不维护 Skill 内独立测试目录;统一入口仍为根 `bash check.sh`。日常修改仅做必要的定向检查,以下完整收尾流程在提交或推送上库前统一执行,用户明确要求提前全量验证时除外。44- Ingest 的验证由全库确定性命令和仓库 `evals/` 承担,不维护 Skill 内独立测试目录;统一入口仍为根 `bash check.sh`。日常修改仅做必要的定向检查,以下完整收尾流程在提交或推送上库前统一执行,用户明确要求提前全量验证时除外。
@@ -8,18 +8,24 @@
8 8 
9| 路径 Profile | OKF type | 额外必填 | 允许可选 |9| 路径 Profile | OKF type | 额外必填 | 允许可选 |
10|---|---|---|---|10|---|---|---|---|
11-| `concepts` | `Concept` | `platforms` | `cann_versions`, `aliases`, `verified`, `replaced_by` |11+| `concepts` | `Concept` | `platforms` | `cann_versions`, `aliases`, `verified`, `replaced_by`, `scope` |
12| `apis` | `API` | `platforms` | `cann_versions`, `source_headers`, `aliases`, `verified`, `replaced_by` |12| `apis` | `API` | `platforms` | `cann_versions`, `source_headers`, `aliases`, `verified`, `replaced_by` |
13| `examples` | `Example` | `platforms` | `cann_versions`, `verified`, `replaced_by` |13| `examples` | `Example` | `platforms` | `cann_versions`, `verified`, `replaced_by` |
14-| `guides` | `Guide` | `platforms` | `cann_versions`, `aliases`, `verified`, `replaced_by` |14+| `guides` | `Guide` | `platforms` | `cann_versions`, `aliases`, `verified`, `replaced_by`, `scope` |
15| `operators` | `Operator` | `platforms` | `cann_versions`, `verified`, `replaced_by` |15| `operators` | `Operator` | `platforms` | `cann_versions`, `verified`, `replaced_by` |
16-| `optimizations` | `Optimization` | `platforms` | `cann_versions`, `verified`, `replaced_by` |16+| `optimizations` | `Optimization` | `platforms` | `cann_versions`, `verified`, `replaced_by`, `scope` |
17-| `runbooks` | `Runbook` | `platforms` | `aliases`, `verified`, `replaced_by` |17+| `runbooks` | `Runbook` | `platforms` | `aliases`, `verified`, `replaced_by`, `scope` |
18| `interoperability` | `Interoperability` | `platforms`, `source_technology`, `target_technology` | `cann_versions`, `verified`, `replaced_by` |18| `interoperability` | `Interoperability` | `platforms`, `source_technology`, `target_technology` | `cann_versions`, `verified`, `replaced_by` |
19| `glossaries` | `Glossary` | 无 | `aliases`, `verified`, `replaced_by` |19| `glossaries` | `Glossary` | 无 | `aliases`, `verified`, `replaced_by` |
20 20 
21路径选择 Profile,Profile 的 `okf_type` 生成卡片 `type`;不得手工创造另一种映射。平台无关写 `[agnostic]`,未知写 `[]` 并保持 `draft`。21路径选择 Profile,Profile 的 `okf_type` 生成卡片 `type`;不得手工创造另一种映射。平台无关写 `[agnostic]`,未知写 `[]` 并保持 `draft`。
22 22 
23+## 场景适用条件
24+ 
25+`scope` 可选且允许部分声明,只填写来源支持并有过滤价值的条件;未知维度省略,不以 `any` 或否定条件补齐。不同领域使用各自注册的 Schema、适用范围和维度语义,共享形状和三态协议。具体语法见治理 [Frontmatter](../../../../governance/specs/frontmatter.md#场景适用条件scope) 与 [机器契约](../../../../governance/specs/schemas.md),不在 producer 中维护第二份词表。
26+ 
27+当前 `create` 参数保持原样,不提供场景 Scope 选项;审阅后人工补充的声明仍由完整候选校验、Lint、索引及构图复用同一 Contract 验证。
28+ 
23## Trust29## Trust
24 30 
25没有 `verified` 表示 unverified。只有真实执行验证或人工复核后,才记录带 actor 和时间的 OKF event:31没有 `verified` 表示 unverified。只有真实执行验证或人工复核后,才记录带 actor 和时间的 OKF event:
@@ -9,6 +9,8 @@
9- 平台无关只在定义本身不会随硬件变化时使用 `agnostic`,不能与具体平台混用。9- 平台无关只在定义本身不会随硬件变化时使用 `agnostic`,不能与具体平台混用。
10- 来源固定、lint 通过和单次成功都不自动生成 `verified` 事件。10- 来源固定、lint 通过和单次成功都不自动生成 `verified` 事件。
11 11 
12+可选 `scope` 只记录来源支持且对过滤有帮助的条件;缺少 Scope 或缺少维度不意味着普遍适用。不得按标题、移动目录或检索相关性扩大适用范围,也不为提升填写率补造条件。不同领域的约束使用各自注册的 Schema,不套用模型推理维度。
13+ 
12## 证据合同14## 证据合同
13 15 
14- 官方 Git 证据优先记录固定 revision、仓库相对 path/symbol 和精确 blob,并能把正文 claim 映射回来源。16- 官方 Git 证据优先记录固定 revision、仓库相对 path/symbol 和精确 blob,并能把正文 claim 映射回来源。
@@ -33,6 +33,8 @@ python3 <skill-dir>/scripts/knowledge_lint.py --knowledge-root <repo> --sample 1
334. 修改 Skill 时运行 skill-creator 的 quick validation、相关回归和本仓统一质量门禁。334. 修改 Skill 时运行 skill-creator 的 quick validation、相关回归和本仓统一质量门禁。
345. 用户要提交或整理问题时,按 [Issue 报告](references/issue-report.md)收集已有运行材料与实际复现率;补充复现仍受当前任务授权和本 Skill 只读边界约束。关键材料缺失则输出 `needs-info`,不凭记忆补齐或虚增运行次数。345. 用户要提交或整理问题时,按 [Issue 报告](references/issue-report.md)收集已有运行材料与实际复现率;补充复现仍受当前任务授权和本 Skill 只读边界约束。关键材料缺失则输出 `needs-info`,不凭记忆补齐或虚增运行次数。
35 35 
36+可选 `scope` 由同版本 Contract 校验;允许的 Profile、namespace、路径绑定和维度配置分别以治理事实源为准。缺字段或缺少部分维度不报质量错误;非法声明仍拒绝。具体边界见 [Frontmatter](../../../governance/specs/frontmatter.md#场景适用条件scope)。
37+ 
36## 页面内容检查38## 页面内容检查
37 39 
38仅当用户明确要求内容或语义检查并指定页面时使用;普通 Lint 和 Issue 工作流仍只执行确定性检查。40仅当用户明确要求内容或语义检查并指定页面时使用;普通 Lint 和 Issue 工作流仍只执行确定性检查。
@@ -10,6 +10,7 @@
10|---|---|---|10|---|---|---|
11| `path` | P0 | domain-route、Profile、内容型 section、Operator/Runbook 附加层级和小写 snake_case 卡名合法 |11| `path` | P0 | domain-route、Profile、内容型 section、Operator/Runbook 附加层级和小写 snake_case 卡名合法 |
12| `profile` | P0 | YAML mapping 无重复 key;JSON Schema、公共/Profile 必选字段、非空标题摘要和路径到 OKF type 的映射一致;缺失与空白分别报告 |12| `profile` | P0 | YAML mapping 无重复 key;JSON Schema、公共/Profile 必选字段、非空标题摘要和路径到 OKF type 的映射一致;缺失与空白分别报告 |
13+| `scope` | P0 | 可选声明的 namespace、Profile/路径绑定、分支、维度和值符合 Schema、Registry 与 Contract;未知或缺失条件不补全,不允许卡片使用查询专用否定 |
13| `body` | P0 | 正文非空且围栏外至少有一个 H1;除 Registry 文档归档主来源的 API/Guide 受控转换可保留来源中的多个 H1 外,其余卡恰有一个 H1;摘要不含发布占位,正文无 Git 冲突标记或压平到单行的 Markdown 表格 |14| `body` | P0 | 正文非空且围栏外至少有一个 H1;除 Registry 文档归档主来源的 API/Guide 受控转换可保留来源中的多个 H1 外,其余卡恰有一个 H1;摘要不含发布占位,正文无 Git 冲突标记或压平到单行的 Markdown 表格 |
14| `images` | P0 | Markdown/HTML 图片使用含 40 位 commit 和具体文件路径的 HTTPS Git raw URL,忽略代码中的伪图片 |15| `images` | P0 | Markdown/HTML 图片使用含 40 位 commit 和具体文件路径的 HTTPS Git raw URL,忽略代码中的伪图片 |
15| `tags` | P0 | 标签受控、无重复,且至少包含一个 task 和一个 topic |16| `tags` | P0 | 标签受控、无重复,且至少包含一个 task 和一个 topic |
@@ -41,6 +41,8 @@
41 41 
42Schema、Profile、Registry、Contract、Spec、Skill 或测试若互相矛盾,不要自行选择其中一份绕过。先定位唯一事实源,再在同一变更中修正执行逻辑、文档和回归。42Schema、Profile、Registry、Contract、Spec、Skill 或测试若互相矛盾,不要自行选择其中一份绕过。先定位唯一事实源,再在同一变更中修正执行逻辑、文档和回归。
43 43 
44+场景适用条件 `scope` 是可选字段,只填写来源支持且有过滤价值的部分条件,不要求每张卡填写,不以填写率验收;未知不补成 `any`,也不写入查询专用的否定条件。各领域独立在 Registry 的 `scope_schemas` 中定义 namespace、适用范围、维度和匹配语义,与路径 route 的 `scopes` 分区区分。Schema、Profile 和同版本 Contract 共同校验,匹配扩展只能使用受审查的 Contract 处理器;消费端是否启用场景过滤以其实际公开接口为准。
45+ 
44## 修改知识卡46## 修改知识卡
45 47 
461. 先用 `knowledge-query` 搜索同一主题,阅读候选卡和来源,再明确采用 create、update、merge、split、deprecate 或 defer。不要只因标题不同就新建卡片,也不要自动覆盖已有卡片。481. 先用 `knowledge-query` 搜索同一主题,阅读候选卡和来源,再明确采用 create、update、merge、split、deprecate 或 defer。不要只因标题不同就新建卡片,也不要自动覆盖已有卡片。
@@ -215,6 +215,8 @@ Git 来源固定到提交;普通文档来源优先使用稳定版本页,Regi
215 215 
216仓库级文档描述能力、输入输出语义和边界;Skill 内部脚本、命令和参数只在对应 `SKILL.md` 或 `references/` 中说明。修改文档时检查所有本地链接,并确认示例描述的是已实现行为。216仓库级文档描述能力、输入输出语义和边界;Skill 内部脚本、命令和参数只在对应 `SKILL.md` 或 `references/` 中说明。修改文档时检查所有本地链接,并确认示例描述的是已实现行为。
217 217 
218+CI 的知识检查使用目标分支的受信规则和实现读取 PR 的知识树;同一 PR 中新增的 Schema、Profile 或 Contract 不会自动成为该检查的规则。知识依赖尚未合入的治理能力时,先提交兼容现有知识的基础治理 PR,由 committer 检视并验证新实现,再提交或同步依赖它的知识变更。删除旧目录或收紧旧字段的规则与对应迁移一起生效,避免基础能力尚未接入就拒绝现有知识。
219+ 
218## 5. 贡献公共组件(含检索 Skill)220## 5. 贡献公共组件(含检索 Skill)
219 221 
220### 5.1 适用范围与兼容性原则222### 5.1 适用范围与兼容性原则
@@ -0,0 +1,485 @@
1+# ----------------------------------------------------------------------------------------------------------
2+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4+# CANN Open Software License Agreement Version 2.0 (the "License").
5+# Please refer to the License for details. You may not use this file except in compliance with the License.
6+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+# See LICENSE in the root of the software repository for the full text of the License.
9+# ----------------------------------------------------------------------------------------------------------
10+ 
11+"""Optional Scope governance, strict acceptance and legacy-consumer compatibility."""
12+ 
13+from __future__ import annotations
14+ 
15+import copy
16+import json
17+from pathlib import Path
18+import shutil
19+import sqlite3
20+import subprocess
21+import sys
22+import tempfile
23+import unittest
24+from unittest.mock import patch
25+ 
26+import yaml
27+ 
28+from evals.test_retrieval_index import ROOT, QUERY, make_root, run_index
29+from governance.contracts.knowledge import Contract
30+from governance.contracts.scope import ScopeRegistry
31+from governance.contracts.scope_matchers import DIMENSION_MATCHERS, DimensionMatch, object_fields
32+ 
33+ 
34+def scope(**conditions):
35+ return {"schema": "model.inference.v1", "any_of": [{"all_of": conditions}]}
36+ 
37+ 
38+class ScopeGovernanceContractTests(unittest.TestCase):
39+ @classmethod
40+ def setUpClass(cls):
41+ cls.contract = Contract(ROOT)
42+ cls.registry = cls.contract.scope_registry
43+ 
44+ def state(self, query, card):
45+ return self.registry.compare(self.registry.normalize(query, query=True), card)["state"]
46+ 
47+ 
48+ def test_decision_table(self):
49+ cases = [
50+ (scope(phase=["prefill"], execution_mode=["ge_graph"]), scope(phase=["prefill"], execution_mode=["ge_graph"]), "match"),
51+ (scope(phase=["prefill"], execution_mode=["ge_graph"]), scope(phase=["prefill"]), "unknown"),
52+ (scope(phase=["prefill"]), scope(phase=["prefill"], execution_mode=["ge_graph"]), "unknown"),
53+ (scope(phase=["prefill"], execution_mode=["ge_graph"]), scope(phase=["prefill"], execution_mode=["any"]), "match"),
54+ (scope(phase=["any"]), scope(phase=["prefill"]), "unknown"),
55+ (scope(phase=["prefill"], execution_mode=["ge_graph"]), scope(phase=["prefill"], execution_mode=["eager"]), "conflict"),
56+ (scope(phase=["prefill"]), scope(phase=["decode"]), "conflict"),
57+ (scope(model_features={"all_of": ["moe", "mla"]}), scope(model_features={"all_of": ["moe"]}), "match"),
58+ (scope(model_features={"all_of": ["moe"]}), scope(model_features={"all_of": ["moe", "mla"]}), "unknown"),
59+ (scope(model_features={"all_of": ["moe"], "none_of": ["mla"]}), scope(model_features={"all_of": ["moe", "mla"]}), "conflict"),
60+ (scope(phase=["prefill"]), None, "unknown"),
61+ (scope(phase=["prefill", "decode"]), scope(phase=["decode"]), "match"),
62+ (scope(objective=["throughput"]), scope(objective=["latency"]), "conflict"),
63+ (scope(objective=["tpot"]), scope(objective=["latency"]), "conflict"),
64+ (scope(objective=["tpot"]), scope(objective=["latency", "tpot"]), "match"),
65+ (scope(objective=["ttft"]), scope(objective=["latency", "tpot"]), "conflict"),
66+ (scope(objective=["throughput"]), scope(phase=["decode"]), "unknown"),
67+ (scope(model_features={"all_of": ["moe", "mla", "dsa"]}), scope(model_features={"all_of": ["moe", "mla"]}), "match"),
68+ (scope(model_features={"none_of": ["mla"]}), scope(model_features={"all_of": ["moe"]}), "unknown"),
69+ (scope(phase=["decode"]), scope(phase=["any"]), "match"),
70+ (scope(phase=["any"]), scope(phase=["any"]), "match"),
71+ (scope(phase=["prefill"]), scope(phase=["prefill"], objective=["ttft"]), "unknown"),
72+ (scope(phase=["prefill"], objective=["ttft"]), scope(phase=["decode"]), "conflict"),
73+ (scope(parallelism={"all_of": ["dp", "ep"]}), scope(parallelism={"all_of": ["ep"]}), "match"),
74+ (scope(parallelism={"all_of": ["ep"]}), scope(parallelism={"all_of": ["dp", "ep"]}), "unknown"),
75+ (scope(context_length=["long_context"], batch_size=["small_batch"]), scope(context_length=["long_context"], batch_size=["small_batch"]), "match"),
76+ (scope(context_length=["long_context"], batch_size=["large_batch"]), scope(context_length=["long_context"], batch_size=["small_batch"]), "conflict"),
77+ (scope(serving_mode=["online"], deployment_topology={"all_of": ["afd", "pd_disaggregated"]}), scope(serving_mode=["online"], deployment_topology={"all_of": ["afd", "pd_disaggregated"]}), "match"),
78+ (scope(framework=["vllm"]), scope(framework=["sglang"]), "conflict"),
79+ (scope(quantization=["w8a8"]), scope(quantization=["w8a8", "bf16"]), "match"),
80+ (scope(concurrency=["single_request"]), scope(concurrency=["high_concurrency"]), "conflict"),
81+ (scope(execution_mode=["ge_graph"]), scope(execution_mode=["any"], model_features={"all_of": ["moe"]}), "unknown"),
82+ (scope(phase=["prefill"]), scope(execution_mode=["any"]), "unknown"),
83+ ]
84+ self.assertGreaterEqual(len(cases), 30)
85+ for query, card, expected in cases:
86+ with self.subTest(query=query, card=card):
87+ self.assertEqual(expected, self.state(query, card))
88+ 
89+ def test_branches_never_form_cartesian_product(self):
90+ card = scope(phase=["prefill"], execution_mode=["ge_graph"])
91+ card["any_of"] += scope(phase=["decode"], execution_mode=["eager"])["any_of"]
92+ for phase, mode, expected in [
93+ ("prefill", "ge_graph", "match"), ("decode", "eager", "match"),
94+ ("prefill", "eager", "conflict"), ("decode", "ge_graph", "conflict"),
95+ ]:
96+ self.assertEqual(expected, self.state(scope(phase=[phase], execution_mode=[mode]), card))
97+ result = self.registry.compare(self.registry.normalize(scope(phase=["decode"], execution_mode=["eager"]), query=True), card)
98+ self.assertEqual([1], [p["card_profile"] for p in result["profiles"] if p["state"] == "match"])
99+ 
100+ def test_branch_unknown_is_preserved_when_other_branch_conflicts(self):
101+ card = scope(phase=["prefill"], execution_mode=["ge_graph"])
102+ card["any_of"] += scope(phase=["decode"])["any_of"]
103+ self.assertEqual("unknown", self.state(scope(phase=["prefill"]), card))
104+ 
105+ def test_query_alternatives_and_matched_intersection_are_reported(self):
106+ query = scope(phase=["prefill", "decode"])
107+ query["any_of"] += scope(execution_mode=["eager"])["any_of"]
108+ result = self.registry.compare(self.registry.normalize(query, query=True), scope(phase=["prefill"]))
109+ self.assertEqual("match", result["state"])
110+ self.assertEqual(["prefill"], result["profiles"][0]["matched_values"]["phase"])
111+ 
112+ def test_invalid_scope_is_rejected(self):
113+ invalid = [None, [], {}, {"schema": "missing.v1", "any_of": []},
114+ {"schema": "model.inference.v1", "any_of": []},
115+ scope(), scope(phase=[]), scope(phase=["prefill", "prefill"]),
116+ scope(phase=["prefill", "any"]), scope(phase=["unknown"]),
117+ scope(load=["long_context"]), scope(deployment=["online"]),
118+ scope(model_features={"all_of": ["any"]}),
119+ scope(model_features={"all_of": ["moe"], "none_of": ["moe"]}),
120+ scope(deployment_topology={"none_of": ["afd"]}),
121+ {**scope(phase=["prefill"]), "confirmed": True}]
122+ for value in invalid:
123+ with self.subTest(value=value), self.assertRaises(ValueError):
124+ self.registry.normalize(value, query=True)
125+ 
126+ def test_aliases_are_only_normalized_for_explicit_queries(self):
127+ value = scope(phase=["预填充"], execution_mode=["GE"])
128+ self.assertEqual(scope(execution_mode=["ge_graph"], phase=["prefill"]), self.registry.normalize(value, query=True))
129+ with self.assertRaises(ValueError):
130+ self.registry.normalize(value)
131+ with self.assertRaises(ValueError):
132+ self.registry.normalize(scope(phase=["Prefill", "prefill"]), query=True)
133+ 
134+ def test_duplicate_keys_and_equivalent_branches_fail(self):
135+ with self.assertRaises(yaml.YAMLError):
136+ self.contract.parse_scope("schema: model.inference.v1\nschema: model.inference.v1\nany_of: []")
137+ value = scope(model_features={"all_of": ["moe", "mla"]})
138+ value["any_of"] += scope(model_features={"all_of": ["mla", "moe"]})["any_of"]
139+ with self.assertRaises(ValueError):
140+ self.registry.normalize(value)
141+ 
142+ def test_only_query_supports_feature_none_of(self):
143+ value = scope(model_features={"none_of": ["mla"]})
144+ self.assertEqual(value, self.registry.normalize(value, query=True))
145+ with self.assertRaises(ValueError):
146+ self.registry.normalize(value)
147+ 
148+ def test_parallelism_absence_requires_explicit_evidence(self):
149+ cases = [
150+ ({"none_of": ["cp"]}, {"none_of": ["cp"]}, "match"),
151+ ({"none_of": ["cp", "ep"]}, {"none_of": ["cp"]}, "match"),
152+ ({"none_of": ["cp", "ep"]}, {"none_of": ["cp", "ep"]}, "match"),
153+ ({"none_of": ["cp"]}, {"none_of": ["cp", "ep"]}, "unknown"),
154+ ({"all_of": ["cp"]}, {"none_of": ["cp", "ep"]}, "conflict"),
155+ ({"all_of": ["cp"]}, {"none_of": ["cp"]}, "conflict"),
156+ ({"none_of": ["cp"]}, {"all_of": ["cp"]}, "conflict"),
157+ ({"all_of": ["tp"]}, {"none_of": ["cp"]}, "unknown"),
158+ ({"none_of": ["ep"]}, {"none_of": ["cp"]}, "unknown"),
159+ ({"all_of": ["tp"], "none_of": ["cp"]}, {"none_of": ["cp"]}, "match"),
160+ ({"all_of": ["tp"], "none_of": ["cp"]}, {"all_of": ["tp"], "none_of": ["cp"]}, "match"),
161+ ({"all_of": ["tp"]}, {"all_of": ["tp"], "none_of": ["cp"]}, "unknown"),
162+ ({"none_of": ["cp"]}, {"all_of": ["tp"], "none_of": ["cp"]}, "unknown"),
163+ ({"all_of": ["cp"]}, {"all_of": ["tp"], "none_of": ["cp"]}, "conflict"),
164+ ({"none_of": ["tp"]}, {"all_of": ["tp"], "none_of": ["cp"]}, "conflict"),
165+ ({"all_of": ["tp"], "none_of": ["cp"]}, {"all_of": ["tp"]}, "match"),
166+ ]
167+ for query, card, expected in cases:
168+ with self.subTest(query=query, card=card):
169+ self.assertEqual(expected, self.state(scope(parallelism=query), scope(parallelism=card)))
170+ card = scope(phase=["prefill"], parallelism={"none_of": ["cp"]})
171+ self.assertEqual("unknown", self.state(scope(phase=["prefill"]), card))
172+ result = self.registry.compare(
173+ self.registry.normalize(scope(phase=["prefill"], parallelism={"all_of": ["tp"]}), query=True), card)
174+ self.assertEqual("missing_query_absences", result["profiles"][0]["reasons"][0]["reason"])
175+ self.assertEqual(["cp"], result["profiles"][0]["reasons"][0]["features"])
176+ 
177+ def test_parallelism_negative_branches_are_not_merged(self):
178+ card = scope(phase=["prefill"], parallelism={"none_of": ["cp"]})
179+ card["any_of"] += scope(phase=["decode"], parallelism={"all_of": ["cp"]})["any_of"]
180+ for phase, condition, expected in [
181+ ("prefill", {"none_of": ["cp"]}, "match"),
182+ ("decode", {"all_of": ["cp"]}, "match"),
183+ ("prefill", {"all_of": ["cp"]}, "conflict"),
184+ ("decode", {"none_of": ["cp"]}, "conflict"),
185+ ("prefill", {"all_of": ["tp"]}, "unknown"),
186+ ]:
187+ with self.subTest(phase=phase, condition=condition):
188+ self.assertEqual(expected, self.state(scope(phase=[phase], parallelism=condition), card))
189+ 
190+ def test_parallelism_negation_is_registered_and_validated_for_cards_and_queries(self):
191+ value = scope(parallelism={"none_of": ["ep", "cp"], "all_of": ["tp", "dp"]})
192+ canonical = scope(parallelism={"all_of": ["dp", "tp"], "none_of": ["cp", "ep"]})
193+ for query in (False, True):
194+ self.assertEqual(canonical, self.registry.normalize(value, query=query))
195+ for condition in [{}, {"none_of": []}, {"none_of": "cp"},
196+ {"none_of": ["any"]}, {"none_of": ["unknown"]},
197+ {"none_of": ["cp", "cp"]},
198+ {"all_of": [], "none_of": ["cp"]},
199+ {"all_of": ["cp"], "none_of": ["cp"]}]:
200+ with self.subTest(query=query, condition=condition), self.assertRaises(ValueError):
201+ self.registry.normalize(scope(parallelism=condition), query=query)
202+ with self.assertRaises(ValueError):
203+ self.registry.normalize(scope(deployment_topology={"none_of": ["afd"]}), query=query)
204+ self.assertEqual(scope(parallelism={"none_of": ["cp"]}),
205+ self.registry.normalize(scope(parallelism={"none_of": ["CP"]}), query=True))
206+ with self.assertRaises(ValueError):
207+ self.registry.normalize(scope(parallelism={"none_of": ["CP"]}))
208+ with self.assertRaises(ValueError):
209+ self.registry.normalize(scope(parallelism={"none_of": ["cp", "CP"]}), query=True)
210+ for permission in ("query_none_of", "card_none_of"):
211+ for name, invalid in [("phase", True), ("parallelism", "true")]:
212+ definitions = copy.deepcopy(self.registry.definitions)
213+ definitions["model.inference.v1"]["dimensions"][name][permission] = invalid
214+ with self.subTest(permission=permission, name=name), self.assertRaises(ValueError):
215+ ScopeRegistry(definitions)
216+ 
217+ def test_other_registered_schema_is_unknown(self):
218+ definitions = copy.deepcopy(self.registry.definitions)
219+ definitions["ops.compat.v1"] = {"description": "夹具", "routes": ["ops/ascendc"], "dimensions": {"mode": {"kind": "enum", "values": ["direct"]}}}
220+ registry = ScopeRegistry(definitions)
221+ result = registry.compare(scope(phase=["prefill"]), {"schema": "ops.compat.v1", "any_of": [{"all_of": {"mode": ["direct"]}}]})
222+ self.assertEqual("schema_mismatch", result["reason"])
223+ self.assertEqual("unknown", result["state"])
224+ 
225+ def test_registry_rejects_ambiguous_alias_and_unregistered_route(self):
226+ definitions = copy.deepcopy(self.registry.definitions)
227+ definitions["model.inference.v1"]["dimensions"]["phase"]["aliases"]["PREFILL"] = "decode"
228+ with self.assertRaises(ValueError):
229+ ScopeRegistry(definitions)
230+ with tempfile.TemporaryDirectory() as temporary:
231+ root = Path(temporary)
232+ shutil.copytree(ROOT / "governance", root / "governance")
233+ path = root / "governance/schemas/registries.yaml"
234+ data = yaml.safe_load(path.read_text())
235+ data["scope_schemas"]["model.inference.v1"]["routes"] = ["ops/inference"]
236+ path.write_text(yaml.safe_dump(data, allow_unicode=True))
237+ with self.assertRaises(ValueError):
238+ Contract(root)
239+ 
240+ 
241+class CapacityFixture:
242+ """Test-only non-enumerated handler; no production domain is preregistered."""
243+ 
244+ def __init__(self, definition, location):
245+ object_fields(definition, {"kind", "unit"}, location)
246+ if definition.get("unit") != "fixture_units":
247+ raise ValueError("fixture unit required")
248+ 
249+ def normalize(self, value, *, query, location):
250+ del query
251+ value = object_fields(value, {"capacity"}, location)
252+ capacity = value.get("capacity")
253+ if type(capacity) is not int or capacity <= 0:
254+ raise ValueError("positive fixture capacity required")
255+ return {"capacity": capacity}
256+ 
257+ def compare(self, query, card):
258+ if query is None:
259+ return DimensionMatch([{"state": "unknown", "reason": "fixture_missing_capacity"}])
260+ if query["capacity"] < card["capacity"]:
261+ return DimensionMatch([{"state": "conflict", "reason": "fixture_insufficient_capacity"}])
262+ return DimensionMatch()
263+ 
264+ 
265+def fixture_scope(**conditions):
266+ return {"schema": "runtime.fixture.v1", "any_of": [{"all_of": conditions}]}
267+ 
268+ 
269+class ScopeExtensionTests(unittest.TestCase):
270+ def test_domain_kind_controls_configuration_values_and_matching(self):
271+ definitions = {"runtime.fixture.v1": {
272+ "description": "隔离扩展夹具", "domains": ["runtime"],
273+ "dimensions": {"budget": {"kind": "fixture_capacity", "unit": "fixture_units"}},
274+ }}
275+ with patch.dict(DIMENSION_MATCHERS, {"fixture_capacity": CapacityFixture}):
276+ registry = ScopeRegistry(definitions)
277+ card = fixture_scope(budget={"capacity": 4})
278+ for capacity, expected in [(8, "match"), (4, "match"), (2, "conflict")]:
279+ query = registry.normalize(fixture_scope(budget={"capacity": capacity}), query=True)
280+ self.assertEqual(expected, registry.compare(query, card)["state"])
281+ for capacity in [0, -1, True, "4", [4]]:
282+ with self.subTest(capacity=capacity), self.assertRaises(ValueError):
283+ registry.normalize(fixture_scope(budget={"capacity": capacity}), query=True)
284+ missing = copy.deepcopy(definitions)
285+ missing["runtime.fixture.v1"]["dimensions"]["mode"] = {"kind": "enum", "values": ["safe"]}
286+ with patch.dict(DIMENSION_MATCHERS, {"fixture_capacity": CapacityFixture}):
287+ registry = ScopeRegistry(missing)
288+ self.assertEqual("unknown", registry.compare(registry.normalize(fixture_scope(mode=["safe"])), card)["state"])
289+ missing["runtime.fixture.v1"]["dimensions"]["budget"]["unit"] = "unregistered"
290+ with self.assertRaises(ValueError):
291+ ScopeRegistry(missing)
292+ with self.assertRaises(ValueError):
293+ ScopeRegistry(definitions)
294+ 
295+ def test_registry_cannot_load_handlers_from_data(self):
296+ for kind in ["unregistered", "path.to.callable", "../handler", "__import__", []]:
297+ with self.subTest(kind=kind), self.assertRaises(ValueError):
298+ ScopeRegistry({"runtime.fixture.v1": {
299+ "description": "拒绝未登记处理器", "domains": ["runtime"],
300+ "dimensions": {"mode": {"kind": kind}},
301+ }})
302+ 
303+ def test_matcher_must_return_valid_decision_reasons(self):
304+ for reasons in [None, [{}], [{"state": "match", "reason": "wrong"}],
305+ [{"state": "unknown", "reason": ""}]]:
306+ with self.subTest(reasons=reasons), self.assertRaises(ValueError):
307+ DimensionMatch(reasons)
308+ definitions = {"runtime.fixture.v1": {
309+ "description": "错误返回值夹具", "domains": ["runtime"],
310+ "dimensions": {"budget": {"kind": "fixture_capacity", "unit": "fixture_units"}},
311+ }}
312+ with patch.dict(DIMENSION_MATCHERS, {"fixture_capacity": CapacityFixture}):
313+ registry = ScopeRegistry(definitions)
314+ value = registry.normalize(fixture_scope(budget={"capacity": 1}))
315+ with patch.object(CapacityFixture, "compare", return_value=None), self.assertRaises(ValueError):
316+ registry.compare(value, value)
317+ 
318+ def test_partial_scope_can_exclude_despite_another_unknown_dimension(self):
319+ registry = Contract(ROOT).scope_registry
320+ card = {"schema": "model.inference.v1", "any_of": [{"all_of": {"phase": ["prefill"]}}]}
321+ for phase, expected in [("prefill", "unknown"), ("decode", "conflict")]:
322+ query = {"schema": "model.inference.v1", "any_of": [{"all_of": {
323+ "phase": [phase], "execution_mode": ["ge_graph"],
324+ }}]}
325+ self.assertEqual(expected, registry.compare(registry.normalize(query, query=True), card)["state"])
326+ self.assertEqual("unknown", registry.compare(registry.normalize(card, query=True), None)["state"])
327+ 
328+ def test_bindings_are_explicit_and_do_not_spread_across_routes(self):
329+ registry = ScopeRegistry({"runtime.fixture.v1": {
330+ "description": "显式绑定夹具", "domains": ["runtime"], "routes": ["model/inference"],
331+ "dimensions": {"mode": {"kind": "enum", "values": ["sync"]}},
332+ }})
333+ self.assertTrue(registry.applies_to("runtime.fixture.v1", "runtime", None))
334+ self.assertTrue(registry.applies_to("runtime.fixture.v1", "model", "inference"))
335+ self.assertFalse(registry.applies_to("runtime.fixture.v1", "model", "training"))
336+ self.assertFalse(registry.applies_to("runtime.fixture.v1", "runtime", "inference"))
337+ for bindings in [{}, {"domains": []}, {"domains": "runtime"}, {"routes": []}]:
338+ with self.subTest(bindings=bindings), self.assertRaises(ValueError):
339+ ScopeRegistry({"runtime.fixture.v1": {"description": "非法绑定", "dimensions": {
340+ "mode": {"kind": "enum", "values": ["sync"]}}, **bindings}})
341+ 
342+ 
343+class ScopeGovernanceWorkflowTests(unittest.TestCase):
344+ def setUp(self):
345+ self.temporary = tempfile.TemporaryDirectory()
346+ self.addCleanup(self.temporary.cleanup)
347+ self.root = Path(self.temporary.name)
348+ make_root(self.root)
349+ self.contract = Contract(self.root)
350+ self.metadata = {
351+ "type": "Concept", "title": "ScopeGovernanceProbe 场景夹具",
352+ "description": "独立验证可选适用条件及旧消费流程。",
353+ "tags": ["task.lookup", "topic.architecture"], "status": "draft",
354+ "sources": [], "platforms": ["a2"],
355+ "created_at": "2026-08-18T00:00:00Z", "updated_at": "2026-08-18T00:00:00Z",
356+ }
357+ self.relative = Path("knowledge/model/inference/concepts/probe.md")
358+ self.body = "# ScopeGovernanceProbe 场景夹具\n\n用于治理回归,不包含设备技术结论。\n"
359+ 
360+ def write_card(self, value):
361+ card = self.root / self.relative
362+ card.parent.mkdir(parents=True, exist_ok=True)
363+ card.write_text("---\n" + yaml.safe_dump(value, allow_unicode=True) + "---\n\n" + self.body)
364+ return card
365+ 
366+ def run_ingest(self, *arguments):
367+ return subprocess.run([
368+ sys.executable, str(ROOT / ".agents/skills/knowledge-ingest/scripts/knowledge_ingest.py"),
369+ "--knowledge-root", str(self.root), *arguments,
370+ ], capture_output=True, text=True)
371+ 
372+ def test_optional_partial_scope_and_supported_profiles(self):
373+ profiles = [
374+ ("concepts/probe.md", "Concept"), ("guides/probe.md", "Guide"),
375+ ("optimizations/rules/probe.md", "Optimization"),
376+ ("runbooks/performance/probe.md", "Runbook"),
377+ ]
378+ for tail, kind in profiles:
379+ relative = Path("knowledge/model/inference") / tail
380+ for declared in (None, scope(phase=["prefill"])):
381+ value = {**self.metadata, "type": kind}
382+ if declared is not None:
383+ value["scope"] = declared
384+ with self.subTest(profile=tail, scope=declared):
385+ self.assertEqual([], self.contract.validate_card(relative, value, self.body))
386+ 
387+ def test_unsupported_profile_wrong_route_and_unknown_fields_are_rejected(self):
388+ value = {**self.metadata, "scope": scope(phase=["prefill"])}
389+ for relative, kind, expected in [
390+ ("knowledge/model/inference/apis/probe.md", "API", "frontmatter.extra"),
391+ ("knowledge/model/inference/examples/probe.md", "Example", "frontmatter.extra"),
392+ ("knowledge/model/training/concepts/probe.md", "Concept", "scope.invalid"),
393+ ("knowledge/ops/ascendc/concepts/probe.md", "Concept", "scope.invalid"),
394+ ]:
395+ issues = self.contract.validate_card(Path(relative), {**value, "type": kind}, self.body)
396+ self.assertIn(expected, {item.code for item in issues})
397+ value["unexpected_field"] = True
398+ self.assertIn("frontmatter.schema", {item.code for item in self.contract.validate_card(self.relative, value, self.body)})
399+ 
400+ def test_legacy_experience_paths_remain_valid_until_knowledge_migration(self):
401+ for section, kind in [("guides/experience/interventions", "Guide"),
402+ ("optimizations/experience/rules", "Optimization")]:
403+ relative = Path("knowledge/model/inference") / section
404+ self.contract.derive_card_path(relative / "probe.md")
405+ self.assertEqual([], self.contract.validate_index_path(relative / "index.md"))
406+ value = {**self.metadata, "type": kind, "scope": scope(phase=["prefill"])}
407+ self.assertEqual([], self.contract.validate_card(relative / "probe.md", value, self.body))
408+ 
409+ def test_direct_domain_schema_is_explicit_and_still_obeys_profile_boundaries(self):
410+ path = self.root / "governance/schemas/registries.yaml"
411+ registry = yaml.safe_load(path.read_text())
412+ registry["scope_schemas"]["runtime.fixture.v1"] = {
413+ "description": "直接领域夹具", "domains": ["runtime"],
414+ "dimensions": {"mode": {"kind": "enum", "values": ["sync", "async"]}},
415+ }
416+ path.write_text(yaml.safe_dump(registry, allow_unicode=True))
417+ contract = Contract(self.root)
418+ value = {**self.metadata, "scope": fixture_scope(mode=["sync"])}
419+ self.assertEqual([], contract.validate_card(Path("knowledge/runtime/concepts/probe.md"), value, self.body))
420+ issues = contract.validate_card(self.relative, value, self.body)
421+ self.assertIn("scope.invalid", {issue.code for issue in issues})
422+ registry["scope_schemas"]["runtime.fixture.v1"]["domains"] = ["model"]
423+ path.write_text(yaml.safe_dump(registry, allow_unicode=True))
424+ with self.assertRaises(ValueError):
425+ Contract(self.root)
426+ 
427+ def test_index_preserves_scope_without_changing_unscoped_query_scores(self):
428+ value = {**self.metadata, "status": "stable", "sources": [{"resource": "https://example.com/scope-governance"}]}
429+ self.write_card(value)
430+ synced = self.run_ingest("sync-indexes")
431+ self.assertEqual(0, synced.returncode, synced.stdout + synced.stderr)
432+ def search():
433+ built = run_index(self.root, "build")
434+ self.assertEqual(0, built.returncode, built.stdout + built.stderr)
435+ result = subprocess.run([
436+ sys.executable, str(QUERY), "search", "--knowledge-root", str(self.root),
437+ "--query", "ScopeGovernanceProbe", "--platform", "a2", "--domain", "model",
438+ "--technology", "inference", "--match", "literal", "--json", "--k", "5",
439+ ], capture_output=True, text=True)
440+ self.assertEqual(0, result.returncode, result.stderr)
441+ hits = json.loads(result.stdout)["hits"]
442+ self.assertEqual(1, len(hits))
443+ return [(item["path"], item["rank"], item["score"]) for item in hits]
444+ before = search()
445+ value["scope"] = scope(phase=["prefill"])
446+ card = self.write_card(value)
447+ declared = card.read_bytes()
448+ self.assertEqual(before, search())
449+ with sqlite3.connect(self.root / "artifacts/indexes/knowledge.sqlite3") as connection:
450+ stored, = connection.execute("SELECT metadata_json FROM documents WHERE relative_path = ?", (str(self.relative),)).fetchone()
451+ self.assertEqual(value["scope"], json.loads(stored)["scope"])
452+ built = self.run_ingest("build-graph")
453+ self.assertEqual(0, built.returncode, built.stdout + built.stderr)
454+ graph = json.loads((self.root / "artifacts/graphs/graph.json").read_text())
455+ self.assertIn("model/inference/concepts/probe", {node["id"] for node in graph["nodes"]})
456+ self.assertEqual(declared, card.read_bytes())
457+ 
458+ def test_invalid_scope_is_rejected_by_lint_index_and_graph_without_card_writes(self):
459+ value = {**self.metadata, "scope": scope(phase=["not_registered"])}
460+ card = self.write_card(value)
461+ before = card.read_bytes()
462+ lint = subprocess.run([
463+ sys.executable, str(ROOT / ".agents/skills/knowledge-lint/scripts/knowledge_lint.py"),
464+ "--knowledge-root", str(self.root), "--card", str(self.relative), "--json",
465+ ], capture_output=True, text=True)
466+ self.assertEqual(1, lint.returncode, lint.stdout + lint.stderr)
467+ self.assertIn("scope.invalid", {item["code"] for item in json.loads(lint.stdout)["findings"]})
468+ for result in (run_index(self.root, "build"), self.run_ingest("build-graph")):
469+ self.assertNotEqual(0, result.returncode)
470+ self.assertIn("scope.invalid", result.stdout + result.stderr)
471+ self.assertEqual(before, card.read_bytes())
472+ self.assertFalse((self.root / "artifacts/graphs/graph.json").exists())
473+ 
474+ def test_external_root_cannot_execute_a_replacement_scope_handler(self):
475+ marker = self.root / "executed"
476+ target = self.root / "governance/contracts/scope_matchers.py"
477+ target.write_text(f"from pathlib import Path\nPath({str(marker)!r}).touch()\n")
478+ result = run_index(self.root, "build")
479+ self.assertEqual(2, result.returncode, result.stderr)
480+ self.assertIn("target governance contract does not match", result.stderr)
481+ self.assertFalse(marker.exists())
482+ 
483+ 
484+if __name__ == "__main__":
485+ unittest.main()
@@ -32,6 +32,7 @@ from . import (
32 path,32 path,
33 platforms,33 platforms,
34 profile,34 profile,
35+ scope,
35 source_navigation,36 source_navigation,
36 sources,37 sources,
37 tags,38 tags,
@@ -105,6 +106,7 @@ class Check:
105CHECKS: tuple[Check, ...] = (106CHECKS: tuple[Check, ...] = (
106 Check("path", "P0", document=path.check, card=path.check),107 Check("path", "P0", document=path.check, card=path.check),
107 Check("profile", "P0", document=profile.check, card=profile.check),108 Check("profile", "P0", document=profile.check, card=profile.check),
109+ Check("scope", "P0", document=scope.check, card=scope.check),
108 Check("body", "P0", document=body.check, card=body.check_card),110 Check("body", "P0", document=body.check, card=body.check_card),
109 Check("images", "P0", document=images.check, card=images.check),111 Check("images", "P0", document=images.check, card=images.check),
110 Check("tags", "P0", document=tags.check, card=tags.check),112 Check("tags", "P0", document=tags.check, card=tags.check),
@@ -0,0 +1,32 @@
1+# ----------------------------------------------------------------------------------------------------------
2+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4+# CANN Open Software License Agreement Version 2.0 (the "License").
5+# Please refer to the License for details. You may not use this file except in compliance with the License.
6+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+# See LICENSE in the root of the software repository for the full text of the License.
9+# ----------------------------------------------------------------------------------------------------------
10+ 
11+"""Validate optional applicability metadata with the shared Scope contract."""
12+ 
13+from __future__ import annotations
14+ 
15+from ..knowledge import Issue
16+ 
17+ 
18+def check(contract, document, context) -> list[Issue]:
19+ del context
20+ if document.metadata is None or document.path_info is None:
21+ return []
22+ if "scope" not in document.metadata:
23+ return []
24+ try:
25+ value = contract.scope_registry.normalize(document.metadata["scope"])
26+ info = document.path_info
27+ binding = f"{info.domain}/{info.route}"
28+ if not contract.scope_registry.applies_to(value["schema"], info.domain, info.route):
29+ raise ValueError(f"scope schema {value['schema']} is not registered for {binding}")
30+ except ValueError as exc:
31+ return [Issue("scope.invalid", str(exc))]
32+ return []
@@ -23,6 +23,7 @@ from yaml.constructor import ConstructorError
23from yaml.nodes import MappingNode23from yaml.nodes import MappingNode
24 24 
25from .schema import SchemaValidator25from .schema import SchemaValidator
26+from .scope import ScopeRegistry
26 27 
27 28 
28ID_RE = re.compile(r"^[a-z0-9_-]+$")29ID_RE = re.compile(r"^[a-z0-9_-]+$")
@@ -139,6 +140,23 @@ class Contract:
139 self.operator_families = set(self.operator_categories)140 self.operator_families = set(self.operator_categories)
140 self.runbook_areas = self._string_set("runbook_areas")141 self.runbook_areas = self._string_set("runbook_areas")
141 self._validate_registry_contract()142 self._validate_registry_contract()
143+ self.scope_registry = ScopeRegistry(self.registries.get("scope_schemas", {}))
144+ for namespace, definition in self.scope_registry.definitions.items():
145+ for domain in definition.get("domains", []):
146+ if domain not in self.domains or not self.domains[domain].get("direct_types"):
147+ raise ValueError(f"{namespace} has an unregistered direct-profile domain: {domain}")
148+ for binding in definition.get("routes", []):
149+ parts = binding.split("/")
150+ if len(parts) != 2:
151+ raise ValueError(f"{namespace} requires domain/route bindings")
152+ domain, route = parts
153+ registered = {**self.technologies, **self.scopes}.get(route, {})
154+ if domain not in self.domains or domain not in registered.get("domains", []):
155+ raise ValueError(f"{namespace} has an unregistered route: {binding}")
156+ 
157+ def parse_scope(self, text: str, *, query: bool = False) -> dict[str, Any]:
158+ """Reject duplicate keys for JSON/YAML Scope inputs through one parser."""
159+ return self.scope_registry.normalize(_yaml_load(text), query=query)
142 160 
143 def _section(self, name: str) -> dict[str, Any]:161 def _section(self, name: str) -> dict[str, Any]:
144 value = self.registries.get(name)162 value = self.registries.get(name)
@@ -0,0 +1,121 @@
1+# ----------------------------------------------------------------------------------------------------------
2+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4+# CANN Open Software License Agreement Version 2.0 (the "License").
5+# Please refer to the License for details. You may not use this file except in compliance with the License.
6+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+# See LICENSE in the root of the software repository for the full text of the License.
9+# ----------------------------------------------------------------------------------------------------------
10+ 
11+"""Registry-driven applicability validation and three-valued Scope matching."""
12+ 
13+from __future__ import annotations
14+ 
15+import re
16+from typing import Any
17+ 
18+from .scope_matchers import (
19+ DIMENSION_MATCHERS, DimensionMatch, DimensionMatcher, VALUE_RE,
20+ object_fields as _object, string_list as _strings,
21+)
22+ 
23+NAMESPACE_RE = re.compile(r"^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+\.v[1-9][0-9]*$")
24+class ScopeRegistry:
25+ """Shared branch protocol; domains select their own dimension handlers."""
26+ 
27+ def __init__(self, definitions: Any):
28+ if not isinstance(definitions, dict):
29+ raise ValueError("registries.scope_schemas must be an object")
30+ self.definitions = definitions
31+ self.matchers: dict[tuple[str, str], DimensionMatcher] = {}
32+ for namespace, definition in definitions.items():
33+ if not isinstance(namespace, str) or not NAMESPACE_RE.fullmatch(namespace):
34+ raise ValueError(f"invalid scope schema: {namespace}")
35+ _object(definition, {"description", "routes", "domains", "dimensions"}, namespace)
36+ if not isinstance(definition.get("description"), str) or not definition["description"].strip():
37+ raise ValueError(f"{namespace} requires a description")
38+ if not any(key in definition for key in ("routes", "domains")):
39+ raise ValueError(f"{namespace} requires routes or direct-profile domains")
40+ for key in ("routes", "domains"):
41+ if key in definition:
42+ _strings(definition[key], f"{namespace}.{key}")
43+ dimensions = definition.get("dimensions")
44+ if not isinstance(dimensions, dict) or not dimensions:
45+ raise ValueError(f"{namespace}.dimensions must be a non-empty object")
46+ for name, dimension in dimensions.items():
47+ if not isinstance(name, str) or not VALUE_RE.fullmatch(name):
48+ raise ValueError(f"invalid scope dimension: {name}")
49+ if not isinstance(dimension, dict):
50+ raise ValueError(f"{namespace}.{name} must be an object")
51+ kind = dimension.get("kind")
52+ if not isinstance(kind, str) or kind not in DIMENSION_MATCHERS:
53+ raise ValueError(f"{namespace}.{name} has an unregistered matcher kind: {kind}")
54+ self.matchers[namespace, name] = DIMENSION_MATCHERS[kind](dimension, f"{namespace}.{name}")
55+ 
56+ def applies_to(self, namespace: str, domain: str, route: str | None) -> bool:
57+ definition = self.definitions[namespace]
58+ if route is None:
59+ return domain in definition.get("domains", [])
60+ return f"{domain}/{route}" in definition.get("routes", [])
61+ 
62+ def normalize(self, value: Any, *, query: bool = False) -> dict[str, Any]:
63+ value = _object(value, {"schema", "any_of"}, "scope")
64+ namespace = value.get("schema")
65+ if not isinstance(namespace, str) or namespace not in self.definitions:
66+ raise ValueError(f"unknown scope schema: {namespace}")
67+ profiles = value.get("any_of")
68+ if not isinstance(profiles, list) or not profiles:
69+ raise ValueError("scope.any_of must be a non-empty list")
70+ dimensions = self.definitions[namespace]["dimensions"]
71+ normalized = []
72+ for index, profile in enumerate(profiles):
73+ location = f"scope.any_of[{index}]"
74+ profile = _object(profile, {"all_of"}, location)
75+ conditions = profile.get("all_of")
76+ if not isinstance(conditions, dict) or not conditions:
77+ raise ValueError(f"{location}.all_of must be a non-empty object")
78+ if any(not isinstance(key, str) for key in conditions):
79+ raise ValueError(f"{location}.all_of keys must be strings")
80+ result = {}
81+ for name, condition in sorted(conditions.items()):
82+ if name not in dimensions:
83+ raise ValueError(f"unknown scope dimension in {namespace}: {name}")
84+ target = f"{location}.all_of.{name}"
85+ result[name] = self.matchers[namespace, name].normalize(condition, query=query, location=target)
86+ entry = {"all_of": result}
87+ if entry in normalized:
88+ raise ValueError("scope.any_of contains equivalent duplicate branches")
89+ normalized.append(entry)
90+ return {"schema": namespace, "any_of": normalized}
91+ 
92+ def compare(self, query: dict[str, Any], card: Any) -> dict[str, Any]:
93+ """Compare canonical queries; never merge conditions across branches."""
94+ if card is None:
95+ return {"state": "unknown", "reason": "missing_card_scope", "profiles": []}
96+ card = self.normalize(card)
97+ if card["schema"] != query["schema"]:
98+ return {"state": "unknown", "reason": "schema_mismatch", "profiles": []}
99+ pairs = []
100+ for qi, query_profile in enumerate(query["any_of"]):
101+ for ci, card_profile in enumerate(card["any_of"]):
102+ known, required = query_profile["all_of"], card_profile["all_of"]
103+ reasons = []
104+ intersections = {}
105+ for name in sorted(set(known) | set(required)):
106+ qvalue, cvalue = known.get(name), required.get(name)
107+ if cvalue is None:
108+ reasons.append({"state": "unknown", "dimension": name, "reason": "missing_card_dimension"})
109+ else:
110+ result = self.matchers[query["schema"], name].compare(qvalue, cvalue)
111+ if not isinstance(result, DimensionMatch):
112+ raise ValueError(f"Scope matcher for {name} returned an invalid result")
113+ reasons.extend({**reason, "dimension": name} for reason in result.reasons)
114+ if result.matched_values is not None:
115+ intersections[name] = result.matched_values
116+ states = {item["state"] for item in reasons}
117+ state = "conflict" if "conflict" in states else "unknown" if states else "match"
118+ pairs.append({"query_profile": qi, "card_profile": ci, "state": state, "matched_values": intersections, "reasons": reasons})
119+ states = {pair["state"] for pair in pairs}
120+ state = "match" if "match" in states else "unknown" if "unknown" in states else "conflict"
121+ return {"state": state, "profiles": pairs}
@@ -0,0 +1,173 @@
1+# ----------------------------------------------------------------------------------------------------------
2+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4+# CANN Open Software License Agreement Version 2.0 (the "License").
5+# Please refer to the License for details. You may not use this file except in compliance with the License.
6+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+# See LICENSE in the root of the software repository for the full text of the License.
9+# ----------------------------------------------------------------------------------------------------------
10+ 
11+"""Trusted dimension handlers; Registry selects kinds, never executable modules."""
12+ 
13+from __future__ import annotations
14+ 
15+from collections.abc import Callable
16+from dataclasses import dataclass, field
17+import re
18+from typing import Any, Protocol
19+ 
20+ 
21+VALUE_RE = re.compile(r"^[a-z][a-z0-9_]*$")
22+ 
23+ 
24+def object_fields(value: Any, allowed: set[str], location: str) -> dict[str, Any]:
25+ if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
26+ raise ValueError(f"{location} must be an object with string keys")
27+ extra = set(value) - allowed
28+ if extra:
29+ raise ValueError(f"{location} has unknown fields: {', '.join(sorted(extra))}")
30+ return value
31+ 
32+ 
33+def string_list(value: Any, location: str) -> list[str]:
34+ if (
35+ not isinstance(value, list)
36+ or not value
37+ or any(not isinstance(item, str) or not item.strip() for item in value)
38+ ):
39+ raise ValueError(f"{location} must be a non-empty string list")
40+ if len(value) != len(set(value)):
41+ raise ValueError(f"{location} contains duplicates")
42+ return value
43+ 
44+ 
45+@dataclass
46+class DimensionMatch:
47+ """Empty reasons mean match; otherwise conflict wins over unknown."""
48+ 
49+ reasons: list[dict[str, Any]] = field(default_factory=list)
50+ matched_values: Any = None
51+ 
52+ def __post_init__(self):
53+ if not isinstance(self.reasons, list) or any(
54+ not isinstance(reason, dict)
55+ or reason.get("state") not in {"unknown", "conflict"}
56+ or not isinstance(reason.get("reason"), str)
57+ or not reason["reason"].strip()
58+ for reason in self.reasons
59+ ):
60+ raise ValueError("Scope matcher must return explicit unknown/conflict reasons")
61+ 
62+ 
63+class DimensionMatcher(Protocol):
64+ """A kind owns definition validation, canonical values and comparison semantics."""
65+ 
66+ def normalize(self, value: Any, *, query: bool, location: str) -> Any: ...
67+ 
68+ def compare(self, query: Any, card: Any) -> DimensionMatch: ...
69+ 
70+ 
71+class _Choices:
72+ def __init__(self, definition: dict[str, Any], location: str):
73+ object_fields(definition, {"kind", "values", "aliases", "query_none_of", "card_none_of"}, location)
74+ self.definition = definition
75+ self.values = string_list(definition.get("values"), f"{location}.values")
76+ if any(not VALUE_RE.fullmatch(item) or item == "any" for item in self.values):
77+ raise ValueError(f"{location} values must be canonical IDs, excluding any")
78+ for permission in ("query_none_of", "card_none_of"):
79+ negative = definition.get(permission, False)
80+ if not isinstance(negative, bool) or (negative and definition["kind"] != "required_set"):
81+ raise ValueError(f"{location}.{permission} requires a required_set")
82+ aliases = definition.get("aliases", {})
83+ if not isinstance(aliases, dict):
84+ raise ValueError(f"{location}.aliases must be an object")
85+ self.aliases = {item.casefold(): item for item in self.values}
86+ for alias, canonical in aliases.items():
87+ if not isinstance(alias, str) or not alias.strip() or alias.casefold() == "any":
88+ raise ValueError(f"invalid alias in {location}")
89+ if not isinstance(canonical, str) or canonical not in self.values:
90+ raise ValueError(f"unknown alias target in {location}: {canonical}")
91+ key = alias.casefold()
92+ if key in self.aliases and self.aliases[key] != canonical:
93+ raise ValueError(f"ambiguous alias in {location}: {alias}")
94+ self.aliases[key] = canonical
95+ 
96+ def _values(self, raw: Any, *, query: bool, location: str, allow_any: bool = False) -> list[str]:
97+ items = string_list(raw, location)
98+ if allow_any and items == ["any"]:
99+ return items
100+ canonical = []
101+ for item in items:
102+ candidate = self.aliases.get(item.casefold()) if query else item
103+ if candidate not in self.values:
104+ raise ValueError(f"unknown scope value at {location}: {item}")
105+ canonical.append(candidate)
106+ if len(set(canonical)) != len(canonical):
107+ raise ValueError(f"{location} contains equivalent duplicate values")
108+ return sorted(canonical)
109+ 
110+ 
111+def _missing_query(card: Any) -> DimensionMatch:
112+ return DimensionMatch([{"state": "unknown", "reason": "missing_query_condition", "required": card}])
113+ 
114+ 
115+class EnumMatcher(_Choices):
116+ """Alternatives match on intersection; only an explicit card any is universal."""
117+ 
118+ def normalize(self, value: Any, *, query: bool, location: str) -> list[str]:
119+ return self._values(value, query=query, location=location, allow_any=True)
120+ 
121+ def compare(self, query: Any, card: list[str]) -> DimensionMatch:
122+ if card == ["any"]:
123+ return DimensionMatch()
124+ if query is None or query == ["any"]:
125+ return _missing_query(card)
126+ overlap = sorted(set(query) & set(card))
127+ if overlap:
128+ return DimensionMatch(matched_values=overlap)
129+ return DimensionMatch([{"state": "conflict", "reason": "disjoint_values", "query": query, "required": card}])
130+ 
131+ 
132+class RequiredSetMatcher(_Choices):
133+ """Presence and absence each require explicit query evidence."""
134+ 
135+ def normalize(self, value: Any, *, query: bool, location: str) -> dict[str, list[str]]:
136+ allowed = {"all_of"}
137+ if self.definition.get("query_none_of" if query else "card_none_of"):
138+ allowed.add("none_of")
139+ value = object_fields(value, allowed, location)
140+ if not value:
141+ raise ValueError(f"{location} requires all_of or an allowed none_of")
142+ result = {key: self._values(items, query=query, location=location) for key, items in sorted(value.items())}
143+ if set(result.get("all_of", [])) & set(result.get("none_of", [])):
144+ raise ValueError(f"{location} requires and excludes the same feature")
145+ return result
146+ 
147+ def compare(self, query: Any, card: dict[str, list[str]]) -> DimensionMatch:
148+ if query is None:
149+ return _missing_query(card)
150+ needed = set(card.get("all_of", []))
151+ forbidden = set(card.get("none_of", []))
152+ enabled = set(query.get("all_of", []))
153+ disabled = set(query.get("none_of", []))
154+ reasons = []
155+ absent, missing = needed & disabled, needed - enabled
156+ if absent:
157+ reasons.append({"state": "conflict", "reason": "excluded_required_features", "features": sorted(absent)})
158+ elif missing:
159+ reasons.append({"state": "unknown", "reason": "missing_query_features", "features": sorted(missing)})
160+ prohibited, unconfirmed = forbidden & enabled, forbidden - disabled
161+ if prohibited:
162+ reasons.append({"state": "conflict", "reason": "present_forbidden_features", "features": sorted(prohibited)})
163+ elif unconfirmed:
164+ reasons.append({"state": "unknown", "reason": "missing_query_absences", "features": sorted(unconfirmed)})
165+ return DimensionMatch(reasons)
166+ 
167+ 
168+# Extend only in the installed Contract package with reviewed, deterministic code.
169+# Registry/card/query data cannot name a Python module, import path or executable.
170+DIMENSION_MATCHERS: dict[str, Callable[[dict[str, Any], str], DimensionMatcher]] = {
171+ "enum": EnumMatcher,
172+ "required_set": RequiredSetMatcher,
173+}
@@ -29,6 +29,25 @@
29 "created_at": {"type": "string", "format": "date-time"},29 "created_at": {"type": "string", "format": "date-time"},
30 "updated_at": {"type": "string", "format": "date-time"},30 "updated_at": {"type": "string", "format": "date-time"},
31 "platforms": {"type": "array", "uniqueItems": true, "items": {"type": "string"}},31 "platforms": {"type": "array", "uniqueItems": true, "items": {"type": "string"}},
32+ "scope": {
33+ "type": "object",
34+ "additionalProperties": false,
35+ "required": ["schema", "any_of"],
36+ "properties": {
37+ "schema": {"type": "string", "minLength": 1},
38+ "any_of": {
39+ "type": "array",
40+ "minItems": 1,
41+ "uniqueItems": true,
42+ "items": {
43+ "type": "object",
44+ "additionalProperties": false,
45+ "required": ["all_of"],
46+ "properties": {"all_of": {"type": "object"}}
47+ }
48+ }
49+ }
50+ },
32 "verified": {51 "verified": {
33 "oneOf": [52 "oneOf": [
34 {53 {
@@ -22,7 +22,7 @@ profiles:
22 concepts:22 concepts:
23 okf_type: Concept23 okf_type: Concept
24 required: [platforms]24 required: [platforms]
25- optional: [cann_versions, aliases, verified, replaced_by]25+ optional: [cann_versions, aliases, verified, replaced_by, scope]
26 apis:26 apis:
27 okf_type: API27 okf_type: API
28 required: [platforms]28 required: [platforms]
@@ -34,7 +34,7 @@ profiles:
34 guides:34 guides:
35 okf_type: Guide35 okf_type: Guide
36 required: [platforms]36 required: [platforms]
37- optional: [cann_versions, aliases, verified, replaced_by]37+ optional: [cann_versions, aliases, verified, replaced_by, scope]
38 operators:38 operators:
39 okf_type: Operator39 okf_type: Operator
40 required: [platforms]40 required: [platforms]
@@ -42,11 +42,11 @@ profiles:
42 optimizations:42 optimizations:
43 okf_type: Optimization43 okf_type: Optimization
44 required: [platforms]44 required: [platforms]
45- optional: [cann_versions, verified, replaced_by]45+ optional: [cann_versions, verified, replaced_by, scope]
46 runbooks:46 runbooks:
47 okf_type: Runbook47 okf_type: Runbook
48 required: [platforms]48 required: [platforms]
49- optional: [aliases, verified, replaced_by]49+ optional: [aliases, verified, replaced_by, scope]
50 interoperability:50 interoperability:
51 okf_type: Interoperability51 okf_type: Interoperability
52 required: [platforms, source_technology, target_technology]52 required: [platforms, source_technology, target_technology]
@@ -90,6 +90,58 @@ scopes:
90 description: 具身智能模型与系统知识90 description: 具身智能模型与系统知识
91 domains: [model]91 domains: [model]
92 92 
93+scope_schemas:
94+ # Scope 可选且可部分声明;各领域独立注册维度与匹配 kind,不以填写率验收。
95+ # routes 绑定 domain/route;可选 domains 仅绑定领域的无 route 直接 Profile。
96+ # kind 只能选择安装的 Contract 中受审查的处理器,不接受模块路径或表达式。
97+ model.inference.v1:
98+ description: 模型推理知识的适用条件,查询仅排除明确冲突并保留未知项
99+ routes: [model/inference]
100+ dimensions:
101+ phase:
102+ kind: enum
103+ values: [prefill, decode]
104+ aliases: {预填充: prefill, 解码: decode}
105+ objective:
106+ kind: enum
107+ values: [throughput, latency, ttft, tpot, memory, bringup]
108+ aliases: {吞吐: throughput, 时延: latency, 显存: memory}
109+ context_length:
110+ kind: enum
111+ values: [short_context, long_context, ultra_long_context]
112+ aliases: {短上下文: short_context, 长上下文: long_context, 超长上下文: ultra_long_context}
113+ batch_size:
114+ kind: enum
115+ values: [small_batch, large_batch]
116+ concurrency:
117+ kind: enum
118+ values: [single_request, high_concurrency]
119+ model_features:
120+ kind: required_set
121+ values: [dense, moe, mla, dsa, dit, multimodal]
122+ query_none_of: true
123+ execution_mode:
124+ kind: enum
125+ values: [eager, ge_graph, npugraph_ex]
126+ aliases: {GE: ge_graph}
127+ quantization:
128+ kind: enum
129+ values: [bf16, w8a8, w8a8c8, fp8, mxfp4]
130+ parallelism:
131+ kind: required_set
132+ values: [tp, ep, dp, cp, kvp]
133+ query_none_of: true
134+ card_none_of: true
135+ serving_mode:
136+ kind: enum
137+ values: [offline, online]
138+ deployment_topology:
139+ kind: required_set
140+ values: [afd, pd_disaggregated, pd_hybrid]
141+ framework:
142+ kind: enum
143+ values: [cann_executor, sglang, vllm]
144+ 
93local_sources:145local_sources:
94 ops_cv:146 ops_cv:
95 description: 固定到算子知识来源提交的 CANN ops-cv 源码147 description: 固定到算子知识来源提交的 CANN ops-cv 源码
@@ -41,18 +41,74 @@ platforms: []
41 41 
42| 路径 Profile | OKF `type` | 额外必选字段 | 可选字段 |42| 路径 Profile | OKF `type` | 额外必选字段 | 可选字段 |
43|---|---|---|---|43|---|---|---|---|
44-| `concepts` | `Concept` | `platforms` | `cann_versions`, `aliases`, `verified`, `replaced_by` |44+| `concepts` | `Concept` | `platforms` | `cann_versions`, `aliases`, `verified`, `replaced_by`, `scope` |
45| `apis` | `API` | `platforms` | `cann_versions`, `source_headers`, `aliases`, `verified`, `replaced_by` |45| `apis` | `API` | `platforms` | `cann_versions`, `source_headers`, `aliases`, `verified`, `replaced_by` |
46| `examples` | `Example` | `platforms` | `cann_versions`, `verified`, `replaced_by` |46| `examples` | `Example` | `platforms` | `cann_versions`, `verified`, `replaced_by` |
47-| `guides` | `Guide` | `platforms` | `cann_versions`, `aliases`, `verified`, `replaced_by` |47+| `guides` | `Guide` | `platforms` | `cann_versions`, `aliases`, `verified`, `replaced_by`, `scope` |
48| `operators` | `Operator` | `platforms` | `cann_versions`, `verified`, `replaced_by` |48| `operators` | `Operator` | `platforms` | `cann_versions`, `verified`, `replaced_by` |
49-| `optimizations` | `Optimization` | `platforms` | `cann_versions`, `verified`, `replaced_by` |49+| `optimizations` | `Optimization` | `platforms` | `cann_versions`, `verified`, `replaced_by`, `scope` |
50-| `runbooks` | `Runbook` | `platforms` | `aliases`, `verified`, `replaced_by` |50+| `runbooks` | `Runbook` | `platforms` | `aliases`, `verified`, `replaced_by`, `scope` |
51| `interoperability` | `Interoperability` | `platforms`, `source_technology`, `target_technology` | `cann_versions`, `verified`, `replaced_by` |51| `interoperability` | `Interoperability` | `platforms`, `source_technology`, `target_technology` | `cann_versions`, `verified`, `replaced_by` |
52| `glossaries` | `Glossary` | 无 | `aliases`, `verified`, `replaced_by` |52| `glossaries` | `Glossary` | 无 | `aliases`, `verified`, `replaced_by` |
53 53 
54新增字段必须先有稳定语义和实际消费者,再同时修改 Schema、Profile、Contract、文档和回归;不能为单张卡增加临时字段。54新增字段必须先有稳定语义和实际消费者,再同时修改 Schema、Profile、Contract、文档和回归;不能为单张卡增加临时字段。
55 55 
56+## 场景适用条件(`scope`)
57+ 
58+`scope` 是可选的适用条件声明,不代替路径、平台、标签、来源或复核事件。字段结构由 Schema 定义,维度类型、值和别名由 Registry 的 `scope_schemas` 定义,统一 Scope Contract 执行维度校验与三态匹配。当前仅注册 `model.inference.v1`,绑定 `model/inference` 路由;字段只用于 Profile 允许的卡片,不为其他领域预注册占位 namespace。
59+ 
60+维护时遵守三条原则:
61+ 
62+1. **可选**:不是每张卡都需要 Scope;没有明确限制或没有过滤价值时可以省略,不以填写率作为卡片质量或迁移完成标准。
63+2. **允许部分声明**:来源支持且能帮助过滤的条件即可提取,不要求全部维度已知。例如只声明 `phase: [prefill]`,Contract 与已确认的 Decode 条件比较时仍能识别冲突;查询额外指定了卡片未声明的执行模式时,比较结果保留为未知。不得为凑齐字段补写 `any`、否定条件或未核实的范围。
64+3. **按领域扩展**:各领域独立定义 namespace、适用范围、维度和匹配语义。公共部分只共享对象外壳、场景 AND/OR 组合与三态结果协议;不要求其他领域使用 Prefill、Decode、MoE 等模型推理字段。已有匹配类型不足时,在受审查的 Contract 处理器中扩展,再由 Registry 选择,不在知识卡中保存可执行判断。注册与权限边界见 [机器契约](schemas.md)。
65+ 
66+以下维度、词表和否定权限属于当前模型推理 Schema;其他领域按各自注册定义使用。
67+ 
68+```yaml
69+scope:
70+ schema: model.inference.v1
71+ any_of:
72+ - all_of:
73+ phase: [prefill]
74+ execution_mode: [ge_graph]
75+ objective: [ttft]
76+ context_length: [long_context]
77+ batch_size: [small_batch]
78+ - all_of:
79+ phase: [decode]
80+ execution_mode: [eager]
81+ model_features:
82+ all_of: [moe, mla]
83+```
84+ 
85+多个场景分支之间是 OR;分支内不同维度是 AND;普通枚举数组内是 OR;`required_set` 的 `all_of` 表示必需集合。`model_features`、`parallelism`、`deployment_topology` 是集合维度。序列长度、batch、并发、服务形态和部署拓扑分别声明,定性值不隐含数值阈值。`objective` 在 Contract 中按精确值匹配;`latency` 不隐含 `ttft` 或 `tpot`,来源支持多个目标时在卡片显式列出,例如 `objective: [latency, tpot]`。
86+ 
87+卡片缺少维度表示未声明,查询缺少卡片所需条件表示前提未确认,两者均不能推导普遍适用。枚举维度的 `[any]` 独占使用,表示来源足以支持结论不受该维度限制;集合维度不接受 `any`。集合否定条件由 Registry 分别声明卡片和查询的准入:`model_features.none_of` 仍仅用于显式已确认的查询,卡片只声明必需模型特征;`parallelism.none_of` 可用于来源支持的卡片和显式已确认的查询,表示列出的并行方式没有启用。未提及某个特征或并行方式仍是未知。
88+ 
89+```yaml
90+scope:
91+ schema: model.inference.v1
92+ any_of:
93+ - all_of:
94+ phase: [prefill]
95+ parallelism:
96+ none_of: [cp]
97+```
98+ 
99+上例要求 Prefill 且未启用 CP,不代表单卡,也不排除 TP、EP 等其他并行。集合内可同时写 `all_of` 与 `none_of`,分别表示必须启用与必须未启用,两者为 AND;`none_of` 列出的每一项都须确认未启用。至少出现一项非空列表,且不能要求和排除同一个值。不能根据来源没有提及某种并行就为卡片补否定条件。
100+ 
101+| 查询已确认条件 | 卡片要求 `parallelism.none_of: [cp]` 时 |
102+| --- | --- |
103+| `parallelism.all_of: [cp]` | `conflict`,明确冲突 |
104+| `parallelism.none_of: [cp]` | 满足这一项限制;其他维度继续核对 |
105+| 只声明 `parallelism.all_of: [tp]` | `unknown`,TP 不证明 CP 已关闭 |
106+| 没有声明 `parallelism` | `unknown`,CP 状态待确认 |
107+ 
108+Scope Contract 的比较结果为 `match`、`unknown` 或 `conflict`。缺少卡片 Scope、缺少必要条件或不同的已注册 namespace 均保留未知,非法 Scope 报错。匹配表示声明条件相容,不替代正文、来源与版本核对,也不证明性能已经验证。
109+ 
110+当前基础治理在加载卡片、Lint 和构图时校验该字段,并提供统一的三态比较接口。Query 的现有公开接口不接受场景 Scope 过滤参数;卡片声明 Scope 不代表检索已启用该过滤。检索参数及结果结构以 Query Skill 的实际能力为准,无 Scope 的原查询流程保持不变。
111+ 
56## 生命周期与信任112## 生命周期与信任
57 113 
58| `status` | 含义 | 默认 Query |114| `status` | 含义 | 默认 Query |
@@ -28,6 +28,7 @@ Contract 直接消费这些声明,只保留现有的可加载性和 `okf_type`
28| `domains` | 顶层领域、允许直辖的 `direct_types` 及是否 `precreate_routes` | 路径校验、domain 过滤、协作骨架 |28| `domains` | 顶层领域、允许直辖的 `direct_types` 及是否 `precreate_routes` | 路径校验、domain 过滤、协作骨架 |
29| `technologies` | DSL、语言、框架或引擎及允许的 domain | route、technology 和跨技术端点校验 |29| `technologies` | DSL、语言、框架或引擎及允许的 domain | route、technology 和跨技术端点校验 |
30| `scopes` | `shared`、平台、工具链、推理/训练等非技术 route | route、scope 和 shared 过滤 |30| `scopes` | `shared`、平台、工具链、推理/训练等非技术 route | route、scope 和 shared 过滤 |
31+| `scope_schemas` | 场景 Scope namespace、适用路径、维度类型、值与别名 | 卡片适用条件校验及 Contract 三态比较;与路径 route 的 `scopes` 分区独立 |
31| `paths` | Operator call mode、family→category 映射和 Runbook area | 受控附加层级校验 |32| `paths` | Operator call mode、family→category 映射和 Runbook area | 受控附加层级校验 |
32| `platforms` | 卡片适用平台 ID | Frontmatter 校验和 platform 过滤 |33| `platforms` | 卡片适用平台 ID | Frontmatter 校验和 platform 过滤 |
33| `tags` | task、topic、paradigm 三个维度的规范标签 | 标签校验、任务映射、过滤和召回 |34| `tags` | task、topic、paradigm 三个维度的规范标签 | 标签校验、任务映射、过滤和召回 |
@@ -46,6 +47,20 @@ technology 与 scope 的 ID 不得重叠;每个 route 必须声明允许出现
46 47 
47新增注册值必须同时满足:现有值无法准确表达需求、归属和语义明确、有真实知识或确定性消费者即将使用,并在同一变更补充接受/拒绝测试和检索回归。若属于 `ops` 或 `model`,还要同步其 route 结构入口;其他领域由维护者随真实内容创建路径。不要只为来源别名、任意占位或一次性任务扩充 Registry。48新增注册值必须同时满足:现有值无法准确表达需求、归属和语义明确、有真实知识或确定性消费者即将使用,并在同一变更补充接受/拒绝测试和检索回归。若属于 `ops` 或 `model`,还要同步其 route 结构入口;其他领域由维护者随真实内容创建路径。不要只为来源别名、任意占位或一次性任务扩充 Registry。
48 49 
50+Scope 的公共对象和场景分支形状由 Frontmatter Schema 定义;各 namespace 的适用范围、维度配置、值与别名由 `scope_schemas` 独立定义。`contracts/scope.py` 组织校验与三态匹配,`contracts/scope_matchers.py` 的受控处理器表按维度 `kind` 分派定义校验、值归一化和比较;卡片加载、Ingest、Lint 与构图共用这一路径,不各自定义语法。Contract 提供比较接口,Query 的场景 Scope 参数及过滤接入由其公开消费能力决定。
51+ 
52+现有处理器为 `enum` 与 `required_set`。枚举使用独占 `[any]`,必需集合使用非空 `all_of`,并由维度的布尔配置 `query_none_of`、`card_none_of` 分别控制查询与卡片能否声明非空 `none_of`。允许否定的集合可只写 `none_of`,或与 `all_of` 同时声明;正反集合不得相交,缺失成员不表示未启用。当前模型推理的 `parallelism` 对卡片和查询均允许否定,`model_features` 仍只允许查询否定,其他维度不自动获得此权限。这些模型字段和权限不约束其他领域的维度设计。
53+ 
54+namespace 至少声明一种非空适用范围:`routes` 绑定合法的 `domain/route`;`domains` 只绑定 Registry 允许直接承载 Profile 的领域及其无 route 路径,例如 Runtime 的直接 Concept 或 Runbook。两者可同时声明,但不会隐式扩展到未列出的 route;Profile 自身仍须允许 `scope`。新增领域应随真实过滤需求登记 namespace 和回归,不为未来领域建立占位 Schema。未注册 schema、处理器类型、非法值或错误路径绑定失败关闭。
55+ 
56+领域新增匹配语义时,在当前安装的 Contract 包内增加确定性处理器并登记到受控表,使用 `normalize` 校验及规范化卡片/查询条件,使用 `compare` 返回 `DimensionMatch`。处理器独立检查其配置与值形状,可以使用不同于枚举、集合的条件结构;必须以明确原因报告未知或冲突,不能把缺证据当作匹配。公共聚合仍保持场景内 AND、场景间 OR、任一明确冲突不被未知掩盖,且不跨 namespace 推断。处理器不得联网、执行上游源码或写知识卡;Registry、知识卡、查询参数及外部知识根不能指定 Python 模块、文件路径或执行任意表达式。扩展必须覆盖接受、拒绝、未知、原有语义兼容及实际消费者;修改同版本 Contract 后由维护者重建索引。
57+ 
58+Scope 本身始终可选,可只写有来源依据且有过滤价值的部分条件。Lint 不把缺少 Scope 或缺少维度视为质量错误,不以填写率验收;当前 Query 仍使用原有过滤与排序,不能从字段存在推断场景过滤已接入。
59+ 
60+集合匹配分别检查卡片要求启用与未启用的成员。查询明确关闭必需成员、或明确启用卡片禁止的成员时为冲突;任何必需状态未确认时为未知。只要任一必要条件明确冲突,就不能用另一项未知掩盖冲突;多个场景仍逐对匹配,不跨分支合并条件。
61+ 
62+Registry 或 Contract 更新后,原索引按治理指纹失效,必须由维护者重建;Query 不自行重建。旧 receipt 仍按完整 provenance 校验,不跨治理版本自动放行。回退前先移除依赖新 Schema 的声明,再还原同版本治理文件并重建索引;不得让旧消费者静默忽略无法识别的条件。
63+ 
49## 执行关系64## 执行关系
50 65 
51```text66```text
@@ -1,5 +1,34 @@
1# 2026-10-261# 2026-10-26
2 2 
3+## [21:03] check | Scope 基础治理独立分支的提交前验证
4+ 
5+### Summary
6+ 
7+完成 scope_governance_bootstrap 的定向和上库前验证,保留全部存量知识、旧目录与公开 Query 行为。CI 两个检查入口本地通过,完整门禁仍因已确认的存量 Markdown 标题检查退出 1;本条不表示远端 PR 或 CI 已完成。
8+ 
9+### Details
10+ 
11+- bash check.sh 的全库严格 Lint 为 9026 文件、P0/P1/P2 均为 0,712 个导航 stale=0,8314 卡索引 build/full verify 通过;默认 72 题质量门禁 Recall@k=0.560、MRR=0.466 通过,未调整原用例、排序或阈值。
12+- 全量 evals 337 项中 335 通过、1 项来源快照检查因独立工作区未恢复源码而跳过,唯一失败为 test_markdown_structure。按原测试复现的 375 个失败文件均与目标分支 b1f947c14035664ace88e023987e883d829ac4d5 字节一致;未为通过门禁改写无关知识或恢复滚动来源。47 项文档定向回归亦只复现这一失败。
13+- CI FormatCheck 本地复跑为 9026 文件零 Finding,GraphCheck 为 8314 节点、17768 条边并通过;Trace producer 36 项、1908 卡正文 HARD、全 Skill/governance/evals Ruff、Shell 语法和官方镜像 diff-check 均通过。正文检查的 105 张存量缺章节提示保留;两个修改 Skill 的 quick validation 通过。
14+- 8 个核心治理文件与前序兼容实验草稿 SHA-256 全部一致;该实验在 PR143 的知识快照上得到 FormatCheck 9082 文件零 Finding、GraphCheck 8369 节点通过。本轮没有把此结果写成远端 CI 或人工 A/B 已通过。
15+- 完整变更限制为 19 个文件;相对最新目标分支的 knowledge/、.gitcode/、Query、原检索用例、安装/检查入口、LICENSE 与 OAT.xml 差异为空,9026 份知识 Markdown 与目标逐字节一致。未提交 RFC、报告、实验输入输出或其他可重建产物。AGENTS 已重新核对并同步 Scope 长期约束,没有提前禁用 experience 或宣称新增 Query 参数。
16+- 原始检查、字节核对、375 个失败文件的基线证明与 21 输入治理 A/B 记录位于 ignored 的 artifacts/rfc47/scope_bootstrap_20261026。完整 bash check.sh 结果仍为未通过,待 committer 检视;本次授权为推送独立评审分支和新建 PR,不合入 master。
17+ 
18+## [20:53] update | 拆出兼容旧知识的可选 Scope 基础治理
19+ 
20+### Summary
21+ 
22+按用户授权从上游 master b1f947c14035664ace88e023987e883d829ac4d5 建立 scope_governance_bootstrap 独立分支,整理 RFC47 的 Scope 基础治理与说明、回归。知识正文、目录、Query 参数和排序、.gitcode 配置均未修改;本条记录尚不表示已推送或新 PR 已创建。
23+ 
24+### Details
25+ 
26+- 接入可选字段的 Schema 与四类 Profile、model.inference.v1 Registry,以及静态受审查的 enum/required_set 处理器、维度校验和三态比较 Contract。Lint、卡片加载、索引和构图复用同一 Contract;既有 create 与 Query CLI 保持原接口。基础规则不禁用 experience,目录禁用随依赖 PR 的知识迁移生效。
27+- 在 AGENTS、Frontmatter/机器契约 Spec、Ingest/Lint Skill 与参考资料说明 Scope 可选、允许来源支持的部分声明、领域自定义扩展和执行边界;贡献指南说明目标分支受信治理先于依赖知识接入。未把三态比较接口误写成 Query 已启用场景过滤。
28+- 25 项定向回归通过,覆盖匹配决策、AND/OR 分支、未知与冲突、正反集合、字段/Profile/路径边界、直接领域及自定义处理器、外部 Contract 代码拒绝、Lint/索引/构图拒绝非法 Scope,以及声明字段前后无 Scope 查询的路径、排序和分数一致。两个修改 Skill 的 quick validation 与定向 Ruff 通过。
29+- 确定性 A/B 使用同一 21 个输入比较原版与新 Contract:6 个合法 Scope 声明由拒绝变为接受,9 个非法输入全部拒绝,7 个无 Scope 输入的接受/拒绝决策不变,新版 21/21 符合预期。此结果是治理接受/拒绝实验,不是人工适用性、设备性能或检索质量提升。
30+- 原始定向测试、固定输入、A/B 输出及后续提交前检查材料位于 ignored 的 artifacts/rfc47/scope_bootstrap_20261026,不提交生成报告或本地 RFC。完整 bash check.sh、CI FormatCheck/GraphCheck 与文档检查正在运行,未将其写为已通过。
31+ 
3## [17:15] docs | 新增精炼规约预案(refinery)32## [17:15] docs | 新增精炼规约预案(refinery)
4 33 
5### Summary34### Summary