@vibgrate/cli 2026.625.1 → 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 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
- - [Dashboard Upload](#dashboard-upload)
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. Dashboard upload remains optional.
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 . --format sarif --fail-on error` |
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 . --baseline .vibgrate/baseline.json --drift-budget 40 --drift-worsening 5 --fail-on error
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 dashboard after a successful scan |
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 . --emit-facts > vibgrate-facts.json
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 . --exclude "legacy/**" --exclude "vendor/**"
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 . --exclude "legacy/**,vendor/**;**/fixtures/**"
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 . --format json --out scan.json
245
+ vibgrate scan --format json --out scan.json
246
246
 
247
247
  # Skip vendored and generated code
248
- vibgrate scan . --exclude "vendor/**,**/*.generated.ts;dist/**"
248
+ vibgrate scan --exclude "vendor/**,**/*.generated.ts;dist/**"
249
249
 
250
250
  # CI gate with baseline regression protection
251
- vibgrate scan . --baseline .vibgrate/baseline.json --drift-budget 40 --drift-worsening 5 --fail-on error
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 . --push --strict
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 dashboard API.
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 . --format sarif --out vibgrate.sarif --fail-on error
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 dashboard
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 . --format sarif --out vibgrate.sarif --fail-on error
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 . --format sarif --out vibgrate.sarif --fail-on error
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
- ## Dashboard Upload
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 . --push
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 . --offline --package-manifest ./package-versions.zip --max-privacy --format json --out scan.json
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 . --fail-on error --drift-budget 40
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 . --exclude "legacy/**,vendor/**" --exclude "**/*.generated.ts"
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 . --offline --package-manifest ./latest-packages.zip --format json --out scan.json
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 . --format sarif --out vibgrate.sarif --fail-on error
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 . --baseline .vibgrate/baseline.json
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 . --push --format sarif --out vibgrate.sarif --fail-on error
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 . --format sarif --out vibgrate.sarif --fail-on error
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 . --push --fail-on error
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 . --push
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-I65B3ZRL.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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}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">
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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}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" />