certainty-units 0.2.0 → 0.2.2

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/NOTICE ADDED
@@ -0,0 +1,13 @@
1
+ certainty-units
2
+ Copyright (c) 2026 Nhan Nguyen
3
+
4
+ This software is licensed under the MIT License (see LICENSE).
5
+
6
+ "Propozel" and "Certainty Units" are trademarks of their respective owner.
7
+ The MIT License covers the source code only; it does not grant any rights
8
+ to use these names, the Propozel logo, or other brand assets. Forks and
9
+ derivative works must use their own product names and branding.
10
+
11
+ The Certainty Units scoring methodology is documented at
12
+ https://propozel.com and may be freely implemented; this notice restricts
13
+ branding, not ideas.
package/README.md CHANGED
@@ -177,7 +177,8 @@ linear:
177
177
 
178
178
  ## License
179
179
 
180
- MIT — free to use, modify, and embed.
180
+ MIT — free to use, modify, and embed. "Propozel" and "Certainty Units" are
181
+ trademarks; the license covers the code, not the branding (see [NOTICE](NOTICE)).
181
182
 
182
183
  ---
183
184
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "certainty-units",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Certainty scoring for project work items — connects to Linear, Jira, Notion, GitHub Issues",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,12 +10,22 @@
10
10
  "src",
11
11
  "examples",
12
12
  "LICENSE",
13
+ "NOTICE",
13
14
  "README.md"
14
15
  ],
15
16
  "scripts": {
16
17
  "test": "node --test"
17
18
  },
18
- "keywords": ["project-management", "certainty", "linear", "jira", "notion", "github", "agile", "hill-chart"],
19
+ "keywords": [
20
+ "project-management",
21
+ "certainty",
22
+ "linear",
23
+ "jira",
24
+ "notion",
25
+ "github",
26
+ "agile",
27
+ "hill-chart"
28
+ ],
19
29
  "license": "MIT",
20
30
  "author": "Nhan Nguyen <nhan@naucode.com>",
