@vibgrate/cli 2026.625.2 → 2026.628.1
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/DOCS.md +24 -24
- package/README.md +18 -18
- package/dist/baseline-QM3PG3RY.js +1 -0
- package/dist/{chunk-YF3NML7I.js → chunk-CKBCROBF.js} +1 -1
- package/dist/chunk-RKH4N26K.js +75 -0
- package/dist/{chunk-BHWXSLTQ.js → chunk-ROPIO52N.js} +2 -2
- package/dist/cli.js +145 -145
- package/dist/{dist-AP3RH35M.js → dist-LFSEE53Z.js} +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/baseline-2B6YIW6Z.js +0 -1
- package/dist/chunk-I65B3ZRL.js +0 -78
package/DOCS.md
CHANGED
|
@@ -53,7 +53,7 @@ For a quick overview, see the [README](./README.md). This document covers everyt
|
|
|
53
53
|
- [Azure DevOps](#azure-devops)
|
|
54
54
|
- [GitLab CI](#gitlab-ci)
|
|
55
55
|
- [Generic Pipelines](#generic-pipelines)
|
|
56
|
-
- [
|
|
56
|
+
- [Vibgrate Cloud Upload](#vibgrate-cloud-upload)
|
|
57
57
|
- [DSN Tokens](#dsn-tokens)
|
|
58
58
|
- [Data Residency](#data-residency)
|
|
59
59
|
- [Privacy & Security](#privacy--security)
|
|
@@ -73,7 +73,7 @@ Vibgrate recursively scans your repository for `package.json` (Node/TypeScript),
|
|
|
73
73
|
4. **Generates** a deterministic Upgrade Drift Score (0–100)
|
|
74
74
|
5. **Produces** findings, a full JSON artifact, and optional SARIF output
|
|
75
75
|
|
|
76
|
-
Core drift analysis does not execute source code.
|
|
76
|
+
Core drift analysis does not execute source code. Vibgrate Cloud upload remains optional.
|
|
77
77
|
|
|
78
78
|
---
|
|
79
79
|
|
|
@@ -86,8 +86,8 @@ Most teams adopt Vibgrate in two steps:
|
|
|
86
86
|
|
|
87
87
|
| Mode | Benefits | Typical command |
|
|
88
88
|
| ------------------ | --------------------------------------------------------------------------- | --------------------------------------------------------- |
|
|
89
|
-
| One-off scan | Fast snapshot of current upgrade debt, useful for audits and planning | `npx @vibgrate/cli scan
|
|
90
|
-
| CI-integrated scan | Continuous governance with automated failure thresholds and SARIF surfacing | `npx @vibgrate/cli scan
|
|
89
|
+
| One-off scan | Fast snapshot of current upgrade debt, useful for audits and planning | `npx @vibgrate/cli scan` |
|
|
90
|
+
| CI-integrated scan | Continuous governance with automated failure thresholds and SARIF surfacing | `npx @vibgrate/cli scan --format sarif --fail-on error` |
|
|
91
91
|
|
|
92
92
|
In practice, one-off scans tell you where you are today; CI keeps you from drifting back tomorrow.
|
|
93
93
|
|
|
@@ -117,13 +117,13 @@ Example:
|
|
|
117
117
|
|
|
118
118
|
```bash
|
|
119
119
|
# Step 1: first scan
|
|
120
|
-
vibgrate scan
|
|
120
|
+
vibgrate scan
|
|
121
121
|
|
|
122
122
|
# Step 2: baseline
|
|
123
|
-
vibgrate baseline
|
|
123
|
+
vibgrate baseline
|
|
124
124
|
|
|
125
125
|
# Step 3: policy in CI
|
|
126
|
-
vibgrate scan
|
|
126
|
+
vibgrate scan --baseline .vibgrate/baseline.json --drift-budget 40 --drift-worsening 5 --fail-on error
|
|
127
127
|
|
|
128
128
|
# Step 4: produce report
|
|
129
129
|
vibgrate report --in .vibgrate/scan_result.json --format md
|
|
@@ -176,7 +176,7 @@ vibgrate scan [path] [--format text|json|sarif|md] [--out <file>] [--fail-on war
|
|
|
176
176
|
| `--concurrency <n>` | `8` | Max concurrent npm registry calls |
|
|
177
177
|
| `--drift-budget <score>` | — | Fitness gate: fail if drift score is above this budget |
|
|
178
178
|
| `--drift-worsening <percent>` | — | Fitness gate: fail if drift worsens by more than % vs baseline |
|
|
179
|
-
| `--push` | — | Upload scan artifact to
|
|
179
|
+
| `--push` | — | Upload scan artifact to Vibgrate Cloud after a successful scan |
|
|
180
180
|
| `--dsn <dsn>` | `VIBGRATE_DSN` env | DSN used for `--push` authentication |
|
|
181
181
|
| `--region <region>` | — | Override data residency (`us`, `eu`) during push |
|
|
182
182
|
| `--strict` | — | Fail scan command if push fails |
|
|
@@ -198,7 +198,7 @@ document is the predicate body for the `https://vibgrate.com/attestations/hcs/v0
|
|
|
198
198
|
in-toto attestation, ready to bind to a published artifact:
|
|
199
199
|
|
|
200
200
|
```bash
|
|
201
|
-
vibgrate scan
|
|
201
|
+
vibgrate scan --emit-facts > vibgrate-facts.json
|
|
202
202
|
cosign attest --yes \
|
|
203
203
|
--type https://vibgrate.com/attestations/hcs/v0.5 \
|
|
204
204
|
--predicate vibgrate-facts.json "$IMAGE_REF"
|
|
@@ -215,10 +215,10 @@ Use `--exclude` (alias `-e`) to skip directories or files from the scan. Values
|
|
|
215
215
|
|
|
216
216
|
```bash
|
|
217
217
|
# Repeat the flag
|
|
218
|
-
vibgrate scan
|
|
218
|
+
vibgrate scan --exclude "legacy/**" --exclude "vendor/**"
|
|
219
219
|
|
|
220
220
|
# Or list multiple patterns in one flag (comma- or semicolon-separated)
|
|
221
|
-
vibgrate scan
|
|
221
|
+
vibgrate scan --exclude "legacy/**,vendor/**;**/fixtures/**"
|
|
222
222
|
```
|
|
223
223
|
|
|
224
224
|
CLI excludes are **additive**: they are merged (and de-duplicated) with any `exclude` patterns from your [config file](#configuration), so command-line excludes never replace your committed defaults.
|
|
@@ -239,19 +239,19 @@ Examples:
|
|
|
239
239
|
|
|
240
240
|
```bash
|
|
241
241
|
# Standard text scan
|
|
242
|
-
vibgrate scan
|
|
242
|
+
vibgrate scan
|
|
243
243
|
|
|
244
244
|
# JSON output for automation
|
|
245
|
-
vibgrate scan
|
|
245
|
+
vibgrate scan --format json --out scan.json
|
|
246
246
|
|
|
247
247
|
# Skip vendored and generated code
|
|
248
|
-
vibgrate scan
|
|
248
|
+
vibgrate scan --exclude "vendor/**,**/*.generated.ts;dist/**"
|
|
249
249
|
|
|
250
250
|
# CI gate with baseline regression protection
|
|
251
|
-
vibgrate scan
|
|
251
|
+
vibgrate scan --baseline .vibgrate/baseline.json --drift-budget 40 --drift-worsening 5 --fail-on error
|
|
252
252
|
|
|
253
253
|
# Upload result in the same command
|
|
254
|
-
vibgrate scan
|
|
254
|
+
vibgrate scan --push --strict
|
|
255
255
|
```
|
|
256
256
|
|
|
257
257
|
Expected results:
|
|
@@ -341,7 +341,7 @@ See [`docs/SIGNING-AND-PROVENANCE.md`](../../docs/SIGNING-AND-PROVENANCE.md).
|
|
|
341
341
|
|
|
342
342
|
### vibgrate push
|
|
343
343
|
|
|
344
|
-
Upload scan results to the Vibgrate
|
|
344
|
+
Upload scan results to the Vibgrate Cloud API.
|
|
345
345
|
|
|
346
346
|
```bash
|
|
347
347
|
vibgrate push [--dsn <dsn>] [--file <file>] [--region <region>] [--strict]
|
|
@@ -405,7 +405,7 @@ Recommended workflow:
|
|
|
405
405
|
|
|
406
406
|
1. Create baseline once on main branch:
|
|
407
407
|
```bash
|
|
408
|
-
vibgrate baseline
|
|
408
|
+
vibgrate baseline
|
|
409
409
|
```
|
|
410
410
|
2. In CI, run scan with comparison and gates:
|
|
411
411
|
```bash
|
|
@@ -413,7 +413,7 @@ Recommended workflow:
|
|
|
413
413
|
```
|
|
414
414
|
3. When planned upgrades land, refresh baseline:
|
|
415
415
|
```bash
|
|
416
|
-
vibgrate baseline
|
|
416
|
+
vibgrate baseline
|
|
417
417
|
```
|
|
418
418
|
|
|
419
419
|
This makes drift a formal quality gate (fitness function), not just reporting.
|
|
@@ -709,14 +709,14 @@ Use the maintained templates in this package for copy-paste setup:
|
|
|
709
709
|
```yaml
|
|
710
710
|
steps:
|
|
711
711
|
- name: Vibgrate Scan
|
|
712
|
-
run: npx @vibgrate/cli scan
|
|
712
|
+
run: npx @vibgrate/cli scan --format sarif --out vibgrate.sarif --fail-on error
|
|
713
713
|
|
|
714
714
|
- name: Upload SARIF
|
|
715
715
|
uses: github/codeql-action/upload-sarif@v3
|
|
716
716
|
with:
|
|
717
717
|
sarif_file: vibgrate.sarif
|
|
718
718
|
|
|
719
|
-
# Optional: push metrics to
|
|
719
|
+
# Optional: push metrics to Vibgrate Cloud
|
|
720
720
|
- name: Push Vibgrate Metrics
|
|
721
721
|
env:
|
|
722
722
|
VIBGRATE_DSN: ${{ secrets.VIBGRATE_DSN }}
|
|
@@ -727,7 +727,7 @@ steps:
|
|
|
727
727
|
|
|
728
728
|
```yaml
|
|
729
729
|
steps:
|
|
730
|
-
- script: npx @vibgrate/cli scan
|
|
730
|
+
- script: npx @vibgrate/cli scan --format sarif --out vibgrate.sarif --fail-on error
|
|
731
731
|
displayName: Vibgrate Scan
|
|
732
732
|
|
|
733
733
|
- task: PublishBuildArtifacts@1
|
|
@@ -741,7 +741,7 @@ steps:
|
|
|
741
741
|
```yaml
|
|
742
742
|
vibgrate:
|
|
743
743
|
script:
|
|
744
|
-
- npx @vibgrate/cli scan
|
|
744
|
+
- npx @vibgrate/cli scan --format sarif --out vibgrate.sarif --fail-on error
|
|
745
745
|
artifacts:
|
|
746
746
|
reports:
|
|
747
747
|
sast: vibgrate.sarif
|
|
@@ -758,7 +758,7 @@ Vibgrate works in any CI environment. The CLI:
|
|
|
758
758
|
|
|
759
759
|
---
|
|
760
760
|
|
|
761
|
-
##
|
|
761
|
+
## Vibgrate Cloud Upload
|
|
762
762
|
|
|
763
763
|
### DSN Tokens
|
|
764
764
|
|
package/README.md
CHANGED
|
@@ -54,14 +54,14 @@ Recommended rollout: start with a one-off scan now, then add Vibgrate to CI this
|
|
|
54
54
|
Run instantly (no install):
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
|
-
npx @vibgrate/cli scan
|
|
57
|
+
npx @vibgrate/cli scan
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
Or install locally:
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
63
|
npm install -D @vibgrate/cli
|
|
64
|
-
npx vibgrate scan
|
|
64
|
+
npx vibgrate scan
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
Add an npm script:
|
|
@@ -69,7 +69,7 @@ Add an npm script:
|
|
|
69
69
|
```json
|
|
70
70
|
{
|
|
71
71
|
"scripts": {
|
|
72
|
-
"drift": "vibgrate scan
|
|
72
|
+
"drift": "vibgrate scan"
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
```
|
|
@@ -87,7 +87,7 @@ Set up Vibgrate for upgrade drift tracking:
|
|
|
87
87
|
1. Install: npm install -g @vibgrate/cli@latest
|
|
88
88
|
2. Create DSN: npx vibgrate dsn create --workspace new
|
|
89
89
|
3. Save DSN: echo 'export VIBGRATE_DSN="<dsn>"' >> ~/.zshrc && source ~/.zshrc
|
|
90
|
-
4. Scan: npx vibgrate scan
|
|
90
|
+
4. Scan: npx vibgrate scan --push
|
|
91
91
|
Then explain my drift score and top 3 upgrade priorities.
|
|
92
92
|
```
|
|
93
93
|
|
|
@@ -160,7 +160,7 @@ This keeps reports plain and actionable, so teams can go from scan output to bac
|
|
|
160
160
|
Take a baseline snapshot, then enforce dependency drift fitness functions in CI:
|
|
161
161
|
|
|
162
162
|
```bash
|
|
163
|
-
npx vibgrate baseline
|
|
163
|
+
npx vibgrate baseline
|
|
164
164
|
npx vibgrate scan --baseline .vibgrate/baseline.json --drift-worsening 5 --drift-budget 40
|
|
165
165
|
```
|
|
166
166
|
|
|
@@ -185,7 +185,7 @@ Vibgrate now supports explicit privacy controls:
|
|
|
185
185
|
Example:
|
|
186
186
|
|
|
187
187
|
```bash
|
|
188
|
-
vibgrate scan
|
|
188
|
+
vibgrate scan --offline --package-manifest ./package-versions.zip --max-privacy --format json --out scan.json
|
|
189
189
|
```
|
|
190
190
|
|
|
191
191
|
When offline mode runs without a package manifest, package freshness is marked as unknown and drift scoring is necessarily partial.
|
|
@@ -205,7 +205,7 @@ vibgrate dsn create --workspace <id|new> [--region us|eu] [--write <path>]
|
|
|
205
205
|
|
|
206
206
|
```bash
|
|
207
207
|
# 1) Scan current repo (text output)
|
|
208
|
-
npx @vibgrate/cli scan
|
|
208
|
+
npx @vibgrate/cli scan
|
|
209
209
|
```
|
|
210
210
|
|
|
211
211
|
Expected result:
|
|
@@ -216,7 +216,7 @@ Expected result:
|
|
|
216
216
|
|
|
217
217
|
```bash
|
|
218
218
|
# 2) Scan with CI gating
|
|
219
|
-
npx @vibgrate/cli scan
|
|
219
|
+
npx @vibgrate/cli scan --fail-on error --drift-budget 40
|
|
220
220
|
```
|
|
221
221
|
|
|
222
222
|
Expected result:
|
|
@@ -228,7 +228,7 @@ Expected result:
|
|
|
228
228
|
# 3) Scan while excluding vendored / generated paths
|
|
229
229
|
# --exclude is repeatable and accepts comma/semicolon-separated globs;
|
|
230
230
|
# patterns are merged with `exclude` from vibgrate.config.*
|
|
231
|
-
npx @vibgrate/cli scan
|
|
231
|
+
npx @vibgrate/cli scan --exclude "legacy/**,vendor/**" --exclude "**/*.generated.ts"
|
|
232
232
|
```
|
|
233
233
|
|
|
234
234
|
Expected result:
|
|
@@ -238,7 +238,7 @@ Expected result:
|
|
|
238
238
|
|
|
239
239
|
```bash
|
|
240
240
|
# 4) Offline scan using local package-version bundle
|
|
241
|
-
npx @vibgrate/cli scan
|
|
241
|
+
npx @vibgrate/cli scan --offline --package-manifest ./latest-packages.zip --format json --out scan.json
|
|
242
242
|
```
|
|
243
243
|
|
|
244
244
|
Expected result:
|
|
@@ -262,14 +262,14 @@ Common usage:
|
|
|
262
262
|
|
|
263
263
|
```bash
|
|
264
264
|
# Standard scan
|
|
265
|
-
npx @vibgrate/cli scan
|
|
265
|
+
npx @vibgrate/cli scan
|
|
266
266
|
|
|
267
267
|
# CI-ready SARIF output
|
|
268
|
-
npx @vibgrate/cli scan
|
|
268
|
+
npx @vibgrate/cli scan --format sarif --out vibgrate.sarif --fail-on error
|
|
269
269
|
|
|
270
270
|
# Baseline and compare drift deltas over time
|
|
271
|
-
npx @vibgrate/cli baseline
|
|
272
|
-
npx @vibgrate/cli scan
|
|
271
|
+
npx @vibgrate/cli baseline
|
|
272
|
+
npx @vibgrate/cli scan --baseline .vibgrate/baseline.json
|
|
273
273
|
```
|
|
274
274
|
|
|
275
275
|
---
|
|
@@ -288,7 +288,7 @@ Use the maintained templates in this package for copy-paste setup:
|
|
|
288
288
|
- name: Vibgrate scan
|
|
289
289
|
env:
|
|
290
290
|
VIBGRATE_DSN: ${{ secrets.VIBGRATE_DSN }}
|
|
291
|
-
run: npx @vibgrate/cli scan
|
|
291
|
+
run: npx @vibgrate/cli scan --push --format sarif --out vibgrate.sarif --fail-on error
|
|
292
292
|
|
|
293
293
|
- name: Upload SARIF
|
|
294
294
|
if: always()
|
|
@@ -300,7 +300,7 @@ Use the maintained templates in this package for copy-paste setup:
|
|
|
300
300
|
### Azure DevOps
|
|
301
301
|
|
|
302
302
|
```yaml
|
|
303
|
-
- script: npx @vibgrate/cli scan
|
|
303
|
+
- script: npx @vibgrate/cli scan --format sarif --out vibgrate.sarif --fail-on error
|
|
304
304
|
displayName: Vibgrate scan
|
|
305
305
|
```
|
|
306
306
|
|
|
@@ -310,7 +310,7 @@ Use the maintained templates in this package for copy-paste setup:
|
|
|
310
310
|
vibgrate:
|
|
311
311
|
image: node:20
|
|
312
312
|
script:
|
|
313
|
-
- npx @vibgrate/cli scan
|
|
313
|
+
- npx @vibgrate/cli scan --push --fail-on error
|
|
314
314
|
```
|
|
315
315
|
|
|
316
316
|
---
|
|
@@ -323,7 +323,7 @@ If you want trend analysis across runs/repos, push scan artifacts with a DSN:
|
|
|
323
323
|
|
|
324
324
|
```bash
|
|
325
325
|
VIBGRATE_DSN="vibgrate+https://<key_id>:<secret>@us.ingest.vibgrate.com/<workspace_id>" \
|
|
326
|
-
npx @vibgrate/cli scan
|
|
326
|
+
npx @vibgrate/cli scan --push
|
|
327
327
|
```
|
|
328
328
|
|
|
329
329
|
You can also upload an existing artifact:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{h as baselineCommand,g as runBaseline}from'./chunk-ROPIO52N.js';import'./chunk-RKH4N26K.js';import'./chunk-MKDRULJ6.js';import'./chunk-XTHPCEME.js';import'./chunk-EK7ODJWE.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {na as na$1,ua as ua$1}from'./chunk-
|
|
1
|
+
import {na as na$1,ua as ua$1}from'./chunk-RKH4N26K.js';import {a}from'./chunk-MKDRULJ6.js';import {j as j$1,n,i,o,p,d}from'./chunk-XTHPCEME.js';import {b}from'./chunk-EK7ODJWE.js';import*as I from'path';import*as V from'fs/promises';import*as w from'typescript';var Y={healthyMax:33,elevatedMax:66},D={healthy:"#3FB0A4",elevated:"#D9A441",critical:"#D0463B"},na={healthy:"Healthy",elevated:"Elevated",critical:"Critical"};function X(e){return e==null||Number.isNaN(e)?null:e<=Y.healthyMax?"healthy":e<=Y.elevatedMax?"elevated":"critical"}function H(e){let a=X(e);return a?D[a]:"#6B7785"}function le(e){let a=X(e);return a?na[a]:"Not scored"}function de(e){return e==null||Number.isNaN(e)||e===0?"flat":e<0?"good":"bad"}function ce(e){if(e==null||Number.isNaN(e))return "\u2014";if(e===0)return "\xB1 0";let a=Math.abs(e);return e>0?`\u25B2 +${a}`:`\u25BC \u2212${a}`}var Ce=8;function ue(e){return `Q${Math.floor(e.getUTCMonth()/3)+1} ${e.getUTCFullYear()}`}function sa(e){return e<=30?30:e<=60?60:e<=90?90:180}function oa(e){return e>0?"breached":e>=-5?"at-risk":"on-track"}function ra(e){let a=e.kpis;if(!a||a.estateDriftScore==null)return "No scan data yet \u2014 run a scan to establish a baseline.";let s=e.budgets.filter(r=>r.current-r.budget>0).length,n=[],t=de(a.driftDelta);return t==="good"&&a.driftDelta!=null?n.push(`Portfolio drift improved ${Math.abs(a.driftDelta)} pts`):t==="bad"&&a.driftDelta!=null?n.push(`Portfolio drift rose ${a.driftDelta} pts`):n.push(`Portfolio drift held at ${a.estateDriftScore}`),s>0?n.push(`${s} ${s===1?"scope is":"scopes are"} over budget`):n.push("all scopes within budget"),`${n.join("; ")}.`}function ia(e){if(e.length<2)return null;let a=e[e.length-1],s=e[e.length-2],n=a.score-s.score;if(n<=0)return "At current pace, portfolio drift is flat or improving \u2014 no band crossing forecast.";let t=Y.healthyMax+1;if(a.score>=t)return "Portfolio is already in the amber band \u2014 prioritise remediation to return to healthy.";let r=Math.ceil((t-a.score)/n);return `At current pace, portfolio crosses into amber in ~${r} period${r===1?"":"s"}.`}function la(e){let a=e.generatedAt??new Date,s=e.period??ue(a),n=e.kpis,t=ca(e.trend),r=e.topRisks.slice(0,5).map((i,d)=>({rank:d+1,label:i.label,scope:i.scope,score:i.score,delta:i.delta??null,driver:i.driver,owner:i.owner})),o=e.budgets.map(i=>{let d=i.current-i.budget;return {...i,variance:d,state:oa(d)}}).sort((i,d)=>d.variance-i.variance),c=e.horizon.filter(i=>i.daysRemaining>=0).map(i=>({...i,lane:sa(i.daysRemaining)})).sort((i,d)=>i.daysRemaining-d.daysRemaining),u=o.filter(i=>i.state==="breached").length,p=e.benchmark?{...e.benchmark,anonymitySatisfied:e.benchmark.cohortSize>=Ce}:null;return {instanceId:`${s}-${(e.asOf??a.toISOString()).slice(0,10)}`,org:e.org,period:s,preparedFor:e.preparedFor,asOf:e.asOf??a.toISOString(),generatedAt:a.toISOString(),confidentiality:"Confidential",headline:{score:n?.estateDriftScore??null,priorScore:n?.priorScore??null,delta:n?.driftDelta??null,breachCount:u,verdict:ra(e)},commentary:e.commentary,trajectory:t,forecastNote:ia(t),risks:r,budgets:o,horizon:c,benchmark:p,roi:e.roi}}function ca(e){if(e.length===0)return [];let a=[...e].sort((t,r)=>t.day.localeCompare(r.day)),s=[],n=null;for(let t of a){let r=Math.round(t.avg_score),o=n==null?0:r-n;s.push({period:t.day,score:r,added:o>0?o:0,remediated:o<0?-o:0}),n=r;}return s}var N={navy:"#182346",teal:"#3FB0A4",ink:"#0E2330",paper:"#F4F6F5"};function x(e){return e==null?"":String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function we(e,a){if(!e)return "\u2014";try{return new Date(e).toLocaleDateString(a,{year:"numeric",month:"short",day:"numeric"})}catch{return x(e)}}function Re(e,a){if(!e)return "\u2014";try{return new Date(e).toLocaleString(a,{year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit",timeZoneName:"short"})}catch{return x(e)}}function pa(e){if(e.length<2)return '<div class="chart-empty">Baseline established \u2014 trend available next cycle.</div>';let t=e.map(b=>b.score),r=Math.min(...t,0),c=Math.max(...t,100)-r||1,u=592/(e.length-1),p=b=>24+b*u,i=b=>156-(b-r)/c*132,d=e.map((b,C)=>`${C===0?"M":"L"}${p(C).toFixed(1)},${i(b.score).toFixed(1)}`).join(" "),g=e[e.length-1],m=e[e.length-2],y=g.score-m.score,l={x:p(e.length-1)+u,v:g.score+y},f={x:l.x+u,v:g.score+y*2},h=`M${p(e.length-1).toFixed(1)},${i(g.score).toFixed(1)} L${l.x.toFixed(1)},${i(l.v).toFixed(1)} L${f.x.toFixed(1)},${i(f.v).toFixed(1)}`,k=i(34),v=e.map((b,C)=>`<circle cx="${p(C).toFixed(1)}" cy="${i(b.score).toFixed(1)}" r="2.5" fill="${H(b.score)}" />`).join("");return `<svg viewBox="0 0 ${640+u*2} 180" width="100%" role="img" aria-label="DriftScore trend with forecast">
|
|
2
2
|
<line x1="24" y1="${k.toFixed(1)}" x2="${640+u*2-24}" y2="${k.toFixed(1)}" stroke="${D.elevated}" stroke-width="1" stroke-dasharray="2 3" opacity="0.6" />
|
|
3
3
|
<path d="${d}" fill="none" stroke="${N.teal}" stroke-width="2.5" />
|
|
4
4
|
<path d="${h}" fill="none" stroke="${N.teal}" stroke-width="2" stroke-dasharray="4 4" opacity="0.7" />
|