pomaster 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMMERCIAL_LICENSE.md +35 -0
- package/LICENSE +83 -0
- package/README.md +396 -0
- package/SECURITY.md +22 -0
- package/TRADEMARKS.md +12 -0
- package/catalog/candidates/candidates-draft.json +198 -0
- package/catalog/candidates/rejected.json +422 -0
- package/catalog/catalog-lock.draft.json +822 -0
- package/catalog/gates/gate.be.api.contract_checks.json +138 -0
- package/catalog/gates/gate.be.chg.contract_change_checks.json +138 -0
- package/catalog/gates/gate.chg.prechange_checks.json +155 -0
- package/catalog/gates/gate.web.api.request_checks.json +149 -0
- package/catalog/gates/gate.web.grid.checks.json +149 -0
- package/catalog/knowledge/knowledge.api.example_stable_error_code.json +80 -0
- package/catalog/knowledge/knowledge.chg.example_compat_window.json +87 -0
- package/catalog/knowledge/knowledge.chg.example_prop_migration.json +87 -0
- package/catalog/knowledge/knowledge.fp.api.field_drift_no_contract.json +83 -0
- package/catalog/knowledge/knowledge.fp.api.per_page_http_client.json +83 -0
- package/catalog/knowledge/knowledge.fp.be.contract_drift.json +87 -0
- package/catalog/knowledge/knowledge.fp.chg.page_local_padding.json +88 -0
- package/catalog/knowledge/knowledge.fp.grid.global_td_width.json +87 -0
- package/catalog/knowledge/knowledge.web.api.example_get_entity_list.json +83 -0
- package/catalog/knowledge/knowledge.web.grid.example_column_preset.json +83 -0
- package/catalog/policies/authority.be.api_contract_ownership.json +77 -0
- package/catalog/policies/authority.be.contract_ownership.json +73 -0
- package/catalog/policies/authority.chg.change_owners.json +75 -0
- package/catalog/policies/authority.web.api.request_owners.json +82 -0
- package/catalog/policies/authority.web.grid.ownership.json +78 -0
- package/catalog/policies/policy.api.backward_compat_defaults.json +83 -0
- package/catalog/policies/policy.api.contract_impl_consistency.json +81 -0
- package/catalog/policies/policy.api.no_informal_contract.json +82 -0
- package/catalog/policies/policy.arch.decision_tradeoff_record.json +78 -0
- package/catalog/policies/policy.cache.lifecycle_definition.json +79 -0
- package/catalog/policies/policy.chg.affect_templates.json +79 -0
- package/catalog/policies/policy.chg.breaking_versioning.json +81 -0
- package/catalog/policies/policy.chg.compat_migration_rollback.json +82 -0
- package/catalog/policies/policy.chg.deprecate_before_delete.json +82 -0
- package/catalog/policies/policy.chg.fix_at_root_cause_level.json +78 -0
- package/catalog/policies/policy.chg.observable_evolution.json +78 -0
- package/catalog/policies/policy.chg.prechange_consumer_scan.json +83 -0
- package/catalog/policies/policy.chg.staged_rollout.json +79 -0
- package/catalog/policies/policy.chg.sync_contract_docs_tests.json +81 -0
- package/catalog/policies/policy.conflict.priority_ladder.json +79 -0
- package/catalog/policies/policy.dep.admission_six_dimension_check.json +84 -0
- package/catalog/policies/policy.dep.change_surface_review.json +82 -0
- package/catalog/policies/policy.dep.introduction_review.json +83 -0
- package/catalog/policies/policy.derived.single_implementation.json +77 -0
- package/catalog/policies/policy.obs.correlation_context_minimum.json +81 -0
- package/catalog/policies/policy.obs.rum_dimension_whitelist.json +77 -0
- package/catalog/policies/policy.proc.pre_code_declaration.json +78 -0
- package/catalog/policies/policy.rel.pre_release_confirmation.json +77 -0
- package/catalog/policies/policy.role.domain_decision_authority.json +77 -0
- package/catalog/policies/policy.sec.third_party_execution_register.json +78 -0
- package/catalog/policies/policy.spec.admission_criteria.json +79 -0
- package/catalog/policies/policy.spec.derived_view_regeneration.json +79 -0
- package/catalog/policies/policy.spec.draft_not_baseline.json +77 -0
- package/catalog/policies/policy.spec.file_structure_contract.json +76 -0
- package/catalog/policies/policy.spec.metadata_required.json +76 -0
- package/catalog/policies/policy.spec.post_dev_backfill_classify.json +74 -0
- package/catalog/policies/policy.spec.procedural_record_not_surrogate.json +76 -0
- package/catalog/policies/policy.spec.semantic_identity.json +77 -0
- package/catalog/policies/policy.spec.unresolved_ledger_gate.json +79 -0
- package/catalog/policies/policy.stack.no_implicit_selection.json +77 -0
- package/catalog/policies/policy.tool.scoped_scan_boundary.json +78 -0
- package/catalog/policies/policy.web.api.auth_app_client_split.json +93 -0
- package/catalog/policies/policy.web.api.client_change_impact.json +86 -0
- package/catalog/policies/policy.web.api.domain_api_only.json +84 -0
- package/catalog/policies/policy.web.api.error_normalization.json +82 -0
- package/catalog/policies/policy.web.api.generated_client.json +82 -0
- package/catalog/policies/policy.web.api.late_result_guard.json +80 -0
- package/catalog/policies/policy.web.api.mutation_idempotency.json +84 -0
- package/catalog/policies/policy.web.api.no_network_optimism.json +82 -0
- package/catalog/policies/policy.web.api.query_cancel_stale_drop.json +80 -0
- package/catalog/policies/policy.web.api.request_infrastructure.json +95 -0
- package/catalog/policies/policy.web.api.retry_discipline.json +87 -0
- package/catalog/policies/policy.web.api.session_recovery_split.json +94 -0
- package/catalog/policies/policy.web.api.single_http_client.json +88 -0
- package/catalog/policies/policy.web.api.single_loading_source.json +81 -0
- package/catalog/policies/policy.web.api.transport_vs_business.json +81 -0
- package/catalog/policies/policy.web.api.trusted_endpoint_source.json +82 -0
- package/catalog/policies/policy.web.api.typed_domain_naming.json +79 -0
- package/catalog/policies/policy.web.arch.layer_isolation.json +376 -0
- package/catalog/policies/policy.web.arch.naming_conventions.json +207 -0
- package/catalog/policies/policy.web.arch.public_api_barrel.json +134 -0
- package/catalog/policies/policy.web.chg.controlled_change_triggers.json +79 -0
- package/catalog/policies/policy.web.copy.suppression_ledger_discipline.json +78 -0
- package/catalog/policies/policy.web.grid.column_schema_fields.json +78 -0
- package/catalog/policies/policy.web.grid.config_isolation.json +78 -0
- package/catalog/policies/policy.web.grid.freeze_key_columns.json +77 -0
- package/catalog/policies/policy.web.grid.interaction_registry.json +80 -0
- package/catalog/policies/policy.web.grid.keyboard_clipboard.json +79 -0
- package/catalog/policies/policy.web.grid.schema_driven_shell.json +81 -0
- package/catalog/policies/policy.web.grid.schema_versioning.json +78 -0
- package/catalog/policies/policy.web.grid.server_ops_virtualization.json +81 -0
- package/catalog/policies/policy.web.grid.shell_change_matrix.json +76 -0
- package/catalog/policies/policy.web.grid.width_fix_ladder.json +79 -0
- package/catalog/policies/policy.web.perf.budget_skeleton.json +110 -0
- package/catalog/policies/policy.web.style.ownership_matrix.json +207 -0
- package/catalog/policies/policy.web.track.consent_lifecycle.json +78 -0
- package/catalog/policies/policy.web.track.privacy_default_deny.json +81 -0
- package/catalog/policies/policy.web.track.stable_event_keys.json +78 -0
- package/catalog/policies/policy.web.uiux.provider_evidence_not_business_truth.json +86 -0
- package/catalog/projection-presets/registry-tree.yaml +307 -0
- package/catalog/sensors/sensor.browser.deterministic.json +53 -0
- package/catalog/sensors/sensor.browser.interactive.json +58 -0
- package/catalog/sensors/sensor.build.static.json +45 -0
- package/catalog/sensors/sensor.contract.conformance.json +47 -0
- package/catalog/sensors/sensor.performance.budget.json +46 -0
- package/catalog/sensors/sensor.production.metric.json +44 -0
- package/catalog/tools/apply_human_review_pilot_0001.py +488 -0
- package/catalog/tools/materialize_batch4_uplift.py +776 -0
- package/catalog/tools/materialize_catalog_pilot.py +784 -0
- package/dist/bin.js +52362 -0
- package/legal/THIRD_PARTY_NOTICES.md +336 -0
- package/package.json +36 -0
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""MIG-B4 · Universal 上提物化工具(split-ledger 驱动 → catalog/policies/ + catalog-lock 同步)。
|
|
3
|
+
|
|
4
|
+
输入(只读):
|
|
5
|
+
- POMaster_VNext/corpus/master/batch-4/split-ledger.yaml (M1 分拣台账,本工具唯一驱动源)
|
|
6
|
+
- 上游消费项目 10_planned/<asset>.yaml 源文本 (仅作 clean-room LCS 自检对照,只 open 读)
|
|
7
|
+
|
|
8
|
+
输出:
|
|
9
|
+
- catalog/policies/policy.web.{arch,api,perf,style,uiux}.*.json —— 9 个新条目(34 条 universal 半归并)
|
|
10
|
+
- catalog/catalog-lock.draft.json —— 追加 9 entries + 新增 controlled_children(allowed+required 两处)
|
|
11
|
+
|
|
12
|
+
clean-room 纪律(D3 生死线):
|
|
13
|
+
- 34 条 universal 半词面全部由本工具内置常量独立改写(语义等价、词形独立),以 split-ledger
|
|
14
|
+
的 clean_room_rewrite_sketch 为底稿再改写,禁回抄上游源文本;
|
|
15
|
+
- 内置自检:每个条目正文(title/statement/keywords/applies_when/sub_rules/review_notes 拼接,
|
|
16
|
+
去空白+小写归一)与对应上游源文本做精确 20-gram 重叠测试——命中任何 ≥20 字符公共子串即 exit 2;
|
|
17
|
+
另附 difflib 最长公共连续块实测值随报告输出;
|
|
18
|
+
- 零泄漏自检:条目正文 grep 项目专名(PAGE- / API_REQ. / MASTer / 雅黑 / Fira)必须 0 命中;
|
|
19
|
+
sources[].clean_room_note 为固定英文文案且本身零项目专名(与 pilot 既有条目中文注记
|
|
20
|
+
同一先例:clean-room 注记自身不含项目名),全文件原始 grep(含元数据)预期 0 命中,
|
|
21
|
+
命中数随报告单列并逐一定位。
|
|
22
|
+
|
|
23
|
+
catalog-lock 纪律:
|
|
24
|
+
- 追加式合并:读既有 lock,对全部 entries(旧+新)重算 content_sha256 与落盘对账(drift 即 exit 2);
|
|
25
|
+
- 新增 controlled_children 块(allowed=required=全部登记路径,排序):新文件同步 allowed+required
|
|
26
|
+
两处(沿 pomaster directory-lock controlled_children 语义移植到 catalog-lock 管辖面);
|
|
27
|
+
- 幂等:同输入重跑 byte-identical(输出内容构建两遍逐字节比对一致后才落盘);entries 按 id 排序。
|
|
28
|
+
|
|
29
|
+
纪律:
|
|
30
|
+
- 上游消费项目绝对只读(只 open 读,零写入零改名零 touch);
|
|
31
|
+
- 禁墙钟:无时间戳/mtime;批次代号固定 MIG-B4(seq=MIG-B4);
|
|
32
|
+
- 确定性序列化:json indent=2 + ensure_ascii=False + 末尾恰好一个换行;UTF-8 无 BOM;
|
|
33
|
+
- FROZEN 零接触:不动 packages/schemas/**、kernel vocab、golden cases;
|
|
34
|
+
- 词形合法:kind/policy 条目 id 均为在册 15 前缀 POLICY.*;enforcement/classification/lane/axes
|
|
35
|
+
取值逐项对照 vocab-lock.draft.yaml catalog_layer_vocab 与 state_axes(只消费不改动)。
|
|
36
|
+
|
|
37
|
+
用法:
|
|
38
|
+
python materialize_batch4_uplift.py # 物化 + lock 同步(幂等,可重跑)
|
|
39
|
+
python materialize_batch4_uplift.py --verify # 只读核验:lock 对账 + 幂等重演 + 零泄漏扫描
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
import difflib
|
|
43
|
+
import hashlib
|
|
44
|
+
import json
|
|
45
|
+
import os
|
|
46
|
+
import re
|
|
47
|
+
import sys
|
|
48
|
+
|
|
49
|
+
import yaml
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
53
|
+
except Exception:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
HERE = os.path.dirname(os.path.abspath(__file__)) # .../catalog/tools
|
|
57
|
+
CATALOG = os.path.dirname(HERE) # .../catalog
|
|
58
|
+
VNEXT = os.path.dirname(CATALOG) # .../POMaster_VNext
|
|
59
|
+
LEDGER_PATH = os.path.join(VNEXT, "corpus", "master", "batch-4", "split-ledger.yaml")
|
|
60
|
+
LOCK_PATH = os.path.join(CATALOG, "catalog-lock.draft.json")
|
|
61
|
+
UPSTREAM_ROOT = r"D:\Vscode Documents\MASTer_master" # 上游消费项目(绝对只读)
|
|
62
|
+
|
|
63
|
+
BATCH = "MIG-B4"
|
|
64
|
+
CAPTURED_BY = "agent:claude/batch-4-catalog-uplift"
|
|
65
|
+
LEDGER_REF = "POMaster_VNext/corpus/master/batch-4/split-ledger.yaml"
|
|
66
|
+
CLEAN_ROOM_NOTE = "independently rewritten from field corpus batch-4 material; zero verbatim copy"
|
|
67
|
+
LCS_THRESHOLD = 20 # 归一化后 ≥20 字符公共子串即判逐字拷贝(最长在册技术词形 ~14 字符,20 留安全余量)
|
|
68
|
+
|
|
69
|
+
PROJECT_NOUNS = ["PAGE-", "API_REQ.", "MASTer", "雅黑", "Fira"]
|
|
70
|
+
|
|
71
|
+
# 词形合法面(只消费 vocab-lock 冻结值,不改动;镜像用于本工具自检)
|
|
72
|
+
VOCAB_ENFORCEMENT = {"required_when_applicable", "advisory", "deterministic_where_possible"}
|
|
73
|
+
VOCAB_CLASSIFICATION = {
|
|
74
|
+
"CONSTITUTION", "UNIVERSAL_POLICY", "LANE_POLICY", "TECHNOLOGY_PROFILE",
|
|
75
|
+
"PROJECT_BASELINE_TEMPLATE", "CONTRACT_TEMPLATE", "GATE_RECIPE",
|
|
76
|
+
"KNOWLEDGE_PATTERN", "FAILURE_PATTERN", "DEPRECATED", "DUPLICATE", "REJECTED",
|
|
77
|
+
}
|
|
78
|
+
VOCAB_LANES = {"any", "frontend", "backend"}
|
|
79
|
+
VOCAB_AXES = {
|
|
80
|
+
"lifecycle": {"PROPOSED", "CURRENT", "SUPERSEDED", "DEPRECATED", "RETIRED", "REJECTED"},
|
|
81
|
+
"confidence": {"UNRESOLVED", "EXPERIMENTAL", "PROVISIONAL", "LOCKED"},
|
|
82
|
+
"evidence": {"PLANNED", "IMPLEMENTED", "VERIFIED"},
|
|
83
|
+
"change": {"STABLE", "CHALLENGED", "MIGRATING"},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# ======================================================================
|
|
87
|
+
# 34 条 universal 半独立改写词面((asset_id, entry_id) → statement_zh)
|
|
88
|
+
# 以 split-ledger clean_room_rewrite_sketch / split_note 为底稿再改写;
|
|
89
|
+
# 技术栈具名(框架/库/数值/头名)一律不出现(属 project 半参数)。
|
|
90
|
+
# ======================================================================
|
|
91
|
+
REWRITE_TEXT = {
|
|
92
|
+
# ---- architecture-constraints(HYBRID universal 半 = 层职责/禁入方向骨架)----
|
|
93
|
+
("architecture-constraints", "layers.app"):
|
|
94
|
+
"应用外壳层只负责装配:路由表、全局注入器、路由级错误兜底与访问守卫都挂在这一层;"
|
|
95
|
+
"本层不持有业务状态,也不渲染业务内容;单个页面内的数据失败必须被就地吸收,不得穿透到其他页面。",
|
|
96
|
+
("architecture-constraints", "layers.pages"):
|
|
97
|
+
"路由页面层只做布局编排与装配消费:禁止把业务或异步数据留在组件本地状态里,"
|
|
98
|
+
"禁止直接发起网络请求;页面所需数据一律经由下层状态通道获取。",
|
|
99
|
+
("architecture-constraints", "layers.features"):
|
|
100
|
+
"能力切片层负责单个业务能力的组件组织与交互编排:切片之间禁止互引内部实现;"
|
|
101
|
+
"切片内部错误用局部边界吸收、不外溢到相邻切片;本层不持有跨页共享的全局数据。",
|
|
102
|
+
("architecture-constraints", "layers.entities"):
|
|
103
|
+
"服务端数据层持有远端数据的请求生命周期:逐查询的缓存与逐查询的错误都归这一层;"
|
|
104
|
+
"禁止把远端状态复制进客户端仓库做手工双向同步;查询错误先归一化再上抛。",
|
|
105
|
+
("architecture-constraints", "layers.shared-state"):
|
|
106
|
+
"客户端状态层按页面隔离存放表单态与视图模式态:未做页面隔离的状态禁止跨页共享;"
|
|
107
|
+
"全局仓库只保留身份、权限范围这类稳定且真正跨页面的状态。",
|
|
108
|
+
("architecture-constraints", "layers.shared-ui"):
|
|
109
|
+
"设计系统组件层只承载纯展示组件:不携带业务状态,不感知业务语义。",
|
|
110
|
+
("architecture-constraints", "layers.shared-grid"):
|
|
111
|
+
"表格视图层只承载表格列定义与视图配置的组装:可依赖展示组件层令牌与基础工具层,不依赖任何业务上层。",
|
|
112
|
+
("architecture-constraints", "layers.shared-lib"):
|
|
113
|
+
"基础工具层不反向依赖任何上层:格式化、精确计算、网络客户端、错误规整、枚举映射等"
|
|
114
|
+
"与框架无关的设施都住在这一层,供所有上层调用。",
|
|
115
|
+
("architecture-constraints", "deep_import_rule"):
|
|
116
|
+
"任何跨目录引用只允许指向目标目录的公开出口;深入目标目录内部路径或直接引用内部具体文件的写法一律禁止;"
|
|
117
|
+
"可引用出口的具名清单属项目基线参数。",
|
|
118
|
+
("architecture-constraints", "new_file_rule"):
|
|
119
|
+
"新建任何文件之前,必须先写明它归属哪一层哪个切片,以及为什么不能通过扩展既有文件达成;"
|
|
120
|
+
"页面需要新状态时优先扩展该页已有的状态载体,而不是另起全局仓库;"
|
|
121
|
+
"新组件优先归入设计系统层或对应能力切片,避免持续堆积在页面层。",
|
|
122
|
+
# ---- directory-layout(HYBRID universal 半 = 目录解剖模板/禁互引 + id 转目录名规则)----
|
|
123
|
+
("directory-layout", "layers.pages"):
|
|
124
|
+
"页面目录解剖模板:每页一个目录,目录内提供该页唯一公开出口、路由级页面组件与仅本页使用的局部子组件;"
|
|
125
|
+
"局部子组件禁止被其他页面引用。",
|
|
126
|
+
("directory-layout", "layers.features"):
|
|
127
|
+
"能力切片目录解剖模板:每切片一个目录,目录内提供公开出口、组合组件目录与交互逻辑目录;"
|
|
128
|
+
"只为可独立复用的能力建切片,禁止按页面一对一碎片化划分;切片之间禁止互引内部实现。",
|
|
129
|
+
("directory-layout", "layers.entities"):
|
|
130
|
+
"实体目录解剖模板:每实体一个目录,目录内提供公开出口、领域接口目录、数据钩子目录、校验结构与类型定义;"
|
|
131
|
+
"领域接口调用经由统一网络客户端发出,禁止直连网络库。",
|
|
132
|
+
("directory-layout", "naming.composable"):
|
|
133
|
+
"组合式函数文件名统一为 use 前缀接驼峰主体的形式,且一个文件只承载一个组合式函数。",
|
|
134
|
+
("directory-layout", "naming.page_component"):
|
|
135
|
+
"路由级页面组件的文件名由页面语义名接帕斯卡式页面后缀构成。",
|
|
136
|
+
("directory-layout", "naming.feature_component"):
|
|
137
|
+
"切片内的组合组件以 Widget 或 Panel 一类视图组合后缀命名,与路由级页面组件相区分。",
|
|
138
|
+
("directory-layout", "naming.barrel"):
|
|
139
|
+
"每个分层根目录、每个切片目录、每个实体目录与每个页面目录都必须提供唯一的公开出口文件,"
|
|
140
|
+
"作为该目录对外唯一的可见面。",
|
|
141
|
+
("directory-layout", "naming.entity_dir"):
|
|
142
|
+
"实体目录名采用单数名词加小写中划线的形式。",
|
|
143
|
+
("directory-layout", "naming.feature_dir"):
|
|
144
|
+
"能力标识转目录名时统一取小写中划线形式。",
|
|
145
|
+
("directory-layout", "naming.page_dir"):
|
|
146
|
+
"页面标识转目录名时统一取小写中划线形式。",
|
|
147
|
+
("directory-layout", "barrel_rule"):
|
|
148
|
+
"公开出口文件是分层的唯一对外契约面:只转发对外接口,不暴露内部实现;"
|
|
149
|
+
"一切跨层引用必须经由目标方的公开出口完成,深入内部目录的引用一律禁止。",
|
|
150
|
+
("directory-layout", "colocation_rule"):
|
|
151
|
+
"组件按复用范围就近放置:仅单页使用的留在该页目录内;跨页复用的进入能力切片;"
|
|
152
|
+
"只有稳定共享且不携带业务语义的组件才下沉到设计系统层;禁止把带业务语义的组件放进设计系统层。",
|
|
153
|
+
# ---- http-client-policy(HYBRID universal 半)----
|
|
154
|
+
("http-client-policy", "clients.authClient"):
|
|
155
|
+
"会话族请求与业务族请求必须分离为两个独立客户端:会话族客户端只服务登录、令牌刷新、注销与"
|
|
156
|
+
"当前身份及权限读取;其端点以受控白名单枚举,白名单之外的端点不得经由会话客户端发出;"
|
|
157
|
+
"会话客户端不进入业务恢复链路,其刷新动作必须单飞合并。",
|
|
158
|
+
("http-client-policy", "clients.appClient"):
|
|
159
|
+
"业务客户端对认证失效与权限拒绝必须分开处理:认证失效时刷新一次并以原请求重放一次;"
|
|
160
|
+
"权限拒绝直接拒绝、不触发刷新;令牌刷新必须全局单飞;每个请求的重放次数必须设显式上限。",
|
|
161
|
+
("http-client-policy", "global"):
|
|
162
|
+
"超时、中止、追踪、请求标识与基础地址来源必须由统一请求基础设施单点声明默认挂点:"
|
|
163
|
+
"每个出站请求都可中止、可追踪、可关联;具体超时数值、头名与重试策略标识由项目基线供给,不入目录正本。",
|
|
164
|
+
# ---- performance-budget(HYBRID universal 半 = 骨架)----
|
|
165
|
+
("performance-budget", "initial_load"):
|
|
166
|
+
"装载预算骨架:脚本、样式、图片三类载荷各设上限字段,并各自声明计量口径(如压缩后传输体积);"
|
|
167
|
+
"上限数值由项目基线供给。",
|
|
168
|
+
("performance-budget", "runtime"):
|
|
169
|
+
"运行时预算骨架:首绘时间、可交互时间、长任务时长三族指标各设目标字段;目标数值由项目基线供给。",
|
|
170
|
+
# ---- style-ownership-registry(HYBRID universal 半 = 五域矩阵骨架 + 禁止行为类别学)----
|
|
171
|
+
("style-ownership-registry", "layers.global-reset"):
|
|
172
|
+
"全局重置域:重置样式的唯一所有者是全局样式入口;页面域禁止局部重置、禁止内联改写根元素样式、"
|
|
173
|
+
"禁止私挂全局样式文件。",
|
|
174
|
+
("style-ownership-registry", "layers.design-token"):
|
|
175
|
+
"设计令牌域:令牌源文件的唯一所有者是令牌事实源;页面域禁止私定局部令牌、禁止硬编码颜色与像素值、"
|
|
176
|
+
"禁止引用不存在的幽灵令牌、禁止手改令牌源文件。",
|
|
177
|
+
("style-ownership-registry", "layers.shared-component"):
|
|
178
|
+
"共享组件域:共享组件样式的唯一所有者是共享组件层;页面域禁止书写组件级样式、禁止覆盖基础组件视觉、"
|
|
179
|
+
"禁止私造共享类名。",
|
|
180
|
+
("style-ownership-registry", "layers.page-local"):
|
|
181
|
+
"页面局部域:scoped 样式块是页面局部样式的唯一容器;块内禁止声明全局类、禁止使用强制覆盖标记、"
|
|
182
|
+
"禁止滥用深选择器穿透。",
|
|
183
|
+
("style-ownership-registry", "layers.vendor-adapter"):
|
|
184
|
+
"第三方适配域:第三方主题样式的唯一所有者是适配目录;页面域禁止直接引入第三方样式、"
|
|
185
|
+
"禁止局部覆盖第三方主题、禁止局部改写主题参数。",
|
|
186
|
+
("style-ownership-registry", "scoped_style_rule"):
|
|
187
|
+
"页面内 scoped 样式块只承载本页局部样式;全局样式类只允许声明在共享组件层,"
|
|
188
|
+
"页面禁止声明全局类名或向页面外泄漏全局类名。",
|
|
189
|
+
# ---- uiux-provider-overlay(UNIVERSAL)----
|
|
190
|
+
("uiux-provider-overlay", "authority"):
|
|
191
|
+
"由外部工具从设计原型视觉稿中抽取的内容一律定性为可选参考证据,任何情况下不得当作业务事实;"
|
|
192
|
+
"业务真值的唯一来源是已登记的契约面。",
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# ======================================================================
|
|
196
|
+
# 9 个归并文件的聚合元信息((dir, filename) → meta;statement 为归并正本)
|
|
197
|
+
# ======================================================================
|
|
198
|
+
FILE_META = {
|
|
199
|
+
("policies", "policy.web.arch.layer_isolation.json"): {
|
|
200
|
+
"title_zh": "分层职责与单向依赖隔离",
|
|
201
|
+
"statement_zh": "前端代码按职责分层,每层只承担单一职责,依赖只能自上而下单向流动;"
|
|
202
|
+
"每层内部目录遵循统一解剖模板,跨层访问只能经由该层公开出口完成。",
|
|
203
|
+
"statement_en_keywords": ["layer responsibility", "one-way dependency", "directory anatomy",
|
|
204
|
+
"colocation", "new file declaration"],
|
|
205
|
+
"condition": "创建分层结构、新增目录或放置新文件时",
|
|
206
|
+
},
|
|
207
|
+
("policies", "policy.web.arch.public_api_barrel.json"): {
|
|
208
|
+
"title_zh": "公共出口桶唯一面纪律",
|
|
209
|
+
"statement_zh": "每个分层与切片根目录必须提供唯一公开出口文件;跨目录引用只能经由公开出口完成,"
|
|
210
|
+
"深入内部目录的导入一律禁止。",
|
|
211
|
+
"statement_en_keywords": ["barrel", "public api", "no deep import"],
|
|
212
|
+
"condition": "发起跨目录引用或新建目录时",
|
|
213
|
+
},
|
|
214
|
+
("policies", "policy.web.arch.naming_conventions.json"): {
|
|
215
|
+
"title_zh": "分层命名约定",
|
|
216
|
+
"statement_zh": "分层内文件与目录命名遵循统一约定:组合式函数、路由级页面组件、切片组合组件、"
|
|
217
|
+
"实体目录及标识符转目录名各有固定词形,同类对象全仓同形。",
|
|
218
|
+
"statement_en_keywords": ["naming convention", "composable prefix", "page component suffix",
|
|
219
|
+
"kebab directory"],
|
|
220
|
+
"condition": "新建文件或目录命名时",
|
|
221
|
+
},
|
|
222
|
+
("policies", "policy.web.api.auth_app_client_split.json"): {
|
|
223
|
+
"title_zh": "会话与业务客户端族分离",
|
|
224
|
+
"statement_zh": "会话族端点由独立的会话客户端承载、与业务客户端分离;会话端点以受控白名单枚举,"
|
|
225
|
+
"会话客户端不进入业务恢复链路,刷新动作单飞合并。",
|
|
226
|
+
"statement_en_keywords": ["client split", "auth whitelist", "single flight refresh"],
|
|
227
|
+
"condition": "搭建或调整请求客户端族时",
|
|
228
|
+
"review_notes": [
|
|
229
|
+
"与既有 POLICY.WEB.API.SINGLE_HTTP_CLIENT / POLICY.WEB.API.TRUSTED_ENDPOINT_SOURCE 面审"
|
|
230
|
+
"(MIG-B4 catalog_scope_note):SINGLE_HTTP_CLIENT 约束基础设施单点统一,本条约束客户端族分离"
|
|
231
|
+
"(会话族 vs 业务族两实例),两维度正交不冲突;端点来源纪律归 TRUSTED_ENDPOINT_SOURCE,本条不另立来源条款。",
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
("policies", "policy.web.api.session_recovery_split.json"): {
|
|
235
|
+
"title_zh": "会话恢复与权限拒绝分判",
|
|
236
|
+
"statement_zh": "认证失效与权限拒绝分开处理:认证失效走『单飞刷新一次 + 原请求重放一次』;"
|
|
237
|
+
"权限拒绝不刷新、直接失败;逐请求重放上限显式声明,防止循环重放。",
|
|
238
|
+
"statement_en_keywords": ["401 refresh once", "403 reject no refresh", "single flight",
|
|
239
|
+
"replay cap"],
|
|
240
|
+
"condition": "实现会话失效与权限拒绝处理时",
|
|
241
|
+
"review_notes": [
|
|
242
|
+
"与既有 POLICY.WEB.API.ERROR_NORMALIZATION / POLICY.WEB.API.RETRY_DISCIPLINE 面审(MIG-B4):"
|
|
243
|
+
"本条只覆盖会话恢复链(刷新单飞/重放上限/权限拒绝不刷新),错误归一化与一般重试纪律仍归既有条款管辖。",
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
("policies", "policy.web.api.request_infrastructure.json"): {
|
|
247
|
+
"title_zh": "请求基础设施统一挂点",
|
|
248
|
+
"statement_zh": "超时、中止、追踪、请求标识与基础地址来源五项默认挂点全部挂在唯一请求基础设施入口,"
|
|
249
|
+
"禁止逐请求散落自配;数值与头名等参数由项目基线供给。",
|
|
250
|
+
"statement_en_keywords": ["unified request infrastructure", "abort", "trace id",
|
|
251
|
+
"request id", "base url source"],
|
|
252
|
+
"condition": "搭建或调整请求基础设施时",
|
|
253
|
+
"review_notes": [
|
|
254
|
+
"与既有 POLICY.WEB.API.SINGLE_HTTP_CLIENT 面审(MIG-B4):本条补『统一挂点清单』维度"
|
|
255
|
+
"(中止/追踪/请求标识/默认超时/基础地址来源),单点统一原则本身归既有条款。",
|
|
256
|
+
],
|
|
257
|
+
},
|
|
258
|
+
("policies", "policy.web.perf.budget_skeleton.json"): {
|
|
259
|
+
"title_zh": "性能预算骨架",
|
|
260
|
+
"statement_zh": "性能预算按固定骨架声明:装载预算分脚本、样式、图片三类并各自标注计量口径;"
|
|
261
|
+
"运行时预算至少覆盖首绘、可交互、长任务三族指标;目录正本只定骨架,阈值数值由项目基线供给。",
|
|
262
|
+
"statement_en_keywords": ["load budget", "runtime budget", "budget skeleton"],
|
|
263
|
+
"condition": "制定或校验性能预算时",
|
|
264
|
+
},
|
|
265
|
+
("policies", "policy.web.style.ownership_matrix.json"): {
|
|
266
|
+
"title_zh": "样式所有权五域矩阵",
|
|
267
|
+
"statement_zh": "样式按五域所有权矩阵管理:全局重置、设计令牌、共享组件、页面局部、第三方适配各设唯一"
|
|
268
|
+
"所有者并绑定禁止行为类别;任何样式写入必须落在所属域的所有者路径内,owner 路径与"
|
|
269
|
+
"扫描器禁用词表由项目基线登记。",
|
|
270
|
+
"statement_en_keywords": ["style ownership", "five domains", "forbidden behavior",
|
|
271
|
+
"scoped style"],
|
|
272
|
+
"condition": "编写或评审样式代码时",
|
|
273
|
+
},
|
|
274
|
+
("policies", "policy.web.uiux.provider_evidence_not_business_truth.json"): {
|
|
275
|
+
"title_zh": "原型抽取证据非业务事实",
|
|
276
|
+
"statement_zh": "由外部工具从设计原型视觉稿中抽取的内容一律定性为可选参考证据,永不构成业务事实;"
|
|
277
|
+
"业务真值的唯一来源是已登记的契约面。",
|
|
278
|
+
"statement_en_keywords": ["prototype extraction", "optional evidence", "not business truth"],
|
|
279
|
+
"condition": "登记或消费原型抽取证据时",
|
|
280
|
+
},
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ======================================================================
|
|
285
|
+
# W1-A2 P0.5-1 T3 机器 applicability 标注表(PRD v0.5.2 §5.2/§14 + Owner 裁决 8 ②)
|
|
286
|
+
# 保守派生:仅正文词面有明确证据才标(policy.web.api 族 → CAPABILITY.API_CONTRACT,
|
|
287
|
+
# 与 tests/integration/catalog-applicability-case-b.spec.ts W1-A1 批1 fixture 同值同族);
|
|
288
|
+
# 其余留空回退 lane 缺省(O7 行为零变化)+ x-applicability-review human-review 候选。
|
|
289
|
+
# condition 降级为 applicability_note 保留(PRD §5.2 允许自然语言保留为注记)。
|
|
290
|
+
# ======================================================================
|
|
291
|
+
APPLICABILITY_AXES = {
|
|
292
|
+
"POLICY.WEB.API.AUTH_APP_CLIENT_SPLIT": {
|
|
293
|
+
"capabilities": ["CAPABILITY.API_CONTRACT"],
|
|
294
|
+
"basis": "policy.web.api 族(case-b W1-A1 同族标注);condition『搭建或调整请求客户端族时』",
|
|
295
|
+
},
|
|
296
|
+
"POLICY.WEB.API.SESSION_RECOVERY_SPLIT": {
|
|
297
|
+
"capabilities": ["CAPABILITY.API_CONTRACT"],
|
|
298
|
+
"basis": "policy.web.api 族(case-b W1-A1 同族标注)",
|
|
299
|
+
},
|
|
300
|
+
"POLICY.WEB.API.REQUEST_INFRASTRUCTURE": {
|
|
301
|
+
"capabilities": ["CAPABILITY.API_CONTRACT"],
|
|
302
|
+
"basis": "policy.web.api 族(case-b W1-A1 同族标注);condition『搭建或调整请求基础设施时』",
|
|
303
|
+
},
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
APPLICABILITY_CAMPAIGN = "W1-A2 P0.5-1 T3 标注战役(PRD v0.5.2 §5.2/§14;Owner 裁决 8 ②,2026-09-01)"
|
|
307
|
+
|
|
308
|
+
# 三个 materialize 工具共用的 producer 链 generated_by(同步落在
|
|
309
|
+
# catalog/tools/materialize_catalog_pilot.py 与 corpus/spec-knowledge/materialize-curated.py;
|
|
310
|
+
# 同串保证任一工具最后落锁不丢失其余批次的 provenance 注记)。
|
|
311
|
+
LOCK_GENERATED_BY = (
|
|
312
|
+
"catalog/tools/materialize_catalog_pilot.py(pilot-0001 60 条;entries 按 id 排序)+ "
|
|
313
|
+
"catalog/tools/materialize_batch4_uplift.py(batch-4 语料批 Universal 上提追加 9 条)+ "
|
|
314
|
+
"corpus/spec-knowledge/materialize-curated.py(SPEC-D 汇总池 D5 精选追加 25 条)+ "
|
|
315
|
+
"catalog/sensors/(P1-5 Sensor Capability Catalog Lite 六条目登记,裁决 8 D6/D7)+ "
|
|
316
|
+
"W1-A2 P0.5-1 T3 标注战役(机器 applicability 字段批量标注 + 幂等重锁;PRD v0.5.2 §5.2/§14,裁决 8 ②)"
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def applicability_parts(cid, condition):
|
|
321
|
+
"""lanes 平移(frontend 显式 lane)+ 词面派生轴 + applicability_note 降级位 + review 注记。"""
|
|
322
|
+
merged = {"lane": "frontend", "condition": condition}
|
|
323
|
+
axes_written = ["lanes"]
|
|
324
|
+
merged["lanes"] = ["frontend"]
|
|
325
|
+
axes = APPLICABILITY_AXES.get(cid)
|
|
326
|
+
basis = None
|
|
327
|
+
if axes is not None:
|
|
328
|
+
basis = axes["basis"]
|
|
329
|
+
for key in ("capabilities", "change_classes"):
|
|
330
|
+
if key in axes:
|
|
331
|
+
merged[key] = list(axes[key])
|
|
332
|
+
axes_written.append(key)
|
|
333
|
+
merged["applicability_note"] = condition
|
|
334
|
+
axes_written.append("applicability_note")
|
|
335
|
+
if basis is not None:
|
|
336
|
+
review = {
|
|
337
|
+
"status": "annotated",
|
|
338
|
+
"campaign": APPLICABILITY_CAMPAIGN,
|
|
339
|
+
"axes": axes_written,
|
|
340
|
+
"basis": basis,
|
|
341
|
+
}
|
|
342
|
+
else:
|
|
343
|
+
review = {
|
|
344
|
+
"status": "human_review_candidate",
|
|
345
|
+
"campaign": APPLICABILITY_CAMPAIGN,
|
|
346
|
+
"axes": axes_written,
|
|
347
|
+
"note": "T3 保守派生未见明确词面证据——capabilities/change_classes 留空回退 lane 缺省"
|
|
348
|
+
"(O7 行为零变化);列 Human Review 复核议程",
|
|
349
|
+
}
|
|
350
|
+
return merged, review
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# ======================================================================
|
|
354
|
+
# 通用只读工具
|
|
355
|
+
# ======================================================================
|
|
356
|
+
def norm_text(s):
|
|
357
|
+
"""LCS/泄漏扫描统一归一:去全部空白 + 小写。"""
|
|
358
|
+
return re.sub(r"\s+", "", s).lower()
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def upstream_asset_path(asset_id):
|
|
362
|
+
return os.path.join(UPSTREAM_ROOT, "outputs", "frontend", "10_planned", asset_id + ".yaml")
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def body_text_of(entry):
|
|
366
|
+
"""条目正文词面(LCS 与零泄漏扫描的判定面;元数据 id/path/provenance 不入正文)。"""
|
|
367
|
+
parts = [entry["title_zh"], entry["statement_zh"]]
|
|
368
|
+
parts += entry.get("statement_en_keywords") or []
|
|
369
|
+
parts.append(entry["applies_when"]["condition"])
|
|
370
|
+
for r in entry["sub_rules"]:
|
|
371
|
+
parts.append(r["statement_zh"])
|
|
372
|
+
parts += entry.get("review_notes") or []
|
|
373
|
+
return "\n".join(p for p in parts if p)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def lcs_block_size(a, b):
|
|
377
|
+
"""归一化后最长公共连续块实测值(difflib;报告用)。"""
|
|
378
|
+
sm = difflib.SequenceMatcher(None, a, b, autojunk=False)
|
|
379
|
+
return sm.find_longest_match(0, len(a), 0, len(b)).size
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def dump_json(obj):
|
|
383
|
+
return json.dumps(obj, ensure_ascii=False, indent=2) + "\n"
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def write_if_changed(path, data):
|
|
387
|
+
if os.path.exists(path):
|
|
388
|
+
with open(path, "rb") as f:
|
|
389
|
+
if f.read() == data:
|
|
390
|
+
return False
|
|
391
|
+
with open(path, "wb") as f:
|
|
392
|
+
f.write(data)
|
|
393
|
+
return True
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# ======================================================================
|
|
397
|
+
# 台账驱动:选出全部 UNIVERSAL / HYBRID universal 半,按 destination 归并
|
|
398
|
+
# ======================================================================
|
|
399
|
+
def load_universal_halves():
|
|
400
|
+
with open(LEDGER_PATH, encoding="utf-8") as f:
|
|
401
|
+
ledger = yaml.safe_load(f)
|
|
402
|
+
|
|
403
|
+
denom = ledger["denominator"]
|
|
404
|
+
assert denom["universal"] == 10 and denom["hybrid"] == 24 and denom["project"] == 283, \
|
|
405
|
+
"split-ledger 分母漂移: %r" % denom
|
|
406
|
+
assert denom["total_entries"] == 317
|
|
407
|
+
assert denom["universal"] + denom["project"] + denom["hybrid"] == denom["total_entries"], "恒等式破损"
|
|
408
|
+
|
|
409
|
+
halves = [] # (asset_id, entry_id, decision, dir, filename, merge_group, migration_batch, source_ref)
|
|
410
|
+
seen_pairs = set()
|
|
411
|
+
for e in ledger["entries"]:
|
|
412
|
+
if e["decision"] not in ("UNIVERSAL", "HYBRID"):
|
|
413
|
+
continue
|
|
414
|
+
key = (e["asset_id"], e["entry_id"])
|
|
415
|
+
assert key not in seen_pairs, "台账重复条目: %r" % (key,)
|
|
416
|
+
seen_pairs.add(key)
|
|
417
|
+
dest = e["destination"]["universal"] if e["decision"] == "HYBRID" else e["destination"]
|
|
418
|
+
m = re.match(r"^catalog/(policies|knowledge)/([A-Za-z0-9_.\-]+\.json)", dest)
|
|
419
|
+
assert m, "destination 词形不可解析: %r" % dest
|
|
420
|
+
dirname, fname = m.group(1), m.group(2)
|
|
421
|
+
stem = fname[:-len(".json")]
|
|
422
|
+
want_prefix = "POLICY." if dirname == "policies" else "KNOWLEDGE."
|
|
423
|
+
assert stem.upper().startswith(want_prefix), "id 前缀与目录不符: %s" % fname
|
|
424
|
+
halves.append({
|
|
425
|
+
"asset_id": e["asset_id"], "entry_id": e["entry_id"], "decision": e["decision"],
|
|
426
|
+
"dir": dirname, "filename": fname, "catalog_id": stem.upper(),
|
|
427
|
+
"merge_group": e.get("merge_group"),
|
|
428
|
+
"corpus_batch": e["migration_batch"].replace("MIG-B", "batch-"),
|
|
429
|
+
"upstream_source_ref": e["source_ref"],
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
assert len(halves) == 34, "universal 半总数漂移: %d" % len(halves)
|
|
433
|
+
assert sum(1 for h in halves if h["decision"] == "UNIVERSAL") == 10
|
|
434
|
+
assert sum(1 for h in halves if h["decision"] == "HYBRID") == 24
|
|
435
|
+
|
|
436
|
+
# 改写词面全覆盖 + 无孤儿(双向 fail-closed)
|
|
437
|
+
missing = [k for k in seen_pairs if k not in REWRITE_TEXT]
|
|
438
|
+
assert not missing, "缺独立改写词面: %r" % sorted(missing)
|
|
439
|
+
orphan = [k for k in REWRITE_TEXT if k not in seen_pairs]
|
|
440
|
+
assert not orphan, "改写词面无台账条目对应: %r" % sorted(orphan)
|
|
441
|
+
|
|
442
|
+
return halves
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def build_entries(halves):
|
|
446
|
+
by_file = {}
|
|
447
|
+
for h in halves:
|
|
448
|
+
by_file.setdefault((h["dir"], h["filename"]), []).append(h)
|
|
449
|
+
|
|
450
|
+
assert set(by_file) == set(FILE_META), (
|
|
451
|
+
"归并文件集与台账 destination 不一致: 台账=%r meta=%r"
|
|
452
|
+
% (sorted(by_file), sorted(FILE_META)))
|
|
453
|
+
|
|
454
|
+
entries = []
|
|
455
|
+
for (dirname, fname), members in by_file.items():
|
|
456
|
+
meta = FILE_META[(dirname, fname)]
|
|
457
|
+
sub_rules, sources = [], []
|
|
458
|
+
for h in members: # 台账序(条目顺序=台账 encounter order,确定性)
|
|
459
|
+
sub_rules.append({
|
|
460
|
+
"rule_id": h["entry_id"],
|
|
461
|
+
"statement_zh": REWRITE_TEXT[(h["asset_id"], h["entry_id"])],
|
|
462
|
+
"provenance": {
|
|
463
|
+
"asset_id": h["asset_id"],
|
|
464
|
+
"entry_id": h["entry_id"],
|
|
465
|
+
"decision": h["decision"],
|
|
466
|
+
"merge_group": h["merge_group"],
|
|
467
|
+
"corpus_batch": h["corpus_batch"],
|
|
468
|
+
"split_ledger": LEDGER_REF,
|
|
469
|
+
},
|
|
470
|
+
})
|
|
471
|
+
sources.append({
|
|
472
|
+
"type": "design_seed",
|
|
473
|
+
"ref": LEDGER_REF,
|
|
474
|
+
"captured_by": CAPTURED_BY,
|
|
475
|
+
"locator": {
|
|
476
|
+
"asset": h["asset_id"],
|
|
477
|
+
"entry": h["entry_id"],
|
|
478
|
+
"corpus_batch": h["corpus_batch"],
|
|
479
|
+
"upstream_source_ref": h["upstream_source_ref"],
|
|
480
|
+
},
|
|
481
|
+
"clean_room_note": CLEAN_ROOM_NOTE,
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
catalog_id = members[0]["catalog_id"]
|
|
485
|
+
assert all(m["catalog_id"] == catalog_id for m in members)
|
|
486
|
+
|
|
487
|
+
# W1-A2 P0.5-1 T3:机器 applicability 字段组装(lanes 平移 / 词面派生轴 /
|
|
488
|
+
# applicability_note 降级位;保守派生——拿不准留空回退 lane 缺省 O7)。
|
|
489
|
+
applies_when, applicability_review = applicability_parts(catalog_id, meta["condition"])
|
|
490
|
+
entry = {
|
|
491
|
+
"x-vocab-pr": {
|
|
492
|
+
"status": "vocab_pr_candidate",
|
|
493
|
+
"finding": "kind='policy' 不在 vocab-lock kinds_registry.truth_bodies"
|
|
494
|
+
"(POLICY. 前缀已冻结注册,closed-world)",
|
|
495
|
+
"proposal": "词汇表 PR 登记 policy kind;或 Owner 裁决 policy 条目住 catalog/ 而非 "
|
|
496
|
+
"truth/objects 正文层(与前批 45 条同因同请,合并进同一 vocab PR)",
|
|
497
|
+
"locked_vocab_untouched": True,
|
|
498
|
+
},
|
|
499
|
+
"x-batch4-uplift": {
|
|
500
|
+
"status": "PROPOSAL",
|
|
501
|
+
"package": "batch-4 语料批 Universal 上提(工程策略族 split-ledger 驱动)",
|
|
502
|
+
"human_review_required": True,
|
|
503
|
+
"evidence": "PLANNED",
|
|
504
|
+
"provenance": LEDGER_REF,
|
|
505
|
+
},
|
|
506
|
+
"x-applicability-review": applicability_review,
|
|
507
|
+
"id": catalog_id,
|
|
508
|
+
"kind": "policy",
|
|
509
|
+
"axis_profile": "policy_default",
|
|
510
|
+
"classification": "UNIVERSAL_POLICY",
|
|
511
|
+
"axes": {"lifecycle": "PROPOSED", "confidence": "UNRESOLVED",
|
|
512
|
+
"evidence": "PLANNED", "change": "STABLE"},
|
|
513
|
+
"title_zh": meta["title_zh"],
|
|
514
|
+
"statement_zh": meta["statement_zh"],
|
|
515
|
+
"statement_en_keywords": meta["statement_en_keywords"],
|
|
516
|
+
"applies_when": applies_when,
|
|
517
|
+
"enforcement": "required_when_applicable",
|
|
518
|
+
"authority": {
|
|
519
|
+
"owner": "HUMAN_OWNER",
|
|
520
|
+
"delegates": [],
|
|
521
|
+
"write_policy": "EVOLUTION_CHANNEL",
|
|
522
|
+
"escalation_hint": "catalog-batch4-uplift",
|
|
523
|
+
},
|
|
524
|
+
"origin": "ingested",
|
|
525
|
+
"origin_note": "batch-4 语料批 split-ledger UNIVERSAL/HYBRID universal 半上提;"
|
|
526
|
+
"词面独立改写(clean-room),零逐字拷贝源文本",
|
|
527
|
+
"sub_rules": sub_rules,
|
|
528
|
+
"sources": sources,
|
|
529
|
+
}
|
|
530
|
+
if meta.get("review_notes"):
|
|
531
|
+
entry["review_notes"] = meta["review_notes"]
|
|
532
|
+
|
|
533
|
+
# ---- 词形合法自检(对照 vocab-lock 冻结词轴,只读消费)----
|
|
534
|
+
assert entry["classification"] in VOCAB_CLASSIFICATION
|
|
535
|
+
assert entry["enforcement"] in VOCAB_ENFORCEMENT
|
|
536
|
+
assert entry["applies_when"]["lane"] in VOCAB_LANES
|
|
537
|
+
for axis, vals in entry["axes"].items():
|
|
538
|
+
assert vals in VOCAB_AXES[axis], (axis, vals)
|
|
539
|
+
assert entry["id"] == entry["id"].upper() and " " not in entry["id"]
|
|
540
|
+
assert fname == entry["id"].lower() + ".json"
|
|
541
|
+
|
|
542
|
+
entries.append(entry)
|
|
543
|
+
return entries
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
# ======================================================================
|
|
547
|
+
# clean-room LCS 自检 + 零泄漏自检
|
|
548
|
+
# ======================================================================
|
|
549
|
+
def clean_room_audit(entries, halves):
|
|
550
|
+
src_cache = {}
|
|
551
|
+
|
|
552
|
+
def src_norm(asset_id):
|
|
553
|
+
if asset_id not in src_cache:
|
|
554
|
+
p = upstream_asset_path(asset_id)
|
|
555
|
+
with open(p, "rb") as f: # 上游消费项目只读
|
|
556
|
+
src_cache[asset_id] = norm_text(f.read().decode("utf-8"))
|
|
557
|
+
return src_cache[asset_id]
|
|
558
|
+
|
|
559
|
+
assets_by_file = {}
|
|
560
|
+
for h in halves:
|
|
561
|
+
assets_by_file.setdefault((h["dir"], h["filename"]), set()).add(h["asset_id"])
|
|
562
|
+
|
|
563
|
+
audit = {"threshold": LCS_THRESHOLD, "per_file": {}, "lcs_failures": [], "body_leaks": []}
|
|
564
|
+
for entry in entries:
|
|
565
|
+
fname = entry["id"].lower() + ".json"
|
|
566
|
+
key = ("policies", fname) # 本批条目词形已由 load_universal_halves 断言 POLICY./KNOWLEDGE. 与目录一致
|
|
567
|
+
body = norm_text(body_text_of(entry))
|
|
568
|
+
grams = {body[i:i + LCS_THRESHOLD] for i in range(max(0, len(body) - LCS_THRESHOLD + 1))}
|
|
569
|
+
file_audit = {"sources": sorted(assets_by_file[key]), "max_common_block": 0,
|
|
570
|
+
"gram_hits": 0}
|
|
571
|
+
for asset in sorted(assets_by_file[key]):
|
|
572
|
+
src = src_norm(asset)
|
|
573
|
+
src_grams = {src[i:i + LCS_THRESHOLD]
|
|
574
|
+
for i in range(max(0, len(src) - LCS_THRESHOLD + 1))}
|
|
575
|
+
hits = grams & src_grams
|
|
576
|
+
if hits:
|
|
577
|
+
audit["lcs_failures"].append(
|
|
578
|
+
{"file": fname, "asset": asset, "hit_len": LCS_THRESHOLD,
|
|
579
|
+
"samples": sorted(hits)[:3]})
|
|
580
|
+
file_audit["gram_hits"] += len(hits)
|
|
581
|
+
file_audit["max_common_block"] = max(file_audit["max_common_block"],
|
|
582
|
+
lcs_block_size(body, src))
|
|
583
|
+
for noun in PROJECT_NOUNS:
|
|
584
|
+
if noun in body_text_of(entry):
|
|
585
|
+
audit["body_leaks"].append({"file": fname, "noun": noun})
|
|
586
|
+
audit["per_file"][fname] = file_audit
|
|
587
|
+
|
|
588
|
+
if audit["lcs_failures"] or audit["body_leaks"]:
|
|
589
|
+
print(json.dumps({"verdict": "CLEAN_ROOM_FAIL", "audit": audit},
|
|
590
|
+
ensure_ascii=False, indent=2))
|
|
591
|
+
sys.exit(2)
|
|
592
|
+
return audit
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
# ======================================================================
|
|
596
|
+
# catalog-lock 同步(追加合并 + controlled_children 两处 + 全量 sha 对账)
|
|
597
|
+
# ======================================================================
|
|
598
|
+
def sha_of_path(rel):
|
|
599
|
+
with open(os.path.join(CATALOG, rel.replace("/", os.sep)), "rb") as f:
|
|
600
|
+
return "sha256:" + hashlib.sha256(f.read()).hexdigest()
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def merge_lock(new_entries):
|
|
604
|
+
"""读既有 lock → 按 id 合并 → 全量 content_sha256 与落盘对账(drift 即 fail-closed)。
|
|
605
|
+
|
|
606
|
+
前置:new_entries 的落盘文件必须已存在(main 先写条目文件再走 lock 同步)。
|
|
607
|
+
"""
|
|
608
|
+
with open(LOCK_PATH, encoding="utf-8") as f:
|
|
609
|
+
old_lock = json.load(f)
|
|
610
|
+
|
|
611
|
+
merged = {}
|
|
612
|
+
for e in old_lock["entries"]:
|
|
613
|
+
assert e["id"] not in merged, "旧 lock 重复 id: %s" % e["id"]
|
|
614
|
+
merged[e["id"]] = dict(e)
|
|
615
|
+
|
|
616
|
+
for entry in new_entries:
|
|
617
|
+
fname = entry["id"].lower() + ".json"
|
|
618
|
+
rel = "policies/" + fname
|
|
619
|
+
cid = "sha256:" + hashlib.sha256(dump_json(entry).encode("utf-8")).hexdigest()
|
|
620
|
+
if entry["id"] in merged:
|
|
621
|
+
# 幂等重跑:同 id 以本工具产物为准(sha 必须与落盘一致;source_ref 为
|
|
622
|
+
# LEDGER_REF 纯派生字段,随本工具常量同步刷新,防陈旧路径残留)
|
|
623
|
+
assert merged[entry["id"]]["path"] == rel
|
|
624
|
+
merged[entry["id"]]["content_sha256"] = cid
|
|
625
|
+
merged[entry["id"]]["source_ref"] = LEDGER_REF
|
|
626
|
+
else:
|
|
627
|
+
merged[entry["id"]] = {
|
|
628
|
+
"id": entry["id"], "path": rel, "content_sha256": cid,
|
|
629
|
+
"source_ref": LEDGER_REF,
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
# 全量对账:lock 每条 content_sha256 与落盘文件 0 mismatch(drift 即 fail-closed)
|
|
633
|
+
mismatches = []
|
|
634
|
+
for eid, e in sorted(merged.items()):
|
|
635
|
+
actual = sha_of_path(e["path"])
|
|
636
|
+
if actual != e["content_sha256"]:
|
|
637
|
+
mismatches.append({"id": eid, "lock": e["content_sha256"], "disk": actual})
|
|
638
|
+
if mismatches:
|
|
639
|
+
print(json.dumps({"verdict": "LOCK_DRIFT", "mismatches": mismatches},
|
|
640
|
+
ensure_ascii=False, indent=2))
|
|
641
|
+
sys.exit(2)
|
|
642
|
+
|
|
643
|
+
entries_out = sorted(merged.values(), key=lambda e: e["id"])
|
|
644
|
+
paths = sorted(e["path"] for e in entries_out)
|
|
645
|
+
assert len(paths) == len(set(paths))
|
|
646
|
+
|
|
647
|
+
lock = {
|
|
648
|
+
"catalog_version": old_lock["catalog_version"],
|
|
649
|
+
"profile": old_lock["profile"],
|
|
650
|
+
"generated_by": LOCK_GENERATED_BY,
|
|
651
|
+
"x-digest-ethics": old_lock["x-digest-ethics"],
|
|
652
|
+
"controlled_children": {
|
|
653
|
+
"note": "catalog-lock 管辖面(vocab-lock PR-0001 catalog_layer_vocab 同段语义):"
|
|
654
|
+
"allowed=登记在册可存在;required=必须存在。新增 catalog 文件须同步 allowed+required "
|
|
655
|
+
"两处(batch-4 split-ledger catalog_scope_note 纪律;沿 pomaster directory-lock "
|
|
656
|
+
"controlled_children 语义移植)。",
|
|
657
|
+
"allowed": list(paths),
|
|
658
|
+
"required": list(paths),
|
|
659
|
+
},
|
|
660
|
+
"entries": entries_out,
|
|
661
|
+
"note": old_lock["note"],
|
|
662
|
+
}
|
|
663
|
+
return lock, len(old_lock["entries"])
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def write_entries(entries):
|
|
667
|
+
"""先落条目文件(lock 全量对账要求盘上已有全部条目);构建两遍自证。"""
|
|
668
|
+
plan = [(os.path.join(CATALOG, "policies", e["id"].lower() + ".json"), dump_json(e))
|
|
669
|
+
for e in entries]
|
|
670
|
+
assert plan == [(os.path.join(CATALOG, "policies", e["id"].lower() + ".json"), dump_json(e))
|
|
671
|
+
for e in entries], "确定性序列化自检失败"
|
|
672
|
+
changed = []
|
|
673
|
+
for path, data in plan:
|
|
674
|
+
if write_if_changed(path, data.encode("utf-8")):
|
|
675
|
+
changed.append(os.path.relpath(path, VNEXT))
|
|
676
|
+
return changed
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def write_lock(lock):
|
|
680
|
+
"""lock 本体落盘;构建两遍逐字节比对一致后才写(幂等自证)。"""
|
|
681
|
+
data = dump_json(lock)
|
|
682
|
+
assert data == dump_json(lock), "确定性序列化自检失败"
|
|
683
|
+
return [LOCK_PATH] if write_if_changed(LOCK_PATH, data.encode("utf-8")) else []
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def verify_mode():
|
|
687
|
+
"""只读核验:lock 对账 + 幂等重演 + 零泄漏扫描(零写入)。"""
|
|
688
|
+
halves = load_universal_halves()
|
|
689
|
+
entries = build_entries(halves)
|
|
690
|
+
audit = clean_room_audit(entries, halves)
|
|
691
|
+
|
|
692
|
+
with open(LOCK_PATH, encoding="utf-8") as f:
|
|
693
|
+
lock = json.load(f)
|
|
694
|
+
mismatches = []
|
|
695
|
+
for e in lock["entries"]:
|
|
696
|
+
actual = sha_of_path(e["path"])
|
|
697
|
+
if actual != e["content_sha256"]:
|
|
698
|
+
mismatches.append({"id": e["id"], "lock": e["content_sha256"], "disk": actual})
|
|
699
|
+
paths = sorted(e["path"] for e in lock["entries"])
|
|
700
|
+
children = lock.get("controlled_children") or {}
|
|
701
|
+
children_ok = (sorted(children.get("allowed") or []) == paths
|
|
702
|
+
and sorted(children.get("required") or []) == paths)
|
|
703
|
+
|
|
704
|
+
new_ids = {e["id"] for e in entries}
|
|
705
|
+
raw_hits = []
|
|
706
|
+
for entry in entries:
|
|
707
|
+
fname = entry["id"].lower() + ".json"
|
|
708
|
+
with open(os.path.join(CATALOG, "policies", fname), "rb") as f:
|
|
709
|
+
raw = f.read().decode("utf-8")
|
|
710
|
+
for noun in PROJECT_NOUNS:
|
|
711
|
+
for m in re.finditer(re.escape(noun), raw):
|
|
712
|
+
ctx = raw[max(0, m.start() - 40):m.end() + 40].replace("\n", " ")
|
|
713
|
+
raw_hits.append({"file": fname, "noun": noun, "context": ctx})
|
|
714
|
+
|
|
715
|
+
report = {
|
|
716
|
+
"verdict": "OK" if (not mismatches and children_ok and not audit["lcs_failures"]
|
|
717
|
+
and not audit["body_leaks"]) else "FAIL",
|
|
718
|
+
"lock_entries": len(lock["entries"]),
|
|
719
|
+
"lock_mismatches": mismatches,
|
|
720
|
+
"controlled_children": {
|
|
721
|
+
"present": bool(children),
|
|
722
|
+
"allowed_count": len(children.get("allowed") or []),
|
|
723
|
+
"required_count": len(children.get("required") or []),
|
|
724
|
+
"allowed_eq_required_eq_entries": children_ok,
|
|
725
|
+
},
|
|
726
|
+
"uplift_entries": len(entries),
|
|
727
|
+
"uplift_files": sorted(e["id"] for e in entries),
|
|
728
|
+
"clean_room": audit,
|
|
729
|
+
"raw_grep_hits_full_file": {
|
|
730
|
+
"count": len(raw_hits),
|
|
731
|
+
"note": "正文词面 0 命中(clean_room.body_leaks);全文件原始命中(含元数据)"
|
|
732
|
+
"预期 0(clean_room_note 已零项目专名,与 pilot 注记先例一致)",
|
|
733
|
+
"hits": raw_hits,
|
|
734
|
+
},
|
|
735
|
+
}
|
|
736
|
+
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
737
|
+
sys.exit(0 if report["verdict"] == "OK" else 2)
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def main():
|
|
741
|
+
if "--verify" in sys.argv[1:]:
|
|
742
|
+
verify_mode()
|
|
743
|
+
return
|
|
744
|
+
|
|
745
|
+
halves = load_universal_halves()
|
|
746
|
+
entries = build_entries(halves)
|
|
747
|
+
audit = clean_room_audit(entries, halves)
|
|
748
|
+
changed_files = write_entries(entries) # 先落条目文件(lock 对账前置条件)
|
|
749
|
+
lock, old_count = merge_lock(entries) # 全量 sha 对账(0 mismatch 才放行)
|
|
750
|
+
changed_files += write_lock(lock) # lock 本体落盘
|
|
751
|
+
|
|
752
|
+
n_policy = sum(1 for e in entries if e["id"].startswith("POLICY."))
|
|
753
|
+
n_knowledge = sum(1 for e in entries if e["id"].startswith("KNOWLEDGE."))
|
|
754
|
+
print(json.dumps({
|
|
755
|
+
"verdict": "OK",
|
|
756
|
+
"batch": BATCH,
|
|
757
|
+
"universal_halves_total": len(halves),
|
|
758
|
+
"merged_catalog_entries": len(entries),
|
|
759
|
+
"from_universal": sum(1 for h in halves if h["decision"] == "UNIVERSAL"),
|
|
760
|
+
"from_hybrid": sum(1 for h in halves if h["decision"] == "HYBRID"),
|
|
761
|
+
"materialized": {"policies": n_policy, "knowledge": n_knowledge},
|
|
762
|
+
"merge_groups": sorted({h["merge_group"] for h in halves if h["merge_group"]}),
|
|
763
|
+
"lock": {"before_entries": old_count, "after_entries": len(lock["entries"]),
|
|
764
|
+
"controlled_children_added_files": len(entries),
|
|
765
|
+
"controlled_children_places": "allowed+required(两处)"},
|
|
766
|
+
"clean_room_lcs": {"threshold": LCS_THRESHOLD,
|
|
767
|
+
"max_common_block_observed":
|
|
768
|
+
max(v["max_common_block"] for v in audit["per_file"].values()),
|
|
769
|
+
"failures": 0},
|
|
770
|
+
"zero_leak_body_hits": 0,
|
|
771
|
+
"files_written_or_changed": changed_files,
|
|
772
|
+
}, ensure_ascii=False, indent=2))
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
if __name__ == "__main__":
|
|
776
|
+
main()
|