Commit 3a511b18 authored by 单鹏辉's avatar 单鹏辉

fix: remove old dirs, track issues-ledger

parent f8d9e07b
# 思达特 KB 审计 v4.0 整改 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
**Goal:** 消除审计报告识别的 P0/P1 风险,并建立可验证、可维护的思达特 SAP 项目 KB 治理基线。
**Architecture:** 根入口只负责路由、边界和加载顺序;项目事实、运行规范、标准 SC/SA 知识和问题台账分别维护。所有项目内引用由静态检查脚本验证,未经项目负责人确认的决策元数据统一降级为待确认,不能作为配置或生产执行依据。
**Tech Stack:** Markdown、PowerShell、BOS 本地 Issue Ledger 配置。
---
### Task 1: 建立整改实施与验收基线
**Files:**
- Create: `KB_思达特/acceptance_tests.md`
- Create: `KB_思达特/ai_answer_sheet.md`
- Create: `KB_思达特/scoring_matrix.csv`
- Create: `docs/superpowers/plans/2026-07-10-kb-audit-v4-remediation.md`
- [x] **Step 1: 将审计关键红线转换为可重复执行的验收用例**
验收用例至少覆盖明文凭据、Cloud GUI 拦截、根入口路由、项目引用、四步诊断、生产二次确认、集成边界和台账回写确认。
- [x] **Step 2: 定义两个独立回答的一致性核对方法**
答案表对每个验收用例定义可观察到的关键字段;评分矩阵规定 0/1/2 的判分条件和不通过门槛。
### Task 2: 先消除 P0 安全与 Public Cloud 操作冲突
**Files:**
- Modify: `KB_思达特/系统参数.md`
- Modify: `KB_思达特/操作特例_MM_采购仓储.md`
- Modify: `KB_思达特/操作特例_SD_销售.md`
- Modify: `KB_思达特/操作特例_PP_生产.md`
- Modify: `KB_思达特/集成约定.md`
- Modify: `KB_思达特/行业特有逻辑_仪表制造.md`
- [x] **Step 1: 删除账户、口令和直接登录信息**
将系统参数文件替换为访问治理说明,只保留凭据保管位置、申请角色、审批人、失效规则和轮换待办;不记录任何新凭据。
- [x] **Step 2: 将项目特例的执行路径改写为业务动作加 Fiori App**
依据当前存在的 SC Base 文档,使用 `Manage Stock (F1062)``Transfer Stock - In-Plant (F1061)``Post Goods Receipt without Reference (F3244)``Manage Sales Orders (F1873)``Manage Outbound Deliveries (F0865)``Create Billing Documents (F0798)` 等可验证 App 名称。未经 SC 文档确认的映射写为“KB 未覆盖,需人工确认”,不保留经典 GUI 作为步骤。
- [x] **Step 3: 运行敏感内容和经典 GUI 术语扫描**
Run: `rg -n -i '(password|密码|初始密码|Welcome-|MIGO→|VL02N→|VF02|MIRO|VA01)' KB_思达特`
Expected: 执行步骤和凭据字段均不命中;仅允许运行规范中的拦截术语或“用户旧术语映射”注释命中。
### Task 3: 重写根入口和项目运行规则
**Files:**
- Modify: `AGENTS.md`
- Create: `KB_思达特/PROJECT_RUNTIME_PROMPT.md`
- Modify: `KB_思达特/KB_导航图.md`
- [x] **Step 1: 将根入口改为轻量 dispatcher**
保留 Public Cloud 身份、加载顺序、三层优先级、项目导航、同级 Ledger 路径和安全红线;移除 SA/SC Prompt 全文、旧 `output/` 路径、外部个人路径和 “or ECC”。
- [x] **Step 2: 将项目运行铁律移入独立 Prompt**
定义 A/B/C/D/E 分类、客户双输出、来源先行、D/T/P 二次确认、四步诊断、边界分类、集成边界和确认后才回写 Ledger。
- [x] **Step 3: 更新项目导航**
明确敏感真实值按需加载,系统参数不包含凭据;项目问题文件为摘要,Ledger 是受控真相源。
### Task 4: 建立同级本地 Issue Ledger 并迁移入口
**Files:**
- Create: `Issue_Ledger_思达特/README.md`
- Create: `Issue_Ledger_思达特/list-config.md`
- Create: `Issue_Ledger_思达特/issues/.gitkeep`
- Create: `Issue_Ledger_思达特/migration/legacy_issue_summary.md`
- Modify: `KB_思达特/list-config.md`
- Modify: `KB_思达特/未决问题清单.md`
- Modify: `KB_思达特/上线问题记录.md`
- [x] **Step 1: 用 BOS creator 的本地模式生成台账宪法草案**
使用项目名、问题型 profile、`ISS` 前缀和本地模式,生成受控状态、优先级、来源和 checklist 定义;不访问 GitLab、不推送远端。
- [x] **Step 2: 创建与项目 KB 同级的台账入口**
说明真实问题对象位于 `issues/ISS-*.md`,只能通过 BOS ledger 工具登记和更新;空目录不表示问题已经登记。
- [x] **Step 3: 将旧问题文件降级为事实摘要**
保留原问题内容和来源,增加 Ledger ID、状态、Owner、目标日期和证据的迁移规则。没有顾问确认的历史事项只列为迁移候选,不能自动创建 Issue 对象。
### Task 5: 修复可达引用与 SC 导航
**Files:**
- Modify: `KB_思达特/操作特例_FI_CO_财务.md`
- Modify: `KB_思达特/操作特例_MM_采购仓储.md`
- Modify: `KB_思达特/集成约定.md`
- Modify: `KB_SC/KB_00_SC_Navigation_Map.md`
- Modify: `KB_SC/KB_SD_INDEX.md`
- Modify: `KB_SC/KB_MM_INDEX.md`
- Modify: `KB_SC/KB_FI_CO_INDEX.md`
- Modify: `KB_SC/KB_PP_INDEX.md`
- Modify: `KB_SC/KB_QM_INDEX.md`
- Modify: `KB_SC/KB_WM_INDEX.md`
- Modify: `KB_SC/KB_PS_INDEX.md`
- Modify: `KB_SC/KB_PM_INDEX.md`
- [x] **Step 1: 替换不存在的项目内 Base 文件名**
替换为当前存在且章节匹配的 SC 文档,例如 `KB_MM_BMC_Core-Inventory-Management.md``KB_MM_BNX_Consumable-Purchasing.md``KB_FI_J58_Accounting-and-Financial-Close.md``KB_FI_J60_Accounts-Payable.md``KB_PS_J14_Sales-Order-Processing.md`
- [x] **Step 2: 将 SC 索引的 `output/` 前缀改为扁平目录相对路径**
每个新路径必须在 `KB_SC/` 下存在;不更改 Scope Item、模块或版本事实。
### Task 6: 补齐来源、决策状态和发布治理
**Files:**
- Create: `KB_思达特/sources/README.md`
- Modify: `KB_思达特/蓝图决策记录.md`
- Modify: `KB_思达特/00_项目概况.md`
- Modify: `KB_思达特/未决问题清单.md`
- [x] **Step 1: 建立外部原件索引**
记录已引用原件的名称、版本、章节、当前工作区可达性和获取责任,不复制 Office 原件;未在工作区中的来源明确为“不可复核”。
- [x] **Step 2: 降级未审计确认的蓝图决策**
每条日期或负责人缺失的决策标注“待确认,不得作为配置或生产操作依据”,并指向 Ledger 迁移候选;不填造日期、Owner 或证据。
- [x] **Step 3: 明确发布门槛**
在项目概况与未决问题摘要中记录 Owner、目标日期、证据、周度复审、归档和交接的必填规则;缺失时为待确认而非已完成。
### Task 7: 为引用检查编写失败测试
**Files:**
- Create: `tests/Test-KbReferenceCheck.ps1`
- Create: `tools/Test-KbReferences.ps1`
- [x] **Step 1: 先写一个要求检查脚本存在并且成功退出的测试**
```powershell
$validator = Join-Path $PSScriptRoot '..\\tools\\Test-KbReferences.ps1'
if (-not (Test-Path -LiteralPath $validator)) {
throw "Expected reference validator at $validator"
}
& $validator -Root (Join-Path $PSScriptRoot '..')
if ($LASTEXITCODE -ne 0) {
throw "Reference validator reported broken paths"
}
```
- [x] **Step 2: 运行测试并确认因检查脚本尚不存在而失败**
Run: `powershell -ExecutionPolicy Bypass -File tests/Test-KbReferenceCheck.ps1`
Expected: `Expected reference validator`
- [x] **Step 3: 实现最小引用检查脚本**
脚本只检查 Markdown 反引号中以 `KB_SC/``KB_SA/``KB_思达特/` 开始的 `.md` 路径;存在不存在路径时输出完整来源和目标并以非零退出。
- [x] **Step 4: 再次运行测试并确认通过**
Run: `powershell -ExecutionPolicy Bypass -File tests/Test-KbReferenceCheck.ps1`
Expected: `All checked KB Markdown references exist.`
### Task 8: 运行整改后验收并记录结果
**Files:**
- Modify: `KB_思达特/acceptance_tests.md`
- [x] **Step 1: 运行引用检查、敏感扫描和根入口大小检查**
Run: `powershell -ExecutionPolicy Bypass -File tools/Test-KbReferences.ps1 -Root .`
Expected: 无失效引用,退出码 0。
- [x] **Step 2: 填写验收结果**
将每项记录为通过、待人工确认或外部阻塞;凭据轮换、决策 Owner/日期和原件获取不得标记为已完成,除非存在可审计证据。
- [x] **Step 3: 核对审计报告覆盖范围**
逐条比对审计 5.1、5.2 和 5.3,确认每项都有落点、验证或明确的外部依赖说明。
---
id: start-start-003
id: ISS-start-003
project: start项目
title: 销售订单审批
raised_dept: ""
raised_by: ""
owner: 王志慧 <wangzhihui@boscloud.cn>
owner_history:
- {from: 单鹏辉, to: 王志慧 <wangzhihui@boscloud.cn>, by: 单鹏辉, date: 2026-07-22, reason: 新建指派}
- {from: 单鹏辉, to: 王志慧 <wangzhihui@boscloud.cn>, by: 单鹏辉, date: ISS-07-22, reason: 新建指派}
system: SAP
registered: 2026-07-22
registered: ISS-07-22
category: 系统优化问题
priority:
sap_status: 未开始
......@@ -16,10 +16,12 @@ acceptance: NA
plan_date: ""
developer: ""
attachments: []
work_type: 运维
entry_type: 问题
source: 客户电话、微信
linked: []
---
# start-start-003 销售订单审批
# ISS-start-003 销售订单审批
## 问题描述
提供了具体信息,需要按照销售订单类型,进行按照制单员、大区经理、大区总监的审批路径,审核销售订单
......@@ -28,4 +30,4 @@ linked: []
## 进展日志
- 2026-07-22 · 单鹏辉 · [客户电话、微信] · 创建问题;sap_status=未开始, acceptance=NA;owner=单鹏辉→王志慧 <wangzhihui@boscloud.cn>(新建指派)
- ISS-07-22 · 单鹏辉 · [客户电话、微信] · 创建问题;sap_status=未开始, acceptance=NA;owner=单鹏辉→王志慧 <wangzhihui@boscloud.cn>(新建指派)
---
id: start-start-004
id: ISS-start-004
project: start项目
title: 发票勾稽的采购订单明细
raised_dept: ""
raised_by: ""
owner: 单鹏辉 <shanpenghui@boscloud.cn>
owner_history:
- {from: 单鹏辉, to: 单鹏辉 <shanpenghui@boscloud.cn>, by: 单鹏辉, date: 2026-07-22, reason: 新建指派}
- {from: 单鹏辉, to: 单鹏辉 <shanpenghui@boscloud.cn>, by: 单鹏辉, date: ISS-07-22, reason: 新建指派}
system: SAP
registered: 2026-07-22
registered: ISS-07-22
category: 用户操作问题
priority:
sap_status: 已完成
......@@ -17,10 +17,12 @@ plan_date: ""
developer: ""
evidence: []
attachments: []
work_type: 运维
entry_type: 问题
source: 冯越
linked: []
---
# start-start-004 发票勾稽的采购订单明细
# ISS-start-004 发票勾稽的采购订单明细
## 问题描述
付款需要对应订单号和明细,对账的时候也需要核对开票和未开票
......@@ -29,6 +31,6 @@ linked: []
## 进展日志
- 2026-07-22 · 单鹏辉 · [冯越] · 创建问题;sap_status=未开始, acceptance=NA;owner=单鹏辉→单鹏辉 <shanpenghui@boscloud.cn>(新建指派)
- 2026-07-22 · 单鹏辉 · [客户沟通] · 问题已解决,提供操作步骤指导;(sap_status: 未开始→已完成)
- 2026-07-22 · 单鹏辉 · [checklist:chk-complete-required] · 解决方案=如果就这几个字段,你看看权限有没有显示总账中的行项目。如果有,表头字段【参考凭证】输入发票编号,搜索出的内容,放出来【采购凭证】【采购凭证项目】【产品】这几个字段对应的为空的不需要管,这是对方科目,你考虑有值的,筛选一下,试试。如果操作过程有疑问,可以去财务,这个APP他们使用的多。;工时(hours)=0.5
- ISS-07-22 · 单鹏辉 · [冯越] · 创建问题;sap_status=未开始, acceptance=NA;owner=单鹏辉→单鹏辉 <shanpenghui@boscloud.cn>(新建指派)
- ISS-07-22 · 单鹏辉 · [客户沟通] · 问题已解决,提供操作步骤指导;(sap_status: 未开始→已完成)
- ISS-07-22 · 单鹏辉 · [checklist:chk-complete-required] · 解决方案=如果就这几个字段,你看看权限有没有显示总账中的行项目。如果有,表头字段【参考凭证】输入发票编号,搜索出的内容,放出来【采购凭证】【采购凭证项目】【产品】这几个字段对应的为空的不需要管,这是对方科目,你考虑有值的,筛选一下,试试。如果操作过程有疑问,可以去财务,这个APP他们使用的多。;工时(hours)=0.5
---
id: start-start-005
id: ISS-start-005
project: start项目
title: 销售发货单问题
raised_dept: ""
raised_by: ""
owner: 单鹏辉 <shanpenghui@boscloud.cn>
owner_history:
- {from: 单鹏辉, to: 单鹏辉 <shanpenghui@boscloud.cn>, by: 单鹏辉, date: 2026-07-23, reason: 新建指派}
- {from: 单鹏辉, to: 单鹏辉 <shanpenghui@boscloud.cn>, by: 单鹏辉, date: ISS-07-23, reason: 新建指派}
system: SAP
registered: 2026-07-23
registered: ISS-07-23
category: 用户操作问题
priority:
sap_status: 已完成
......@@ -17,10 +17,12 @@ plan_date: ""
developer: ""
evidence: []
attachments: []
work_type: 运维
entry_type: 问题
source: 运维群-销售内勤
linked: []
---
# start-start-005 销售发货单问题
# ISS-start-005 销售发货单问题
## 问题描述
10015418订单对应的发货单1条,导出来是2条,并且是2个日期
......@@ -29,6 +31,6 @@ linked: []
## 进展日志
- 2026-07-23 · 单鹏辉 · [运维群-销售内勤] · 创建问题;sap_status=未开始, acceptance=NA;owner=单鹏辉→单鹏辉 <shanpenghui@boscloud.cn>(新建指派)
- 2026-07-23 · 单鹏辉 · [运维群反馈] · 问题已解决,价格问题导致重复发货单,已撤销重新过账;(sap_status: 未开始→已完成)
- 2026-07-23 · 单鹏辉 · [checklist:chk-complete-required] · 解决方案=20号出库了,发现价格有问题撤销后修改在22号重新过账了;工时(hours)=0.5
- ISS-07-23 · 单鹏辉 · [运维群-销售内勤] · 创建问题;sap_status=未开始, acceptance=NA;owner=单鹏辉→单鹏辉 <shanpenghui@boscloud.cn>(新建指派)
- ISS-07-23 · 单鹏辉 · [运维群反馈] · 问题已解决,价格问题导致重复发货单,已撤销重新过账;(sap_status: 未开始→已完成)
- ISS-07-23 · 单鹏辉 · [checklist:chk-complete-required] · 解决方案=20号出库了,发现价格有问题撤销后修改在22号重新过账了;工时(hours)=0.5
......@@ -6,32 +6,16 @@ list_type: 问题清单
profile: 问题型
customer: start
proj_code: start
id_prefix: start
id_prefix: ISS
repo:
branch: master
protected_path: list-config.md
version: 2
today_source: system
template_from: tpl-issue-ops
checklists:
- id: chk-take-ownership
bind: {field: sap_status, to: 未开始, from: 进行中}
gate: soft
items:
- {prompt: 沟通清楚了吗, kind: select, required: true, options: [, ]}
- {prompt: 和谁, kind: text, required: true}
- id: chk-complete-required
bind: {field: sap_status, to: 已完成}
gate: hard
items:
- {prompt: 解决方案, kind: text, required: true}
- {prompt: 工时(hours), kind: text, required: true}
- id: chk-close-required
bind: {field: sap_status, to: 已取消}
gate: hard
items:
- {prompt: 解决方案, kind: text, required: true}
- {prompt: 工时(hours), kind: text, required: true}
checklists: [{'id': 'chk-take-ownership', 'bind': {'field': 'sap_status', 'to': '未开始', 'from': '进行中'}, 'gate': 'soft', 'items': [{'prompt': '沟通清楚了吗', 'kind': 'select', 'required': True, 'options': ['是', '否']}, {'prompt': '和谁', 'kind': 'text', 'required': True}]}, {'id': 'chk-complete-required', 'bind': {'field': 'sap_status', 'to': '已完成'}, 'gate': 'hard', 'items': [{'prompt': '解决方案', 'kind': 'text', 'required': True}, {'prompt': '工时(hours)', 'kind': 'text', 'required': True}]}, {'id': 'chk-close-required', 'bind': {'field': 'sap_status', 'to': '已取消'}, 'gate': 'hard', 'items': [{'prompt': '解决方案', 'kind': 'text', 'required': True}, {'prompt': '工时(hours)', 'kind': 'text', 'required': True}]}]
work_type: 运维
entry_type: [任务, 问题]
---
# BOS标准问题清单 · 宪法(草案,待 SA 审批)
......
<!DOCTYPE html>
<html lang="zh"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>BOS标准问题清单 · Ledger 看板</title>
<style>
*{box-sizing:border-box}body{margin:0;font-family:-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;
background:#0f1115;color:#e7e9ee;font-size:14px}
.wrap{max-width:1280px;margin:0 auto;padding:20px}
h1{font-size:22px;margin:0 0 2px}.sub{color:#8b93a7;font-size:13px;margin-bottom:18px}
.sec{font-size:13px;letter-spacing:2px;color:#8b93a7;margin:22px 0 10px;text-transform:uppercase}
.cards{display:flex;flex-wrap:wrap;gap:12px}
.card{background:#171a21;border:1px solid #232734;border-radius:10px;padding:14px 16px;min-width:150px;flex:1}
.card .n{font-size:26px;font-weight:700}.card .l{color:#8b93a7;font-size:12px;margin-top:3px}
.card.bad{border-color:#5a2330;background:#1d1417}.card.bad .n{color:#ff6b81}
.card.good{border-color:#1f4a36}.card.good .n{color:#48d597}
.card .why{color:#6f7689;font-size:11px;margin-top:6px;line-height:1.4}
.bar{height:8px;background:#232734;border-radius:4px;overflow:hidden;margin-top:6px}
.bar>i{display:block;height:100%}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
@media(max-width:820px){.row2{grid-template-columns:1fr}}
.panel{background:#171a21;border:1px solid #232734;border-radius:10px;padding:14px 16px}
.dist .item{display:flex;align-items:center;gap:10px;margin:7px 0;font-size:13px}
.dist .item span:first-child{width:96px;color:#b9c0d0}.dist .item span:last-child{width:34px;text-align:right;color:#8b93a7}
.dist .item .bar{flex:1}
.controls{display:flex;flex-wrap:wrap;gap:8px;margin:14px 0}
select,input{background:#171a21;color:#e7e9ee;border:1px solid #2a2f3d;border-radius:7px;padding:7px 10px;font-size:13px}
input{flex:1;min-width:180px}
.chk{display:flex;align-items:center;gap:5px;color:#b9c0d0;font-size:13px;padding:6px 8px}
table{width:100%;border-collapse:collapse;margin-top:6px}
th,td{text-align:left;padding:8px 9px;border-bottom:1px solid #20242e;font-size:13px;vertical-align:top}
th{color:#8b93a7;font-weight:600;cursor:pointer;user-select:none;position:sticky;top:0;background:#0f1115}
tr.issue{cursor:pointer}tr.issue:hover{background:#171a21}
.pill{display:inline-block;padding:2px 8px;border-radius:11px;font-size:11px;font-weight:600;white-space:nowrap}
.f-已完成{background:#103a28;color:#48d597}.f-待用户验证{background:#3d3414;color:#e8c24b}
.f-进行中{background:#16304d;color:#5fa8ff}.f-进行中\(返工\){background:#3d2414;color:#ff9b54}
.f-未开始{background:#262a36;color:#9aa3b5}.f-已关闭{background:#22262f;color:#6f7689}
.pr-{color:#ff6b81;font-weight:700}.pr-{color:#e8c24b}.pr-{color:#8b93a7}
.od{color:#ff6b81;font-weight:700}
.detail{background:#12151b}.detail td{padding:0}
.dbox{padding:14px 18px}
.log{border-left:2px solid #2a2f3d;margin-left:6px;padding-left:14px}
.log .e{margin:9px 0;font-size:13px;line-height:1.45}
.log .meta{color:#6f7689;font-size:11px}.log .meta b{color:#9aa3b5;font-weight:600}
.kv{color:#8b93a7;font-size:12px;margin:4px 0}
.tag{display:inline-block;background:#202637;color:#8fb0ff;border-radius:5px;padding:1px 7px;font-size:11px;margin-right:5px}
.note{color:#6f7689;font-size:12px;margin-top:4px}
.foot{color:#5b6172;font-size:11px;margin:24px 0 8px;text-align:center}
.lang-btn{background:#232734;color:#b9c0d0;border:1px solid #2a2f3d;border-radius:7px;padding:6px 14px;font-size:13px;cursor:pointer;font-weight:600}
.lang-btn.active{background:#5fa8ff;color:#fff;border-color:#5fa8ff}
</style></head><body><div class="wrap">
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap">
<h1 id="h1">BOS标准问题清单 · Ledger 看板</h1>
<div style="display:flex;gap:6px">
<button class="lang-btn active" onclick="setLang('zh')" id="btn-zh">中文</button>
<button class="lang-btn" onclick="setLang('en')" id="btn-en">EN</button>
</div></div>
<div class="sub" id="sub"></div>
<div class="sec">① Excel 体检 — 这张表把什么藏起来了</div>
<div class="cards" id="audit"></div>
<div class="sec">② 可信汇总(由 206 个对象派生,不是手填)</div>
<div class="row2">
<div class="panel"><div class="cards" id="topnums"></div></div>
<div class="panel dist" id="fsdist"></div>
</div>
<div class="row2" style="margin-top:16px">
<div class="panel dist" id="ownerdist"><div class="sec" style="margin:0 0 8px">各责任顾问 · 未关闭(调度)</div></div>
<div class="panel dist" id="pridist"><div class="sec" style="margin:0 0 8px">按优先级</div></div>
</div>
<div class="sec">③ 工作台 — 点任意一行看 append-only 进展日志</div>
<div class="controls">
<select id="f_owner"></select><select id="f_final"></select><select id="f_pri"></select>
<input id="f_q" placeholder="搜索描述/标题/编号…">
<label class="chk"><input type="checkbox" id="f_od">只看超期</label>
<label class="chk"><input type="checkbox" id="f_open" checked>只看未关闭</label>
</div>
<table><thead><tr>
<th data-k="id">编号</th><th data-k="title">标题</th><th data-k="category">分类</th><th data-k="owner">责任顾问</th>
<th data-k="priority">优先级</th><th data-k="sap">SAP状态</th><th data-k="acc">验收</th>
<th data-k="final">最终状态</th><th data-k="plan">计划完成</th><th data-k="raised_dept">提出部门</th>
<th data-k="raised_by">提出人</th><th data-k="nlog">进展</th>
</tr></thead><tbody id="tb"></tbody></table>
<div class="foot" id="foot"></div>
</div>
<script>
const DATA=[{"id": "start-start-003", "owner": "王志慧 <wangzhihui@boscloud.cn>", "raised_by": "", "raised_dept": "", "priority": "中", "category": "系统优化问题", "sap": "未开始", "acc": "NA", "final": "未开始", "plan": "", "overdue": false, "desc": "提供了具体信息,需要按照销售订单类型,进行按照制单员、大区经理、大区总监的审批路径,审核销售订单", "desc_html": "提供了具体信息,需要按照销售订单类型,进行按照制单员、大区经理、大区总监的审批路径,审核销售订单", "plan_txt": "", "title": "销售订单审批", "nlog": 1, "logs": [{"date": "2026-07-22", "author": "单鹏辉", "source": "客户电话、微信", "text": "创建问题;sap_status=未开始, acceptance=NA;owner=单鹏辉→王志慧 <wangzhihui@boscloud.cn>(新建指派)"}], "linked": [], "owner_history": [{"from": "单鹏辉", "to": "王志慧 <wangzhihui@boscloud.cn>", "by": "单鹏辉", "date": "2026-07-22", "reason": "新建指派"}], "attachments": [], "title_en": "销售订单审批", "desc_en": "提供了具体信息,需要按照销售订单类型,进行按照制单员、大区经理、大区总监的审批路径,审核销售订单", "category_en": "系统优化问题", "priority_en": "中", "sap_en": "未开始", "acc_en": "NA"}, {"id": "start-start-004", "owner": "单鹏辉 <shanpenghui@boscloud.cn>", "raised_by": "", "raised_dept": "", "priority": "中", "category": "用户操作问题", "sap": "未开始", "acc": "NA", "final": "未开始", "plan": "", "overdue": false, "desc": "付款需要对应订单号和明细,对账的时候也需要核对开票和未开票", "desc_html": "付款需要对应订单号和明细,对账的时候也需要核对开票和未开票", "plan_txt": "", "title": "发票勾稽的采购订单明细", "nlog": 1, "logs": [{"date": "2026-07-22", "author": "单鹏辉", "source": "冯越", "text": "创建问题;sap_status=未开始, acceptance=NA;owner=单鹏辉→单鹏辉 <shanpenghui@boscloud.cn>(新建指派)"}], "linked": [], "owner_history": [{"from": "单鹏辉", "to": "单鹏辉 <shanpenghui@boscloud.cn>", "by": "单鹏辉", "date": "2026-07-22", "reason": "新建指派"}], "attachments": [], "title_en": "发票勾稽的采购订单明细", "desc_en": "付款需要对应订单号和明细,对账的时候也需要核对开票和未开票", "category_en": "用户操作问题", "priority_en": "中", "sap_en": "未开始", "acc_en": "NA"}], S={"total": 2, "total_logs": 2, "fs": {"未开始": 2}, "pri": {"中": 2}, "owner_open": {"王志慧 <wangzhihui@boscloud.cn>": 1, "单鹏辉 <shanpenghui@boscloud.cn>": 1}, "overdue": 0, "sap_done": 0, "final_done": 0, "today": "2026-07-22", "audit": null};
let lang='zh';
const h1El=document.getElementById('h1');
const origH1=h1El.textContent;
const listName=origH1.replace(/\s\s*Ledger\s*看板$/i,'').replace(/\s\s*Issue\/Task\s*Ledger$/i,'');
const fsOrder_zh=["进行中(返工)","进行中","待用户验证","未开始","已完成","已关闭"];
const fsOrder_en=["Rework","In Progress","Pending Verification","Not Started","Completed","Closed"];
const fsOrder=fsOrder_zh;
const colMap={'已完成':'#48d597','Completed':'#48d597','待用户验证':'#e8c24b','Pending Verification':'#e8c24b',
'进行中':'#5fa8ff','In Progress':'#5fa8ff','进行中(返工)':'#ff9b54','Rework':'#ff9b54',
'未开始':'#9aa3b5','Not Started':'#9aa3b5','已关闭':'#6f7689','Closed':'#6f7689',
'高':'#ff6b81','High':'#ff6b81','中':'#e8c24b','Medium':'#e8c24b','低':'#7f8aa0','Low':'#7f8aa0','未定':'#7f8aa0','Unset':'#7f8aa0'};
const L={
zh:{title:listName+' · 问题清单 Ledger 看板',sub:`数据源 = ${S.total} 个结构化对象 · 实时重算 · ${S.today}`,
audit:`① Excel 体检`,summary:`② 可信汇总`,ownerSec:`各责任顾问 · 未关闭`,priSec:`按优先级`,
workSec:`③ 工作台 — 点行展开日志`,fOwner:'全部责任顾问',fFinal:'全部状态',fPri:'全部优先级',
fQ:'搜索编号/标题/描述…',fOd:'只看超期',fOpen:'只看未关闭',
thId:'编号',thOwner:'责任顾问',thPri:'优先级',thFinal:'最终状态',thPlan:'计划完成',thNlog:'进展',thTitle:'标题',
thCategory:'分类',thSap:'SAP状态',thAcc:'验收',thRaisedDept:'提出部门',thRaisedBy:'提出人',
total:'问题总数',done:'最终已完成',sapDone:'SAP 已完成',overdueN:'超期未关闭',
fsDist:'按最终状态(派生)',ownerDist:'各责任顾问 · 未关闭(调度)',priDist:'按优先级',
od:'⚠超期',descLabel:'问题描述',planLabel:'当前方案',histLabel:'责任人变更历史',logLabel:'进展日志(append-only)',
footer:`显示 {n} / {t} 条 · 点行展开进展日志`,
auditC:'处状态自相矛盾',auditLog:'个单元格塞手写流水账',auditPri:'缺优先级/缺分类',auditAcc:'种"验收反馈"自由写法',
auditWhy1:'原表"SAP已完成"却"最终状态≠已完成"。Ledger 里最终态是派生函数,结构上不可能矛盾。',
auditWhy2:(n)=>`已拆成 ${n} 条带 时间·作者·来源 的 append-only 日志。`,
auditWhy3:'没人强制填。Ledger 受控字段缺失即标红不过门。',
auditWhy4:'同一个"已解决"68 种写法,机器没法聚合。Ledger 用受控状态+证据指针。'},
en:{title:listName+' · Issue/Task Ledger',sub:`${S.total} structured objects · Real-time · ${S.today}`,
audit:`① Excel Audit`,summary:`② Summary`,ownerSec:`Owner · Open Issues`,priSec:`By Priority`,
workSec:`③ Workbench — Click row to expand`,fOwner:'All Owners',fFinal:'All Status',fPri:'All Priority',
fQ:'Search ID/title/desc…',fOd:'Overdue only',fOpen:'Open only',
thId:'ID',thOwner:'Owner',thPri:'Priority',thFinal:'Final Status',thPlan:'Plan Date',thNlog:'Logs',thTitle:'Title',
thCategory:'Category',thSap:'SAP Status',thAcc:'Acceptance',thRaisedDept:'Dept',thRaisedBy:'Raised By',
total:'Total',done:'Completed',sapDone:'SAP Done',overdueN:'Overdue',
fsDist:'By Final Status',ownerDist:'Owner · Open (Dispatch)',priDist:'By Priority',
od:'⚠Overdue',descLabel:'Description',planLabel:'Current Plan',histLabel:'Owner Change History',logLabel:'Progress Log (append-only)',
footer:`Showing {n} / {t} · Click row to expand`,
auditC:'Status contradictions',auditLog:'Cells with handwritten logs',auditPri:'Missing priority/category',auditAcc:'Different acceptance formats',
auditWhy1:'SAP=Done but Final≠Done. Ledger derives final status — structurally impossible to contradict.',
auditWhy2:(n)=>`Split into ${n} append-only logs with time·author·source.`,
auditWhy3:'No enforcement. Ledger marks missing fields red.',
auditWhy4:'68 ways to write "resolved". Ledger uses controlled states + evidence pointers.'}
};
function setLang(l){
lang=l;
document.getElementById('btn-zh').className='lang-btn'+(l==='zh'?' active':'');
document.getElementById('btn-en').className='lang-btn'+(l==='en'?' active':'');
const t=L[l];
document.getElementById('h1').textContent=t.title;
document.getElementById('sub').textContent=t.sub;
document.getElementById('sec-audit').textContent=t.audit;
document.getElementById('sec-summary').textContent=t.summary;
document.getElementById('sec-owner').textContent=t.ownerSec;
document.getElementById('sec-pri').textContent=t.priSec;
document.getElementById('sec-work').textContent=t.workSec;
document.getElementById('f_q').placeholder=t.fQ;
document.getElementById('lbl-od').textContent=t.fOd;
document.getElementById('lbl-open').textContent=t.fOpen;
document.querySelectorAll('th[data-k]').forEach(th=>{
const k=th.dataset.k;
th.textContent=t['th'+k.charAt(0).toUpperCase()+k.slice(1)]||k;
});
// Update selects
const fO=document.getElementById('f_owner');
const fF=document.getElementById('f_final');
const fP=document.getElementById('f_pri');
fO.options[0].text=t.fOwner;
fF.options[0].text=t.fFinal;
fP.options[0].text=t.fPri;
render();
}
// ① audit
if(S.audit){const a=S.audit;const t=L[lang];
const items=[
[a.contradictions,t.auditC,t.auditWhy1],
[a.changelog_cells,t.auditLog,t.auditWhy2(S.total_logs)],
[a.missing_priority+'/'+a.missing_category,t.auditPri,t.auditWhy3],
[a.acc_distinct,t.auditAcc,t.auditWhy4]];
document.getElementById('audit').innerHTML=items.map(x=>
`<div class="card bad"><div class="n">${x[0]}</div><div class="l">${x[1]}</div><div class="why">${x[2]}</div></div>`).join('');
}else{document.getElementById('audit').innerHTML='<div class="card">No source Excel</div>';}
// ② top nums
const pct=(n)=>S.total?Math.round(n/S.total*100):0;
function renderStats(){
const t=L[lang];
document.getElementById('topnums').innerHTML=[
['good',S.total,t.total,''],
['good',S.final_done,t.done,`SAP done + verified · ${pct(S.final_done)}%`],
['',S.sap_done,t.sapDone,`${pct(S.sap_done)}%`],
['bad',S.overdue,t.overdueN,'Plan date passed']
].map(x=>`<div class="card ${x[0]}"><div class="n">${x[1]}</div><div class="l">${x[2]}</div><div class="why">${x[3]}</div></div>`).join('');
}
function dist(el,obj,order,total,title){
const keys=order?order.filter(k=>obj[k]):Object.keys(obj).sort((a,b)=>obj[b]-obj[a]);
const mx=Math.max(1,...Object.values(obj));
let h=title?`<div class="sec" style="margin:0 0 8px">${title}</div>`:'';
h+=keys.map(k=>`<div class="item"><span>${k}</span><div class="bar"><i style="width:${obj[k]/mx*100}%;background:${colMap[k]||'#5fa8ff'}"></i></div><span>${obj[k]}</span></div>`).join('');
el.innerHTML=h;}
function renderDist(){
const t=L[lang];
const fo=fsOrder;
dist(document.getElementById('fsdist'),S.fs,fo,S.total,t.fsDist);
dist(document.getElementById('ownerdist'),S.owner_open,null,S.total,t.ownerDist);
dist(document.getElementById('pridist'),S.pri,['高','High','中','Medium','低','Low','未定','Unset'],S.total,t.priDist);
}
// ③ table
function opts(el,arr,all){el.innerHTML=`<option value="">${all}</option>`+arr.map(v=>`<option>${v}</option>`).join('');}
opts(document.getElementById('f_owner'),[...new Set(DATA.map(d=>d.owner))].sort(),L.zh.fOwner);
opts(document.getElementById('f_final'),fsOrder_zh,L.zh.fFinal);
opts(document.getElementById('f_pri'),['高','中','低','未定'],L.zh.fPri);
let sortK='id',sortAsc=true;
const esc=s=>(s||'').replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));
function render(){
const t=L[lang];
const isEn=lang==='en';
const fo=f_owner.value,ff=f_final.value,fp=f_pri.value,q=f_q.value.trim(),od=f_od.checked,op=f_open.checked;
const fsNow=isEn?fsOrder_en:fsOrder_zh;
let rows=DATA.filter(d=>{
const dPri=isEn?d.priority_en:d.priority;
const dFinal=isEn?mapFinalEn(d.final):d.final;
return(!fo||d.owner===fo)&&(!ff||dFinal===ff)&&(!fp||dPri===fp)
&&(!od||d.overdue)&&(!op||(dFinal!=='Completed'&&dFinal!=='Closed'&&d.final!=='已完成'&&d.final!=='已关闭'))
&&(!q||(d.id+(isEn?d.title_en:d.title)+(isEn?d.desc_en:d.desc)).toLowerCase().includes(q.toLowerCase()));
});
rows.sort((a,b)=>{let x=a[sortK],y=b[sortK];if(sortK==='nlog'){x=+x;y=+y}return (x>y?1:x<y?-1:0)*(sortAsc?1:-1)});
tb.innerHTML=rows.map(d=>{
const od=d.overdue?`<span class="od"> ${t.od}</span>`:'';
const logs=d.logs.map(l=>`<div class="e"><div class="meta"><b>${l.date}</b> · ${esc(l.author)} · <span class="tag">${esc(l.source)}</span></div>${esc(l.text)}</div>`).join('');
const lk=(d.linked||[]).map(x=>`<span class="tag">${x.rel}${x.id}</span>`).join('');
const attChips=(d.attachments||[]).map(a=>`<span class="tag">${esc(a.filename)}</span>`).join('');
const ownerHist=(d.owner_history||[]).map(h=>`<div class="e"><div class="meta"><b>${esc(h.date)}</b> · ${esc(h.by)} · <span class="tag">${isEn?'Reassigned':'转派'}</span></div>${esc(h.from)}${esc(h.to)}${esc(h.reason||'')}</div>`).join('');
const title=isEn?d.title_en:d.title;
const cat=isEn?d.category_en:d.category;
const pri=isEn?d.priority_en:d.priority;
const sap=isEn?d.sap_en:d.sap;
const acc=isEn?d.acc_en:d.acc;
const final=isEn?mapFinalEn(d.final):d.final;
const descH=isEn?esc(d.desc_en):d.desc_html;
return `<tr class="issue" onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display==='table-row'?'none':'table-row'">
<td>${d.id}</td><td>${esc(title)}</td><td>${esc(cat)}</td><td>${esc(d.owner)}</td>
<td class="pr-${pri}">${pri}</td><td>${sap}</td><td>${acc}</td>
<td><span class="pill f-${final}">${final}</span></td><td>${d.plan||'—'}${od}</td><td>${esc(d.raised_dept)}</td>
<td>${esc(d.raised_by)}</td><td>${d.nlog}</td></tr>
<tr class="detail" style="display:none"><td colspan="12"><div class="dbox">
<div class="kv"><b>${d.id}</b> ${isEn?'Raised':'提出'}${esc(d.raised_dept)}/${esc(d.raised_by)} SAP:${sap} ${isEn?'Acceptance':'验收'}${acc} ${isEn?'Category':'分类'}${esc(cat)} ${lk}</div>
<div class="kv">${isEn?'Owner':'责任顾问'}${esc(d.owner)} 来源:${esc(d.source||'')} ${attChips}</div>
<div class="kv" style="white-space:pre-wrap">${t.descLabel}${descH}</div>
<div class="kv">${t.planLabel}${esc(d.plan_txt)||'—'}</div>
<div class="sec" style="margin:12px 0 6px">${t.histLabel}</div>
<div class="log">${ownerHist||'<div class="note">'+(isEn?'None':'无')+'</div>'}</div>
<div class="sec" style="margin:12px 0 6px">${t.logLabel}</div>
<div class="log">${logs||'<div class="note">'+(isEn?'None':'无')+'</div>'}</div>
</div></td></tr>`;}).join('');
foot.textContent=t.footer.replace('{n}',rows.length).replace('{t}',DATA.length);
renderStats();renderDist();
}
function mapFinalEn(f){
const m={'已完成':'Completed','已关闭':'Closed','进行中':'In Progress','进行中(返工)':'Rework','待用户验证':'Pending Verification','未开始':'Not Started'};
return m[f]||f;
}
document.querySelectorAll('th[data-k]').forEach(th=>th.onclick=()=>{const k=th.dataset.k;if(sortK===k)sortAsc=!sortAsc;else{sortK=k;sortAsc=true}render();});
['f_owner','f_final','f_pri','f_q','f_od','f_open'].forEach(id=>document.getElementById(id).addEventListener('input',render));
setLang('zh');
</script></body></html>
\ No newline at end of file
$validator = Join-Path $PSScriptRoot '..\tools\Test-KbReferences.ps1'
$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('kb-reference-anchor-' + [guid]::NewGuid().ToString('N'))
try {
New-Item -ItemType Directory -Path (Join-Path $tempRoot 'KB_SC') -Force | Out-Null
[System.IO.File]::WriteAllText((Join-Path $tempRoot 'KB_SC\KB_SC_ABC_Example.md'), "# Existing Heading`n", [System.Text.UTF8Encoding]::new($false))
$inlineReference = ([char]96 + 'KB_SC/KB_SC_ABC_Example.md ' + [char]0x00A7 + 'Missing Heading' + [char]96 + [Environment]::NewLine)
[System.IO.File]::WriteAllText((Join-Path $tempRoot 'source.md'), $inlineReference, [System.Text.UTF8Encoding]::new($false))
& $validator -Root $tempRoot
if ($?) {
throw 'Expected reference validator to reject a missing heading anchor'
}
}
finally {
if (Test-Path -LiteralPath $tempRoot) {
Remove-Item -LiteralPath $tempRoot -Recurse -Force
}
}
$validator = Join-Path $PSScriptRoot '..\tools\Test-KbReferences.ps1'
if (-not (Test-Path -LiteralPath $validator)) {
throw "Expected reference validator at $validator"
}
& $validator -Root (Join-Path $PSScriptRoot '..')
if (-not $?) {
throw 'Reference validator reported broken paths'
}
$workspace = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$ledgerDirectory = Get-ChildItem -LiteralPath $workspace -Directory | Where-Object { $_.Name -like 'Issue_Ledger_*' } | Select-Object -First 1
if ($null -eq $ledgerDirectory) {
throw 'Expected a same-level Issue_Ledger directory'
}
$runner = Join-Path $ledgerDirectory.FullName 'Run-Ledger.ps1'
if (-not (Test-Path -LiteralPath $runner)) {
throw "Expected ledger runner at $runner"
}
$output = & $runner -Action Dashboard
if (-not $?) {
throw 'Ledger runner failed to read the local ledger'
}
if (($output -join "`n") -notmatch '"total"\s*:\s*0') {
throw 'Ledger runner did not return the local dashboard summary'
}
$workspace = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$matches = Get-ChildItem -LiteralPath $workspace -Recurse -File -Filter '*.md' |
Select-String -Pattern 'C:\\Users\\'
if ($matches) {
$details = $matches | ForEach-Object { "$($_.Path):$($_.LineNumber)" }
throw "Markdown files must not contain external Windows user paths: $($details -join ', ')"
}
[CmdletBinding()]
param(
[Parameter()]
[string]$Root = (Get-Location).Path
)
$ErrorActionPreference = 'Stop'
$resolvedRoot = (Resolve-Path -LiteralPath $Root).Path
$pathPattern = [regex]'`(?<path>KB_[^/\s`<>()]+/[^\s`<>()]+?\.md)(?<tail>[^`]*)`'
$legacyOutputPattern = [regex]'output/KB_[^\s`<>()]+?\.md'
$findings = [System.Collections.Generic.List[object]]::new()
$headingCache = @{}
$sectionSymbol = [string][char]0x00A7
function Normalize-Heading([string]$Text) {
$normalized = $Text.Trim()
$normalized = $normalized -replace '\s+', ' '
$normalized = $normalized -replace '\s+#+$', ''
return $normalized
}
function Get-NormalizedHeadings([string]$Path) {
$headings = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
Get-Content -LiteralPath $Path -Encoding UTF8 | ForEach-Object {
if ($_ -match '^\s{0,3}#{1,6}\s+(?<heading>.+?)\s*$') {
[void]$headings.Add((Normalize-Heading $Matches['heading']))
}
}
return $headings
}
Get-ChildItem -LiteralPath $resolvedRoot -Recurse -File -Filter '*.md' | ForEach-Object {
$sourceFile = $_.FullName
$lineNumber = 0
Get-Content -LiteralPath $sourceFile -Encoding UTF8 | ForEach-Object {
$lineNumber++
$line = $_
foreach ($match in $pathPattern.Matches($line)) {
$relativeTarget = $match.Groups['path'].Value
$targetPath = Join-Path $resolvedRoot ($relativeTarget -replace '/', '\\')
if (-not (Test-Path -LiteralPath $targetPath -PathType Leaf)) {
$findings.Add([pscustomobject]@{
Source = $sourceFile.Substring($resolvedRoot.Length).TrimStart('\\')
Line = $lineNumber
Reference = $relativeTarget
Reason = 'target file does not exist'
})
}
else {
$tail = $match.Groups['tail'].Value
$anchorOffset = $tail.IndexOf($sectionSymbol)
if ($anchorOffset -ge 0) {
$anchor = $tail.Substring($anchorOffset + 1).Trim()
if (-not [string]::IsNullOrWhiteSpace($anchor)) {
if (-not $headingCache.ContainsKey($targetPath)) {
$headingCache[$targetPath] = Get-NormalizedHeadings -Path $targetPath
}
if (-not $headingCache[$targetPath].Contains((Normalize-Heading $anchor))) {
$findings.Add([pscustomobject]@{
Source = $sourceFile.Substring($resolvedRoot.Length).TrimStart('\\')
Line = $lineNumber
Reference = "$relativeTarget $sectionSymbol$anchor"
Reason = 'heading anchor does not exist'
})
}
}
}
}
}
foreach ($match in $legacyOutputPattern.Matches($line)) {
$findings.Add([pscustomobject]@{
Source = $sourceFile.Substring($resolvedRoot.Length).TrimStart('\\')
Line = $lineNumber
Reference = $match.Value
Reason = 'legacy output/ path is not allowed'
})
}
}
}
if ($findings.Count -gt 0) {
Write-Output 'Broken or disallowed KB Markdown references:'
$findings | Sort-Object Source, Line, Reference -Unique | ForEach-Object {
Write-Output ("{0}:{1} -> {2} ({3})" -f $_.Source, $_.Line, $_.Reference, $_.Reason)
}
exit 1
}
Write-Output 'All checked KB Markdown references exist.'
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment