@prism-d1/cli 1.0.99 → 1.2.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.
- package/dist/assets/github-workflows/prism-agent-eval.yml +2 -1
- package/dist/assets/github-workflows/prism-eval-gate-kiro.yml +11 -1
- package/dist/assets/gitlab-workflows/prism-eval-gate.yml +8 -0
- package/dist/assets/infra/lib/constructs/otel-collector-construct.ts +312 -0
- package/dist/assets/infra/lib/dashboard-stack.ts +74 -45
- package/dist/assets/infra/lib/lambda/otel-receiver.ts +324 -12
- package/dist/assets/infra/lib/lambda/pre-token-generation.ts +79 -0
- package/dist/assets/infra/lib/lambda/pricing/litellm-snapshot.json +3769 -0
- package/dist/assets/infra/lib/lambda/pricing/price-source.ts +56 -0
- package/dist/assets/infra/lib/lambda/pricing/price-table.ts +240 -0
- package/dist/assets/infra/lib/lambda/pricing/prices-key.ts +6 -0
- package/dist/assets/infra/lib/lambda/pricing/reprice.ts +108 -0
- package/dist/assets/infra/lib/lambda/pricing-refresh.ts +352 -0
- package/dist/assets/infra/lib/lambda/velocity-widget.ts +397 -1
- package/dist/assets/infra/lib/metrics-pipeline-stack.ts +13 -0
- package/dist/assets/infra/package.json +5 -2
- package/dist/assets/metric-hooks/prepare-commit-msg +3 -1
- package/dist/src/codewatch/config.d.ts +17 -0
- package/dist/src/codewatch/config.d.ts.map +1 -0
- package/dist/src/codewatch/config.js +40 -0
- package/dist/src/codewatch/config.js.map +1 -0
- package/dist/src/codewatch/gitleaks.d.ts +49 -0
- package/dist/src/codewatch/gitleaks.d.ts.map +1 -0
- package/dist/src/codewatch/gitleaks.js +160 -0
- package/dist/src/codewatch/gitleaks.js.map +1 -0
- package/dist/src/codewatch/globalHooks.d.ts +52 -0
- package/dist/src/codewatch/globalHooks.d.ts.map +1 -0
- package/dist/src/codewatch/globalHooks.js +235 -0
- package/dist/src/codewatch/globalHooks.js.map +1 -0
- package/dist/src/codewatch/hooks.d.ts +38 -0
- package/dist/src/codewatch/hooks.d.ts.map +1 -0
- package/dist/src/codewatch/hooks.js +176 -0
- package/dist/src/codewatch/hooks.js.map +1 -0
- package/dist/src/codewatch/prepush.d.ts +37 -0
- package/dist/src/codewatch/prepush.d.ts.map +1 -0
- package/dist/src/codewatch/prepush.js +231 -0
- package/dist/src/codewatch/prepush.js.map +1 -0
- package/dist/src/codewatch/results.d.ts +90 -0
- package/dist/src/codewatch/results.d.ts.map +1 -0
- package/dist/src/codewatch/results.js +121 -0
- package/dist/src/codewatch/results.js.map +1 -0
- package/dist/src/codewatch/reviewers/claude.d.ts +9 -0
- package/dist/src/codewatch/reviewers/claude.d.ts.map +1 -0
- package/dist/src/codewatch/reviewers/claude.js +48 -0
- package/dist/src/codewatch/reviewers/claude.js.map +1 -0
- package/dist/src/codewatch/reviewers/index.d.ts +39 -0
- package/dist/src/codewatch/reviewers/index.d.ts.map +1 -0
- package/dist/src/codewatch/reviewers/index.js +97 -0
- package/dist/src/codewatch/reviewers/index.js.map +1 -0
- package/dist/src/codewatch/reviewers/kiro.d.ts +14 -0
- package/dist/src/codewatch/reviewers/kiro.d.ts.map +1 -0
- package/dist/src/codewatch/reviewers/kiro.js +32 -0
- package/dist/src/codewatch/reviewers/kiro.js.map +1 -0
- package/dist/src/codewatch/reviewers/types.d.ts +56 -0
- package/dist/src/codewatch/reviewers/types.d.ts.map +1 -0
- package/dist/src/codewatch/reviewers/types.js +91 -0
- package/dist/src/codewatch/reviewers/types.js.map +1 -0
- package/dist/src/codewatch/rules.d.ts +12 -0
- package/dist/src/codewatch/rules.d.ts.map +1 -0
- package/dist/src/codewatch/rules.js +27 -0
- package/dist/src/codewatch/rules.js.map +1 -0
- package/dist/src/commands/_top/dashboard.d.ts +17 -0
- package/dist/src/commands/_top/dashboard.d.ts.map +1 -0
- package/dist/src/commands/_top/dashboard.js +76 -0
- package/dist/src/commands/_top/dashboard.js.map +1 -0
- package/dist/src/commands/bedrock-protection/scan-account.d.ts.map +1 -1
- package/dist/src/commands/bedrock-protection/scan-account.js +2 -1
- package/dist/src/commands/bedrock-protection/scan-account.js.map +1 -1
- package/dist/src/commands/bedrock-protection/scan-org.d.ts.map +1 -1
- package/dist/src/commands/bedrock-protection/scan-org.js +16 -4
- package/dist/src/commands/bedrock-protection/scan-org.js.map +1 -1
- package/dist/src/commands/bootstrapper/install-eval-harness.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/install-eval-harness.js +15 -48
- package/dist/src/commands/bootstrapper/install-eval-harness.js.map +1 -1
- package/dist/src/commands/bootstrapper/install-git-hooks.d.ts +13 -0
- package/dist/src/commands/bootstrapper/install-git-hooks.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/install-git-hooks.js +28 -147
- package/dist/src/commands/bootstrapper/install-git-hooks.js.map +1 -1
- package/dist/src/commands/bootstrapper/setup-otel-sync.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/setup-otel-sync.js +5 -39
- package/dist/src/commands/bootstrapper/setup-otel-sync.js.map +1 -1
- package/dist/src/commands/codewatch/pre-commit.d.ts +7 -0
- package/dist/src/commands/codewatch/pre-commit.d.ts.map +1 -0
- package/dist/src/commands/codewatch/pre-commit.js +30 -0
- package/dist/src/commands/codewatch/pre-commit.js.map +1 -0
- package/dist/src/commands/codewatch/pre-push.d.ts +16 -0
- package/dist/src/commands/codewatch/pre-push.d.ts.map +1 -0
- package/dist/src/commands/codewatch/pre-push.js +32 -0
- package/dist/src/commands/codewatch/pre-push.js.map +1 -0
- package/dist/src/commands/codewatch/report.d.ts +22 -0
- package/dist/src/commands/codewatch/report.d.ts.map +1 -0
- package/dist/src/commands/codewatch/report.js +64 -0
- package/dist/src/commands/codewatch/report.js.map +1 -0
- package/dist/src/commands/enable/codewatch.d.ts +15 -0
- package/dist/src/commands/enable/codewatch.d.ts.map +1 -0
- package/dist/src/commands/enable/codewatch.js +186 -0
- package/dist/src/commands/enable/codewatch.js.map +1 -0
- package/dist/src/commands/git/claude-session-context.d.ts.map +1 -1
- package/dist/src/commands/git/claude-session-context.js +2 -1
- package/dist/src/commands/git/claude-session-context.js.map +1 -1
- package/dist/src/commands/git/commit-trailers.js +1 -1
- package/dist/src/commands/git/commit-trailers.js.map +1 -1
- package/dist/src/commands/workshop/verify-setup.d.ts.map +1 -1
- package/dist/src/commands/workshop/verify-setup.js +5 -3
- package/dist/src/commands/workshop/verify-setup.js.map +1 -1
- package/dist/src/dashboard/codewatch.d.ts +106 -0
- package/dist/src/dashboard/codewatch.d.ts.map +1 -0
- package/dist/src/dashboard/codewatch.js +119 -0
- package/dist/src/dashboard/codewatch.js.map +1 -0
- package/dist/src/dashboard/commits.d.ts +96 -0
- package/dist/src/dashboard/commits.d.ts.map +1 -0
- package/dist/src/dashboard/commits.js +207 -0
- package/dist/src/dashboard/commits.js.map +1 -0
- package/dist/src/dashboard/page.d.ts +10 -0
- package/dist/src/dashboard/page.d.ts.map +1 -0
- package/dist/src/dashboard/page.js +315 -0
- package/dist/src/dashboard/page.js.map +1 -0
- package/dist/src/dashboard/payload.d.ts +111 -0
- package/dist/src/dashboard/payload.d.ts.map +1 -0
- package/dist/src/dashboard/payload.js +93 -0
- package/dist/src/dashboard/payload.js.map +1 -0
- package/dist/src/dashboard/report.d.ts +99 -0
- package/dist/src/dashboard/report.d.ts.map +1 -0
- package/dist/src/dashboard/report.js +300 -0
- package/dist/src/dashboard/report.js.map +1 -0
- package/dist/src/dashboard/repos.d.ts +58 -0
- package/dist/src/dashboard/repos.d.ts.map +1 -0
- package/dist/src/dashboard/repos.js +111 -0
- package/dist/src/dashboard/repos.js.map +1 -0
- package/dist/src/dashboard/server.d.ts +52 -0
- package/dist/src/dashboard/server.d.ts.map +1 -0
- package/dist/src/dashboard/server.js +163 -0
- package/dist/src/dashboard/server.js.map +1 -0
- package/dist/src/dashboard/setup.d.ts +52 -0
- package/dist/src/dashboard/setup.d.ts.map +1 -0
- package/dist/src/dashboard/setup.js +129 -0
- package/dist/src/dashboard/setup.js.map +1 -0
- package/dist/src/dashboard/usage.d.ts +43 -0
- package/dist/src/dashboard/usage.d.ts.map +1 -0
- package/dist/src/dashboard/usage.js +88 -0
- package/dist/src/dashboard/usage.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +40 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/utils/audit-bedrock.d.ts +1 -0
- package/dist/src/utils/audit-bedrock.d.ts.map +1 -1
- package/dist/src/utils/audit-bedrock.js +133 -0
- package/dist/src/utils/audit-bedrock.js.map +1 -1
- package/dist/src/utils/audit-org.d.ts +1 -0
- package/dist/src/utils/audit-org.d.ts.map +1 -1
- package/dist/src/utils/audit-org.js +155 -0
- package/dist/src/utils/audit-org.js.map +1 -1
- package/dist/src/utils/otel-sync-schedule.d.ts +22 -0
- package/dist/src/utils/otel-sync-schedule.d.ts.map +1 -0
- package/dist/src/utils/otel-sync-schedule.js +68 -0
- package/dist/src/utils/otel-sync-schedule.js.map +1 -0
- package/dist/src/utils/tool-check.d.ts +14 -0
- package/dist/src/utils/tool-check.d.ts.map +1 -0
- package/dist/src/utils/tool-check.js +42 -0
- package/dist/src/utils/tool-check.js.map +1 -0
- package/package.json +3 -2
|
@@ -27,6 +27,8 @@ const RECEIVER_FUNCTION = process.env.RECEIVER_FUNCTION || 'prism-d1-otel-receiv
|
|
|
27
27
|
interface WidgetEvent {
|
|
28
28
|
describe?: boolean;
|
|
29
29
|
view?: string;
|
|
30
|
+
/** Developer filter for views that support it; 'all' or unset = organization. */
|
|
31
|
+
user?: string;
|
|
30
32
|
widgetContext?: { timeRange?: { start: number; end: number }; theme?: string };
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -37,7 +39,8 @@ the attribution store. Full history, real event timestamps.
|
|
|
37
39
|
### Parameters
|
|
38
40
|
| Name | Type | Default | Description |
|
|
39
41
|
|------|------|---------|-------------|
|
|
40
|
-
| view | string | dora | dora, aidora, repos, eval, governance, agents, security, exec, exec-security, ciso-exposure, ciso-sla, ciso-risk, ciso-shiftleft, ciso-classes |
|
|
42
|
+
| view | string | dora | dora, aidora, repos, eval, governance, agents, security, exec, exec-security, ciso-exposure, ciso-sla, ciso-risk, ciso-shiftleft, ciso-classes, tool-usage-top, tool-usage-trend |
|
|
43
|
+
| user | string | all | Developer identity for tool-usage-* views |
|
|
41
44
|
|
|
42
45
|
The \`exec\` view additionally computes an **observed** PRISM level from
|
|
43
46
|
outcome metrics (capped at L4 — L5 requires an autonomy signal that no
|
|
@@ -1648,6 +1651,275 @@ async function renderMergeRateKpi(fromIso: string, toIso: string, p: Palette): P
|
|
|
1648
1651
|
`;
|
|
1649
1652
|
}
|
|
1650
1653
|
|
|
1654
|
+
// ---- View: cost-per-commit-kpi (single big-number KPI for $ / shipped commit) ----
|
|
1655
|
+
|
|
1656
|
+
/**
|
|
1657
|
+
* Replaces a CloudWatch `AICostUSD / MergedAICommits` ratio tile.
|
|
1658
|
+
*
|
|
1659
|
+
* Three things were wrong with the metric-math version, and all three are
|
|
1660
|
+
* properties of CloudWatch rather than of the expression:
|
|
1661
|
+
*
|
|
1662
|
+
* 1. CloudWatch rejects datapoints older than two weeks, so a backfill lands
|
|
1663
|
+
* partially outside the ingestion window. Measured on a live 30-day range,
|
|
1664
|
+
* the metric path saw $1,387.61 / 141 commits where the store held
|
|
1665
|
+
* $1,496.24 / 164 — both terms short, in the same direction.
|
|
1666
|
+
* 2. AICostUSD and MergedAICommits carry no user dimension, so the tile stayed
|
|
1667
|
+
* org-wide while the attribution-backed tile beside it scoped to the
|
|
1668
|
+
* dashboard's Developer variable. Same screen, near-identical labels, one
|
|
1669
|
+
* responding to the filter and one not.
|
|
1670
|
+
* 3. The metrics are timestamped at ingest, not at commit time.
|
|
1671
|
+
*
|
|
1672
|
+
* Reading the store fixes all three, and makes this tile agree with the
|
|
1673
|
+
* `$ / Shipped Commit` cells on Team Velocity and the Executive Readout, which
|
|
1674
|
+
* already read the same field.
|
|
1675
|
+
*/
|
|
1676
|
+
async function renderCostPerCommitKpi(fromIso: string, toIso: string, p: Palette): Promise<string> {
|
|
1677
|
+
let report: any;
|
|
1678
|
+
try {
|
|
1679
|
+
report = await fetchProductivity(fromIso, toIso);
|
|
1680
|
+
} catch {
|
|
1681
|
+
return emptyState('cost per shipped commit', 'Attribution store unreachable.', p);
|
|
1682
|
+
}
|
|
1683
|
+
const costPer = report?.totals?.ratios?.costPerShippedCommit ?? null;
|
|
1684
|
+
const cost = report?.totals?.usage?.costUsd ?? null;
|
|
1685
|
+
const mergedAi = report?.totals?.commits?.mergedAi ?? 0;
|
|
1686
|
+
// Deliberately uncoloured: unit economics has no org-independent "good"
|
|
1687
|
+
// threshold the way merge rate does, and a red/green here would invent one.
|
|
1688
|
+
const sub = mergedAi > 0 && cost !== null
|
|
1689
|
+
? `$${num(Math.round(cost))} / ${num(mergedAi)} merged AI commits`
|
|
1690
|
+
: cost !== null
|
|
1691
|
+
? `$${num(Math.round(cost))} spend · no merged AI commits attributed yet`
|
|
1692
|
+
: 'no attribution data in range';
|
|
1693
|
+
return `
|
|
1694
|
+
<div style="text-align:center;padding:12px">
|
|
1695
|
+
<div style="color:${p.mut};font-size:11px">$ / Shipped Commit</div>
|
|
1696
|
+
<div style="color:${p.fg};font-size:36px;font-weight:700;margin:4px 0">${costPer !== null ? `$${costPer.toFixed(2)}` : '—'}</div>
|
|
1697
|
+
<div style="color:${p.mut};font-size:10px">${sub}</div>
|
|
1698
|
+
</div>
|
|
1699
|
+
`;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// ---- View: ai-spend-kpi (single big-number KPI for total AI spend in range) ----
|
|
1703
|
+
|
|
1704
|
+
/**
|
|
1705
|
+
* Replaces a CloudWatch `Sum(AICostUSD)` tile, for the same ingestion-window
|
|
1706
|
+
* reason as the ratio above: the metric sum is short by whatever part of the
|
|
1707
|
+
* range predates CloudWatch's two-week cutoff, so a tile labelled "range"
|
|
1708
|
+
* silently reports less than the range holds.
|
|
1709
|
+
*/
|
|
1710
|
+
async function renderAiSpendKpi(fromIso: string, toIso: string, p: Palette): Promise<string> {
|
|
1711
|
+
let report: any;
|
|
1712
|
+
try {
|
|
1713
|
+
report = await fetchProductivity(fromIso, toIso);
|
|
1714
|
+
} catch {
|
|
1715
|
+
return emptyState('AI spend', 'Attribution store unreachable.', p);
|
|
1716
|
+
}
|
|
1717
|
+
const usage = report?.totals?.usage ?? null;
|
|
1718
|
+
const cost = usage?.costUsd ?? null;
|
|
1719
|
+
const calls = usage?.calls ?? 0;
|
|
1720
|
+
const tokens = (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0);
|
|
1721
|
+
return `
|
|
1722
|
+
<div style="text-align:center;padding:12px">
|
|
1723
|
+
<div style="color:${p.mut};font-size:11px">AI Spend (range)</div>
|
|
1724
|
+
<div style="color:${p.fg};font-size:36px;font-weight:700;margin:4px 0">${cost !== null ? `$${num(Math.round(cost))}` : '—'}</div>
|
|
1725
|
+
<div style="color:${p.mut};font-size:10px">${cost !== null ? `${num(calls)} calls · ${num(Math.round(tokens / 1000))}k tokens` : 'no usage data in range'}</div>
|
|
1726
|
+
</div>
|
|
1727
|
+
`;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// ---- View: ai-spend-trend (AI spend over time) ----
|
|
1731
|
+
|
|
1732
|
+
/**
|
|
1733
|
+
* Replaces the CloudWatch `Sum(AICostUSD)` spend graphs.
|
|
1734
|
+
*
|
|
1735
|
+
* Same reason as the KPI tiles: CloudWatch drops datapoints older than two
|
|
1736
|
+
* weeks, so a graph titled "spend trend" showed a flat or absent left-hand side
|
|
1737
|
+
* for any range longer than that — not because spend was low then, but because
|
|
1738
|
+
* the datapoints were refused at ingest. Reading OTEL_DAY rollups gives the real
|
|
1739
|
+
* curve over full history, and puts this graph on the same source as the
|
|
1740
|
+
* cost-per-commit trend beside it so the two can be read together.
|
|
1741
|
+
*/
|
|
1742
|
+
async function renderAiSpendTrend(fromIso: string, toIso: string, days: number, p: Palette): Promise<string> {
|
|
1743
|
+
let data: any;
|
|
1744
|
+
try {
|
|
1745
|
+
data = await invokeReceiver('/v1/commits-daily', `from=${fromIso.slice(0, 10)}&to=${toIso.slice(0, 10)}`);
|
|
1746
|
+
} catch (err) {
|
|
1747
|
+
return emptyState('AI spend trend', `Attribution store unreachable (${(err as Error).message}).`, p);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
const dayBuckets: Array<{ date: string; costUsd: number }> = data?.days ?? [];
|
|
1751
|
+
if (dayBuckets.length === 0) {
|
|
1752
|
+
return emptyState('AI spend trend', 'No usage data in range.', p);
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
const useWeekly = days > 21;
|
|
1756
|
+
let buckets: Array<{ label: string; costUsd: number }>;
|
|
1757
|
+
if (useWeekly) {
|
|
1758
|
+
const weeks = new Map<string, { label: string; costUsd: number }>();
|
|
1759
|
+
for (const d of dayBuckets) {
|
|
1760
|
+
const dt = new Date(d.date);
|
|
1761
|
+
const weekStart = new Date(dt);
|
|
1762
|
+
weekStart.setDate(dt.getDate() - dt.getDay());
|
|
1763
|
+
const key = weekStart.toISOString().slice(0, 10);
|
|
1764
|
+
let w = weeks.get(key);
|
|
1765
|
+
if (!w) { w = { label: key, costUsd: 0 }; weeks.set(key, w); }
|
|
1766
|
+
w.costUsd += d.costUsd ?? 0;
|
|
1767
|
+
}
|
|
1768
|
+
buckets = [...weeks.values()].sort((a, b) => a.label.localeCompare(b.label));
|
|
1769
|
+
} else {
|
|
1770
|
+
buckets = dayBuckets.map(d => ({ label: d.date, costUsd: d.costUsd ?? 0 }));
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
const total = buckets.reduce((s, b) => s + b.costUsd, 0);
|
|
1774
|
+
const maxC = Math.max(...buckets.map(b => b.costUsd), 0);
|
|
1775
|
+
if (maxC === 0) {
|
|
1776
|
+
return emptyState('AI spend trend', 'No AI spend recorded in range.', p);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// Bars rather than a line: spend is a per-bucket quantity, and a line implies
|
|
1780
|
+
// interpolation between buckets that a sum does not have.
|
|
1781
|
+
const chartW = 500;
|
|
1782
|
+
const chartH = 100;
|
|
1783
|
+
const slot = chartW / buckets.length;
|
|
1784
|
+
const barW = Math.max(2, slot * 0.7);
|
|
1785
|
+
const bars = buckets.map((b, i) => {
|
|
1786
|
+
const h = (b.costUsd / maxC) * (chartH - 10);
|
|
1787
|
+
return `<rect x="${i * slot + (slot - barW) / 2}" y="${chartH - h}" width="${barW}" height="${h}" fill="${p.accent}" opacity="0.85" />`;
|
|
1788
|
+
}).join('');
|
|
1789
|
+
|
|
1790
|
+
return `
|
|
1791
|
+
<div style="padding:8px 12px">
|
|
1792
|
+
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:4px">
|
|
1793
|
+
<span style="color:${p.mut};font-size:11px">AI Spend (${useWeekly ? 'weekly' : 'daily'})</span>
|
|
1794
|
+
<span style="color:${p.fg};font-size:13px;font-weight:600">$${num(Math.round(total))} total</span>
|
|
1795
|
+
</div>
|
|
1796
|
+
<svg viewBox="0 0 ${chartW} ${chartH + 16}" style="width:100%;height:auto">
|
|
1797
|
+
${bars}
|
|
1798
|
+
<text x="2" y="${chartH + 13}" fill="${p.mut}" font-size="9">${esc(buckets[0]!.label)}</text>
|
|
1799
|
+
<text x="${chartW - 2}" y="${chartH + 13}" fill="${p.mut}" font-size="9" text-anchor="end">${esc(buckets[buckets.length - 1]!.label)}</text>
|
|
1800
|
+
</svg>
|
|
1801
|
+
<div style="color:${p.mut};font-size:10px;margin-top:2px">peak $${maxC.toFixed(2)} per ${useWeekly ? 'week' : 'day'} · attribution store, full history</div>
|
|
1802
|
+
</div>
|
|
1803
|
+
`;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// ---- View: cost-per-commit-trend (cost per shipped commit over time) ----
|
|
1807
|
+
|
|
1808
|
+
/**
|
|
1809
|
+
* Replaces the CloudWatch "Cost per Shipped Commit (Weekly)" graph.
|
|
1810
|
+
*
|
|
1811
|
+
* That graph divided two ingest-timestamped metrics, which made it report
|
|
1812
|
+
* efficiency changes that never happened. Measured on a live 30-day range the
|
|
1813
|
+
* day attribution was first backfilled: the two earlier weeks plotted as gaps
|
|
1814
|
+
* (spend, zero commits) and the current week read $0.55, because 141 commits
|
|
1815
|
+
* spanning months were all stamped with their ingest time and divided into that
|
|
1816
|
+
* one week's $77.33 of spend. The graph's premise — "spend can rise while this
|
|
1817
|
+
* falls, which means AI is getting more efficient" — inverts under that
|
|
1818
|
+
* artifact, so a reader draws exactly the wrong conclusion.
|
|
1819
|
+
*
|
|
1820
|
+
* Bucketing both terms by when the work happened is what makes the trend mean
|
|
1821
|
+
* what its title says. A bucket with spend but no merged commits renders as a
|
|
1822
|
+
* gap rather than as zero: no commits shipped is not a cost of zero per commit,
|
|
1823
|
+
* and FILL(..., 0) on the metric version drew it as a floor.
|
|
1824
|
+
*/
|
|
1825
|
+
async function renderCostPerCommitTrend(fromIso: string, toIso: string, days: number, p: Palette): Promise<string> {
|
|
1826
|
+
let data: any;
|
|
1827
|
+
try {
|
|
1828
|
+
data = await invokeReceiver('/v1/commits-daily', `from=${fromIso.slice(0, 10)}&to=${toIso.slice(0, 10)}`);
|
|
1829
|
+
} catch (err) {
|
|
1830
|
+
return emptyState('cost per commit trend', `Attribution store unreachable (${(err as Error).message}).`, p);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
const dayBuckets: Array<{ date: string; mergedAi: number; costUsd: number }> = data?.days ?? [];
|
|
1834
|
+
if (dayBuckets.length === 0) {
|
|
1835
|
+
return emptyState('cost per commit trend', 'No commit or usage data in range.', p);
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// Same weekly/daily threshold as renderMergeRateTrend, so the two trends on a
|
|
1839
|
+
// dashboard share an x-axis granularity.
|
|
1840
|
+
const useWeekly = days > 21;
|
|
1841
|
+
type Bucket = { label: string; mergedAi: number; costUsd: number };
|
|
1842
|
+
let buckets: Bucket[];
|
|
1843
|
+
if (useWeekly) {
|
|
1844
|
+
const weeks = new Map<string, Bucket>();
|
|
1845
|
+
for (const d of dayBuckets) {
|
|
1846
|
+
const dt = new Date(d.date);
|
|
1847
|
+
const weekStart = new Date(dt);
|
|
1848
|
+
weekStart.setDate(dt.getDate() - dt.getDay());
|
|
1849
|
+
const key = weekStart.toISOString().slice(0, 10);
|
|
1850
|
+
let w = weeks.get(key);
|
|
1851
|
+
if (!w) { w = { label: key, mergedAi: 0, costUsd: 0 }; weeks.set(key, w); }
|
|
1852
|
+
w.mergedAi += d.mergedAi ?? 0;
|
|
1853
|
+
w.costUsd += d.costUsd ?? 0;
|
|
1854
|
+
}
|
|
1855
|
+
buckets = [...weeks.values()].sort((a, b) => a.label.localeCompare(b.label));
|
|
1856
|
+
} else {
|
|
1857
|
+
buckets = dayBuckets.map(d => ({ label: d.date, mergedAi: d.mergedAi ?? 0, costUsd: d.costUsd ?? 0 }));
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
const ratios = buckets.map(b => (b.mergedAi > 0 ? b.costUsd / b.mergedAi : null));
|
|
1861
|
+
const present = ratios.filter((r): r is number => r !== null);
|
|
1862
|
+
if (present.length === 0) {
|
|
1863
|
+
const spend = buckets.reduce((s, b) => s + b.costUsd, 0);
|
|
1864
|
+
return emptyState(
|
|
1865
|
+
'cost per commit trend',
|
|
1866
|
+
spend > 0
|
|
1867
|
+
? `$${num(Math.round(spend))} of AI spend in range with no merged AI commits attributed — run \`codeburn sync push --attribution\`.`
|
|
1868
|
+
: 'No AI spend or merged commits in range.',
|
|
1869
|
+
p,
|
|
1870
|
+
);
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
const chartW = 500;
|
|
1874
|
+
const chartH = 100;
|
|
1875
|
+
const step = buckets.length > 1 ? (chartW - 20) / (buckets.length - 1) : 0;
|
|
1876
|
+
const maxR = Math.max(...present);
|
|
1877
|
+
const yOf = (r: number): number => chartH - (maxR > 0 ? (r / maxR) * (chartH - 10) : 0);
|
|
1878
|
+
|
|
1879
|
+
// Gaps break the polyline into segments instead of interpolating across a
|
|
1880
|
+
// bucket that shipped nothing, which would draw a slope the data does not have.
|
|
1881
|
+
const segments: string[] = [];
|
|
1882
|
+
let current: string[] = [];
|
|
1883
|
+
ratios.forEach((r, i) => {
|
|
1884
|
+
if (r === null) {
|
|
1885
|
+
if (current.length > 1) segments.push(current.join(' '));
|
|
1886
|
+
current = [];
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
current.push(`${10 + i * step},${yOf(r)}`);
|
|
1890
|
+
});
|
|
1891
|
+
if (current.length > 1) segments.push(current.join(' '));
|
|
1892
|
+
|
|
1893
|
+
const dots = ratios.map((r, i) => r === null ? '' :
|
|
1894
|
+
`<circle cx="${10 + i * step}" cy="${yOf(r)}" r="2.5" fill="${p.accent}" />`).join('');
|
|
1895
|
+
const lines = segments.map(s =>
|
|
1896
|
+
`<polyline points="${s}" fill="none" stroke="${p.accent}" stroke-width="2" />`).join('');
|
|
1897
|
+
|
|
1898
|
+
const first = present[0];
|
|
1899
|
+
const last = present[present.length - 1];
|
|
1900
|
+
const delta = first > 0 ? ((last - first) / first) * 100 : null;
|
|
1901
|
+
const gaps = ratios.filter(r => r === null).length;
|
|
1902
|
+
|
|
1903
|
+
return `
|
|
1904
|
+
<div style="padding:8px 12px">
|
|
1905
|
+
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:4px">
|
|
1906
|
+
<span style="color:${p.mut};font-size:11px">Cost per Shipped Commit (${useWeekly ? 'weekly' : 'daily'})</span>
|
|
1907
|
+
<span style="color:${p.fg};font-size:13px;font-weight:600">$${last.toFixed(2)}${
|
|
1908
|
+
delta !== null ? ` <span style="color:${delta <= 0 ? p.ok : p.warn};font-size:11px">${delta <= 0 ? '▼' : '▲'} ${Math.abs(delta).toFixed(0)}%</span>` : ''
|
|
1909
|
+
}</span>
|
|
1910
|
+
</div>
|
|
1911
|
+
<svg viewBox="0 0 ${chartW} ${chartH + 16}" style="width:100%;height:auto">
|
|
1912
|
+
${lines}${dots}
|
|
1913
|
+
<text x="10" y="${chartH + 13}" fill="${p.mut}" font-size="9">${esc(buckets[0]!.label)}</text>
|
|
1914
|
+
<text x="${chartW - 10}" y="${chartH + 13}" fill="${p.mut}" font-size="9" text-anchor="end">${esc(buckets[buckets.length - 1]!.label)}</text>
|
|
1915
|
+
</svg>
|
|
1916
|
+
<div style="color:${p.mut};font-size:10px;margin-top:2px">
|
|
1917
|
+
peak $${maxR.toFixed(2)} · attribution store, bucketed by commit date${gaps > 0 ? ` · ${gaps} ${useWeekly ? 'week' : 'day'}(s) with spend but no merged AI commits shown as gaps` : ''}
|
|
1918
|
+
</div>
|
|
1919
|
+
</div>
|
|
1920
|
+
`;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1651
1923
|
// ---- View: merge-rate-trend (line chart of merge rate over time) ----
|
|
1652
1924
|
|
|
1653
1925
|
async function renderMergeRateTrend(fromIso: string, toIso: string, days: number, p: Palette): Promise<string> {
|
|
@@ -1721,6 +1993,124 @@ async function renderMergeRateTrend(fromIso: string, toIso: string, days: number
|
|
|
1721
1993
|
|
|
1722
1994
|
// ---- Handler ----
|
|
1723
1995
|
|
|
1996
|
+
// ---- Views: tool-usage-top / tool-usage-trend (tools the model invoked) ----
|
|
1997
|
+
|
|
1998
|
+
/** Dashboard PATTERN token left unreplaced, or blank, means org scope. */
|
|
1999
|
+
function widgetUser(user: string | undefined): string {
|
|
2000
|
+
const u = (user ?? '').trim();
|
|
2001
|
+
return !u || u === 'PRISM_USER_FILTER_TOKEN' ? 'all' : u;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
async function fetchToolUsage(fromIso: string, toIso: string, user: string): Promise<any> {
|
|
2005
|
+
return invokeReceiver('/v1/tool-usage',
|
|
2006
|
+
`user=${encodeURIComponent(user)}&from=${fromIso.slice(0, 10)}&to=${toIso.slice(0, 10)}`);
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
const TOOL_USAGE_NOTE =
|
|
2010
|
+
'Counts model calls that invoked each tool (codeburn ai.tools), by the day of the call. ' +
|
|
2011
|
+
'Providers that do not report tool calls contribute nothing here.';
|
|
2012
|
+
|
|
2013
|
+
/**
|
|
2014
|
+
* Top tools and MCP servers. Reads the receiver's OTEL#TOOLDAY rollups rather
|
|
2015
|
+
* than a CloudWatch metric for the same reasons as the spend tiles: full
|
|
2016
|
+
* history, bucketed by call date, and scoped by the Developer variable.
|
|
2017
|
+
*/
|
|
2018
|
+
async function renderToolUsageTop(fromIso: string, toIso: string, user: string, p: Palette): Promise<string> {
|
|
2019
|
+
let r: any;
|
|
2020
|
+
try {
|
|
2021
|
+
r = await fetchToolUsage(fromIso, toIso, user);
|
|
2022
|
+
} catch (err) {
|
|
2023
|
+
return emptyState('tool usage', `Attribution store unreachable (${(err as Error).message}).`, p);
|
|
2024
|
+
}
|
|
2025
|
+
const tools: Array<{ name: string; mcpServer: string; calls: number; byAiTool: Record<string, number> }> = r?.tools ?? [];
|
|
2026
|
+
if (tools.length === 0) {
|
|
2027
|
+
return emptyState('tool usage', 'Run `codeburn sync push` with a codeburn version that sends ai.tools.', p);
|
|
2028
|
+
}
|
|
2029
|
+
const servers: Array<{ server: string; calls: number; tools: number }> = r?.mcpServers ?? [];
|
|
2030
|
+
const mcpCalls = servers.reduce((s, x) => s + x.calls, 0);
|
|
2031
|
+
const total = r?.totalToolCalls ?? 0;
|
|
2032
|
+
|
|
2033
|
+
const top = tools.slice(0, 12);
|
|
2034
|
+
const maxT = top[0]?.calls ?? 0;
|
|
2035
|
+
const toolRows = top.map(t => [
|
|
2036
|
+
`${esc(t.name)}${bar(t.calls, maxT, p.accent)}`,
|
|
2037
|
+
esc(Object.entries(t.byAiTool).sort((a, b) => b[1] - a[1]).map(([k]) => k).join(', ')),
|
|
2038
|
+
num(t.calls),
|
|
2039
|
+
]);
|
|
2040
|
+
const topS = servers.slice(0, 8);
|
|
2041
|
+
const maxS = topS[0]?.calls ?? 0;
|
|
2042
|
+
const serverRows = topS.map(s => [`${esc(s.server)}${bar(s.calls, maxS, p.ok)}`, num(s.tools), num(s.calls)]);
|
|
2043
|
+
|
|
2044
|
+
return `
|
|
2045
|
+
${kpiRow([
|
|
2046
|
+
{ label: 'Tool calls', value: num(total), note: user === 'all' ? 'organization' : user },
|
|
2047
|
+
{ label: 'Distinct tools', value: num(tools.length) },
|
|
2048
|
+
{ label: 'MCP share', value: pct(total > 0 ? (mcpCalls / total) * 100 : null), note: `${num(servers.length)} MCP servers` },
|
|
2049
|
+
], p)}
|
|
2050
|
+
<div style="display:flex;gap:16px;flex-wrap:wrap">
|
|
2051
|
+
<div style="flex:3;min-width:280px">${table(['Tool', 'AI tools', 'Calls'], toolRows, p, 2)}</div>
|
|
2052
|
+
<div style="flex:2;min-width:220px">${serverRows.length > 0
|
|
2053
|
+
? table(['MCP server', 'Tools', 'Calls'], serverRows, p, 1)
|
|
2054
|
+
: `<div style="color:${p.mut};font-size:11px;margin-top:14px">No MCP tool calls in range.</div>`}</div>
|
|
2055
|
+
</div>
|
|
2056
|
+
${footnote(TOOL_USAGE_NOTE, p)}
|
|
2057
|
+
`;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
/** Daily (or weekly, past 21 days) tool calls, same bucketing as ai-spend-trend. */
|
|
2061
|
+
async function renderToolUsageTrend(fromIso: string, toIso: string, days: number, user: string, p: Palette): Promise<string> {
|
|
2062
|
+
let r: any;
|
|
2063
|
+
try {
|
|
2064
|
+
r = await fetchToolUsage(fromIso, toIso, user);
|
|
2065
|
+
} catch (err) {
|
|
2066
|
+
return emptyState('tool usage', `Attribution store unreachable (${(err as Error).message}).`, p);
|
|
2067
|
+
}
|
|
2068
|
+
const dayBuckets: Array<{ date: string; calls: number }> = r?.days ?? [];
|
|
2069
|
+
if (dayBuckets.length === 0) {
|
|
2070
|
+
return emptyState('tool usage', 'Run `codeburn sync push` with a codeburn version that sends ai.tools.', p);
|
|
2071
|
+
}
|
|
2072
|
+
const useWeekly = days > 21;
|
|
2073
|
+
let buckets: Array<{ label: string; calls: number }>;
|
|
2074
|
+
if (useWeekly) {
|
|
2075
|
+
const weeks = new Map<string, number>();
|
|
2076
|
+
for (const d of dayBuckets) {
|
|
2077
|
+
const dt = new Date(d.date);
|
|
2078
|
+
dt.setDate(dt.getDate() - dt.getDay());
|
|
2079
|
+
const key = dt.toISOString().slice(0, 10);
|
|
2080
|
+
weeks.set(key, (weeks.get(key) ?? 0) + d.calls);
|
|
2081
|
+
}
|
|
2082
|
+
buckets = [...weeks.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([label, calls]) => ({ label, calls }));
|
|
2083
|
+
} else {
|
|
2084
|
+
buckets = dayBuckets.map(d => ({ label: d.date, calls: d.calls }));
|
|
2085
|
+
}
|
|
2086
|
+
const total = buckets.reduce((s, b) => s + b.calls, 0);
|
|
2087
|
+
const maxC = Math.max(...buckets.map(b => b.calls), 0);
|
|
2088
|
+
const chartW = 500;
|
|
2089
|
+
const chartH = 100;
|
|
2090
|
+
const slot = chartW / buckets.length;
|
|
2091
|
+
const barW = Math.max(2, slot * 0.7);
|
|
2092
|
+
const bars = buckets.map((b, i) => {
|
|
2093
|
+
const h = maxC > 0 ? (b.calls / maxC) * (chartH - 10) : 0;
|
|
2094
|
+
return `<rect x="${i * slot + (slot - barW) / 2}" y="${chartH - h}" width="${barW}" height="${h}" fill="${p.accent}" opacity="0.85" />`;
|
|
2095
|
+
}).join('');
|
|
2096
|
+
return `
|
|
2097
|
+
<div style="padding:8px 12px">
|
|
2098
|
+
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:4px">
|
|
2099
|
+
<span style="color:${p.mut};font-size:11px">Tool calls (${useWeekly ? 'weekly' : 'daily'})${user === 'all' ? '' : ` · ${esc(user)}`}</span>
|
|
2100
|
+
<span style="color:${p.fg};font-size:13px;font-weight:600">${num(total)} total</span>
|
|
2101
|
+
</div>
|
|
2102
|
+
<svg viewBox="0 0 ${chartW} ${chartH + 16}" style="width:100%;height:auto">
|
|
2103
|
+
${bars}
|
|
2104
|
+
<text x="2" y="${chartH + 13}" fill="${p.mut}" font-size="9">${esc(buckets[0]!.label)}</text>
|
|
2105
|
+
<text x="${chartW - 2}" y="${chartH + 13}" fill="${p.mut}" font-size="9" text-anchor="end">${esc(buckets[buckets.length - 1]!.label)}</text>
|
|
2106
|
+
</svg>
|
|
2107
|
+
${footnote(TOOL_USAGE_NOTE, p)}
|
|
2108
|
+
</div>
|
|
2109
|
+
`;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// ---- Handler ----
|
|
2113
|
+
|
|
1724
2114
|
export async function handler(event: WidgetEvent): Promise<string> {
|
|
1725
2115
|
if (event.describe) return DOCS;
|
|
1726
2116
|
|
|
@@ -1754,7 +2144,13 @@ export async function handler(event: WidgetEvent): Promise<string> {
|
|
|
1754
2144
|
case 'commits-bar': body = await renderCommitsBar(fromIso, toIso, days, p); break;
|
|
1755
2145
|
case 'ai-share-kpi': body = await renderAiShareKpi(fromIso, toIso, p); break;
|
|
1756
2146
|
case 'merge-rate-kpi': body = await renderMergeRateKpi(fromIso, toIso, p); break;
|
|
2147
|
+
case 'cost-per-commit-kpi': body = await renderCostPerCommitKpi(fromIso, toIso, p); break;
|
|
2148
|
+
case 'ai-spend-kpi': body = await renderAiSpendKpi(fromIso, toIso, p); break;
|
|
2149
|
+
case 'cost-per-commit-trend': body = await renderCostPerCommitTrend(fromIso, toIso, days, p); break;
|
|
2150
|
+
case 'ai-spend-trend': body = await renderAiSpendTrend(fromIso, toIso, days, p); break;
|
|
1757
2151
|
case 'merge-rate-trend': body = await renderMergeRateTrend(fromIso, toIso, days, p); break;
|
|
2152
|
+
case 'tool-usage-top': body = await renderToolUsageTop(fromIso, toIso, widgetUser(event.user), p); break;
|
|
2153
|
+
case 'tool-usage-trend': body = await renderToolUsageTrend(fromIso, toIso, days, widgetUser(event.user), p); break;
|
|
1758
2154
|
default: body = `<div style="color:${p.danger}">Unknown view: ${esc(view)}</div>`;
|
|
1759
2155
|
}
|
|
1760
2156
|
return `<div style="font-family:'Amazon Ember',Helvetica,Arial,sans-serif;color:${p.fg};font-size:13px">${body}</div>`;
|
|
@@ -355,6 +355,13 @@ export class MetricsPipelineStack extends cdk.Stack {
|
|
|
355
355
|
// OTEL Collector (on by default — server side of `codeburn sync`)
|
|
356
356
|
// Skip with: npx cdk deploy --context skipOtelCollector=true
|
|
357
357
|
// BYO IdP: -c otelIssuer=... -c otelClientId=... [-c otelIdentityClaim=email]
|
|
358
|
+
// Federated IdP behind Cognito (keeps the M2M agent client):
|
|
359
|
+
// -c otelIdpName=CorpIdP
|
|
360
|
+
// -c otelIdpIssuer=https://idp.example.com
|
|
361
|
+
// -c otelIdpClientId=...
|
|
362
|
+
// -c otelIdpClientSecretArn=<secretsmanager name or arn>
|
|
363
|
+
// [-c otelIdpClientSecretJsonKey=clientSecret]
|
|
364
|
+
// [-c otelIdpScopes='openid email'] (default: openid email)
|
|
358
365
|
// Per-user AI usage flows directly from codeburn sync to the
|
|
359
366
|
// ai-usage table (spans + daily aggregates).
|
|
360
367
|
// -------------------------------------------------------
|
|
@@ -366,6 +373,12 @@ export class MetricsPipelineStack extends cdk.Stack {
|
|
|
366
373
|
externalIssuer: this.node.tryGetContext('otelIssuer') as string | undefined,
|
|
367
374
|
externalClientId: this.node.tryGetContext('otelClientId') as string | undefined,
|
|
368
375
|
identityClaim: this.node.tryGetContext('otelIdentityClaim') as string | undefined,
|
|
376
|
+
idpName: this.node.tryGetContext('otelIdpName') as string | undefined,
|
|
377
|
+
idpIssuer: this.node.tryGetContext('otelIdpIssuer') as string | undefined,
|
|
378
|
+
idpClientId: this.node.tryGetContext('otelIdpClientId') as string | undefined,
|
|
379
|
+
idpClientSecretArn: this.node.tryGetContext('otelIdpClientSecretArn') as string | undefined,
|
|
380
|
+
idpClientSecretJsonKey: this.node.tryGetContext('otelIdpClientSecretJsonKey') as string | undefined,
|
|
381
|
+
idpScopes: this.node.tryGetContext('otelIdpScopes') as string | undefined,
|
|
369
382
|
});
|
|
370
383
|
|
|
371
384
|
// ---------------------------------------------------
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-d1-velocity-infra",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "PRISM D1 Velocity - AWS CDK Infrastructure for AI-DORA Metrics Platform",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "tsc",
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
"diff": "cdk diff",
|
|
12
12
|
"check:metrics": "tsx scripts/check-metric-coverage.ts",
|
|
13
13
|
"check:attribution": "tsx scripts/check-attribution-contract.ts",
|
|
14
|
-
"check:transitions": "tsx scripts/check-commit-transitions.ts"
|
|
14
|
+
"check:transitions": "tsx scripts/check-commit-transitions.ts",
|
|
15
|
+
"check:pricing": "tsx scripts/check-pricing.ts",
|
|
16
|
+
"check:tools": "tsx scripts/check-tool-usage.ts",
|
|
17
|
+
"pricing:snapshot": "tsx scripts/refresh-pricing-snapshot.ts"
|
|
15
18
|
},
|
|
16
19
|
"dependencies": {
|
|
17
20
|
"aws-cdk-lib": "^2.170.0",
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
# (Linux/macOS/Windows Git Bash) with NO dependency on jq, bc, sed, or grep -P.
|
|
7
7
|
# Token tracking powered by codeburn / prism-cli.
|
|
8
8
|
#
|
|
9
|
-
#
|
|
9
|
+
# RETIRED: no command installs this hook any more. `prism-cli enable codewatch`
|
|
10
|
+
# (and its deprecated alias `bootstrapper install-git-hooks`) removes it when found.
|
|
11
|
+
# Kept for reference only.
|
|
10
12
|
|
|
11
13
|
set -uo pipefail
|
|
12
14
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const CONFIG_PATH = ".prism/config.json";
|
|
2
|
+
export declare const RULES_PATH = ".prism/review/rules.md";
|
|
3
|
+
export declare const GITLEAKS_BASELINE_PATH = ".prism/gitleaks-baseline.json";
|
|
4
|
+
export declare const GITLEAKS_CONFIG_PATH = ".gitleaks.toml";
|
|
5
|
+
/** Per-clone override of the team's reviewer choice: `git config prism.reviewer claude`. */
|
|
6
|
+
export declare const REVIEWER_GIT_CONFIG_KEY = "prism.reviewer";
|
|
7
|
+
export declare const DEFAULT_REVIEW_TIMEOUT_SECONDS = 300;
|
|
8
|
+
export interface EvalConfig {
|
|
9
|
+
reviewer?: string;
|
|
10
|
+
model?: string;
|
|
11
|
+
timeout_seconds?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function readPrismConfig(repoRoot: string): Record<string, any>;
|
|
14
|
+
export declare function readEvalConfig(repoRoot: string): EvalConfig;
|
|
15
|
+
/** Merges `patch` into the `eval` block, keeping every other key in the file. */
|
|
16
|
+
export declare function writeEvalConfig(repoRoot: string, patch: EvalConfig): void;
|
|
17
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/codewatch/config.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,WAAW,uBAAuB,CAAC;AAChD,eAAO,MAAM,UAAU,2BAA2B,CAAC;AACnD,eAAO,MAAM,sBAAsB,kCAAkC,CAAC;AACtE,eAAO,MAAM,oBAAoB,mBAAmB,CAAC;AACrD,4FAA4F;AAC5F,eAAO,MAAM,uBAAuB,mBAAmB,CAAC;AACxD,eAAO,MAAM,8BAA8B,MAAM,CAAC;AAElD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CASrE;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAG3D;AAED,iFAAiF;AACjF,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAMzE"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.prism/config.json` access for codewatch (the local git-hook gate).
|
|
3
|
+
*
|
|
4
|
+
* The file is shared with other PRISM installers (team_id etc.) and is usually
|
|
5
|
+
* committed, so reads tolerate any shape and writes merge rather than replace.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { dirname, resolve } from 'node:path';
|
|
9
|
+
export const CONFIG_PATH = '.prism/config.json';
|
|
10
|
+
export const RULES_PATH = '.prism/review/rules.md';
|
|
11
|
+
export const GITLEAKS_BASELINE_PATH = '.prism/gitleaks-baseline.json';
|
|
12
|
+
export const GITLEAKS_CONFIG_PATH = '.gitleaks.toml';
|
|
13
|
+
/** Per-clone override of the team's reviewer choice: `git config prism.reviewer claude`. */
|
|
14
|
+
export const REVIEWER_GIT_CONFIG_KEY = 'prism.reviewer';
|
|
15
|
+
export const DEFAULT_REVIEW_TIMEOUT_SECONDS = 300;
|
|
16
|
+
export function readPrismConfig(repoRoot) {
|
|
17
|
+
const p = resolve(repoRoot, CONFIG_PATH);
|
|
18
|
+
if (!existsSync(p))
|
|
19
|
+
return {};
|
|
20
|
+
try {
|
|
21
|
+
const v = JSON.parse(readFileSync(p, 'utf8'));
|
|
22
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? v : {};
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function readEvalConfig(repoRoot) {
|
|
29
|
+
const e = readPrismConfig(repoRoot).eval;
|
|
30
|
+
return e && typeof e === 'object' ? e : {};
|
|
31
|
+
}
|
|
32
|
+
/** Merges `patch` into the `eval` block, keeping every other key in the file. */
|
|
33
|
+
export function writeEvalConfig(repoRoot, patch) {
|
|
34
|
+
const p = resolve(repoRoot, CONFIG_PATH);
|
|
35
|
+
const cfg = readPrismConfig(repoRoot);
|
|
36
|
+
cfg.eval = { ...(cfg.eval && typeof cfg.eval === 'object' ? cfg.eval : {}), ...patch };
|
|
37
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
38
|
+
writeFileSync(p, JSON.stringify(cfg, null, 2) + '\n');
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/codewatch/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE7C,MAAM,CAAC,MAAM,WAAW,GAAG,oBAAoB,CAAC;AAChD,MAAM,CAAC,MAAM,UAAU,GAAG,wBAAwB,CAAC;AACnD,MAAM,CAAC,MAAM,sBAAsB,GAAG,+BAA+B,CAAC;AACtE,MAAM,CAAC,MAAM,oBAAoB,GAAG,gBAAgB,CAAC;AACrD,4FAA4F;AAC5F,MAAM,CAAC,MAAM,uBAAuB,GAAG,gBAAgB,CAAC;AACxD,MAAM,CAAC,MAAM,8BAA8B,GAAG,GAAG,CAAC;AAQlD,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,MAAM,CAAC,GAAG,OAAO,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IACzC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IAC9B,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;QAC9C,OAAO,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAClE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,QAAgB;IAC7C,MAAM,CAAC,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC;IACzC,OAAO,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC7C,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAE,KAAiB;IACjE,MAAM,CAAC,GAAG,OAAO,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IACzC,MAAM,GAAG,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;IACtC,GAAG,CAAC,IAAI,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC;IACvF,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,aAAa,CAAC,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AACxD,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type Finding, type PrismResult } from './results.js';
|
|
2
|
+
/**
|
|
3
|
+
* Starter gitleaks config, written only when the repo has none.
|
|
4
|
+
*
|
|
5
|
+
* The secret-scan step in the workflow runs with or without this file --
|
|
6
|
+
* gitleaks ships usable defaults. What the defaults do not know is which
|
|
7
|
+
* credential-shaped strings in THIS repo are deliberate, so the first PR that
|
|
8
|
+
* touches a test fixture fails the gate on a fake key. A team that hits that
|
|
9
|
+
* with no obvious place to record the exception tends to delete the gate rather
|
|
10
|
+
* than tune it, so the tuning surface ships up front and is documented inline.
|
|
11
|
+
*
|
|
12
|
+
* `useDefault = true` is what keeps this additive: without it, declaring a
|
|
13
|
+
* config REPLACES the entire default ruleset and the gate silently stops
|
|
14
|
+
* detecting anything it does not name explicitly.
|
|
15
|
+
*/
|
|
16
|
+
export declare const GITLEAKS_STARTER_CONFIG = "# gitleaks configuration for the PRISM eval gate.\n#\n# The gate scans only the commits a PR introduces, so anything flagged here is\n# something this PR added. Two ways to handle a false positive:\n#\n# 1. Allowlist the path or pattern below (preferred -- reviewable, and it\n# keeps working as the file changes).\n# 2. Add an inline \"gitleaks:allow\" comment on the offending line.\n#\n# A real exposure is NOT a false positive: rotate the credential first, because\n# deleting it from the branch does not un-expose what was already pushed.\n\n[extend]\n# Keep gitleaks' built-in ruleset. Removing this line replaces the defaults\n# entirely rather than adding to them, which turns the gate into a no-op for\n# every credential type not restated in this file.\nuseDefault = true\n\n[[allowlists]]\ndescription = \"Test fixtures and documented examples\"\npaths = [\n '''(^|/)tests?/''',\n '''(^|/)__fixtures__/''',\n '''(^|/)__tests__/''',\n '''\\.example($|\\.)''',\n]\n";
|
|
17
|
+
/**
|
|
18
|
+
* One-line, platform-specific gitleaks install hint. Never a curl-piped-to-shell.
|
|
19
|
+
* Single source for the installer, the pre-commit warning and the dashboard.
|
|
20
|
+
*/
|
|
21
|
+
export declare function gitleaksInstallHint(platform?: NodeJS.Platform): string;
|
|
22
|
+
/** Writes the starter config unless the repo already has one. Returns true when written. */
|
|
23
|
+
export declare function installGitleaksConfig(gitRoot: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Converts a gitleaks JSON report to schema findings.
|
|
26
|
+
*
|
|
27
|
+
* Only location and rule fields are projected. `Secret` and `Match` are never
|
|
28
|
+
* read, so a result file cannot carry the credential even if --redact was lost.
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseGitleaksReport(json: string): Finding[];
|
|
31
|
+
export interface PreCommitOutcome {
|
|
32
|
+
/** Process exit code for the hook: 1 blocks the commit. */
|
|
33
|
+
exitCode: number;
|
|
34
|
+
result: PrismResult;
|
|
35
|
+
resultPath: string | null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* `gitleaks protect --staged` against the repo config and baseline.
|
|
39
|
+
*
|
|
40
|
+
* Blocks (exit 1) on a finding and when gitleaks runs but cannot produce a
|
|
41
|
+
* verdict (bad config, unreadable report): a broken secret scan should be
|
|
42
|
+
* fixed, not silently passed. A MISSING gitleaks does not block -- it warns and
|
|
43
|
+
* records a `skipped` result with reason `gitleaks-missing`; CI's gitleaks
|
|
44
|
+
* step remains the backstop.
|
|
45
|
+
*/
|
|
46
|
+
export declare function runPreCommit(repoRoot: string, gitleaksBin?: string): PreCommitOutcome;
|
|
47
|
+
/** A result that cannot be written must never change the hook's verdict. */
|
|
48
|
+
export declare function safeWrite(repoRoot: string, r: PrismResult): string | null;
|
|
49
|
+
//# sourceMappingURL=gitleaks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitleaks.d.ts","sourceRoot":"","sources":["../../../src/codewatch/gitleaks.ts"],"names":[],"mappings":"AAQA,OAAO,EAAoD,KAAK,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAIhH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,uBAAuB,g+BA0BnC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,MAAM,CAKxF;AAED,4FAA4F;AAC5F,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAK9D;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAW3D;AAED,MAAM,WAAW,gBAAgB;IAC/B,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAOD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,SAAa,GAAG,gBAAgB,CAyCzF;AAED,4EAA4E;AAC5E,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAEzE"}
|