segreant 0.1.0 → 0.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.
Files changed (142) hide show
  1. package/LICENSING.md +5 -3
  2. package/README.md +57 -19
  3. package/bin/segreant-statusline.mjs +20 -0
  4. package/bin/segreant.mjs +19 -8
  5. package/dist/alerts/detect.js +13 -0
  6. package/dist/billing/epistemic.js +4 -0
  7. package/dist/billing/importer.js +29 -10
  8. package/dist/billing/reconcile.js +122 -32
  9. package/dist/billing/scope.js +7 -0
  10. package/dist/budget/guard.js +33 -4
  11. package/dist/cli/allocCmd.js +73 -4
  12. package/dist/cli/askModelPrices.js +59 -0
  13. package/dist/cli/backupCmd.js +2 -1
  14. package/dist/cli/basisLines.js +45 -0
  15. package/dist/cli/billingCmd.js +68 -9
  16. package/dist/cli/capitalCmd.js +2 -1
  17. package/dist/cli/connectCmd.js +50 -9
  18. package/dist/cli/egressCmd.js +15 -3
  19. package/dist/cli/evidenceCmd.js +6 -3
  20. package/dist/cli/flags.js +68 -0
  21. package/dist/cli/homeCmd.js +117 -0
  22. package/dist/cli/importCmd.js +210 -54
  23. package/dist/cli/launchCmd.js +2 -1
  24. package/dist/cli/opsCmd.js +49 -2
  25. package/dist/cli/planCmd.js +181 -0
  26. package/dist/cli/pricingDeclareCmd.js +178 -0
  27. package/dist/cli/quotaCmd.js +190 -0
  28. package/dist/cli/runCmd.js +203 -51
  29. package/dist/cli/showCmd.js +37 -11
  30. package/dist/cli/teamCmd.js +5 -5
  31. package/dist/cli/ui.js +24 -8
  32. package/dist/cli/valueCmd.js +189 -14
  33. package/dist/cli.js +102 -21
  34. package/dist/config.js +95 -6
  35. package/dist/connect/antigravity.js +310 -0
  36. package/dist/connect/claudeCode.js +43 -3
  37. package/dist/connect/codex.js +38 -1
  38. package/dist/connect/importShared.js +58 -5
  39. package/dist/connect/opencode.js +37 -9
  40. package/dist/connect/protobuf.js +126 -0
  41. package/dist/connect/registry.js +46 -0
  42. package/dist/connect/repoRoot.js +112 -0
  43. package/dist/cost/basis.js +168 -0
  44. package/dist/cost/declarations.js +37 -0
  45. package/dist/cost/exactPricing.js +4 -1
  46. package/dist/cost/firstSight.js +98 -0
  47. package/dist/cost/overrides.js +230 -0
  48. package/dist/cost/pricing.js +228 -15
  49. package/dist/cost/reprice.js +130 -0
  50. package/dist/dashboard/claim-support.js +11 -8
  51. package/dist/dashboard/contracts.js +23 -3
  52. package/dist/dashboard/routes.js +980 -112
  53. package/dist/dashboard/sessions.js +345 -0
  54. package/dist/dashboard/settings.js +2 -1
  55. package/dist/dashboard/value-cache.js +105 -0
  56. package/dist/dashboard/web/app/components/claimInspector.js +6 -5
  57. package/dist/dashboard/web/app/components/drawer.js +105 -35
  58. package/dist/dashboard/web/app/components/ruleFields.js +144 -0
  59. package/dist/dashboard/web/app/components/spine.js +54 -24
  60. package/dist/dashboard/web/app/components/workflow.js +82 -0
  61. package/dist/dashboard/web/app/core/actions.js +734 -23
  62. package/dist/dashboard/web/app/core/api.js +37 -1
  63. package/dist/dashboard/web/app/core/chain.js +11 -4
  64. package/dist/dashboard/web/app/core/claimLayers.js +179 -51
  65. package/dist/dashboard/web/app/core/claimTypes.js +30 -0
  66. package/dist/dashboard/web/app/core/fmt.js +19 -2
  67. package/dist/dashboard/web/app/core/generated-contract.js +23 -3
  68. package/dist/dashboard/web/app/core/generated-payload-contract.js +1944 -465
  69. package/dist/dashboard/web/app/core/generated-types.js +1 -1
  70. package/dist/dashboard/web/app/core/griffin.js +10 -0
  71. package/dist/dashboard/web/app/core/jobs.js +12 -0
  72. package/dist/dashboard/web/app/core/period.js +35 -0
  73. package/dist/dashboard/web/app/core/periodWords.js +10 -0
  74. package/dist/dashboard/web/app/core/periods.js +32 -0
  75. package/dist/dashboard/web/app/core/pricingActions.js +264 -0
  76. package/dist/dashboard/web/app/core/privacyText.js +78 -0
  77. package/dist/dashboard/web/app/core/reconcileWords.js +63 -0
  78. package/dist/dashboard/web/app/core/registry.js +79 -46
  79. package/dist/dashboard/web/app/core/slug.js +9 -0
  80. package/dist/dashboard/web/app/core/theme.js +48 -0
  81. package/dist/dashboard/web/app/core/view.js +69 -0
  82. package/dist/dashboard/web/app/main.js +197 -15
  83. package/dist/dashboard/web/app/views/allocation.js +249 -61
  84. package/dist/dashboard/web/app/views/control.js +57 -16
  85. package/dist/dashboard/web/app/views/data.js +4 -4
  86. package/dist/dashboard/web/app/views/evidence.js +336 -87
  87. package/dist/dashboard/web/app/views/privacy.js +13 -0
  88. package/dist/dashboard/web/app/views/spend.js +30 -15
  89. package/dist/dashboard/web/app/views/statement.js +189 -0
  90. package/dist/dashboard/web/app/views/system.js +44 -4
  91. package/dist/dashboard/web/app/views/value.js +105 -59
  92. package/dist/dashboard/web/app/views/workbench.js +312 -0
  93. package/dist/dashboard/web/classic.html +16 -4
  94. package/dist/dashboard/web/index.html +1 -1
  95. package/dist/dashboard/web/styles/app.css +536 -23
  96. package/dist/demo/seed.js +153 -14
  97. package/dist/economics/ledger.js +1 -1
  98. package/dist/egress/policy.js +129 -5
  99. package/dist/export/allocationCsv.js +70 -0
  100. package/dist/export/billingCsv.js +6 -2
  101. package/dist/export/csv.js +10 -2
  102. package/dist/export/economic.js +6 -2
  103. package/dist/export/statementCsv.js +104 -0
  104. package/dist/git/correlate.js +86 -9
  105. package/dist/git/quality.js +107 -24
  106. package/dist/git/repoScope.js +207 -0
  107. package/dist/git/survivalStore.js +186 -0
  108. package/dist/guide.js +9 -4
  109. package/dist/plans/detect.js +147 -0
  110. package/dist/proxy/server.js +134 -14
  111. package/dist/proxy/usage.js +9 -4
  112. package/dist/quota/limits.js +138 -0
  113. package/dist/quota/statusline.js +68 -0
  114. package/dist/quota/view.js +122 -0
  115. package/dist/scan/knownApps.js +6 -2
  116. package/dist/scan/scan.js +9 -0
  117. package/dist/store/allocation.js +14 -0
  118. package/dist/store/db.js +259 -34
  119. package/dist/store/realization.js +78 -14
  120. package/dist/store/schema.js +59 -1
  121. package/dist/store/scopeEvidence.js +67 -0
  122. package/dist/util/pool.js +19 -0
  123. package/dist/util/time.js +7 -0
  124. package/dist/value/realization.js +321 -30
  125. package/dist/value/report.js +28 -8
  126. package/dist/value/selfReported.js +10 -9
  127. package/docs/ARCHITECTURE.md +8 -4
  128. package/docs/CAPABILITY-EVIDENCE-CONTRACT.md +33 -11
  129. package/docs/CLI-JSON-CONTRACT.md +38 -0
  130. package/docs/DATA-BOUNDARIES.md +66 -6
  131. package/docs/FAQ.md +43 -9
  132. package/docs/GETTING-STARTED.md +19 -12
  133. package/docs/GUIDE.md +81 -18
  134. package/docs/INTEGRATIONS.md +9 -9
  135. package/docs/METHODOLOGY.md +3 -2
  136. package/docs/NEUTRALITY.md +1 -1
  137. package/docs/PRIVACY.md +51 -0
  138. package/docs/README.md +1 -0
  139. package/docs/RELEASE-GATE.md +52 -7
  140. package/docs/THREAT-MODEL.md +2 -1
  141. package/package.json +3 -2
  142. package/pricing/models.json +1366 -60
