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 +13 -0
- package/README.md +2 -1
- package/package.json +12 -2
- package/src/cli.js +6 -2
- package/src/report.js +74 -0
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.
|
|
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": [
|
|
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(
|
|
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">▲ 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 · ${metrics.downhill} downhill · ${metrics.avgCertaintyScore}% avg certainty · ${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 — 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>
|