| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498 |
- /**
- * T07 坐标校验与无地图位置 纯逻辑检查(票 07)。
- *
- * 零依赖 node 断言脚本(仓库没配测试运行器,约定见 t01-panel-catalog.mjs / t08-map-summary.mjs)。
- * 运行:node .scratch/cockpit-shell-panel-layout/verify/t07-coordinate.mjs
- *
- * 分八段:
- * A 唯一准入判据(只认后端 renderable;前端不纠偏、不查范围、不用默认坐标)
- * B 原因文案(后端各 reason 逐一命中;「缺失」与「非法」分开;认不出的不泄露原因码)
- * C 切分不静默丢对象(不上图 ≠ 不存在,两堆都要交出去)
- * D 两个口径分开(后端全量 vs 前端已加载,不混成一个数)
- * E 后端汇总进领域批次(票 05 遗留的漏读;且不得跨页累加)
- * F 契约化 mock 的三桶分法照抄后端
- * G 静态守卫(domain 不重算坐标范围;界面零原因码;不上图清单由领域层供货)
- * H 「已上图」措辞不再把被拦下的对象算进去
- *
- * 契约依据是**后端实现**:`CockpitCoordinateValidator`(state 三态、renderable 是结论)
- * 与 `CockpitMapSnapshotService`(汇总按整份快照算、三桶分法)。
- *
- * 2026-10-12 后端裁定:取消无入口的人工校核前置,改为「结构合法 + 落在主场景区内」自动准入。
- * 后端 reason 因此增为七个(新增 `OUTSIDE_PRIMARY_AREA`);`UNVERIFIED_COORDINATE` 仍在词表里,
- * 供历史快照与「后端未给校验段」的防御路径使用,但不再是新数据的常态结论。
- */
- import { readFileSync } from 'node:fs'
- import { createPinia, setActivePinia } from 'pinia'
- import {
- COORDINATE_REASON_TEXT,
- NO_MAP_LOCATION_TEXT,
- UNVALIDATED_TEXT,
- blockReason,
- blockedEntries,
- coordinateOverview,
- coordinateSummaryText,
- isRenderable,
- splitByCoordinate,
- toCoordinateSummary,
- } from '../../../src/modules/cockpit/domain/coordinate.js'
- import { mergeMapPage, summaryView, toMapBatch } from '../../../src/modules/cockpit/domain/mapSummary.js'
- import { fetchMapSnapshot } from '../../../src/modules/cockpit/services/mapApi.js'
- import {
- getMockTransportPaths,
- registerMockTransport,
- requestCockpitJson,
- resetMockTransportsForTest,
- } from '../../../src/modules/cockpit/services/cockpitApi.js'
- import { registerCockpitMocks } from '../../../src/modules/cockpit/mocks/contractMocks.js'
- import { MAP_SUMMARY_PAGE_SIZE, useMapStore } from '../../../src/modules/cockpit/stores/mapStore.js'
- const checks = []
- function check(name, pass, evidence) {
- checks.push({ name, pass })
- console.log(`${pass ? 'PASS' : 'FAIL'} ${name}${evidence ? ` — ${evidence}` : ''}`)
- }
- const mockConfig = { dataSource: 'mock', api: { baseUrl: '', token: '' } }
- /** 造一个领域地图对象(形状同 `domain/mapSummary.js`)。 */
- function object({ code = 'X', kind = 'monitoring-point', name = '', longitude = 110.1, latitude = 28.4, coordinate = null } = {}) {
- return {
- kind,
- code,
- name: name || `${code} 对象`,
- status: '',
- statusReasons: [],
- longitude,
- latitude,
- coordinate: coordinate || { state: 'VALID', reason: '', crs: 'WGS84', renderable: true, validated: true },
- missing: [],
- }
- }
- /** 后端校验段的取值(照抄 `CockpitCoordinateValidator`;2026-10-12 起含区外原因)。 */
- const BACKEND_CASES = [
- { state: 'VALID', reason: null, renderable: true },
- { state: 'UNVERIFIED', reason: 'UNVERIFIED_COORDINATE', renderable: false },
- { state: 'INVALID', reason: 'MISSING_LONGITUDE', renderable: false },
- { state: 'INVALID', reason: 'MISSING_LATITUDE', renderable: false },
- { state: 'INVALID', reason: 'LONGITUDE_OUT_OF_RANGE', renderable: false },
- { state: 'INVALID', reason: 'LATITUDE_OUT_OF_RANGE', renderable: false },
- { state: 'INVALID', reason: 'ZERO_COORDINATE', renderable: false },
- { state: 'INVALID', reason: 'OUTSIDE_PRIMARY_AREA', renderable: false },
- ]
- /** 后端载荷里的对象 → 领域对象(走真实的适配层,不手搓 coordinate 段)。 */
- function rawToObject(raw, kind = 'monitoring-point') {
- const payload =
- kind === 'monitoring-point'
- ? { monitoringPoints: [raw], unassociatedEquipment: [] }
- : { monitoringPoints: [], unassociatedEquipment: [raw] }
- return toMapBatch(payload).objects[0]
- }
- // ================================================================ A 唯一准入判据
- {
- // 后端每一种结论都如实照搬,不做二次判断。
- const mapped = BACKEND_CASES.map((item) => ({
- expected: item.renderable,
- actual: isRenderable(rawToObject({ pointCode: 'MP-A', coordinateValidation: item })),
- state: item.state,
- reason: item.reason,
- }))
- check(
- 'A1-admission-follows-renderable',
- mapped.every((item) => item.actual === item.expected),
- JSON.stringify(mapped.map((item) => `${item.state}/${item.reason || '-'}→${item.actual}`)),
- )
- // 关键对抗:state 说 VALID 但后端 renderable=false 时,**必须**听 renderable。
- // 这一条守着「前端不另立一套判据」——否则两个答案必然有一个是错的,而错的会画到地图上。
- const contradictory = rawToObject({
- pointCode: 'MP-C',
- coordinateValidation: { state: 'VALID', reason: null, renderable: false, crs: 'WGS84' },
- })
- check('A2-renderable-beats-state', isRenderable(contradictory) === false, 'state=VALID 但 renderable=false → 不上图')
- // 反向:state 说 INVALID 但后端 renderable=true,同样听后端(前端不做"再校验一遍")。
- const permissive = rawToObject({
- pointCode: 'MP-P',
- coordinateValidation: { state: 'INVALID', reason: 'LONGITUDE_OUT_OF_RANGE', renderable: true, crs: 'WGS84' },
- })
- check('A3-no-frontend-recheck', isRenderable(permissive) === true, '后端说可绘制就绘制,前端不推翻')
- // 前端不纠偏、不用默认坐标:坐标值原样带出,不被改成"看起来合法"的数。
- const outOfRange = toMapBatch({
- page: 1,
- pageSize: 1,
- snapshotAt: 'S',
- total: 1,
- complete: true,
- unassociatedEquipment: [
- {
- deviceCode: 'EQ-X',
- deviceName: '超范围设备',
- longitude: 181,
- latitude: 28.455,
- coordinateValidation: { state: 'INVALID', reason: 'LONGITUDE_OUT_OF_RANGE', renderable: false, crs: 'WGS84' },
- },
- ],
- }).objects[0]
- check(
- 'A4-no-correction-no-default',
- outOfRange.longitude === 181 && outOfRange.latitude === 28.455 && isRenderable(outOfRange) === false,
- `原值 ${outOfRange.longitude},${outOfRange.latitude} 未被纠正,且不上图`,
- )
- }
- // ================================================================ B 原因文案
- {
- const texts = BACKEND_CASES.filter((item) => item.reason).map((item) =>
- blockReason(
- rawToObject({
- pointCode: 'MP-B',
- coordinateValidation: { ...item, crs: 'WGS84' },
- }),
- ),
- )
- check(
- 'B1-every-reason-has-text',
- texts.length === BACKEND_CASES.filter((item) => item.reason).length &&
- texts.every((item) => item.text && item.text !== NO_MAP_LOCATION_TEXT && item.text !== UNVALIDATED_TEXT),
- JSON.stringify(texts.map((item) => `${item.code}→${item.text}`)),
- )
- check(
- 'B2-reason-dictionary-covers-backend',
- Object.keys(COORDINATE_REASON_TEXT).length === 7 &&
- BACKEND_CASES.filter((item) => item.reason).every((item) => !!COORDINATE_REASON_TEXT[item.reason]),
- JSON.stringify(Object.keys(COORDINATE_REASON_TEXT)),
- )
- // 后端整段没给校验段(票 05 的 validated:false)→「坐标未校验」,这是「缺失」而不是「非法」。
- const missing = rawToObject({ pointCode: 'MP-N' })
- const missingReason = blockReason(missing)
- check(
- 'B3-unvalidated-is-missing-not-invalid',
- missingReason.text === UNVALIDATED_TEXT && missingReason.code === '',
- JSON.stringify(missingReason),
- )
- // 认不出的原因:只说「无地图位置」,不把原因码甩给值班人员。
- const unknown = object({ code: 'MP-U', coordinate: { state: 'INVALID', reason: 'SOMETHING_NEW', crs: 'WGS84', renderable: false, validated: true } })
- check('B4-unknown-reason-generic', blockReason(unknown).text === NO_MAP_LOCATION_TEXT, JSON.stringify(blockReason(unknown)))
- // 界面上给的是中文:六个原因里不得出现后端原因码原文。
- const leaked = texts.filter((item) => /^[A-Z_]+$/.test(item.text))
- check('B5-no-reason-code-as-text', leaked.length === 0, leaked.length ? JSON.stringify(leaked) : '文案全是中文,零原因码')
- }
- // ================================================================ C 不静默丢对象
- {
- const objects = [
- object({ code: 'OK-1' }),
- object({ code: 'BAD-1', coordinate: { state: 'INVALID', reason: 'ZERO_COORDINATE', crs: 'WGS84', renderable: false, validated: true } }),
- object({ code: 'OK-2' }),
- object({ code: 'BAD-2', coordinate: { state: 'UNVERIFIED', reason: 'UNVERIFIED_COORDINATE', crs: 'WGS84', renderable: false, validated: true } }),
- ]
- const { renderable, blocked } = splitByCoordinate(objects)
- check(
- 'C1-split-keeps-both',
- renderable.length === 2 && blocked.length === 2 && renderable.length + blocked.length === objects.length,
- `可上图 ${renderable.length} / 不上图 ${blocked.length} / 输入 ${objects.length}`,
- )
- check(
- 'C2-blocked-keeps-identity',
- blocked.map((item) => item.code).join(',') === 'BAD-1,BAD-2',
- '被拦下的对象仍带编码与名称(列表要用),不是被丢掉的黑洞',
- )
- const entries = blockedEntries(objects)
- check(
- 'C3-entries-carry-reason',
- entries.length === 2 && entries[0].reasonText === '零坐标(经纬度均为 0)' && entries[1].reasonText === '坐标未经校验',
- JSON.stringify(entries.map((item) => `${item.code}:${item.reasonText}`)),
- )
- check('C4-empty-input-safe', splitByCoordinate(null).blocked.length === 0 && splitByCoordinate(undefined).renderable.length === 0, '空输入不抛错')
- }
- // ================================================================ D 两个口径分开
- {
- // 后端说全量 20 个、可上图 12、非法 5、未校验 3;前端只加载了 4 个(3 好 1 坏)。
- const backend = toCoordinateSummary({ crs: 'WGS84', totalObjectCount: 20, renderableCount: 12, invalidCount: 5, unverifiedCount: 3 })
- const loaded = [
- object({ code: 'L-1' }),
- object({ code: 'L-2' }),
- object({ code: 'L-3' }),
- object({ code: 'L-4', coordinate: { state: 'INVALID', reason: 'ZERO_COORDINATE', crs: 'WGS84', renderable: false, validated: true } }),
- ]
- const overview = coordinateOverview(loaded, backend)
- check(
- 'D1-two-calibers-kept-apart',
- overview.backend.total === 20 && overview.backend.renderable === 12 && overview.loaded.total === 4 && overview.loaded.renderable === 3 && overview.loaded.blocked === 1,
- `后端 ${overview.backend.renderable}/${overview.backend.total} · 已加载 ${overview.loaded.renderable}/${overview.loaded.total}`,
- )
- check(
- 'D2-texts-name-their-subject',
- overview.backendText.includes('全量') && overview.backendText.includes('12/20') && overview.loadedText.includes('已加载'),
- `${overview.backendText} | ${overview.loadedText}`,
- )
- check(
- 'D3-blocked-entry-listed',
- overview.blockedEntries.length === 1 && overview.blockedEntries[0].code === 'L-4',
- JSON.stringify(overview.blockedEntries),
- )
- // 后端没给汇总时:不补 0(0 会被读成"一个坏坐标都没有"),如实说没有。
- const absent = coordinateOverview(loaded, toCoordinateSummary(null))
- check(
- 'D4-missing-summary-not-zero-faked',
- absent.backend.available === false && absent.backend.total === null && coordinateSummaryText(absent.backend).includes('未取到'),
- coordinateSummaryText(absent.backend),
- )
- // 后端汇总字段缺失时同样不补 0。
- const partial = toCoordinateSummary({ crs: 'WGS84', totalObjectCount: 7 })
- check(
- 'D5-null-counts-stay-null',
- partial.available === true && partial.total === 7 && partial.renderable === null && partial.invalid === null,
- JSON.stringify(partial),
- )
- }
- // ================================================================ E 汇总进领域批次(票 05 的漏读)
- {
- const batch = toMapBatch({
- page: 1,
- pageSize: 2,
- snapshotAt: '2026-09-02T10:00:00+08:00',
- total: 5,
- complete: false,
- monitoringPoints: [
- { pointCode: 'MP-1', pointName: '一', longitude: 110.1, latitude: 28.4, coordinateValidation: { state: 'VALID', reason: null, renderable: true, crs: 'WGS84' } },
- ],
- unassociatedEquipment: [
- { deviceCode: 'EQ-1', deviceName: '二', longitude: 181, latitude: 28.4, coordinateValidation: { state: 'INVALID', reason: 'LONGITUDE_OUT_OF_RANGE', renderable: false, crs: 'WGS84' } },
- ],
- coordinateValidation: { crs: 'WGS84', totalObjectCount: 5, renderableCount: 2, invalidCount: 2, unverifiedCount: 1 },
- })
- check(
- 'E1-batch-carries-backend-summary',
- batch.coordinateSummary.available === true && batch.coordinateSummary.total === 5 && batch.coordinateSummary.invalid === 2 && batch.coordinateSummary.unverified === 1,
- JSON.stringify(batch.coordinateSummary),
- )
- // 跨页合并:汇总是全量口径,取最新一次响应,**不得累加**(累加会变成"页数 × 全量")。
- const first = mergeMapPage(null, batch)
- const second = mergeMapPage(first, { ...batch, page: 2, coordinateSummary: batch.coordinateSummary })
- check(
- 'E2-no-accumulation-across-pages',
- second.coordinateSummary.total === 5 && second.coordinateSummary.renderable === 2,
- `两页之后 total=${second.coordinateSummary.total}(不是 10)`,
- )
- // 后端没给汇总的页不会把已拿到的汇总清掉。
- const third = mergeMapPage(second, { page: 3, objects: [], coordinateSummary: toCoordinateSummary(null) })
- check('E3-absent-summary-keeps-last', third.coordinateSummary.total === 5, `total=${third.coordinateSummary.total}`)
- // 空态的初值也有这个字段(界面不必判 undefined)。
- check('E4-empty-state-has-field', mergeMapPage(null, { page: 1, objects: [] }).coordinateSummary.available === false, '初值 available=false')
- }
- // ================================================================ F mock 三桶分法照抄后端
- {
- resetMockTransportsForTest()
- registerCockpitMocks({
- scenario: 'success',
- mapSnapshot: { pageSize: MAP_SUMMARY_PAGE_SIZE },
- handlers: { registerMockTransport, getMockTransportPaths, resetMockTransportsForTest },
- })
- const first = await fetchMapSnapshot({ page: 1, pageSize: MAP_SUMMARY_PAGE_SIZE, config: mockConfig })
- const summary = first.data.coordinateSummary
- // mock 的五条样例:2 个 VALID、1 个 UNVERIFIED、2 个 INVALID(超范围 + 零坐标)。
- check(
- 'F1-mock-summary-three-buckets',
- summary.total === 5 && summary.renderable === 2 && summary.invalid === 2 && summary.unverified === 1 && summary.crs === 'WGS84',
- JSON.stringify(summary),
- )
- // 全量口径不随页码变化(后端是按整份快照算完再切片)。
- const second = await fetchMapSnapshot({
- page: 2,
- pageSize: MAP_SUMMARY_PAGE_SIZE,
- snapshotAt: first.data.snapshotAt,
- config: mockConfig,
- })
- check(
- 'F2-summary-stable-across-pages',
- second.data.coordinateSummary.total === summary.total && second.data.coordinateSummary.renderable === summary.renderable,
- `第 2 页 total=${second.data.coordinateSummary.total}`,
- )
- // 已加载口径随翻页增长,与后端全量口径**本来就不等**——这正是要分开报的原因。
- const merged = mergeMapPage(mergeMapPage(null, first.data), second.data)
- const overview = coordinateOverview(merged.objects, merged.coordinateSummary)
- check(
- 'F3-loaded-differs-from-backend',
- overview.backend.renderable === 2 && overview.loaded.total === 4 && overview.loaded.renderable === 2 && overview.loaded.blocked === 2,
- `后端可上图 ${overview.backend.renderable}/5 · 已加载 ${overview.loaded.renderable}/${overview.loaded.total}(无地图位置 ${overview.loaded.blocked})`,
- )
- // 空样例:0 是真实值(不是缺失)。
- // 注意这里读**原始载荷**:`empty` 在票 05 的词汇里是没有领域载荷的(`data: null`),
- // 所以 `fetchMapSnapshot` 看不到汇总段,得直接问统一出口要后端回的那一份。
- resetMockTransportsForTest()
- registerCockpitMocks({ scenario: 'empty', handlers: { registerMockTransport, getMockTransportPaths, resetMockTransportsForTest } })
- const emptyRaw = await requestCockpitJson('/map-snapshot?page=1&pageSize=2', { config: mockConfig })
- const emptySummary = toCoordinateSummary(emptyRaw.data && emptyRaw.data.coordinateValidation)
- check(
- 'F4-empty-scenario-zeroes',
- emptyRaw.ok === true && emptySummary.available === true && emptySummary.total === 0 && emptySummary.invalid === 0,
- JSON.stringify(emptySummary),
- )
- // 字段缺失样例:没给 coordinateValidation 的对象落进「未校验」桶(与后端同一分法)。
- resetMockTransportsForTest()
- registerCockpitMocks({ scenario: 'degraded', handlers: { registerMockTransport, getMockTransportPaths, resetMockTransportsForTest } })
- const degraded = await fetchMapSnapshot({ page: 1, pageSize: MAP_SUMMARY_PAGE_SIZE, config: mockConfig })
- check(
- 'F5-degraded-counts-unverified',
- degraded.data.coordinateSummary.total === 1 && degraded.data.coordinateSummary.unverified === 1 && degraded.data.coordinateSummary.renderable === 0,
- JSON.stringify(degraded.data.coordinateSummary),
- )
- }
- // ================================================================ G store 供货(组件不自己算)
- {
- setActivePinia(createPinia())
- resetMockTransportsForTest()
- registerCockpitMocks({
- scenario: 'success',
- mapSnapshot: { pageSize: MAP_SUMMARY_PAGE_SIZE },
- handlers: { registerMockTransport, getMockTransportPaths, resetMockTransportsForTest },
- })
- const mapStore = useMapStore()
- mapStore.reset()
- mapStore.setup({ config: mockConfig })
- await mapStore.advance()
- const afterFirst = mapStore.coordinate
- await mapStore.advance()
- const afterSecond = mapStore.coordinate
- check(
- 'G1-store-supplies-overview',
- afterFirst.loaded.total === 2 && afterFirst.loaded.renderable === 2 && afterFirst.loaded.blocked === 0,
- JSON.stringify(afterFirst.loaded),
- )
- check(
- 'G2-overview-grows-with-pages',
- afterSecond.loaded.total === 4 && afterSecond.loaded.renderable === 2 && afterSecond.loaded.blocked === 2 && afterSecond.blockedEntries.length === 2,
- `${afterSecond.loadedText} | ${JSON.stringify(afterSecond.blockedEntries.map((item) => item.reasonText))}`,
- )
- check(
- 'G3-backend-caliber-unchanged',
- afterSecond.backend.total === 5 && afterSecond.backend.renderable === 2,
- afterSecond.backendText,
- )
- }
- // ================================================================ H 「已上图」措辞不再骗人
- {
- const blocked = object({ code: 'BAD', coordinate: { state: 'INVALID', reason: 'ZERO_COORDINATE', crs: 'WGS84', renderable: false, validated: true } })
- const state = { phase: 'complete', loaded: 2, total: 2, objects: [object({ code: 'OK' }), blocked] }
- const view = summaryView(state)
- check(
- 'H1-complete-label-separates-blocked',
- view.label.includes('已上图 1 个') && view.label.includes('无地图位置 1 个'),
- view.label,
- )
- // 没有对象可判时(纯文案单测的老用法)保持票 08 的原措辞,不凭空造数。
- const legacy = summaryView({ phase: 'complete', loaded: 7, total: 7 })
- check('H2-legacy-usage-unchanged', legacy.label.includes('已上图 7 个') && !legacy.label.includes('无地图位置'), legacy.label)
- // 终态措辞不得把被拦下的对象算进「已上图」。
- const allBlocked = summaryView({ phase: 'complete', loaded: 2, total: 2, objects: [blocked, object({ code: 'BAD2', coordinate: { state: 'INVALID', reason: 'ZERO_COORDINATE', crs: 'WGS84', renderable: false, validated: true } })] })
- check('H3-all-blocked-reports-zero', allBlocked.label.includes('已上图 0 个') && allBlocked.label.includes('无地图位置 2 个'), allBlocked.label)
- }
- // ================================================================ I 静态守卫
- {
- const moduleRoot = new URL('../../../src/modules/cockpit/', import.meta.url)
- /**
- * 读一个模块并**去掉注释**再断言。
- * 守卫必须看代码而不是说明文字:本模块的注释里就写着「为什么连 `state === 'VALID'` 都不作为判据」
- * 这句反例(第一版守卫正是被自己的注释判红的),拿注释当证据等于没守。
- */
- const stripComments = (text) =>
- text
- .replace(/\/\*[\s\S]*?\*\//g, ' ')
- // 行注释;`://` 里的双斜杠是 URL,别误伤。
- .replace(/(^|[^:])\/\/.*$/gm, '$1')
- const read = (relative) => stripComments(readFileSync(new URL(relative, moduleRoot), 'utf8'))
- // 票 07 验收第 4 条:前端不自动纠偏。落在代码上就是"没有任何地方再查一遍经纬度范围"。
- // 查数值边界比较(180 / 90 与经纬度变量的比较)—— 判定只许来自后端 renderable。
- const RANGE_PATTERN = /(longitude|latitude|lon|lat)\s*[<>]=?\s*-?(90|180)\b|-?(90|180)\s*[<>]=?\s*(longitude|latitude|lon|lat)\b/i
- const domainFiles = ['domain/coordinate.js', 'domain/mapSummary.js', 'domain/fields.js', 'domain/pointDetail.js']
- const rechecked = domainFiles.filter((file) => RANGE_PATTERN.test(read(file)))
- check(
- 'I1-no-frontend-range-recheck',
- rechecked.length === 0,
- rechecked.length ? `出现前端范围重算:${rechecked.join(', ')}` : '领域层零范围重算,准入只认后端结论',
- )
- // 准入判据只有一处:只许与 renderable 比较,不许出现 `state === 'VALID'` 当第二套判据。
- const coordinateText = read('domain/coordinate.js')
- check(
- 'I2-single-admission-path',
- /coordinate\.renderable === true/.test(coordinateText) && !/state === ['"]VALID['"]/.test(coordinateText),
- '准入只看 renderable,没有第二套 state 判据',
- )
- // 界面不得自己判坐标:只许读 store 算好的数,不许调判据函数、不许碰后端校验段。
- const UI_JUDGES = /coordinateValidation|isRenderable\(|blockReason\(|splitByCoordinate\(|\.renderable\s*===/
- const uiFiles = [
- 'components/MapCanvas.vue',
- 'components/MapCoordinateBlocked.vue',
- 'CockpitShell.vue',
- ]
- const uiJudging = uiFiles.filter((file) => UI_JUDGES.test(read(file)))
- check(
- 'I3-ui-does-not-judge',
- uiJudging.length === 0,
- uiJudging.length ? `界面自己判坐标:${uiJudging.join(', ')}` : '三个界面文件零坐标判据,全部取自 store',
- )
- // 内部原因码不上界面:清单组件只渲染领域层给的中文文案字段。
- const listText = read('components/MapCoordinateBlocked.vue')
- const rawCodes = Object.keys(COORDINATE_REASON_TEXT).filter((code) => listText.includes(code))
- check('I4-no-reason-code-in-list-ui', rawCodes.length === 0, rawCodes.length ? JSON.stringify(rawCodes) : '清单组件零后端原因码')
- // 「保留在列表里」的可达性:清单组件确实渲染了原因文案与对象名(不是只有计数)。
- check(
- 'I5-list-renders-reason-and-name',
- /item\.reasonText/.test(listText) && /item\.name/.test(listText) && /blockedEntries/.test(coordinateText),
- '清单渲染对象名与中文原因',
- )
- // 后端全量口径只许经领域层转换,界面与 store 不出现后端裸字段名(票 05 的 D1 同一条纪律)。
- const backendField = /totalObjectCount|renderableCount|invalidCount|unverifiedCount/
- const leaked = ['stores/mapStore.js', 'components/MapCanvas.vue', 'components/MapCoordinateBlocked.vue'].filter((file) =>
- backendField.test(read(file)),
- )
- check(
- 'I6-backend-summary-fields-confined',
- leaked.length === 0,
- leaked.length ? `裸字段泄漏:${leaked.join(', ')}` : '后端汇总字段只出现在 domain/coordinate.js 与 mock',
- )
- }
- const failed = checks.filter((item) => !item.pass)
- console.log('')
- console.log(`T07 逻辑检查:${checks.length - failed.length}/${checks.length} 项通过。`)
- if (failed.length) {
- console.log('未通过:' + failed.map((item) => item.name).join(', '))
- process.exit(1)
- }
|