@@ -17,12 +17,12 @@
17
17
  */
18
18
  export const TERRITORIES = [
19
19
  { id: 'spend', label: 'Spend', plain: 'What AI is costing you, and where it went.', icon: 'meter' },
20
- { id: 'control', label: 'Control', plain: 'Budgets and alerts, so nothing surprises you.', icon: 'shield' },
21
- { id: 'allocation', label: 'Allocation', plain: 'Whose cost this is — projects and cost centres.', icon: 'split' },
22
- { id: 'evidence', label: 'Evidence', plain: 'Whether the numbers hold up against the provider bill.', icon: 'seal' },
20
+ { id: 'control', label: 'Protect', plain: 'Budgets and alerts, so nothing surprises you.', icon: 'shield' },
21
+ { id: 'allocation', label: 'Allocate', plain: 'Whose cost this is — projects and cost centres.', icon: 'split' },
22
+ { id: 'evidence', label: 'Close', plain: 'Whether the numbers hold up against the provider bill.', icon: 'seal' },
23
23
  { id: 'value', label: 'Value', plain: 'What the spend produced, with the limits stated.', icon: 'yield' },
24
24
  { id: 'data', label: 'Data', plain: 'Getting your usage in, from tools and providers.', icon: 'inflow' },
25
- { id: 'system', label: 'System', plain: 'Pricing, settings, maintenance, and this table.', icon: 'gear' },
25
+ { id: 'system', label: 'Settings', plain: 'Pricing, settings, maintenance, and this table.', icon: 'gear' },
26
26
  ];
