@dsh-triage/core 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,376 @@
1
+ /**
2
+ * Local outcome records: the whitelist that proves a record carries no PII.
3
+ *
4
+ * WHY THIS MODULE EXISTS (spec `docs/specs/M5-reflow.md` §3–§5, `T-501`):
5
+ * "we only store the four-state conclusion" is a claim; a whitelist projection that
6
+ * *rejects* the whole line when one unknown key appears is an enforcement. Everything
7
+ * the local store can hold is declared here, and there is exactly one function that
8
+ * decides whether a candidate record is allowed to reach disk (`sanitizeOutcome`) and
9
+ * one that decides whether an existing line may be read back (`assertOutcomeLine`).
10
+ *
11
+ * WHAT THIS MODULE MUST NEVER DO (§12 of the spec, machine-checked by the
12
+ * `outcome-privacy` check of `scripts/gate_governance.mjs`):
13
+ * - no network client of any kind, no `fs`, no clock, no `process.env`;
14
+ * - no randomness: `id` and `at` are injected by the caller, exactly like `generatedAt`
15
+ * in `verdict.ts`;
16
+ * - no repair: a rejected record is rejected, never "cleaned up and written anyway".
17
+ *
18
+ * TIER DISCIPLINE (`ADR-0011` option A): a `LocalPrior` is an *annotation*. It never
19
+ * enters `conclusion` / `confidence` / `evidence[]`, and there is no evidence tier for
20
+ * it (`measured-local` is reused as a label for "this machine measured this before").
21
+ */
22
+ import { valid } from 'semver';
23
+ import { CONCLUSION_VALUES } from './types.js';
24
+ export const OUTCOME_OS_VALUES = ['win32', 'darwin', 'linux', 'freebsd', 'other'];
25
+ export const OUTCOME_ARCH_VALUES = ['x64', 'arm64', 'ia32', 'other'];
26
+ export const OUTCOME_PROFILE_KIND_VALUES = ['web', 'tui', 'headless', 'unknown'];
27
+ export const OUTCOME_SOURCE_VALUES = ['check', 'probe'];
28
+ export const TIMING_BUCKET_VALUES = ['lt5s', '5to20s', '20to60s', 'gte60s'];
29
+ /** The five evidence tiers, as a value list, so the sanitizer does not have to trust a cast. */
30
+ export const EVIDENCE_TIER_VALUES = ['inferred', 'declared', 'scanned', 'measured-registry', 'measured-local'];
31
+ /** Exactly the keys of {@link OutcomeRecord}. Anything else is an `unknown-field` rejection. */
32
+ const ALLOWED_FIELDS = new Set([
33
+ 'schema_version',
34
+ 'id',
35
+ 'at',
36
+ 'source',
37
+ 'package',
38
+ 'version',
39
+ 'dsh_version',
40
+ 'os',
41
+ 'arch',
42
+ 'node_major',
43
+ 'profile_kind',
44
+ 'conclusion',
45
+ 'reason_message_ids',
46
+ 'evidence_tiers',
47
+ 'probe',
48
+ ]);
49
+ const PROBE_FIELDS = new Set(['total_ms_bucket', 'boot_ms_bucket']);
50
+ /** npm's own name grammar (scope-aware), kept literal so a package name containing `/` is legal. */
51
+ const PACKAGE_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
52
+ const ID_RE = /^[0-9a-f]{32}$/;
53
+ const ISO_UTC_MS_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
54
+ const MESSAGE_ID_RE = /^(admission|collision|registry|runtime|remediation|missing|probe|tool|prior)\.[a-z0-9.-]+$/;
55
+ const MAX_PACKAGE = 214;
56
+ const MAX_VERSION = 64;
57
+ const MAX_REASON_IDS = 12;
58
+ const MAX_TIERS = 8;
59
+ /** One line, 1024 bytes. A defence line, not a formatting preference (§5.5). */
60
+ export const MAX_OUTCOME_BYTES = 1024;
61
+ /**
62
+ * Forbidden shapes, scanned over every final string value (§5.3).
63
+ *
64
+ * WHY the context around `/`: a scoped package name (`@scope/name`) is legal, so a bare
65
+ * separator cannot be the signal. A path needs a drive letter, a leading separator after
66
+ * whitespace/`"`/`=`, a UNC prefix, or a `file://` scheme — those are the shapes that
67
+ * actually carry a username.
68
+ */
69
+ const FORBIDDEN_SHAPES = [
70
+ /[A-Za-z]:[\\/]/,
71
+ /\\\\[A-Za-z0-9._-]+\\/,
72
+ /(^|[\s"=])\/(Users|home|root|tmp)\//,
73
+ /file:\/\//i,
74
+ /~\//,
75
+ /sk-[A-Za-z0-9]{20,}/,
76
+ /ghp_[A-Za-z0-9]{20,}/,
77
+ /npm_[A-Za-z0-9]{20,}/,
78
+ /bearer\s/i,
79
+ /token=/i,
80
+ ];
81
+ function isPlainObject(value) {
82
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
83
+ }
84
+ function hasForbiddenShape(value) {
85
+ return FORBIDDEN_SHAPES.some((pattern) => pattern.test(value));
86
+ }
87
+ /** UTF-8 length without touching a Node-only global (core stays bundle-safe). */
88
+ function utf8Length(value) {
89
+ return new TextEncoder().encode(value).length;
90
+ }
91
+ /** Every final string of the assembled record, so the forbidden-shape scan cannot skip one. */
92
+ function recordStrings(record) {
93
+ const values = [
94
+ record.id,
95
+ record.at,
96
+ record.source,
97
+ record.package,
98
+ record.version,
99
+ record.os,
100
+ record.arch,
101
+ record.profile_kind,
102
+ record.conclusion,
103
+ ...record.reason_message_ids,
104
+ ...record.evidence_tiers,
105
+ ];
106
+ if (record.dsh_version !== null)
107
+ values.push(record.dsh_version);
108
+ if (record.probe !== null) {
109
+ values.push(record.probe.total_ms_bucket);
110
+ if (record.probe.boot_ms_bucket !== null)
111
+ values.push(record.probe.boot_ms_bucket);
112
+ }
113
+ return values;
114
+ }
115
+ /** A string array that is deduplicated AND ascending: order is part of the contract, not noise. */
116
+ function readSortedStrings(value, limit, accept) {
117
+ if (!Array.isArray(value) || value.length > limit)
118
+ return null;
119
+ const out = [];
120
+ for (const [index, item] of value.entries()) {
121
+ if (typeof item !== 'string' || !accept(item))
122
+ return null;
123
+ // Strictly ascending ⇒ sorted AND deduplicated. A caller that hands us an unordered
124
+ // list has already lost the guarantee the gate recomputes, so we refuse instead of sorting.
125
+ if (index > 0) {
126
+ const previous = out[index - 1];
127
+ if (previous === undefined || !(previous < item))
128
+ return null;
129
+ }
130
+ out.push(item);
131
+ }
132
+ return out;
133
+ }
134
+ function fail(reason, field) {
135
+ return { ok: false, reason, field };
136
+ }
137
+ const isEnumValue = (values, value) => typeof value === 'string' && values.includes(value);
138
+ /**
139
+ * Project a candidate record onto the whitelist, or refuse it.
140
+ *
141
+ * Order of operations is the contract (§5): unknown keys → value domains → forbidden
142
+ * shapes over the *assembled* record → byte cap. A rejection never mutates the input and
143
+ * never returns a partially cleaned record.
144
+ */
145
+ export function sanitizeOutcome(input) {
146
+ if (!isPlainObject(input))
147
+ return fail('not-an-object', null);
148
+ // 1. Whitelist projection: an unknown key means the caller already holds data it should
149
+ // not have, so the whole record dies (§5.1). Silently dropping it would hide that path.
150
+ for (const key of Object.keys(input)) {
151
+ if (!ALLOWED_FIELDS.has(key))
152
+ return fail('unknown-field', key);
153
+ }
154
+ if (input['schema_version'] !== 1)
155
+ return fail('value-shape', 'schema_version');
156
+ const id = input['id'];
157
+ if (typeof id !== 'string' || !ID_RE.test(id))
158
+ return fail('value-shape', 'id');
159
+ const at = input['at'];
160
+ if (typeof at !== 'string' || !ISO_UTC_MS_RE.test(at) || Number.isNaN(Date.parse(at))) {
161
+ return fail('bad-timestamp', 'at');
162
+ }
163
+ const source = input['source'];
164
+ if (!isEnumValue(OUTCOME_SOURCE_VALUES, source))
165
+ return fail('value-shape', 'source');
166
+ const packageName = input['package'];
167
+ if (typeof packageName !== 'string' || packageName.length > MAX_PACKAGE || !PACKAGE_RE.test(packageName)) {
168
+ return fail('value-shape', 'package');
169
+ }
170
+ // `+build` would smuggle a build fingerprint into a version string, so it is refused
171
+ // rather than stripped (`semver.valid` keeps it, which is why the check is explicit).
172
+ const version = input['version'];
173
+ if (typeof version !== 'string' ||
174
+ version.length > MAX_VERSION ||
175
+ version.includes('+') ||
176
+ valid(version) === null) {
177
+ return fail('value-shape', 'version');
178
+ }
179
+ const dshVersion = input['dsh_version'];
180
+ if (dshVersion !== null) {
181
+ if (typeof dshVersion !== 'string' || dshVersion.length > MAX_VERSION || valid(dshVersion) === null) {
182
+ return fail('value-shape', 'dsh_version');
183
+ }
184
+ }
185
+ const os = input['os'];
186
+ if (!isEnumValue(OUTCOME_OS_VALUES, os))
187
+ return fail('value-shape', 'os');
188
+ const arch = input['arch'];
189
+ if (!isEnumValue(OUTCOME_ARCH_VALUES, arch))
190
+ return fail('value-shape', 'arch');
191
+ const nodeMajor = input['node_major'];
192
+ if (typeof nodeMajor !== 'number' || !Number.isInteger(nodeMajor) || nodeMajor < 18 || nodeMajor > 99) {
193
+ return fail('value-shape', 'node_major');
194
+ }
195
+ const profileKind = input['profile_kind'];
196
+ if (!isEnumValue(OUTCOME_PROFILE_KIND_VALUES, profileKind))
197
+ return fail('value-shape', 'profile_kind');
198
+ const conclusion = input['conclusion'];
199
+ if (!isEnumValue(CONCLUSION_VALUES, conclusion))
200
+ return fail('value-shape', 'conclusion');
201
+ const reasonIds = readSortedStrings(input['reason_message_ids'], MAX_REASON_IDS, (item) => MESSAGE_ID_RE.test(item));
202
+ if (reasonIds === null)
203
+ return fail('value-shape', 'reason_message_ids');
204
+ const tiers = readSortedStrings(input['evidence_tiers'], MAX_TIERS, (item) => isEnumValue(EVIDENCE_TIER_VALUES, item));
205
+ if (tiers === null)
206
+ return fail('value-shape', 'evidence_tiers');
207
+ const rawProbe = input['probe'];
208
+ let probe = null;
209
+ if (rawProbe !== null) {
210
+ if (!isPlainObject(rawProbe))
211
+ return fail('value-shape', 'probe');
212
+ for (const key of Object.keys(rawProbe)) {
213
+ if (!PROBE_FIELDS.has(key))
214
+ return fail('unknown-field', `probe.${key}`);
215
+ }
216
+ // Only a probe run can carry probe facts; a `check` record with buckets would be a
217
+ // claim about a measurement that never happened.
218
+ if (source !== 'probe')
219
+ return fail('value-shape', 'probe');
220
+ const total = rawProbe['total_ms_bucket'];
221
+ if (!isEnumValue(TIMING_BUCKET_VALUES, total))
222
+ return fail('value-shape', 'probe.total_ms_bucket');
223
+ const boot = rawProbe['boot_ms_bucket'];
224
+ if (boot !== null && !isEnumValue(TIMING_BUCKET_VALUES, boot)) {
225
+ return fail('value-shape', 'probe.boot_ms_bucket');
226
+ }
227
+ probe = { total_ms_bucket: total, boot_ms_bucket: boot };
228
+ }
229
+ const record = {
230
+ schema_version: 1,
231
+ id,
232
+ at,
233
+ source,
234
+ package: packageName,
235
+ version,
236
+ dsh_version: dshVersion,
237
+ os,
238
+ arch,
239
+ node_major: nodeMajor,
240
+ profile_kind: profileKind,
241
+ conclusion,
242
+ reason_message_ids: reasonIds,
243
+ evidence_tiers: tiers,
244
+ probe,
245
+ };
246
+ // 3. Forbidden shapes, over the assembled record (never over the raw input: only what we
247
+ // are about to write matters).
248
+ for (const value of recordStrings(record)) {
249
+ if (hasForbiddenShape(value))
250
+ return fail('value-shape', fieldOfShapeHit(record, value));
251
+ }
252
+ // 5. Byte cap. Free text cannot fit in a record that is capped at one short line.
253
+ if (utf8Length(JSON.stringify(record)) > MAX_OUTCOME_BYTES)
254
+ return fail('too-long', null);
255
+ return { ok: true, record };
256
+ }
257
+ /** Name the field a forbidden shape was found in, so the rejection is actionable. */
258
+ function fieldOfShapeHit(record, value) {
259
+ if (value === record.id)
260
+ return 'id';
261
+ if (value === record.at)
262
+ return 'at';
263
+ if (value === record.package)
264
+ return 'package';
265
+ if (value === record.version)
266
+ return 'version';
267
+ if (value === record.dsh_version)
268
+ return 'dsh_version';
269
+ if (value === record.source)
270
+ return 'source';
271
+ if (value === record.os)
272
+ return 'os';
273
+ if (value === record.arch)
274
+ return 'arch';
275
+ if (value === record.profile_kind)
276
+ return 'profile_kind';
277
+ if (value === record.conclusion)
278
+ return 'conclusion';
279
+ if (record.reason_message_ids.includes(value))
280
+ return 'reason_message_ids';
281
+ if (record.evidence_tiers.includes(value))
282
+ return 'evidence_tiers';
283
+ return 'probe';
284
+ }
285
+ /**
286
+ * Read back one stored line.
287
+ *
288
+ * WHY A SECOND FUNCTION: reading is where a hand-edited or corrupted library turns into fake
289
+ * evidence. A line that does not validate is dropped by the caller and counted as
290
+ * `rejected_lines`; it is never repaired, and it never aborts the whole read.
291
+ */
292
+ export function assertOutcomeLine(line) {
293
+ if (typeof line !== 'string' || line.trim() === '')
294
+ return { ok: false, reason: 'not-json', field: null };
295
+ let parsed;
296
+ try {
297
+ parsed = JSON.parse(line);
298
+ }
299
+ catch {
300
+ return { ok: false, reason: 'not-json', field: null };
301
+ }
302
+ const result = sanitizeOutcome(parsed);
303
+ if (!result.ok)
304
+ return result;
305
+ return { ok: true, record: result.record };
306
+ }
307
+ /** The seven-tuple of a record (§2). One definition, so the store, the prior and D4 agree. */
308
+ export function outcomeKeyOf(record) {
309
+ return {
310
+ package: record.package,
311
+ version: record.version,
312
+ dsh: record.dsh_version,
313
+ os: record.os,
314
+ arch: record.arch,
315
+ node_major: record.node_major,
316
+ profile_kind: record.profile_kind,
317
+ };
318
+ }
319
+ /** A key is usable for prior matching only when all seven positions are known (§2, B10). */
320
+ export function isCompleteOutcomeKey(key) {
321
+ return key.dsh !== null && key.profile_kind !== 'unknown';
322
+ }
323
+ export function sameOutcomeKey(left, right) {
324
+ return (left.package === right.package &&
325
+ left.version === right.version &&
326
+ left.dsh === right.dsh &&
327
+ left.os === right.os &&
328
+ left.arch === right.arch &&
329
+ left.node_major === right.node_major &&
330
+ left.profile_kind === right.profile_kind);
331
+ }
332
+ /**
333
+ * The local prior for one key: counts, strict majority, agreement.
334
+ *
335
+ * `majority` is a **strict majority** (`counts[majority] * 2 > n`), so a tie - and any
336
+ * plurality that does not exceed half - reads as "no majority" rather than as a winner.
337
+ * Stricter than a plain mode on purpose: this number is shown to a user next to a verdict
338
+ * that it must not influence (`ADR-0011` §4.1), and "2 of 4 said go" is not a majority.
339
+ *
340
+ * Returns `null` when the key is incomplete or `n = 0`, and the caller then omits the whole
341
+ * `prior` key (§10.2: "nothing" must be distinguishable from "empty").
342
+ */
343
+ export function computeLocalPrior(records, key) {
344
+ if (!isCompleteOutcomeKey(key))
345
+ return null;
346
+ const counts = { go: 0, risky: 0, blocked: 0, unknown: 0 };
347
+ let n = 0;
348
+ for (const record of records) {
349
+ if (!sameOutcomeKey(outcomeKeyOf(record), key))
350
+ continue;
351
+ counts[record.conclusion] += 1;
352
+ n += 1;
353
+ }
354
+ if (n < 1)
355
+ return null;
356
+ let majority = null;
357
+ for (const conclusion of CONCLUSION_VALUES) {
358
+ if (counts[conclusion] * 2 > n)
359
+ majority = conclusion;
360
+ }
361
+ return {
362
+ schema_version: 1,
363
+ annotation_only: true,
364
+ key,
365
+ source: 'local-outcomes',
366
+ method: 'outcome-log',
367
+ n,
368
+ counts,
369
+ majority,
370
+ agreement: majority === null ? null : counts[majority] / n,
371
+ tier: 'measured-local',
372
+ evidence_id: 'prior-local',
373
+ note_id: 'prior.annotation-only',
374
+ };
375
+ }
376
+ //# sourceMappingURL=outcome.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outcome.js","sourceRoot":"","sources":["../src/outcome.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The upgrade-day card (`docs/specs/M6-upgrade-day.md`, T-801).
3
+ *
4
+ * WHY THIS IS NOT ANOTHER ENGINE: the four buckets, their evidence and their remediation are decided by
5
+ * `judgeUpgradeImpact` (`upgrade.ts`). This module only decides how the answer is *said*: one conclusion line,
6
+ * one small table, the units that matter, and the boundary. A card that re-derived a bucket would be a second
7
+ * engine, and two engines drift — that is the failure this project exists to attribute.
8
+ *
9
+ * WHY MARKDOWN: the only social action in an upgrade day is pasting the answer into the release discussion or
10
+ * an issue. Terminal colour does not survive that trip; a table does.
11
+ *
12
+ * WHAT IT MUST NEVER DO:
13
+ * - print a number that `impact.counts` did not produce (control C2/N2);
14
+ * - promise that upgrading fixes anything. `improved` is only witnessed in the downgrade geometry today
15
+ * (`BS-42`), so the boundary says "the target version admits it" and nothing stronger (C4/N4);
16
+ * - leak a home path. Callers pass `redactRoots`; a shape-based fallback catches the rest (C6/N5);
17
+ * - read the clock. The card is a pure function of its `impact` (C5/N6).
18
+ */
19
+ import { type Locale } from './messages.js';
20
+ import { type UpgradeImpact } from './upgrade.js';
21
+ /** At most this many units per bucket are printed; the rest become a count (a card is not a log). */
22
+ export declare const MAX_UNITS_PER_BUCKET = 8;
23
+ export interface UpgradeCardOptions {
24
+ readonly locale: Locale;
25
+ /** Absolute roots to erase from the text (`homedir()`, the dsh home). */
26
+ readonly redactRoots?: readonly string[];
27
+ /** The command to re-run. Defaults to `dsh-triage upgrade --to <target> --card`. */
28
+ readonly command?: string;
29
+ }
30
+ /**
31
+ * Erase absolute roots and path shapes.
32
+ *
33
+ * The card is meant to be pasted in public, so a home path is a leak even when it is "only" the profile
34
+ * directory. Roots are erased first (exact match), then a shape fallback catches paths the caller did not list.
35
+ */
36
+ export declare function scrubCardText(text: string, roots?: readonly string[]): string;
37
+ /**
38
+ * Render the card.
39
+ *
40
+ * Every number in the header and the table comes from `impact.counts`; the unit lists are only a
41
+ * projection of `impact.units`. That split is what makes control N2 meaningful.
42
+ */
43
+ export declare function renderUpgradeCard(impact: UpgradeImpact, options: UpgradeCardOptions): string;
44
+ //# sourceMappingURL=upgrade-card.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upgrade-card.d.ts","sourceRoot":"","sources":["../src/upgrade-card.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAsB,KAAK,MAAM,EAAE,MAAM,eAAe,CAAA;AAC/D,OAAO,EAAoC,KAAK,aAAa,EAAoB,MAAM,cAAc,CAAA;AAErG,qGAAqG;AACrG,eAAO,MAAM,oBAAoB,IAAI,CAAA;AAKrC,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,yEAAyE;IACzE,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACxC,oFAAoF;IACpF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAC1B;AA8FD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,SAAS,MAAM,EAAO,GAAG,MAAM,CAWjF;AAoCD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,kBAAkB,GAAG,MAAM,CA8E5F"}
@@ -0,0 +1,210 @@
1
+ /**
2
+ * The upgrade-day card (`docs/specs/M6-upgrade-day.md`, T-801).
3
+ *
4
+ * WHY THIS IS NOT ANOTHER ENGINE: the four buckets, their evidence and their remediation are decided by
5
+ * `judgeUpgradeImpact` (`upgrade.ts`). This module only decides how the answer is *said*: one conclusion line,
6
+ * one small table, the units that matter, and the boundary. A card that re-derived a bucket would be a second
7
+ * engine, and two engines drift — that is the failure this project exists to attribute.
8
+ *
9
+ * WHY MARKDOWN: the only social action in an upgrade day is pasting the answer into the release discussion or
10
+ * an issue. Terminal colour does not survive that trip; a table does.
11
+ *
12
+ * WHAT IT MUST NEVER DO:
13
+ * - print a number that `impact.counts` did not produce (control C2/N2);
14
+ * - promise that upgrading fixes anything. `improved` is only witnessed in the downgrade geometry today
15
+ * (`BS-42`), so the boundary says "the target version admits it" and nothing stronger (C4/N4);
16
+ * - leak a home path. Callers pass `redactRoots`; a shape-based fallback catches the rest (C6/N5);
17
+ * - read the clock. The card is a pure function of its `impact` (C5/N6).
18
+ */
19
+ import { catalogFor, format } from './messages.js';
20
+ import { compareUnits } from './upgrade.js';
21
+ /** At most this many units per bucket are printed; the rest become a count (a card is not a log). */
22
+ export const MAX_UNITS_PER_BUCKET = 8;
23
+ /** At most this many blocking reasons / fixes per unit. */
24
+ const MAX_DETAIL_PER_UNIT = 2;
25
+ const BUCKET_ORDER = ['would-be-denied', 'cannot-tell', 'improved', 'likely-fine'];
26
+ const ZH = {
27
+ title: (target) => `# 升级日 · ${target}`,
28
+ conclusion: (p) => `**${p.profile}:${p.total} 个单元(${p.bundles} bundle + ${p.rows} 行)升到 ${p.target} → ${p.denied} 会被拒 · ${p.cannot} 判不了 · ${p.improved} 会变好 · ${p.fine} 像没事**`,
29
+ meta: (p) => `> profile \`${p.profile}\`(${p.source})· 当前 ${p.current} · 目标${p.relation}`,
30
+ scopeNote: '> 判的是**声明层**:只看「声明区间 vs 目标运行时」,不是运行期实测。',
31
+ tableHead: '| 桶 | 数量 | 意思 |\n|---|---|---|',
32
+ bucket: {
33
+ 'would-be-denied': { label: '升上去会被摘掉', meaning: '目标版本的 admission 会拒绝这一行/包' },
34
+ 'cannot-tell': { label: '判不了', meaning: '声明不足或目标还没发布,证据不够' },
35
+ improved: { label: '今天被拒、目标放行', meaning: '今天是坏的,目标版本会放行它' },
36
+ 'likely-fine': { label: '声明上不冲突', meaning: '不等于升级后能跑,只表示没有声明冲突' },
37
+ },
38
+ reason: '原因',
39
+ now: '现在',
40
+ fix: '修复',
41
+ alreadyDenied: '今天已经是坏的(不是这次升级造成的)',
42
+ material: '——',
43
+ more: (count) => `- … 另外 ${count} 个`,
44
+ boundaryTitle: '## 边界(请一起读)',
45
+ boundary: [
46
+ '- `improved` 只说明「**目标版本会放行它**」;今天没有「升到更新版本变好」的真实样本(`BS-42`)。',
47
+ '- 声明层判不了运行期行为;`likely-fine` 不等于升级后能跑。',
48
+ ],
49
+ recompute: '复算',
50
+ emptyProfile: (profile) => `这个 profile(${profile})没有可判单元:没有任何 bundle 或 patch 行可以对着目标版本判。`,
51
+ relation: { same: '等于当前', newer: '比当前新', older: '比当前旧', unknown: '无法判断新旧' },
52
+ };
53
+ const EN = {
54
+ title: (target) => `# Upgrade day · ${target}`,
55
+ conclusion: (p) => `**${p.profile}: ${p.total} unit(s) (${p.bundles} bundle + ${p.rows} row) against ${p.target} → ${p.denied} would be denied · ${p.cannot} cannot tell · ${p.improved} would improve · ${p.fine} look fine**`,
56
+ meta: (p) => `> profile \`${p.profile}\` (${p.source}) · current ${p.current} · target is ${p.relation}`,
57
+ scopeNote: '> This judges the **declaration layer**: declared ranges against the target runtime, not runtime behaviour.',
58
+ tableHead: '| bucket | count | meaning |\n|---|---|---|',
59
+ bucket: {
60
+ 'would-be-denied': { label: 'would be dropped', meaning: 'the target build denies this row/package' },
61
+ 'cannot-tell': { label: 'cannot tell', meaning: 'not enough declaration evidence' },
62
+ improved: { label: 'denied today, admitted at target', meaning: 'broken now, the target admits it' },
63
+ 'likely-fine': { label: 'no declared conflict', meaning: 'not a promise that it runs after the upgrade' },
64
+ },
65
+ reason: 'reason',
66
+ now: 'now',
67
+ fix: 'fix',
68
+ alreadyDenied: 'already broken today (this upgrade did not cause it)',
69
+ material: '—',
70
+ more: (count) => `- … ${count} more`,
71
+ boundaryTitle: '## Boundaries (read these too)',
72
+ boundary: [
73
+ '- `improved` only means "**the target admits it**"; there is no real sample of a newer build fixing anything today (`BS-42`).',
74
+ '- The declaration layer cannot judge runtime behaviour; `likely-fine` is not "it runs after the upgrade".',
75
+ ],
76
+ recompute: 'Recompute',
77
+ emptyProfile: (profile) => `This profile (${profile}) has no judgeable unit: nothing to judge against the target build.`,
78
+ relation: { same: 'equal to current', newer: 'newer than current', older: 'older than current', unknown: 'unknown relation' },
79
+ };
80
+ const copyFor = (locale) => (locale === 'zh' ? ZH : EN);
81
+ /**
82
+ * Erase absolute roots and path shapes.
83
+ *
84
+ * The card is meant to be pasted in public, so a home path is a leak even when it is "only" the profile
85
+ * directory. Roots are erased first (exact match), then a shape fallback catches paths the caller did not list.
86
+ */
87
+ export function scrubCardText(text, roots = []) {
88
+ let out = text;
89
+ for (const root of roots) {
90
+ if (root === '')
91
+ continue;
92
+ for (const variant of [root, root.replaceAll('\\', '/'), root.replaceAll('/', '\\')]) {
93
+ out = out.split(variant).join('<path>');
94
+ }
95
+ }
96
+ out = out.replace(/[A-Za-z]:[\\/]+Users[\\/]+[^\\/\s"'`]+(\.[^\\/\s"'`]+)?(?:[\\/][^\s"'`]*)?/g, '<path>');
97
+ out = out.replace(/\/(?:home|Users)\/[^/\s"'`]+(?:\/[^\s"'`]*)?/g, '<path>');
98
+ return out;
99
+ }
100
+ /** One blocking reason, said the way the CLI says it (same catalogue, same params). */
101
+ function reasonLines(unit, locale, kind) {
102
+ const catalog = catalogFor(locale);
103
+ const say = (messageId, params) => {
104
+ const template = catalog.messages[messageId];
105
+ return template === undefined ? `?${messageId}` : format(template, params);
106
+ };
107
+ const reasons = kind === 'target'
108
+ ? unit.reasons.filter((reason) => reason.severity === 'block')
109
+ : unit.current.unmet.map((requirement) => ({
110
+ messageId: requirement.key === 'dsh' ? 'admission.unmet.engines' : 'admission.unmet.peer',
111
+ params: { key: requirement.key, range: requirement.range, runtime: unit.current.runtime ?? 'unknown' },
112
+ }));
113
+ return reasons.slice(0, MAX_DETAIL_PER_UNIT).map((reason) => say(reason.messageId, 'params' in reason ? reason.params : undefined));
114
+ }
115
+ function fixLines(unit, locale) {
116
+ return unit.remediation.slice(0, MAX_DETAIL_PER_UNIT).map((fix) => {
117
+ if (fix.messageId !== undefined) {
118
+ const catalog = catalogFor(locale);
119
+ const template = catalog.messages[fix.messageId];
120
+ return template === undefined ? `?${fix.messageId}` : format(template, fix.params);
121
+ }
122
+ // A note is prose unless it reads like a command; wrapping prose in backticks makes it look like one.
123
+ return /^(dsh|npx|npm|pnpm)\b/.test(fix.note) ? `\`${fix.note}\`` : fix.note;
124
+ });
125
+ }
126
+ function unitHeading(unit) {
127
+ const version = unit.version === null ? '' : `@${unit.version}`;
128
+ return `\`${unit.id}\` · ${unit.name}${version}`;
129
+ }
130
+ /**
131
+ * Render the card.
132
+ *
133
+ * Every number in the header and the table comes from `impact.counts`; the unit lists are only a
134
+ * projection of `impact.units`. That split is what makes control N2 meaningful.
135
+ */
136
+ export function renderUpgradeCard(impact, options) {
137
+ const copy = copyFor(options.locale);
138
+ const command = options.command ?? `dsh-triage upgrade --to ${impact.target} --card`;
139
+ const lines = [];
140
+ lines.push(copy.title(impact.target));
141
+ lines.push('');
142
+ lines.push(copy.conclusion({
143
+ profile: impact.profile,
144
+ total: impact.counts.total,
145
+ bundles: impact.counts.bundles,
146
+ rows: impact.counts.rows,
147
+ target: impact.target,
148
+ denied: impact.counts.would_be_denied,
149
+ cannot: impact.counts.cannot_tell,
150
+ improved: impact.counts.improved,
151
+ fine: impact.counts.likely_fine,
152
+ }));
153
+ lines.push('');
154
+ lines.push(copy.meta({
155
+ profile: impact.profile,
156
+ source: impact.profile_source,
157
+ current: impact.current ?? 'unknown',
158
+ relation: copy.relation[impact.target_relation],
159
+ }));
160
+ lines.push(copy.scopeNote);
161
+ lines.push('');
162
+ if (impact.counts.total === 0) {
163
+ lines.push(copy.emptyProfile(impact.profile));
164
+ lines.push('');
165
+ return scrubCardText(`${lines.join('\n')}\n`, options.redactRoots);
166
+ }
167
+ lines.push(copy.tableHead);
168
+ for (const bucket of BUCKET_ORDER) {
169
+ const count = bucket === 'would-be-denied'
170
+ ? impact.counts.would_be_denied
171
+ : bucket === 'cannot-tell'
172
+ ? impact.counts.cannot_tell
173
+ : bucket === 'improved'
174
+ ? impact.counts.improved
175
+ : impact.counts.likely_fine;
176
+ lines.push(`| \`${bucket}\` | ${count} | ${copy.bucket[bucket].label} —— ${copy.bucket[bucket].meaning} |`);
177
+ }
178
+ lines.push('');
179
+ for (const bucket of BUCKET_ORDER) {
180
+ const units = impact.units.filter((unit) => unit.bucket === bucket).sort(compareUnits);
181
+ if (units.length === 0)
182
+ continue;
183
+ lines.push(`## ${copy.bucket[bucket].label}(${units.length})`);
184
+ lines.push('');
185
+ for (const unit of units.slice(0, MAX_UNITS_PER_BUCKET)) {
186
+ lines.push(`- ${unitHeading(unit)}`);
187
+ if (unit.already_denied_at_current)
188
+ lines.push(` - ${copy.alreadyDenied}`);
189
+ for (const reason of reasonLines(unit, options.locale, 'target'))
190
+ lines.push(` - ${copy.reason}:${reason}`);
191
+ if (bucket === 'improved') {
192
+ for (const reason of reasonLines(unit, options.locale, 'current'))
193
+ lines.push(` - ${copy.now}:${reason}`);
194
+ }
195
+ for (const fix of fixLines(unit, options.locale))
196
+ lines.push(` - ${copy.fix}:${fix}`);
197
+ }
198
+ if (units.length > MAX_UNITS_PER_BUCKET)
199
+ lines.push(copy.more(units.length - MAX_UNITS_PER_BUCKET));
200
+ lines.push('');
201
+ }
202
+ lines.push(copy.boundaryTitle);
203
+ lines.push('');
204
+ lines.push(...copy.boundary);
205
+ lines.push('');
206
+ lines.push(`**${copy.recompute}**:\`${command}\``);
207
+ lines.push('');
208
+ return scrubCardText(`${lines.join('\n')}`, options.redactRoots);
209
+ }
210
+ //# sourceMappingURL=upgrade-card.js.map