@oj-module/cli 0.2.2 → 0.2.6
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/devkit/README.md +14 -3
- package/devkit/SKILL.md +16 -4
- package/devkit/framework-dev.md +8 -6
- package/devkit/manual.md +243 -23
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +143 -8
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/args.ts +67 -0
- package/src/build.ts +11 -3
- package/src/bundle.ts +197 -0
- package/src/contract/__tests__/handler-scan.test.ts +82 -0
- package/src/contract/check.ts +2 -1
- package/src/contract/handler-scan.ts +32 -0
- package/src/deterministic-tar.ts +120 -0
- package/src/dev.ts +4 -2
- package/src/enable.ts +28 -0
- package/src/index.ts +63 -2
- package/src/info.ts +3 -2
- package/src/init.ts +189 -14
- package/src/install.ts +268 -0
- package/src/keygen.ts +88 -0
- package/src/layout.ts +31 -0
- package/src/lifecycle/__tests__/authoring.test.ts +109 -0
- package/src/lifecycle/__tests__/bundle.test.ts +150 -0
- package/src/lifecycle/__tests__/deterministic-tar.test.ts +145 -0
- package/src/lifecycle/__tests__/fixtures/README.md +59 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/EXPECTED-VIOLATIONS.md +27 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/api/src/Demo/manifest.yaml +6 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/manifest.yaml +40 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/entry.ts +14 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/en-US.json +12 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/zh-CN.json +12 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/manifest.yaml +9 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/migrations/0001__init.sql +6 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/ping/api.ts +9 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/schema.yaml +9 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/manifest.yaml +96 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/entry.ts +23 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/en-US.json +29 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/zh-CN.json +29 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz +0 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.bad.sig +7 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.sig +7 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.unknown.sig +7 -0
- package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.d.mts +21 -0
- package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.mjs +196 -0
- package/src/lifecycle/__tests__/helpers.ts +78 -0
- package/src/lifecycle/__tests__/install.test.ts +514 -0
- package/src/lifecycle/__tests__/keygen.test.ts +61 -0
- package/src/lifecycle/__tests__/manifest-contract.test.ts +228 -0
- package/src/lifecycle/__tests__/publish.test.ts +80 -0
- package/src/lifecycle/__tests__/sig-fixtures.test.ts +100 -0
- package/src/lifecycle/__tests__/sign.test.ts +131 -0
- package/src/lifecycle/__tests__/smoke.test.ts +61 -0
- package/src/lifecycle/__tests__/trust.test.ts +177 -0
- package/src/lifecycle/__tests__/uninstall.test.ts +87 -0
- package/src/lifecycle/keygen.ts +40 -0
- package/src/lifecycle/sign.ts +74 -0
- package/src/manifest-contract.ts +696 -0
- package/src/oj-cert.ts +19 -0
- package/src/oj-cmd.ts +3 -3
- package/src/preview.ts +2 -2
- package/src/publish.ts +150 -0
- package/src/sign.ts +128 -0
- package/src/site.ts +146 -0
- package/src/trust.ts +116 -0
- package/src/uninstall.ts +29 -0
- package/src/usage.ts +35 -1
- package/templates/README.md +1 -1
- package/templates/api/config.yaml +47 -0
- package/templates/api/seed.mysql.sql +1 -1
- package/templates/api/seed.pg.sql +1 -1
- package/templates/api/src/_platform/_lifecycle/atomic.ts +98 -0
- package/templates/api/src/_platform/_lifecycle/history.ts +31 -0
- package/templates/api/src/_platform/_lifecycle/installer.ts +1080 -0
- package/templates/api/src/_platform/_lifecycle/lock.ts +148 -0
- package/templates/api/src/_platform/_lifecycle/semver.ts +61 -0
- package/templates/api/src/_platform/_lifecycle/sha384.ts +187 -0
- package/templates/api/src/_platform/_lifecycle/state.ts +256 -0
- package/templates/api/src/_platform/_lifecycle/steps/deploy-web.ts +182 -0
- package/templates/api/src/_platform/_lifecycle/steps/download.ts +90 -0
- package/templates/api/src/_platform/_lifecycle/steps/extract.ts +239 -0
- package/templates/api/src/_platform/_lifecycle/steps/migrate.ts +363 -0
- package/templates/api/src/_platform/_lifecycle/steps/preflight.ts +590 -0
- package/templates/api/src/_platform/_lifecycle/steps/register.ts +211 -0
- package/templates/api/src/_platform/_lifecycle/steps/reload.ts +81 -0
- package/templates/api/src/_platform/_lifecycle/steps/smoke.ts +216 -0
- package/templates/api/src/_platform/_lifecycle/steps/verify.ts +207 -0
- package/templates/api/src/_platform/_lifecycle/tasks.ts +119 -0
- package/templates/api/src/_platform/_lifecycle/text-edit.ts +110 -0
- package/templates/api/src/_platform/_lifecycle/tgz-guard.ts +41 -0
- package/templates/api/src/_platform/_lifecycle/trust.ts +229 -0
- package/templates/api/src/_platform/_lifecycle/types.ts +94 -0
- package/templates/api/src/_platform/_lifecycle/uninstall.ts +283 -0
- package/templates/api/src/_platform/_lifecycle/verify-checks.ts +228 -0
- package/templates/api/src/_platform/_lifecycle/verify-report.ts +92 -0
- package/templates/api/src/_platform/_shared/service/access.ts +2 -2
- package/templates/api/src/_platform/_shared/service/settings.ts +10 -1
- package/templates/api/src/_platform/_shared/service/solution-perms.ts +14 -7
- package/templates/api/src/_platform/_shared/site-facade.ts +44 -0
- package/templates/api/src/_platform/manifest.yaml +1 -0
- package/templates/api/src/_platform/market/api.ts +883 -0
- package/templates/api/src/_platform/market/bootstrap.ts +39 -0
- package/templates/api/src/_platform/migrations/0010__sys_solution_route.mysql.sql +17 -0
- package/templates/api/src/_platform/migrations/0010__sys_solution_route.pg.sql +16 -0
- package/templates/api/src/_platform/migrations/0010__sys_solution_route.sql +18 -0
- package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.mysql.sql +2 -0
- package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.sql +4 -0
- package/templates/api/src/_platform/migrations/0012__market_history.mysql.sql +13 -0
- package/templates/api/src/_platform/migrations/0012__market_history.pg.sql +12 -0
- package/templates/api/src/_platform/migrations/0012__market_history.sql +14 -0
- package/templates/api/src/_platform/schema.yaml +21 -4
- package/templates/api/src/_platform/seed.sql +2 -2
- package/templates/api/src/_platform/solutions/api.ts +212 -0
- package/templates/api/src/demo/manifest.yaml +5 -0
- package/templates/api/src/demo/migrations/0001__create_todos.sql +9 -0
- package/templates/api/src/demo/schema.yaml +11 -0
- package/templates/api/src/demo/seed.sql +5 -0
- package/templates/api/src/demo/todos/api.ts +59 -0
- package/templates/api/src/home/line/api.ts +57 -0
- package/templates/api/src/home/manifest.yaml +6 -0
- package/templates/api/src/home/migrations/0001__create_home_tables.sql +13 -0
- package/templates/api/src/home/pie/api.ts +40 -0
- package/templates/api/src/home/schema.yaml +14 -0
- package/templates/api/src/home/seed.sql +70 -0
- package/templates/api/src/system/oplog/api.ts +44 -2
- package/templates/api/tests/fixture.ts +16 -0
- package/templates/api/tests/lifecycle/atomic-state.spec.ts +183 -0
- package/templates/api/tests/lifecycle/bootstrap.spec.ts +116 -0
- package/templates/api/tests/lifecycle/deploy-smoke.spec.ts +492 -0
- package/templates/api/tests/lifecycle/installer.spec.ts +1218 -0
- package/templates/api/tests/lifecycle/market-api.spec.ts +971 -0
- package/templates/api/tests/lifecycle/migrate.spec.ts +575 -0
- package/templates/api/tests/lifecycle/preflight.spec.ts +255 -0
- package/templates/api/tests/lifecycle/register.spec.ts +194 -0
- package/templates/api/tests/lifecycle/semver.spec.ts +43 -0
- package/templates/api/tests/lifecycle/sha384.spec.ts +48 -0
- package/templates/api/tests/lifecycle/sig-fixtures.spec.ts +65 -0
- package/templates/api/tests/lifecycle/solutions-api.spec.ts +98 -0
- package/templates/api/tests/lifecycle/steps-fs.spec.ts +623 -0
- package/templates/api/tests/lifecycle/text-edit.spec.ts +177 -0
- package/templates/api/tests/lifecycle/tgz-guard.spec.ts +103 -0
- package/templates/api/tests/lifecycle/trust.spec.ts +310 -0
- package/templates/api/tests/lifecycle/uninstall.spec.ts +386 -0
- package/templates/api/tests/lifecycle/verify-checks.spec.ts +385 -0
- package/templates/api/tests/lifecycle/verify-report.spec.ts +290 -0
- package/templates/api/tests/rbac-settings.spec.ts +8 -0
- package/templates/init.include.yaml +62 -0
- package/templates/openapi.json +527 -0
- package/templates/package.json +52 -0
- package/templates/web/src/_platform/__client/api.schemas.ts +8 -0
- package/templates/web/src/_platform/__client/api.ts +335 -0
- package/templates/web/src/about/entry.ts +42 -0
- package/templates/web/src/about/locales/en-US.json +16 -0
- package/templates/web/src/about/locales/zh-CN.json +16 -0
- package/templates/web/src/about/pages/constants.ts +20 -0
- package/templates/web/src/about/pages/index.tsx +135 -0
- package/templates/web/src/access/entry.ts +74 -0
- package/templates/web/src/access/locales/en-US.json +46 -0
- package/templates/web/src/access/locales/zh-CN.json +46 -0
- package/templates/web/src/access/pages/access-mode/index.tsx +16 -0
- package/templates/web/src/access/pages/admin-visible/index.tsx +16 -0
- package/templates/web/src/access/pages/button-control/index.tsx +181 -0
- package/templates/web/src/access/pages/common-visible/index.tsx +16 -0
- package/templates/web/src/access/pages/page-control/index.tsx +142 -0
- package/templates/web/src/demo/__client/api.schemas.ts +20 -0
- package/templates/web/src/demo/__client/api.ts +89 -0
- package/templates/web/src/exception/entry.ts +80 -0
- package/templates/web/src/exception/locales/en-US.json +13 -0
- package/templates/web/src/exception/locales/zh-CN.json +13 -0
- package/templates/web/src/exception/pages/403/index.tsx +30 -0
- package/templates/web/src/exception/pages/404/index.tsx +30 -0
- package/templates/web/src/exception/pages/500/index.tsx +30 -0
- package/templates/web/src/exception/pages/unknown-component/index.tsx +38 -0
- package/templates/web/src/home/__client/api.schemas.ts +22 -0
- package/templates/web/src/home/__client/api.ts +113 -0
- package/templates/web/src/login/locales/en-US.json +6 -0
- package/templates/web/src/login/locales/zh-CN.json +6 -0
- package/templates/web/src/login/pages/login.tsx +67 -18
- package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +93 -0
- package/templates/web/src/oj-market/__tests__/HistoryDrawer.test.tsx +28 -0
- package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +69 -0
- package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +43 -0
- package/templates/web/src/oj-market/__tests__/UninstallConfirm.test.tsx +30 -0
- package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +70 -0
- package/templates/web/src/oj-market/__tests__/poll.test.ts +24 -0
- package/templates/web/src/oj-market/__tests__/render.tsx +14 -0
- package/templates/web/src/oj-market/__tests__/runtime-stub.ts +14 -0
- package/templates/web/src/oj-market/__tests__/semver.test.ts +26 -0
- package/templates/web/src/oj-market/api/MarketClientProvider.tsx +12 -0
- package/templates/web/src/oj-market/api/client.ts +135 -0
- package/templates/web/src/oj-market/api/context.ts +11 -0
- package/templates/web/src/oj-market/api/defaultClient.ts +7 -0
- package/templates/web/src/oj-market/components/CardGrid.tsx +76 -0
- package/templates/web/src/oj-market/components/CardItem.tsx +102 -0
- package/templates/web/src/oj-market/components/DetailDrawer.tsx +53 -0
- package/templates/web/src/oj-market/components/HistoryDrawer.tsx +75 -0
- package/templates/web/src/oj-market/components/InstallDialog.tsx +112 -0
- package/templates/web/src/oj-market/components/RollbackDialog.tsx +79 -0
- package/templates/web/src/oj-market/components/UninstallConfirm.tsx +74 -0
- package/templates/web/src/oj-market/entry.ts +27 -0
- package/templates/web/src/oj-market/hooks/useMarket.ts +91 -0
- package/templates/web/src/oj-market/locales/en-US.json +100 -0
- package/templates/web/src/oj-market/locales/zh-CN.json +100 -0
- package/templates/web/src/oj-market/pages/MarketApp.tsx +136 -0
- package/templates/web/src/oj-market/types.ts +84 -0
- package/templates/web/src/oj-market/utils/poll.ts +36 -0
- package/templates/web/src/oj-market/utils/semver.ts +14 -0
- package/templates/web/src/outside/entry.ts +80 -0
- package/templates/web/src/outside/locales/en-US.json +10 -0
- package/templates/web/src/outside/locales/zh-CN.json +10 -0
- package/templates/web/src/personal-center/entry.ts +1 -10
- package/templates/web/src/personal-center/locales/en-US.json +1 -5
- package/templates/web/src/personal-center/locales/zh-CN.json +1 -5
- package/templates/web/src/route-nest/entry.ts +73 -0
- package/templates/web/src/route-nest/locales/en-US.json +9 -0
- package/templates/web/src/route-nest/locales/zh-CN.json +9 -0
- package/templates/web/src/route-nest/pages/menu1/menu1-1/index.tsx +5 -0
- package/templates/web/src/route-nest/pages/menu1/menu1-2/index.tsx +5 -0
- package/templates/web/src/route-nest/pages/menu2/index.tsx +5 -0
- package/templates/web/src/system/__client/api.schemas.ts +12 -0
- package/templates/web/src/system/__client/api.ts +25 -1
- package/templates/web/src/system/pages/oplog/index.tsx +1 -1
- package/templates/web/src/system/pages/roles/index.tsx +1 -0
- package/templates/web/src/system/pages/settings/index.tsx +1 -1
- package/templates/web/src/system/pages/users/index.tsx +1 -1
- package/templates/web.config.ts +32 -40
- package/vendor/host-versions.json +3 -3
- package/templates/pnpm-workspace.yaml +0 -14
- package/templates/web/src/personal-center/pages/settings/index.tsx +0 -5
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// 注册步骤(P1-8,十阶段 stage registering;D10 唯一出口):安装器各 stage 之外任何人不得
|
|
2
|
+
// 直写 sys_role/sys_permission/sys_setting/sys_solution_route 四类行——RBAC 经
|
|
3
|
+
// upsertSolutionPerms 出口(AC2-1~6 原样消费,勿改其语义),设置/路由在本文件内收敛。
|
|
4
|
+
// 语义纪律(镜像 solution-perms):只做加法 + 元数据覆盖 + 声明消失行保留标 deprecated
|
|
5
|
+
// (判决不突变,清单汇入返回值供安装器强告警 AC2-4);设置项存在只覆定义列、管理员改过的
|
|
6
|
+
// value 永不复活(AC1-15 同纪律)。本行拥有的行(路由/设置)upsert 时复位 deprecated=0——
|
|
7
|
+
// 重声明行必须回到 queryRegistered 快照,否则 P1-9 冒烟对账假阴(solution-perms 出口未复位
|
|
8
|
+
// 是其既有语义,本文件不越界代改)。DB 级路由占用检查承接 P1-5 备案③(pre-DB 阶段无真源)。
|
|
9
|
+
// db 为 isolate 全局(同模板 service 层,无需 import;测试由夹具装 globalThis.db)。
|
|
10
|
+
|
|
11
|
+
import type { SolutionManifest } from "../../_shared/service/solution-perms";
|
|
12
|
+
import { auditOp } from "../../_shared/service/audit";
|
|
13
|
+
import { upsertSolutionPerms } from "../../_shared/service/solution-perms";
|
|
14
|
+
|
|
15
|
+
export interface RouteDecl {
|
|
16
|
+
path: string
|
|
17
|
+
/** D4 三值(settings/personal 已废弃) */
|
|
18
|
+
category: "main" | "admin" | "hidden"
|
|
19
|
+
order: number
|
|
20
|
+
title_key?: string
|
|
21
|
+
required_permission?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SettingDecl {
|
|
25
|
+
key: string
|
|
26
|
+
group: string
|
|
27
|
+
type: string
|
|
28
|
+
/** 首装初值;缺省落 default_value */
|
|
29
|
+
value?: string
|
|
30
|
+
default_value?: string | null
|
|
31
|
+
label_key?: string | null
|
|
32
|
+
/** 组级 label_key(C2-22;P2-15③ G7 承载——declOf 此前丢弃,组标题落库回填) */
|
|
33
|
+
group_label_key?: string | null
|
|
34
|
+
/** enum 选项(契约 C2-24 string[] 的序列化产物) */
|
|
35
|
+
options?: string | null
|
|
36
|
+
secret?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SolutionDecl extends SolutionManifest {
|
|
40
|
+
/** 版本上下文(审计 OpEntry.version;manifest 切片可缺省) */
|
|
41
|
+
version?: string
|
|
42
|
+
settings?: SettingDecl[]
|
|
43
|
+
routes?: RouteDecl[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 阶段结果(各安装段通用形状,本段 stage 恒为 "registering") */
|
|
47
|
+
export interface StageResult {
|
|
48
|
+
stage: "registering"
|
|
49
|
+
ok: boolean
|
|
50
|
+
message: string
|
|
51
|
+
retryable?: boolean
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface RegisterResult extends StageResult {
|
|
55
|
+
/** 本次被标 deprecated 的 code/name/key/path 清单(AC2-4 安装器强告警数据源) */
|
|
56
|
+
deprecated: string[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 归一化快照(P1-9 冒烟对账消费;全排序、过滤 deprecated 行、零行全空返回 null) */
|
|
60
|
+
export interface SolutionSnapshot {
|
|
61
|
+
module: string
|
|
62
|
+
routes: string[]
|
|
63
|
+
settings: string[]
|
|
64
|
+
roles: string[]
|
|
65
|
+
permissions: string[]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 首段提取(/wiki/docs → wiki)——占用判定粒度:前缀段冲突即整段首段比较(P1-5 备案③) */
|
|
69
|
+
function firstSeg(path: string): string {
|
|
70
|
+
return path.split("/")[1] ?? "";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* DB 级路由占用检查:返回已被其他 module 占用首段的声明路径清单。
|
|
75
|
+
* 框架保留段判断不做(那是 preflight 的活,C2-13/14);deprecated 行仍算占用(行保留即占位,
|
|
76
|
+
* 让位须走卸载),自占排除(module ≠ 自己)。
|
|
77
|
+
*/
|
|
78
|
+
export async function checkOccupancy(module: string, routePaths: string[]): Promise<string[]> {
|
|
79
|
+
const rows = await db.query("select path from sys_solution_route where module != ?", [module]) as Array<{ path: string }>;
|
|
80
|
+
const taken = new Set(rows.map(r => firstSeg(String(r.path))));
|
|
81
|
+
return routePaths.filter(p => taken.has(firstSeg(p)));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 声明消失 → 行保留标 deprecated=1(镜像 solution-perms AC2-3/4),返回被标 key/path 清单 */
|
|
85
|
+
async function deprecateGone(table: "sys_setting" | "sys_solution_route", col: "key" | "path", module: string, declared: Set<string>): Promise<string[]> {
|
|
86
|
+
const old = await db.query(`select ${col} from ${table} where module = ? and deprecated = 0`, [module]) as Array<Record<string, unknown>>;
|
|
87
|
+
const gone: string[] = [];
|
|
88
|
+
for (const r of old) {
|
|
89
|
+
const k = String(r[col]);
|
|
90
|
+
if (!declared.has(k)) {
|
|
91
|
+
await db.exec(`update ${table} set deprecated = 1 where ${col} = ?`, [k]);
|
|
92
|
+
gone.push(k);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return gone;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 注册一次完成(设计 §4 清单 #2/#3/#4):占用预检 → RBAC 出口 → 设置 upsert → 路由 upsert
|
|
100
|
+
* → 声明消失标 deprecated → 审计。失败 StageResult{ok:false}:占用拒装零写入(retryable=false,
|
|
101
|
+
* 须改声明重打包);DB 异常 retryable=true(附录 A registering)。
|
|
102
|
+
*/
|
|
103
|
+
export async function registerSolution(decl: SolutionDecl): Promise<RegisterResult> {
|
|
104
|
+
const routes = decl.routes ?? [];
|
|
105
|
+
const settings = decl.settings ?? [];
|
|
106
|
+
try {
|
|
107
|
+
// 占用预检(brief:registerSolution 开头先跑):非空 → 拒注册零写入
|
|
108
|
+
const occupied = await checkOccupancy(decl.module, routes.map(r => r.path));
|
|
109
|
+
if (occupied.length > 0) {
|
|
110
|
+
return {
|
|
111
|
+
stage: "registering",
|
|
112
|
+
ok: false,
|
|
113
|
+
retryable: false,
|
|
114
|
+
deprecated: [],
|
|
115
|
+
message: `注册失败:路由首段已被其他方案占用(${occupied.join("、")})——同站路由首段唯一,需调整声明后重新打包`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// RBAC(唯一出口):upsert + 声明消失标 deprecated,deprecated 清单原样汇入
|
|
120
|
+
const perms = await upsertSolutionPerms({ module: decl.module, roles: decl.roles });
|
|
121
|
+
|
|
122
|
+
// 设置项 upsert:存在只覆定义列(value/update 语义 = 管理员改过的值不复活,AC1-15),
|
|
123
|
+
// 不存在插 default_value(首装 value)
|
|
124
|
+
for (const s of settings) {
|
|
125
|
+
const exists = await db.query("select key from sys_setting where key = ?", [s.key]) as unknown[];
|
|
126
|
+
if (exists.length) {
|
|
127
|
+
await db.exec(
|
|
128
|
+
"update sys_setting set module = ?, \"group\" = ?, type = ?, default_value = ?, label_key = ?, group_label_key = ?, options = ?, secret = ?, deprecated = 0, update_time = ? where key = ?",
|
|
129
|
+
[decl.module, s.group, s.type, s.default_value ?? null, s.label_key ?? null, s.group_label_key ?? null, s.options ?? null, s.secret ? 1 : 0, Date.now(), s.key],
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
await db.exec(
|
|
134
|
+
"insert into sys_setting (key, module, \"group\", type, value, default_value, label_key, group_label_key, options, secret, update_time, deprecated) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)",
|
|
135
|
+
[s.key, decl.module, s.group, s.type, s.value ?? s.default_value ?? null, s.default_value ?? null, s.label_key ?? null, s.group_label_key ?? null, s.options ?? null, s.secret ? 1 : 0, Date.now()],
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const settingDeps = await deprecateGone("sys_setting", "key", decl.module, new Set(settings.map(s => s.key)));
|
|
140
|
+
|
|
141
|
+
// 路由逐行 upsert:同 path 覆元数据;声明行复位 deprecated=0(重声明回到快照)
|
|
142
|
+
for (const r of routes) {
|
|
143
|
+
const exists = await db.query("select path from sys_solution_route where path = ?", [r.path]) as unknown[];
|
|
144
|
+
if (exists.length) {
|
|
145
|
+
await db.exec(
|
|
146
|
+
"update sys_solution_route set module = ?, category = ?, \"order\" = ?, title_key = ?, required_permission = ?, deprecated = 0 where path = ?",
|
|
147
|
+
[decl.module, r.category, r.order, r.title_key ?? null, r.required_permission ?? null, r.path],
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
await db.exec(
|
|
152
|
+
"insert into sys_solution_route (path, module, category, \"order\", title_key, required_permission, deprecated, enabled) values (?, ?, ?, ?, ?, ?, 0, 1)",
|
|
153
|
+
[r.path, decl.module, r.category, r.order, r.title_key ?? null, r.required_permission ?? null],
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const routeDeps = await deprecateGone("sys_solution_route", "path", decl.module, new Set(routes.map(r => r.path)));
|
|
158
|
+
|
|
159
|
+
const deprecated = [...perms.deprecated, ...settingDeps, ...routeDeps];
|
|
160
|
+
auditOp({
|
|
161
|
+
action: "solution.register",
|
|
162
|
+
module: decl.module,
|
|
163
|
+
version: decl.version,
|
|
164
|
+
target: decl.module,
|
|
165
|
+
detail: { routes: routes.length, settings: settings.length, deprecated },
|
|
166
|
+
});
|
|
167
|
+
return {
|
|
168
|
+
stage: "registering",
|
|
169
|
+
ok: true,
|
|
170
|
+
message: `注册完成:${decl.roles.length} 个角色、${settings.length} 个设置项、${routes.length} 条路由${deprecated.length > 0 ? `;${deprecated.length} 项声明消失已标 deprecated(需关注:${deprecated.join("、")})` : ""}`,
|
|
171
|
+
deprecated,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
return { stage: "registering", ok: false, retryable: true, deprecated: [], message: `注册失败:${String(e)}(附录 A registering)` };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function queryRegistered(name: string): Promise<SolutionSnapshot | null> {
|
|
180
|
+
const routes = (await db.query("select path from sys_solution_route where module = ? and deprecated = 0", [name]) as Array<{ path: string }>).map(r => String(r.path)).sort();
|
|
181
|
+
const settings = (await db.query("select key from sys_setting where module = ? and deprecated = 0", [name]) as Array<{ key: string }>).map(r => String(r.key)).sort();
|
|
182
|
+
const roles = (await db.query("select name from sys_role where module = ? and deprecated = 0", [name]) as Array<{ name: string }>).map(r => String(r.name)).sort();
|
|
183
|
+
const permissions = (await db.query("select code from sys_permission where module = ? and deprecated = 0", [name]) as Array<{ code: string }>).map(r => String(r.code)).sort();
|
|
184
|
+
if (routes.length + settings.length + roles.length + permissions.length === 0) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
return { module: name, routes, settings, roles, permissions };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 全方案快照清单(P1-14 读面/深检对账辅助):四表并集取模块名(queryRegistered 同款查询范式),
|
|
192
|
+
* 按 module 排序逐个快照;deprecated 行不计入存在性(同 queryRegistered 过滤口径),无行 → 空数组。
|
|
193
|
+
*/
|
|
194
|
+
export async function queryAllRegistered(): Promise<SolutionSnapshot[]> {
|
|
195
|
+
const names = new Set<string>();
|
|
196
|
+
for (const table of ["sys_solution_route", "sys_setting", "sys_role", "sys_permission"]) {
|
|
197
|
+
// 表名为字面常量,无拼接注入面
|
|
198
|
+
const rows = await db.query(`select distinct module from ${table} where deprecated = 0`) as Array<{ module: string }>;
|
|
199
|
+
for (const r of rows) {
|
|
200
|
+
names.add(String(r.module));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const out: SolutionSnapshot[] = [];
|
|
204
|
+
for (const name of [...names].sort()) {
|
|
205
|
+
const snap = await queryRegistered(name);
|
|
206
|
+
if (snap !== null) {
|
|
207
|
+
out.push(snap);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// 热更步骤(P1-9,十阶段 stage reloading;R18):经注入 reload 面调 oj server.reloadConfig()
|
|
2
|
+
// (platform 特权 API,oj 0.1.75 起落地;S1-5.2 实测五键形状 {generation, unchanged, applied[],
|
|
3
|
+
// restart_pending[], kept_listener[]},与 POST /_system/config 同一 executor 同一把串行锁——
|
|
4
|
+
// 并发第二次触发抛 busy 409)。终态断言(R18):unchanged = config 改写未实际生效判失败
|
|
5
|
+
// (幂等回显 = 热更目标未达成,重试不自愈);expected 段键须逐一 ∈ applied——缺省 ["vars"]
|
|
6
|
+
// (B-11 安装触发面 = vars.ojm_install_seq 自增,PRD E5 实测 vars 可热生效;S1-5 实测
|
|
7
|
+
// anonymous_paths 热改属 restart_pending 不可热生效,mounts 断言可经 ctx.expected 覆盖)。
|
|
8
|
+
// reload 面经 ctx 注入(DIP):生产 = (globalThis as any).server.reloadConfig 包装(P1-10 接线),
|
|
9
|
+
// 测试 stub 注入成功/unchanged/抛错三态。
|
|
10
|
+
|
|
11
|
+
import type { InstallContext } from "./preflight";
|
|
12
|
+
|
|
13
|
+
/** reloadConfig 返回形状(S1-5.2 实测五键;generation 可缺省容忍) */
|
|
14
|
+
export interface ReloadResult {
|
|
15
|
+
generation?: number
|
|
16
|
+
unchanged: boolean
|
|
17
|
+
applied: string[]
|
|
18
|
+
restart_pending?: string[]
|
|
19
|
+
kept_listener?: string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ReloadFace {
|
|
23
|
+
reloadConfig: () => Promise<ReloadResult>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ReloadContext extends InstallContext {
|
|
27
|
+
/** 注入式 reload 面(生产 = platform 特权 server.reloadConfig 包装;测试 stub) */
|
|
28
|
+
reload: ReloadFace
|
|
29
|
+
/** 须实际生效的段键(R18 终态断言);缺省 ["vars"](B-11 安装触发面) */
|
|
30
|
+
expected?: string[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface StageResult {
|
|
34
|
+
stage: "reloading"
|
|
35
|
+
ok: boolean
|
|
36
|
+
message: string
|
|
37
|
+
retryable?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function reloadAndAssert(ctx: ReloadContext): Promise<StageResult> {
|
|
41
|
+
let r: ReloadResult;
|
|
42
|
+
try {
|
|
43
|
+
r = await ctx.reload.reloadConfig();
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
return { stage: "reloading", ok: false, retryable: true, message: `热更失败:reloadConfig 抛错(${String(e)})——并发 busy 可重试(附录 A reloading;R18)` };
|
|
47
|
+
}
|
|
48
|
+
if (typeof r !== "object" || r === null || !Array.isArray(r.applied) || typeof r.unchanged !== "boolean") {
|
|
49
|
+
return {
|
|
50
|
+
stage: "reloading",
|
|
51
|
+
ok: false,
|
|
52
|
+
retryable: false,
|
|
53
|
+
message: `热更失败:reloadConfig 返回形状违约(S1-5.2 五键形状 {generation, unchanged, applied, restart_pending, kept_listener}):${JSON.stringify(r)?.slice(0, 200)}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (r.unchanged) {
|
|
57
|
+
return {
|
|
58
|
+
stage: "reloading",
|
|
59
|
+
ok: false,
|
|
60
|
+
retryable: false,
|
|
61
|
+
message: "热更失败:reloadConfig 返回 unchanged——config 改写未实际生效(R18 终态断言:幂等回显 = 热更目标未达成,重试不自愈),请检查 config 写入段后重试安装",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// 安装路径缺省期望 vars(B-11:reload 触发变更 = vars.ojm_install_seq 自增,PRD E5 实测
|
|
65
|
+
// vars 可热生效;unchanged 判定基于 config 文档 sha,安装器 reloading 段前置落盘自增)。
|
|
66
|
+
// anonymous 并入即 mounts 段生效的语义保留——调用方经 ctx.expected 显式覆盖。
|
|
67
|
+
const expected = ctx.expected ?? ["vars"];
|
|
68
|
+
const missing = expected.filter(k => !r.applied.includes(k));
|
|
69
|
+
if (missing.length > 0) {
|
|
70
|
+
return {
|
|
71
|
+
stage: "reloading",
|
|
72
|
+
ok: false,
|
|
73
|
+
retryable: false,
|
|
74
|
+
message: `热更失败:applied [${r.applied.join("、")}] 未含预期生效段 ${missing.join("、")}(R18:以 reload 返回的 mounts/vars 实际生效值做终态断言)`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const pending = Array.isArray(r.restart_pending) && r.restart_pending.length > 0
|
|
78
|
+
? `;待重启段 [${r.restart_pending.join("、")}](anonymous_paths 热改属 restart_pending,S1-5)`
|
|
79
|
+
: "";
|
|
80
|
+
return { stage: "reloading", ok: true, message: `热更完成:generation ${r.generation ?? "?"},已生效段 [${r.applied.join("、")}]${pending}` };
|
|
81
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// 冒烟步骤(P1-9,十阶段 stage smoke;设计 §4.1 四项之三 + 一项钉口径不实现),collect-all:
|
|
2
|
+
// ①探活:manifest.health(C2-33)声明则探 <apiPrefix>/<health 去前导斜杠> 断言 2xx;缺省探首个
|
|
3
|
+
// **非匿名**路由同形 URL——连通即可不做语义断言(非匿名无凭证预期 401;D14/Q-A:health 不强制)。
|
|
4
|
+
// URL 形态与 C2-26 段内收敛一致:api 相对路径首段 = 模块段(fixture: health /demo-solution/ping
|
|
5
|
+
// ↔ anonymous demo-solution/ping)。无 health 无路由:无事可验视为通过。
|
|
6
|
+
// ②integrity(R10:验签后包内容口径):对**部署落位后**的 entry 文件重算 sha384 与 modules.json
|
|
7
|
+
// 条目 integrity(sha384-<base64>,C2-28)相等;条目缺 integrity fail-closed(P1-10 必传,缺失即
|
|
8
|
+
// 装配线破损);声明 css 逐一验落位存在性(modules.json 单一 integrity 仅覆盖 entry——C2-28 构建
|
|
9
|
+
// 器只对 entry 产物计算)。manifest 无 web 段本项不适用。
|
|
10
|
+
// ③注册一致性:queryRegistered 快照(ctx.query 可注入,缺省回落 register.ts db 全局)vs 本次
|
|
11
|
+
// manifest 声明——升级态「包含非相等」:快照逐数组 ⊇ 声明全集(可多不可少,少 = 注册丢失判失败;
|
|
12
|
+
// 相等当然过);快照 null = 首装,声明全空过、非空败。
|
|
13
|
+
// ④entry 浏览器加载不在安装器验(Node import 必假绿——设计 §4.1):宿主 error boundary 兜底 +
|
|
14
|
+
// P2 深检可见,本步不实现,仅此处钉口径。
|
|
15
|
+
// 任一失败不首条即停:三项全跑,原因逐项中文齐报(preflight 同纪律)。
|
|
16
|
+
|
|
17
|
+
import type { InstallContext } from "./preflight";
|
|
18
|
+
import type { SolutionSnapshot } from "./register";
|
|
19
|
+
import { fsFace } from "../atomic";
|
|
20
|
+
import { queryRegistered } from "./register";
|
|
21
|
+
|
|
22
|
+
export interface SmokeContext extends InstallContext {
|
|
23
|
+
/** API 前缀(C2-13 api.prefix,缺省 /api/<name>;探针 URL 前缀,注入不硬编码) */
|
|
24
|
+
apiPrefix: string
|
|
25
|
+
/** 注入式探针 fetch 面(生产 = isolate 全局 fetch 同源调用;测试 stub 不真发请求) */
|
|
26
|
+
fetch: (url: string) => Promise<{ ok: boolean, status: number }>
|
|
27
|
+
/** 注入式 sha384 摘要面(base64 裸摘要,不带 sha384- 前缀;同 verify.ts DIP) */
|
|
28
|
+
sha384B64: (data: Uint8Array) => string
|
|
29
|
+
/** 站点 modules.json 路径(integrity 比对真源 + 落位文件定位基准:<dist>/modules.json) */
|
|
30
|
+
modulesJsonPath: string
|
|
31
|
+
/** 注册快照查询;缺省回落 queryRegistered(isolate db 全局;测试注入 stub) */
|
|
32
|
+
query?: (name: string) => Promise<SolutionSnapshot | null>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface StageResult {
|
|
36
|
+
stage: "smoke"
|
|
37
|
+
ok: boolean
|
|
38
|
+
message: string
|
|
39
|
+
retryable?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface Failure {
|
|
43
|
+
item: string
|
|
44
|
+
reason: string
|
|
45
|
+
retryable: boolean
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isObj(v: unknown): v is Record<string, unknown> {
|
|
49
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 探针 URL 形态:<apiPrefix>/<path 去前导斜杠>(apiPrefix 尾斜杠归一;C2-26 段内收敛同构) */
|
|
53
|
+
export function probeUrlOf(apiPrefix: string, path: string): string {
|
|
54
|
+
return `${apiPrefix.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 首个非匿名路由 path:api.anonymous(相对路径)精确命中或前缀命中者跳过 */
|
|
58
|
+
function firstNonAnonymousRoute(m: Record<string, unknown>): string | undefined {
|
|
59
|
+
const api = isObj(m.api) ? m.api : {};
|
|
60
|
+
const anonymous = Array.isArray(api.anonymous) ? api.anonymous.filter((a): a is string => typeof a === "string") : [];
|
|
61
|
+
const routes = Array.isArray(m.routes) ? m.routes : [];
|
|
62
|
+
for (const r of routes) {
|
|
63
|
+
if (!isObj(r) || typeof r.path !== "string") {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const rel = r.path.replace(/^\/+/, "");
|
|
67
|
+
if (!anonymous.some(a => rel === a || rel.startsWith(`${a}/`))) {
|
|
68
|
+
return r.path;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** manifest 数组声明提取(routes[].path / roles[].name / permissions[].code) */
|
|
75
|
+
function declaredStrings(m: Record<string, unknown>, key: string, field: string): string[] {
|
|
76
|
+
const list = Array.isArray(m[key]) ? m[key] : [];
|
|
77
|
+
const out: string[] = [];
|
|
78
|
+
for (const e of list) {
|
|
79
|
+
if (isObj(e) && typeof e[field] === "string") {
|
|
80
|
+
out.push(e[field]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** settings.groups[].items[].key 声明提取(C2-22 分组嵌套) */
|
|
87
|
+
function declaredSettingKeys(m: Record<string, unknown>): string[] {
|
|
88
|
+
const s = m.settings;
|
|
89
|
+
if (!isObj(s) || !Array.isArray(s.groups)) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
const out: string[] = [];
|
|
93
|
+
for (const g of s.groups) {
|
|
94
|
+
if (!isObj(g) || !Array.isArray(g.items)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
for (const it of g.items) {
|
|
98
|
+
if (isObj(it) && typeof it.key === "string") {
|
|
99
|
+
out.push(it.key);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface ManifestEntry {
|
|
107
|
+
name?: string
|
|
108
|
+
entry?: string
|
|
109
|
+
css?: string[]
|
|
110
|
+
integrity?: string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function smoke(ctx: SmokeContext): Promise<StageResult> {
|
|
114
|
+
const failures: Failure[] = [];
|
|
115
|
+
const m = isObj(ctx.manifest) ? ctx.manifest : {};
|
|
116
|
+
const web = isObj(m.web) ? m.web : undefined;
|
|
117
|
+
|
|
118
|
+
// ① 探活:health 优先(2xx),缺省首个非匿名路由(连通即可)
|
|
119
|
+
try {
|
|
120
|
+
const health = typeof m.health === "string" && m.health !== "" ? m.health : undefined;
|
|
121
|
+
const target = health ?? firstNonAnonymousRoute(m);
|
|
122
|
+
if (target !== undefined) {
|
|
123
|
+
const url = probeUrlOf(ctx.apiPrefix, target);
|
|
124
|
+
const res = await ctx.fetch(url);
|
|
125
|
+
if (health !== undefined && !res.ok) {
|
|
126
|
+
failures.push({ item: "探活", retryable: true, reason: `health 端点 ${url} 返回 HTTP ${res.status}(C2-33 冒烟探针要求 2xx)` });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
failures.push({ item: "探活", retryable: true, reason: `探针请求失败(连通性不成立):${String(e)}` });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ② integrity:落位后重算 vs modules.json 条目(web 段存在才适用)
|
|
135
|
+
if (web !== undefined) {
|
|
136
|
+
try {
|
|
137
|
+
const f = fsFace();
|
|
138
|
+
const slash = ctx.modulesJsonPath.lastIndexOf("/");
|
|
139
|
+
const distRoot = slash === -1 ? "." : ctx.modulesJsonPath.slice(0, slash);
|
|
140
|
+
const list = JSON.parse(new TextDecoder().decode(await f.readFile(ctx.modulesJsonPath))) as ManifestEntry[];
|
|
141
|
+
const mine = Array.isArray(list) ? list.find((e: ManifestEntry) => isObj(e) && e.name === ctx.name) : undefined;
|
|
142
|
+
if (mine === undefined) {
|
|
143
|
+
failures.push({ item: "integrity", retryable: false, reason: `modules.json 无 ${ctx.name} 条目(部署清单缺位,deployWeb 未生效?)` });
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
if (typeof mine.entry !== "string" || mine.entry === "") {
|
|
147
|
+
failures.push({ item: "integrity", retryable: false, reason: "modules.json 条目缺 entry(C2-27 web 段存在时必填)" });
|
|
148
|
+
}
|
|
149
|
+
if (typeof mine.integrity !== "string" || !mine.integrity.startsWith("sha384-")) {
|
|
150
|
+
failures.push({ item: "integrity", retryable: false, reason: "modules.json 条目缺 integrity(fail-closed:R10 要求基于包内容重算传入,缺失即装配线破损)" });
|
|
151
|
+
}
|
|
152
|
+
else if (typeof mine.entry === "string" && mine.entry !== "") {
|
|
153
|
+
const actual = `sha384-${ctx.sha384B64(await f.readFile(`${distRoot}/${mine.entry}`))}`;
|
|
154
|
+
if (actual !== mine.integrity) {
|
|
155
|
+
failures.push({ item: "integrity", retryable: false, reason: `entry 落位产物 sha384 与清单不符(R10 包内容口径)期望 ${mine.integrity} 实得 ${actual}` });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const c of Array.isArray(mine.css) ? mine.css : []) {
|
|
159
|
+
if (typeof c !== "string") {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
await f.readFile(`${distRoot}/${c}`);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
failures.push({ item: "integrity", retryable: false, reason: `声明 css ${c} 未落位(部署不完整)` });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
failures.push({ item: "integrity", retryable: false, reason: `modules.json 不可读(${String(e)})——部署清单缺位` });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ③ 注册一致性:快照 ⊇ 声明(升级态「包含非相等」;null = 首装按声明空满判)
|
|
177
|
+
try {
|
|
178
|
+
const declared = {
|
|
179
|
+
routes: declaredStrings(m, "routes", "path"),
|
|
180
|
+
settings: declaredSettingKeys(m),
|
|
181
|
+
roles: declaredStrings(m, "roles", "name"),
|
|
182
|
+
permissions: declaredStrings(m, "permissions", "code"),
|
|
183
|
+
};
|
|
184
|
+
const snap = await (ctx.query ?? queryRegistered)(ctx.name);
|
|
185
|
+
if (snap === null) {
|
|
186
|
+
const total = declared.routes.length + declared.settings.length + declared.roles.length + declared.permissions.length;
|
|
187
|
+
if (total > 0) {
|
|
188
|
+
failures.push({ item: "注册一致性", retryable: false, reason: `注册快照为空(null)而本次声明 ${total} 项非空——声明未注册生效(D10 出口未落地?)` });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
for (const key of ["routes", "settings", "roles", "permissions"] as const) {
|
|
193
|
+
const have = new Set(snap[key]);
|
|
194
|
+
const missing = declared[key].filter(d => !have.has(d));
|
|
195
|
+
if (missing.length > 0) {
|
|
196
|
+
failures.push({ item: "注册一致性", retryable: false, reason: `快照 ${key} 缺少声明项(升级态「包含非相等」:快照可多不可少,少 = 注册丢失):${missing.join("、")}` });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
failures.push({ item: "注册一致性", retryable: false, reason: `注册快照查询失败:${String(e)}` });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ④ entry 浏览器加载不在安装器验(设计 §4.1)——无判定,口径钉于成功 message。
|
|
206
|
+
|
|
207
|
+
if (failures.length === 0) {
|
|
208
|
+
return { stage: "smoke", ok: true, message: "冒烟通过:探活 / integrity / 注册一致性 三项全过(entry 浏览器加载不在安装器验——设计 §4.1,宿主 error boundary + P2 深检兜底)" };
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
stage: "smoke",
|
|
212
|
+
ok: false,
|
|
213
|
+
retryable: failures.every(x => x.retryable),
|
|
214
|
+
message: `冒烟未通过(${failures.length} 项失败,全量列出):\n${failures.map(x => `- 【${x.item}】${x.reason}`).join("\n")}`,
|
|
215
|
+
};
|
|
216
|
+
}
|