Merged
feat(governance): 接入兼容旧知识的可选 Scope 基础契约 #171
汪超Add on 13 days ago
feat(governance): 接入兼容旧知识的可选 Scope 基础契约 #171
Merged
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 | ## Trust | 29 | ## 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 | |||
| 33 | 4. 修改 Skill 时运行 skill-creator 的 quick validation、相关回归和本仓统一质量门禁。 | 33 | 4. 修改 Skill 时运行 skill-creator 的 quick validation、相关回归和本仓统一质量门禁。 |
| 34 | 5. 用户要提交或整理问题时,按 [Issue 报告](references/issue-report.md)收集已有运行材料与实际复现率;补充复现仍受当前任务授权和本 Skill 只读边界约束。关键材料缺失则输出 `needs-info`,不凭记忆补齐或虚增运行次数。 | 34 | 5. 用户要提交或整理问题时,按 [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 | ||
| 42 | Schema、Profile、Registry、Contract、Spec、Skill 或测试若互相矛盾,不要自行选择其中一份绕过。先定位唯一事实源,再在同一变更中修正执行逻辑、文档和回归。 | 42 | Schema、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 | ||
| 46 | 1. 先用 `knowledge-query` 搜索同一主题,阅读候选卡和来源,再明确采用 create、update、merge、split、deprecate 或 defer。不要只因标题不同就新建卡片,也不要自动覆盖已有卡片。 | 48 | 1. 先用 `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: | |||
| 105 | CHECKS: tuple[Check, ...] = ( | 106 | CHECKS: 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 | |||
| 23 | from yaml.nodes import MappingNode | 23 | from yaml.nodes import MappingNode |
| 24 | 24 | ||
| 25 | from .schema import SchemaValidator | 25 | from .schema import SchemaValidator |
| 26 | +from .scope import ScopeRegistry | ||
| 26 | 27 | ||
| 27 | 28 | ||
| 28 | ID_RE = re.compile(r"^[a-z0-9_-]+$") | 29 | ID_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: Concept | 23 | 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: API | 27 | okf_type: API |
| 28 | required: [platforms] | 28 | required: [platforms] |
| @@ -34,7 +34,7 @@ profiles: | |||
| 34 | guides: | 34 | guides: |
| 35 | okf_type: Guide | 35 | 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: Operator | 39 | okf_type: Operator |
| 40 | required: [platforms] | 40 | required: [platforms] |
| @@ -42,11 +42,11 @@ profiles: | |||
| 42 | optimizations: | 42 | optimizations: |
| 43 | okf_type: Optimization | 43 | 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: Runbook | 47 | 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: Interoperability | 51 | 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 | + | ||
| 93 | local_sources: | 145 | local_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 | ```text | 66 | ```text |
| @@ -1,5 +1,34 @@ | |||
| 1 | # 2026-10-26 | 1 | # 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 | ### Summary | 34 | ### Summary |