21
31
  "repository": {
package/src/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { program } from 'commander'
3
- import { writeFileSync } from 'fs'
3
+ import { writeFileSync, readFileSync } from 'fs'
4
4
  import chalk from 'chalk'
5
5
  import { loadConfig, CONFIG_TEMPLATE } from './config.js'
6
6
  import { computeCertaintyScore, computeScoreBreakdown, biggestGaps } from './certainty.js'
@@ -20,10 +20,14 @@ async function loadAdapter(source) {
20
20
  return import(path)
21
21
  }
22
22
 
23
+ const { version } = JSON.parse(
24
+ readFileSync(new URL('../package.json', import.meta.url), 'utf8')
25
+ )
26
+
23
27
  program
24
28
  .name('certainty-units')
25
29
  .description('Certainty scoring for project work items')
26
- .version('0.1.0')
30
+ .version(version)
27
31
 
28
32
  program
29
33
  .command('init')
package/src/report.js CHANGED
@@ -83,6 +83,72 @@ function needsAttention(items) {
83
83
  </div>`
84
84
  }
85
85
 
86
+ // Hill chart — the Propozel visualization, as static inline SVG.
87
+ // Certainty maps onto a sine hill: 0–50 uphill (discovery), 50–100 downhill
88
+ // (execution). Items stack in 5-point buckets; hover a dot for its title.
89
+ export function generateHillChartSVG(items, metrics) {
90
+ const W = 920, H = 320
91
+ const PAD = { top: 56, right: 36, bottom: 64, left: 36 }
92
+ const chartW = W - PAD.left - PAD.right
93
+ const hillH = H - PAD.top - PAD.bottom
94
+ const MAX_STACK = 8
95
+
96
+ const hillX = s => PAD.left + (s / 100) * chartW
97
+ const hillY = s => H - PAD.bottom - Math.sin((s / 100) * Math.PI) * hillH * 0.8
98
+
99
+ // hill outline + fill
100
+ const pts = []
101
+ for (let x = 0; x <= 100; x += 2) pts.push(`${hillX(x).toFixed(1)},${hillY(x).toFixed(1)}`)
102
+ const curve = pts.join(' ')
103
+ const baseline = H - PAD.bottom
104
+
105
+ // bucket items by 5-point score, stack dots upward
106
+ const buckets = {}
107
+ for (const item of items) {
108
+ const bucket = Math.round((item.certainty_score ?? 0) / 5) * 5
109
+ ;(buckets[bucket] ??= []).push(item)
110
+ }
111
+
112
+ const dots = []
113
+ for (const [bucket, group] of Object.entries(buckets)) {
114
+ const score = Number(bucket)
115
+ const x = hillX(score).toFixed(1)
116
+ group.slice(0, MAX_STACK).forEach((item, i) => {
117
+ const y = (hillY(score) - 12 - i * 13).toFixed(1)
118
+ const s = item.certainty_score ?? 0
119
+ dots.push(
120
+ `<circle cx="${x}" cy="${y}" r="5" fill="${levelColor(s)}" stroke="#fff" stroke-width="1.5">` +
121
+ `<title>${esc(item.external_id)} · ${esc(item.title)} — ${s}% (${certaintyLevel(s)})</title></circle>`
122
+ )
123
+ })
124
+ if (group.length > MAX_STACK) {
125
+ const y = (hillY(score) - 12 - MAX_STACK * 13).toFixed(1)
126
+ dots.push(`<text x="${x}" y="${y}" text-anchor="middle" font-size="10" fill="#6f6f6f">+${group.length - MAX_STACK}</text>`)
127
+ }
128
+ }
129
+
130
+ const peakX = hillX(50).toFixed(1)
131
+ const legend = [
132
+ ['high', '#24a148'], ['medium', '#0f62fe'], ['low', '#f1c21b'], ['uncertain', '#da1e28'],
133
+ ].map(([label, color], i) =>
134
+ `<circle cx="${PAD.left + 8 + i * 92}" cy="18" r="4" fill="${color}"/>` +
135
+ `<text x="${PAD.left + 17 + i * 92}" y="21" font-size="10" fill="#525252">${label}</text>`
136
+ ).join('')
137
+
138
+ return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Hill chart" style="width:100%;height:auto;background:#f4f4f4;border:1px solid #e0e0e0">
139
+ <style>text { font-family: 'IBM Plex Sans', system-ui, sans-serif }</style>
140
+ ${legend}
141
+ <polygon points="${PAD.left},${baseline} ${curve} ${PAD.left + chartW},${baseline}" fill="rgba(0,0,0,0.03)"/>
142
+ <polyline points="${curve}" fill="none" stroke="#c6c6c6" stroke-width="2"/>
143
+ <line x1="${peakX}" y1="${PAD.top - 8}" x2="${peakX}" y2="${baseline}" stroke="#c6c6c6" stroke-width="1" stroke-dasharray="4 4"/>
144
+ <text x="${peakX}" y="${PAD.top - 16}" text-anchor="middle" font-size="11" fill="#6f6f6f">&#9650; Peak certainty</text>
145
+ <text x="${hillX(25).toFixed(1)}" y="${baseline + 22}" text-anchor="middle" font-size="11" fill="#6f6f6f">Discovery — figuring it out</text>
146
+ <text x="${hillX(75).toFixed(1)}" y="${baseline + 22}" text-anchor="middle" font-size="11" fill="#6f6f6f">Execution — getting it done</text>
147
+ <text x="${PAD.left}" y="${H - 14}" font-size="10" fill="#525252" font-family="'IBM Plex Mono', monospace">${metrics.uphill} uphill &#183; ${metrics.downhill} downhill &#183; ${metrics.avgCertaintyScore}% avg certainty &#183; ${metrics.velocity} CU delivered</text>
148
+ ${dots.join('\n ')}
149
+ </svg>`
150
+ }
151
+
86
152
  export function generateHTML(items, projectName = 'Project') {
87
153
  const metrics = computeCUMetrics(items)
88
154
  const sorted = [...items].sort((a, b) => (b.certainty_score ?? 0) - (a.certainty_score ?? 0))
@@ -128,6 +194,14 @@ export function generateHTML(items, projectName = 'Project') {
128
194
  ${metricBox('Downhill', metrics.downhill, 'execution zone')}
129
195
  </div>
130
196
 
197
+ <h2>Hill chart</h2>
198
+ <p style="font-size:12px;color:#525252;margin:-8px 0 12px">Left of the peak: still figuring it out. Right: executing with confidence. Hover a dot for the item.</p>
199
+ ${generateHillChartSVG(items, metrics)}
200
+ <p style="font-size:11px;color:#8d8d8d;margin:8px 0 32px">
201
+ Static snapshot of the <a href="https://propozel.com" target="_blank" style="color:#8d8d8d">Certainty Units</a> hill chart.
202
+ In <a href="https://propozel.com" target="_blank" style="color:#0f62fe;text-decoration:none">Propozel</a>, this chart is live &mdash; items move as your team validates assumptions, and stakeholders watch it update.
203
+ </p>
204
+
131
205
  ${needsAttention(items)}
132
206
 
133
207
  <h2>Items by certainty score</h2>