27
27
  export const CAPABILITIES = [
28
28
  // ---- Spend --------------------------------------------------------------
@@ -32,37 +32,64 @@ export const CAPABILITIES = [
32
32
  guiEvidence: { file: 'core/api.ts', token: "{ id: '7d', label: '7 days'", note: 'the spend range picker; the Spend view reads the same overview for the same window' },
33
33
  },
34
34
  {
35
- id: 'month', label: 'This month', plain: 'The current month of spend.', territory: 'spend', consequence: 'read', coverage: 'partial', command: 'segreant month',
36
- guiEvidence: { file: 'core/api.ts', token: "{ id: '30d', label: '30 days'", note: 'the picker offers the last 30 days, not the calendar month the command reports; partial, not full' },
35
+ id: 'month', label: 'Last 30 days', plain: 'Spend over the last 30 days, rolling. The Statement shows a calendar month.', territory: 'spend', consequence: 'read', coverage: 'full', command: 'segreant month',
36
+ guiEvidence: { file: 'core/api.ts', token: "{ id: '30d', label: '30 days'", note: 'the spend range picker; `segreant month` is the same rolling 30 days, and the Spend view reads the same overview for that window' },
37
37
  },
38
- { id: 'usage', label: 'Usage detail', plain: 'Requests and tokens broken down.', territory: 'spend', consequence: 'read', coverage: 'full', command: 'segreant usage' },
39
- { id: 'report', label: 'Period report', plain: 'A summary you can hand to someone.', territory: 'spend', consequence: 'read', coverage: 'partial', command: 'segreant report' },
40
- { id: 'export', label: 'Export CSV', plain: 'Download the ledger as a spreadsheet.', territory: 'spend', consequence: 'read', coverage: 'full', command: 'segreant export' },
38
+ { id: 'usage', label: 'Chat and research value', plain: 'What chat and research sessions produced, from the outcomes you reported. No code needed.', territory: 'spend', consequence: 'read', coverage: 'full', command: 'segreant usage' },
39
+ // Was listed as "Period report: a summary you can hand to someone". The command records an
40
+ // outcome signal; the month summary is the Statement page.
41
+ { id: 'report', label: 'Report an outcome by hand', plain: 'Mark a commit or session as tested, merged, shipped or redone, so realized value can count it.', territory: 'value', consequence: 'local', coverage: 'planned', command: 'segreant report --kind <kind>' },
42
+ { id: 'export', label: 'Export CSV', plain: 'Download the last 30 days of requests as CSV; --all for everything.', territory: 'spend', consequence: 'read', coverage: 'full', command: 'segreant export' },
43
+ { id: 'quota', label: 'Quota', plain: "The vendors' usage meters, the pace to their reset, and whether they match what Segreant counted.", territory: 'spend', consequence: 'read', coverage: 'planned', command: 'segreant quota' },
44
+ { id: 'statusline', label: 'Claude status line', plain: "Records Claude Code's live usage meter from its status line.", territory: 'spend', consequence: 'local', coverage: 'planned', command: 'segreant statusline' },
45
+ { id: 'home', label: 'Home', plain: 'The last 30 days, your plans, and how much of the AI work stayed in the code.', territory: 'spend', consequence: 'read', coverage: 'planned', command: 'segreant home' },
46
+ { id: 'plan', label: 'Plans', plain: 'Which plan each tool is on, the monthly price you entered, and the list-price work it did.', territory: 'spend', consequence: 'local', coverage: 'planned', command: 'segreant plan' },
41
47
  // ---- Control ------------------------------------------------------------
42
48
  { id: 'budget', label: 'Budgets', plain: 'Set a spending cap and see how close you are.', territory: 'control', consequence: 'local', coverage: 'full', command: 'segreant budget' },
43
- { id: 'budget-recommend', label: 'Suggest a budget', plain: 'Propose a cap from your actual history.', territory: 'control', consequence: 'read', coverage: 'partial', command: 'segreant budget --recommend' },
44
- { id: 'alerts', label: 'Alerts', plain: 'Get told before a cap is hit, not after.', territory: 'control', consequence: 'local', coverage: 'partial', command: 'segreant alerts' },
45
- { id: 'exec', label: 'Run under a cap', plain: 'Run a command with a hard spending limit around it.', territory: 'control', consequence: 'local', coverage: 'planned', command: 'segreant exec -- <command>' },
46
- { id: 'launch', label: 'Start a tool metered', plain: 'Start an AI tool pointed at Segreant, only while Segreant is running.', territory: 'control', consequence: 'local', coverage: 'planned', command: 'segreant launch -- <command>' },
49
+ { id: 'budget-recommend', label: 'Suggest a budget', plain: 'Propose a daily cap from your history. With --apply it writes the cap to your config.', territory: 'control', consequence: 'read', coverage: 'partial', command: 'segreant budget --recommend' },
50
+ { id: 'alerts', label: 'Alerts', plain: 'Alerts as spend nears a cap. With --notify they are also posted to a webhook you set.', territory: 'control', consequence: 'local', coverage: 'partial', command: 'segreant alerts' },
51
+ { id: 'exec', label: 'Record a test run', plain: 'Run a command, such as your tests, and record whether it passed for that commit or session. It sets no spending limit.', territory: 'control', consequence: 'local', coverage: 'planned', command: 'segreant exec -- <command>' },
52
+ { id: 'launch', label: 'Start a tool metered', plain: 'Start an AI tool pointed at Segreant. If Segreant is down, it refuses when a cap is set and runs unmetered otherwise, and says so.', territory: 'control', consequence: 'local', coverage: 'planned', command: 'segreant launch -- <command>' },
47
53
  // ---- Allocation ---------------------------------------------------------
48
54
  { id: 'project', label: 'Projects', plain: 'Which project each request belongs to, and how we know.', territory: 'allocation', consequence: 'read', coverage: 'full', command: 'segreant project --coverage' },
49
55
  { id: 'project-alias', label: 'Merge project names', plain: 'Treat two names as the same project.', territory: 'allocation', consequence: 'local', coverage: 'planned', command: 'segreant project alias' },
50
- { id: 'alloc-centres', label: 'Cost centres', plain: 'The teams or budgets that carry the cost.', territory: 'allocation', consequence: 'local', coverage: 'partial', command: 'segreant alloc centre' },
51
- { id: 'alloc-rules', label: 'Allocation rules', plain: 'How spend is split across cost centres.', territory: 'allocation', consequence: 'local', coverage: 'partial', command: 'segreant alloc rule' },
52
- { id: 'alloc-run', label: 'Run an allocation', plain: 'Apply the rules to a period and record the result.', territory: 'allocation', consequence: 'local', coverage: 'partial', command: 'segreant alloc run --apply' },
56
+ {
57
+ id: 'alloc-centres', label: 'Cost centres', plain: 'The teams or budgets that carry the cost.', territory: 'allocation', consequence: 'local', coverage: 'full', command: 'segreant alloc centre',
58
+ guiEvidence: { file: 'views/allocation.ts', token: "op: 'centre'", note: 'Allocate step 1 lists, adds and archives cost centres; the drawer also renames one or changes its owner, as alloc centre <id> --name does, with a preview first' },
59
+ },
60
+ {
61
+ id: 'alloc-rules', label: 'Allocation rules', plain: 'How spend is split across cost centres.', territory: 'allocation', consequence: 'local', coverage: 'full', command: 'segreant alloc rule',
62
+ guiEvidence: { file: 'views/allocation.ts', token: "op: 'rule'", note: 'all three methods, every match field (project, model, provider, tool, user), priority, effective dates, owner and note; a GUI rule equals the CLI rule field by field; the drawer previews a rule or a revocation before saving' },
63
+ },
64
+ {
65
+ id: 'alloc-run', label: 'Run an allocation', plain: 'Apply the rules to a period and record the result.', territory: 'allocation', consequence: 'local', coverage: 'full', command: 'segreant alloc run --apply',
66
+ guiEvidence: { file: 'views/allocation.ts', token: "op: 'run'", note: 'preview then record for last month, this month or any chosen days, on the Allocate page or in its drawer; each recorded split downloads as CSV (segreant alloc export). Periods are whole days; the command also takes times of day' },
67
+ },
53
68
  // ---- Evidence -----------------------------------------------------------
54
- { id: 'billing-scope', label: 'Declare a provider scope', plain: 'Say which provider project this machine is metering.', territory: 'evidence', consequence: 'local', coverage: 'partial', command: 'segreant billing scope set' },
55
- { id: 'billing-readiness', label: 'Reconciliation readiness', plain: 'Whether a provider check would actually match anything.', territory: 'evidence', consequence: 'read', coverage: 'full', command: 'segreant billing reconcile' },
56
- { id: 'billing-adopt', label: 'Adopt a provider export', plain: 'Use a costs file you exported, with no credential.', territory: 'evidence', consequence: 'local', coverage: 'partial', command: 'segreant billing openai-costs adopt --apply' },
57
69
  {
58
- id: 'billing-pull', label: 'Pull provider costs', plain: 'Read your bill directly from the provider.',
70
+ id: 'billing-scope', label: 'Declare a provider scope', plain: 'Declare the OpenAI organisation and project your proxied traffic goes to from now on. Your declaration, not verified by the provider.', territory: 'evidence', consequence: 'local', coverage: 'full', command: 'segreant billing scope set',
71
+ guiEvidence: { file: 'views/evidence.ts', token: "op: 'scope'", note: 'Close step 1 names the declaration, previews and declares, and clears it with a preview first; the drawer does the same with the project optional, as the command allows' },
72
+ },
73
+ {
74
+ id: 'billing-readiness', label: 'Reconciliation readiness', plain: 'Whether a provider check would actually match anything.', territory: 'evidence', consequence: 'read', coverage: 'full', command: 'segreant billing reconcile',
75
+ guiEvidence: { file: 'views/evidence.ts', token: 'readinessPanel(d)', note: 'Close step 3 shows readiness and the steps still missing before it offers a comparison' },
76
+ },
77
+ {
78
+ id: 'billing-adopt', label: 'Adopt a provider export', plain: 'Use a cost export you already imported as the provider side of the comparison. No credential is used.', territory: 'evidence', consequence: 'local', coverage: 'full', command: 'segreant billing openai-costs adopt --apply',
79
+ guiEvidence: { file: 'views/evidence.ts', token: "op: 'adopt'", note: 'Close step 2 imports an export and adopts it, each with a preview first; the drawer adopts any imported export by id, as --import-id does' },
80
+ },
81
+ {
82
+ id: 'billing-pull', label: 'Pull provider costs', plain: 'Fetch the costs OpenAI reports for a period, using an Admin key. A provider observation, not yet reconciled.',
59
83
  territory: 'evidence', consequence: 'credential', coverage: 'partial', command: 'segreant billing openai-costs pull',
60
84
  warning: 'Reads an OpenAI Admin credential from your environment and makes a network request to OpenAI. Segreant never stores it. Check readiness first — on a ledger with no proxy traffic on the declared route, a pull reports your entire bill as unexplained.',
61
85
  },
62
- { id: 'billing-reconcile', label: 'Reconcile', plain: 'Compare what we metered against what you were billed.', territory: 'evidence', consequence: 'local', coverage: 'partial', command: 'segreant billing reconcile --apply' },
63
- { id: 'receipt', label: 'Receipts', plain: 'The evidence behind a single claim.', territory: 'evidence', consequence: 'read', coverage: 'planned', command: 'segreant receipt' },
64
- { id: 'evidence', label: 'Evidence records', plain: 'Signed CI artifacts and verified outcomes.', territory: 'evidence', consequence: 'read', coverage: 'partial', command: 'segreant evidence' },
65
- { id: 'audit', label: 'Audit', plain: 'Check the ledger against itself for inconsistencies.', territory: 'evidence', consequence: 'read', coverage: 'planned', command: 'segreant audit' },
86
+ {
87
+ id: 'billing-reconcile', label: 'Reconcile', plain: 'Compare what Segreant measured with the provider-reported total, project by day, for the declared scope only.', territory: 'evidence', consequence: 'local', coverage: 'full', command: 'segreant billing reconcile --apply',
88
+ guiEvidence: { file: 'views/evidence.ts', token: "op: 'reconcile'", note: 'Close step 3 previews with a materiality threshold and records under it; every recorded comparison opens to its own figures; the drawer previews and records the same comparison under the same threshold' },
89
+ },
90
+ { id: 'receipt', label: 'Receipts', plain: 'Sign and store a receipt for each matured piece of work, creating the signing key on first use; --verify checks one.', territory: 'evidence', consequence: 'local', coverage: 'planned', command: 'segreant receipt' },
91
+ { id: 'evidence', label: 'Evidence records', plain: 'Signed CI test evidence: emit writes a signed artifact, import records it as a verified outcome.', territory: 'evidence', consequence: 'read', coverage: 'partial', command: 'segreant evidence' },
92
+ { id: 'audit', label: 'Audit', plain: 'Attribute recent spend to git commits by timing, a heuristic, and show cost per hundred lines.', territory: 'evidence', consequence: 'read', coverage: 'planned', command: 'segreant audit' },
66
93
  // ---- Value --------------------------------------------------------------
67
94
  { id: 'roi', label: 'Return on Intelligence', plain: 'What the spend produced, with the limits stated.', territory: 'value', consequence: 'read', coverage: 'full', command: 'segreant roi' },
68
95
  {
@@ -74,27 +101,27 @@ export const CAPABILITIES = [
74
101
  id: 'causal', label: 'Causal studies', plain: 'Registered randomized evidence and its qualification gates.', territory: 'value', consequence: 'local', coverage: 'partial', command: 'segreant causal status',
75
102
  guiEvidence: { file: 'views/value.ts', token: 'api.causal()', note: 'the Value view reads causal status; registration and gates stay on the CLI' },
76
103
  },
77
- { id: 'realize', label: 'Realized value', plain: 'Work that actually shipped, not work that was proposed.', territory: 'value', consequence: 'local', coverage: 'partial', command: 'segreant realize' },
104
+ { id: 'realize', label: 'Realized value', plain: 'Work that passed every required step, from commit to surviving in the code, measured from git and the signals you report.', territory: 'value', consequence: 'local', coverage: 'partial', command: 'segreant realize' },
78
105
  {
79
- id: 'market', label: 'Public model market', plain: 'Compare models on public evidence of quality per dollar, before you have your own.',
106
+ id: 'market', label: 'Public model market', plain: 'Compare models on public evidence of quality per dollar, before you have your own. --refresh fetches the public sources over the network.',
80
107
  territory: 'value', consequence: 'read', coverage: 'partial', command: 'segreant market',
81
108
  guiEvidence: { file: 'views/value.ts', token: 'marketCard(', note: 'the Value view shows the bundled public boards; refreshing a source stays on the CLI because it needs an egress grant' },
82
109
  },
83
- { id: 'frontier', label: 'Model comparison', plain: 'Whether a cheaper model would have done the same job.', territory: 'value', consequence: 'read', coverage: 'full', command: 'segreant frontier' },
84
- { id: 'saved', label: 'Savings', plain: 'What routing decisions have avoided so far.', territory: 'value', consequence: 'read', coverage: 'partial', command: 'segreant saved' },
85
- { id: 'yield', label: 'Yield', plain: 'Output per dollar across projects.', territory: 'value', consequence: 'read', coverage: 'planned', command: 'segreant yield' },
86
- { id: 'judge', label: 'Judge', plain: 'Score a change on quality, not just cost.', territory: 'value', consequence: 'local', coverage: 'partial', command: 'segreant judge' },
87
- { id: 'team', label: 'Team view', plain: 'Per-person value on this machine, anonymized.', territory: 'value', consequence: 'read', coverage: 'planned', command: 'segreant team' },
110
+ { id: 'frontier', label: 'Model comparison', plain: 'How models compare on realized return within similar work, with cheaper-model trials to review. Routing is never changed.', territory: 'value', consequence: 'read', coverage: 'full', command: 'segreant frontier' },
111
+ { id: 'saved', label: 'Time reclaimed', plain: 'The manual work-weeks your realized work would have taken, against the AI time measured.', territory: 'value', consequence: 'read', coverage: 'partial', command: 'segreant saved' },
112
+ { id: 'yield', label: 'Yield', plain: 'Code lines still in one repository per AI dollar. Not a measure of quality.', territory: 'value', consequence: 'read', coverage: 'planned', command: 'segreant yield' },
113
+ { id: 'judge', label: 'Judge', plain: 'Score one session\'s efficiency from its structure. A hosted judge, if you configure one, receives session data.', territory: 'value', consequence: 'local', coverage: 'partial', command: 'segreant judge' },
114
+ { id: 'team', label: 'Team view', plain: 'Team value as a distribution. Per-person figures stay off unless you enable them.', territory: 'value', consequence: 'read', coverage: 'planned', command: 'segreant team' },
88
115
  // ---- Data ---------------------------------------------------------------
89
116
  { id: 'sources', label: 'Sources', plain: 'Which tools are feeding data in.', territory: 'data', consequence: 'read', coverage: 'full', command: 'segreant sources' },
90
117
  // `core/api.ts` binds POST /api/discover and no view calls it (D-239): an
91
118
  // API binding is not a screen.
92
- { id: 'discover', label: 'Find tools', plain: 'Look for AI tools installed on this machine.', territory: 'data', consequence: 'read', coverage: 'planned', command: 'segreant discover' },
119
+ { id: 'discover', label: 'Match projects to repositories', plain: 'Match imported projects to their git repositories and measure each one.', territory: 'data', consequence: 'read', coverage: 'planned', command: 'segreant discover' },
93
120
  { id: 'connect', label: 'Connect a tool', plain: 'Point a tool at Segreant so its spend is metered.', territory: 'data', consequence: 'local', coverage: 'partial', command: 'segreant connect <tool>' },
94
121
  { id: 'import', label: 'Import history', plain: 'Read past usage out of tool logs on this machine.', territory: 'data', consequence: 'local', coverage: 'full', command: 'segreant import all' },
95
- { id: 'scan', label: 'Scan', plain: 'Check what is available to import before importing.', territory: 'data', consequence: 'read', coverage: 'full', command: 'segreant scan' },
96
- { id: 'baseline', label: 'Baselines', plain: 'The before-AI reference this compares against.', territory: 'data', consequence: 'local', coverage: 'partial', command: 'segreant baseline' },
97
- { id: 'demo', label: 'Demo data', plain: 'Load labelled sample data to see how it works.', territory: 'data', consequence: 'local', coverage: 'partial', command: 'segreant demo' },
122
+ { id: 'scan', label: 'Scan', plain: 'Find AI tools and repositories on this machine before importing. It remembers the scan to report changes; --setup also imports.', territory: 'data', consequence: 'read', coverage: 'full', command: 'segreant scan' },
123
+ { id: 'baseline', label: 'Baselines', plain: 'The before-AI reference this compares against. --refresh fetches a baseline from a URL you give.', territory: 'data', consequence: 'local', coverage: 'partial', command: 'segreant baseline' },
124
+ { id: 'demo', label: 'Demo data', plain: 'Load labelled sample data to see how it works, replacing any earlier sample data. Your own ledger is untouched.', territory: 'data', consequence: 'local', coverage: 'partial', command: 'segreant demo' },
98
125
  // ---- System -------------------------------------------------------------
99
126
  {
100
127
  id: 'features', label: 'Optional features', plain: 'Switch optional parts of Segreant on or off. Budget caps are not one of them.',
@@ -107,14 +134,20 @@ export const CAPABILITIES = [
107
134
  guiEvidence: { file: 'core/actions.ts', token: 'settings.egress.receipts', note: 'the Settings drawer renders egress mode and receipt-chain health; plan/apply stay on the CLI' },
108
135
  warning: 'The dashboard shows Segreant-process status and receipt-chain health. Use the CLI to plan or apply an exact cloud permission. This is not a machine-wide firewall or a provider-retention guarantee.',
109
136
  },
110
- { id: 'settings', label: 'Settings', plain: 'How Segreant behaves on this machine.', territory: 'system', consequence: 'local', coverage: 'partial', command: 'segreant config' },
137
+ { id: 'settings', label: 'Settings', plain: 'Show how Segreant is configured on this machine and where that is stored.', territory: 'system', consequence: 'local', coverage: 'partial', command: 'segreant config' },
111
138
  {
112
- id: 'pricing', label: 'Pricing', plain: 'The rate cards used to estimate cost.', territory: 'system', consequence: 'read', coverage: 'partial', command: 'segreant pricing --coverage',
139
+ id: 'pricing', label: 'Pricing', plain: 'The rate cards used to estimate cost. --refresh fetches a price file over the network.', territory: 'system', consequence: 'read', coverage: 'partial', command: 'segreant pricing --coverage',
113
140
  guiEvidence: { file: 'core/claimLayers.ts', token: 'pricing.estimatedSpendShare', note: 'the claim layers show the estimated-spend share the rate cards imply; the cards themselves are CLI only' },
114
141
  },
142
+ {
143
+ id: 'pricing-unknown', label: 'Unpriced models', plain: 'Find models that still use guessed prices. Set your price, price like an exact model on the card, mark a local model free, or skip.',
144
+ territory: 'system', consequence: 'local', coverage: 'full', command: 'segreant pricing unknown',
145
+ guiEvidence: { file: 'core/pricingActions.ts', token: 'pricingUnknownAction', note: 'lists current guesses and declares one model at a time, then refreshes and offers re-pricing' },
146
+ },
115
147
  {
116
148
  id: 'reprice', label: 'Reprice history', plain: 'Recalculate past costs against a corrected rate card.',
117
- territory: 'system', consequence: 'destructive', coverage: 'planned', command: 'segreant reprice --apply',
149
+ territory: 'system', consequence: 'destructive', coverage: 'full', command: 'segreant reprice --apply',
150
+ guiEvidence: { file: 'core/pricingActions.ts', token: 'repriceAction', note: 'previews before and after by model, requires typed confirmation, and shows the applied result' },
118
151
  warning: 'Rewrites the recorded cost of past requests. Value snapshots are re-attributed on their own basis, and outcomes are never moved — but the money figures you have already reported will change.',
119
152
  },
120
153
  { id: 'doctor', label: 'Doctor', plain: 'Check that everything is wired up correctly.', territory: 'system', consequence: 'read', coverage: 'partial', command: 'segreant doctor' },
@@ -147,13 +180,13 @@ export const CAPABILITIES = [
147
180
  // compared this list with `src/cli.ts`'s dispatch, so a verb could be added
148
181
  // without a row and the parity denominator would simply not count it.
149
182
  {
150
- id: 'start', label: 'Start Segreant', plain: 'Run the metering proxy and open this dashboard.',
183
+ id: 'start', label: 'Start Segreant', plain: 'Run the metering proxy and this dashboard. Each start also deletes proposals past retention, and refreshes prices if you turned that on.',
151
184
  territory: 'system', consequence: 'local', coverage: 'not_applicable', command: 'segreant start',
152
185
  coverageNote: 'this command is what serves the GUI, so there is no page to click until it has already run; '
153
186
  + 'a GUI surface for it is not planned because it cannot exist',
154
187
  },
155
188
  {
156
- // `local` rather than `read` because `cmdInit` calls `saveConfig` and the
189
+ // `local` rather than `read` because `cmdInit` calls `mutateConfig` and the
157
190
  // file is on disk afterwards. Worth stating plainly: it persists with no
158
191
  // preview and no `--apply`, which is the one command in this list that does
159
192
  // -- the tier recorded here is what any future GUI surface must honour, not
@@ -162,16 +195,16 @@ export const CAPABILITIES = [
162
195
  territory: 'system', consequence: 'local', coverage: 'planned', command: 'segreant init',
163
196
  },
164
197
  {
165
- id: 'economic', label: 'Exact economic ledger', plain: 'The exact-money ledger behind the rounded figures, by period.',
198
+ id: 'economic', label: 'Exact economic ledger', plain: 'The exact-money ledger behind the rounded figures, by period. --finalize and --reopen record a period close.',
166
199
  territory: 'evidence', consequence: 'read', coverage: 'partial', command: 'segreant economic',
167
200
  guiEvidence: { file: 'views/value.ts', token: 'matured?.economic', note: 'the Value view renders exact/partial/legacy coverage of matured spend; the period ledger itself is CLI only' },
168
201
  },
169
202
  {
170
- id: 'capital', label: 'AI-capital review', plain: 'Review committed capacity, consumption, showback, and declared gaps.',
203
+ id: 'capital', label: 'AI-capital review', plain: 'Review a JSON snapshot you supply: committed capacity, consumption, showback, and declared gaps.',
171
204
  territory: 'evidence', consequence: 'read', coverage: 'planned', command: 'segreant capital evaluate --options <file>',
172
205
  },
173
206
  {
174
- id: 'backup', label: 'Back up the ledger', plain: 'Copy the local ledger somewhere safe, with a manifest that proves the copy.',
207
+ id: 'backup', label: 'Back up the ledger', plain: 'Copy the whole ledger to a file, with a checksum manifest. The copy is not encrypted; keep it as private as the ledger.',
175
208
  territory: 'data', consequence: 'local', coverage: 'planned', command: 'segreant backup --out <file>',
176
209
  },
177
210
  {
@@ -185,11 +218,11 @@ export const CAPABILITIES = [
185
218
  territory: 'system', consequence: 'read', coverage: 'planned', command: 'segreant diagnostics',
186
219
  },
187
220
  {
188
- id: 'pack', label: 'Export a verifiable record bundle', plain: 'Write the kernel records as a signed, digest-bound file another machine can check without trusting this one. Truth is never evaluated.',
221
+ id: 'pack', label: 'Export a verifiable record bundle', plain: 'Write the kernel records as a digest-bound file another machine can check without trusting this one, signed with --sign. Truth is never evaluated.',
189
222
  territory: 'data', consequence: 'local', coverage: 'planned', command: 'segreant pack export --out <file>',
190
223
  },
191
224
  {
192
- id: 'plugin', label: 'Run a plugin exchange', plain: 'One bounded exchange with a plugin process; what it reports is recorded as what it said, never as more.',
225
+ id: 'plugin', label: 'Run a plugin exchange', plain: 'Run a plugin program you name, with your permissions and no sandbox, for one bounded exchange. What it reports is recorded as what it said, never as more.',
193
226
  territory: 'data', consequence: 'local', coverage: 'planned', command: 'segreant plugin run --manifest <file> --request <file> --exec <path> --scope <key=value>',
194
227
  },
195
228
  ];
@@ -208,7 +241,7 @@ const API_BINDINGS = Object.freeze({
208
241
  roi: ['/api/value'], market: ['/api/market'], outcome: ['/api/outcome/record', '/api/value'], causal: ['/api/causal'], realize: ['/api/value'], frontier: ['/api/value'], saved: ['/api/value'], yield: ['/api/value'], judge: ['/api/judge'], team: ['/api/value'],
209
242
  sources: ['/api/overview'], discover: ['/api/importers'], connect: ['/api/importers'], import: ['/api/import'], scan: ['/api/scan'], baseline: ['/api/value'], demo: ['/api/overview'],
210
243
  economic: ['/api/economic'],
211
- features: ['/api/settings', '/api/settings/update'], egress: ['/api/settings'], settings: ['/api/settings', '/api/settings/update'], pricing: ['/api/overview'], reprice: ['/api/value'], doctor: ['/api/guide'], guide: ['/api/guide'], 'team-push': [], prune: ['/api/settings'], 'clear-proposals': ['/api/settings/clear-proposals'],
244
+ features: ['/api/settings', '/api/settings/update'], egress: ['/api/settings'], settings: ['/api/settings', '/api/settings/update'], pricing: ['/api/overview'], 'pricing-unknown': ['/api/pricing/unknown', '/api/pricing/declare', '/api/pricing/unset'], reprice: ['/api/reprice'], doctor: ['/api/guide'], guide: ['/api/guide'], 'team-push': [], prune: ['/api/settings'], 'clear-proposals': ['/api/settings/clear-proposals'],
212
245
  });
213
246
  const DOC_BINDINGS = Object.freeze({
214
247
  spend: ['docs/GETTING-STARTED.md', 'docs/ARCHITECTURE.md'],
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The allocation engine's id format (src/alloc/rules.ts: lowercase letters,
3
+ * digits and . _ -, at most 64 characters), derived from a typed name. One
4
+ * copy, so the Allocate page and the cost-centre drawer cannot derive different
5
+ * ids from the same name.
6
+ */
7
+ export function slug(name) {
8
+ return name.toLowerCase().trim().replace(/[^a-z0-9._-]+/g, '-').replace(/^[^a-z0-9]+/, '').replace(/-+$/, '').slice(0, 64);
9
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Appearance: follow the operating system, or a fixed light or dark choice.
3
+ *
4
+ * The stylesheet keys every light-theme rule on `:root[data-theme='light']`, so
5
+ * the theme is resolved here and written to that attribute, rather than
6
+ * repeating the token block under a media query. "System" is the default and
7
+ * tracks the OS live; an explicit choice is kept in this browser only.
8
+ */
9
+ import { signal } from './signal.js';
10
+ const STORAGE_KEY = 'segreant.theme';
11
+ function storedChoice() {
12
+ try {
13
+ const value = localStorage.getItem(STORAGE_KEY);
14
+ return value === 'light' || value === 'dark' ? value : 'system';
15
+ }
16
+ catch {
17
+ return 'system';
18
+ }
19
+ }
20
+ export const themeChoice = signal(storedChoice());
21
+ const prefersLight = typeof matchMedia === 'function' ? matchMedia('(prefers-color-scheme: light)') : null;
22
+ function apply() {
23
+ const choice = themeChoice();
24
+ const resolved = choice === 'system' ? (prefersLight?.matches ? 'light' : 'dark') : choice;
25
+ document.documentElement.dataset.theme = resolved;
26
+ // Native controls, scrollbars and autofill follow the page, not the OS.
27
+ document.documentElement.style.colorScheme = resolved;
28
+ }
29
+ export function setTheme(choice) {
30
+ themeChoice.set(choice);
31
+ try {
32
+ if (choice === 'system')
33
+ localStorage.removeItem(STORAGE_KEY);
34
+ else
35
+ localStorage.setItem(STORAGE_KEY, choice);
36
+ }
37
+ catch {
38
+ // Storage can be unavailable (private windows, blocked site data). The
39
+ // choice still applies for this visit.
40
+ }
41
+ apply();
42
+ }
43
+ /** Run once before the first render, so the page never paints in the wrong theme. */
44
+ export function initTheme() {
45
+ apply();
46
+ prefersLight?.addEventListener('change', () => { if (themeChoice() === 'system')
47
+ apply(); });
48
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * What you see: density, and which parts of the page are shown.
3
+ *
4
+ * The same screens serve a finance reader who wants the four figures and the
5
+ * explanations, and a developer who lives in two jobs and wants them dense.
6
+ * These are display choices only: hiding a part never changes a figure, and
7
+ * everything hidden stays reachable from Settings and by its address. Kept in
8
+ * this browser only, like the theme.
9
+ */
10
+ import { signal } from './signal.js';
11
+ const STORAGE_KEY = 'segreant.view';
12
+ export const VIEW_DEFAULTS = { density: 'comfortable', strip: true, intros: true, hiddenJobs: [] };
13
+ /** Anything unreadable or out of range falls back to the default, field by field. */
14
+ export function parseViewPrefs(raw) {
15
+ if (!raw)
16
+ return { ...VIEW_DEFAULTS, hiddenJobs: [] };
17
+ try {
18
+ const v = JSON.parse(raw);
19
+ return {
20
+ density: v.density === 'compact' ? 'compact' : 'comfortable',
21
+ strip: v.strip !== false,
22
+ intros: v.intros !== false,
23
+ hiddenJobs: Array.isArray(v.hiddenJobs) ? v.hiddenJobs.filter((j) => typeof j === 'string') : [],
24
+ };
25
+ }
26
+ catch {
27
+ return { ...VIEW_DEFAULTS, hiddenJobs: [] };
28
+ }
29
+ }
30
+ function stored() {
31
+ try {
32
+ return parseViewPrefs(localStorage.getItem(STORAGE_KEY));
33
+ }
34
+ catch {
35
+ return { ...VIEW_DEFAULTS, hiddenJobs: [] };
36
+ }
37
+ }
38
+ export const viewPrefs = signal(stored());
39
+ function apply() {
40
+ const p = viewPrefs();
41
+ // Reached through globalThis so the parsing rules can be unit-tested under Node, which has no document.
42
+ const doc = globalThis.document;
43
+ if (!doc)
44
+ return;
45
+ const root = doc.documentElement.dataset;
46
+ root.density = p.density;
47
+ root.intros = p.intros ? 'on' : 'off';
48
+ }
49
+ export function setView(patch) {
50
+ viewPrefs.set({ ...viewPrefs(), ...patch });
51
+ try {
52
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(viewPrefs()));
53
+ }
54
+ catch {
55
+ // Storage can be unavailable; the choice still applies for this visit.
56
+ }
57
+ apply();
58
+ }
59
+ export function setJobShown(id, shown) {
60
+ const rest = viewPrefs().hiddenJobs.filter((j) => j !== id);
61
+ setView({ hiddenJobs: shown ? rest : [...rest, id] });
62
+ }
63
+ export function jobShown(id) {
64
+ return !viewPrefs().hiddenJobs.includes(id);
65
+ }
66
+ /** Run once before the first render, so the page never paints at the wrong density. */
67
+ export function initView() {
68
+ apply();
69
+ }