specpi 0.29.0 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,6 +20,12 @@ import { MAX_STATE_BYTES } from "./sanitize.mjs";
20
20
  * and a TYPESAFE_BASE_URL override) names itself, and so the rule below is true rather than
21
21
  * aspirational: a grant is keyed on this string, so repointing the client really does ask again.
22
22
  */
23
+ export const CONSENT_SCHEMA = 2;
24
+
25
+ export function endpointOrigin() {
26
+ return new URL(baseUrl()).origin;
27
+ }
28
+
23
29
  export function endpointLabel() {
24
30
  const base = baseUrl();
25
31
  try {
@@ -43,12 +49,12 @@ export function loadConsent() {
43
49
  }
44
50
 
45
51
  const stored = JSON.parse(fs.readFileSync(file, "utf8"));
46
- if (stored?.schema !== 1 || stored.granted !== true || typeof stored.endpoint !== "string") {
52
+ if (stored?.schema !== CONSENT_SCHEMA || stored.granted !== true || typeof stored.endpoint !== "string") {
47
53
  return undefined;
48
54
  }
49
55
 
50
56
  // A grant is for the endpoint it was given for. Repointing the client asks again.
51
- return stored.endpoint === endpointLabel() ? stored : undefined;
57
+ return stored.endpoint === endpointLabel() && stored.origin === endpointOrigin() ? stored : undefined;
52
58
  } catch {
53
59
  return undefined;
54
60
  }
@@ -65,9 +71,11 @@ export function saveConsent() {
65
71
  }
66
72
 
67
73
  const stored = {
68
- schema: 1,
74
+ // Earlier consent incorrectly excluded file/command samples. It cannot authorize them.
75
+ schema: CONSENT_SCHEMA,
69
76
  granted: true,
70
77
  endpoint: endpointLabel(),
78
+ origin: endpointOrigin(),
71
79
  maxStateBytes: MAX_STATE_BYTES,
72
80
  grantedAt: new Date().toISOString(),
73
81
  };
@@ -89,15 +97,19 @@ export function consentPath() {
89
97
  return consentFile();
90
98
  }
91
99
 
92
- export const CONSENT_TITLE = "Allow SpecPi to send task summaries to Jev?";
100
+ export const CONSENT_TITLE = "Allow SpecPi to send task summaries and text samples to Jev?";
93
101
 
94
102
  export function consentBody(systemLabel) {
95
103
  return [
96
104
  `${systemLabel} wants to ask TypeSafe's Jev classifier a question about this session.`,
97
105
  "",
98
- `What is sent: a summary object of at most ${MAX_STATE_BYTES} bytes to ${endpointLabel()}, over HTTPS.`,
99
- "It carries tool names, byte counts, relative paths and short descriptions.",
100
- "It never carries file contents, command output, credentials, URLs or session history.",
106
+ `What is sent: a state object of at most ${MAX_STATE_BYTES} bytes to ${endpointLabel()}, with classifier questions.`,
107
+ `Transport: ${new URL(baseUrl()).protocol === "https:" ? "HTTPS" : "not HTTPS (configured endpoint override)"}.`,
108
+ "It may include the current request or objective, relative paths, capability-gap summaries,",
109
+ "and short sampled lines from file contents, command output and fetched content.",
110
+ "Known credential, email, URL and outside-workspace path patterns are redacted before sending.",
111
+ "Redaction is best effort, not a guarantee that every sensitive detail is removed. No session history is read.",
112
+ "Collection stays local unless you separately allow this advisor transmission.",
101
113
  "",
102
114
  "Every call is recorded locally in transmissions.jsonl with a hash of exactly what was sent,",
103
115
  "which you can read with /jev ledger. Turn this off at any time with /jev off.",
@@ -50,12 +50,11 @@
50
50
  // was unreachable, and running the layer could never have revealed it, because a system that
51
51
  // never fires looks exactly like a system whose advice was always to do nothing.
52
52
  //
53
- // compaction's `unresolved_thread` had the same problem at high 0.85: the clearest open
54
- // investigation the fixture can express scores 0.63-0.65. It is lowered to 0.60, which is
55
- // defensible only because of what that branch does -- add one sentence to a summariser prompt
56
- // that is being rebuilt from scratch anyway. It is the cheapest action in the layer, so it can
57
- // afford the loosest gate. gap keeps 0.85 because its Noul reaches 0.96 on the case that matters
58
- // and because a firing there blocks a write.
53
+ // Compaction guidance had the same problem and was lowered to 0.60 to clear it. That system has
54
+ // since been withdrawn -- two tier-6 runs measured the arm carrying it solving fewer long-session
55
+ // tasks than plain SpecPi -- so the loosest gate in the layer is gone with it. gap keeps 0.85
56
+ // because its Noul reaches 0.96 on the case that matters and because a firing there blocks a
57
+ // write.
59
58
 
60
59
  /**
61
60
  * The figures the comment above cites, in a form a test can check against the artifact. A citation
@@ -91,16 +90,6 @@ export const THRESHOLDS = Object.freeze({
91
90
  // Measured 0.06-0.07 on the spent case, so this clears with room.
92
91
  low: 0.1,
93
92
  }),
94
- compaction: Object.freeze({
95
- scoreConfidence: 0.6,
96
- boundary: 0.3,
97
- choiceConfidence: 0.75,
98
- margin: 0.25,
99
- // Lowered from 0.85: the clearest open investigation scores 0.63-0.65, and the action is
100
- // one sentence added to a prompt that is being rebuilt regardless.
101
- high: 0.6,
102
- low: 0.15,
103
- }),
104
93
  gap: Object.freeze({
105
94
  scoreConfidence: 0.6,
106
95
  boundary: 0.3,
@@ -178,14 +167,14 @@ export function thresholdsFor(system) {
178
167
  export function nounTrue(answer, system) {
179
168
  const limits = thresholdsFor(system);
180
169
 
181
- return answer?.kind === "noul" && answer.value >= limits.high;
170
+ return answer?.kind === "noul" && Number.isFinite(answer.value) && answer.value >= limits.high && answer.value <= 1;
182
171
  }
183
172
 
184
173
  /** True when the Noul is confidently no. Not the negation of nounTrue: the middle band is silence. */
185
174
  export function nounFalse(answer, system) {
186
175
  const limits = thresholdsFor(system);
187
176
 
188
- return answer?.kind === "noul" && answer.value <= limits.low;
177
+ return answer?.kind === "noul" && Number.isFinite(answer.value) && answer.value >= 0 && answer.value <= limits.low;
189
178
  }
190
179
 
191
180
  function topTwo(probabilities) {
@@ -204,7 +193,15 @@ function topTwo(probabilities) {
204
193
  * the margin is a live test rather than a branch that silently never runs.
205
194
  */
206
195
  export function choiceValue(answer, system) {
207
- if (answer?.kind !== "choice" || typeof answer.confidence !== "number") {
196
+ if (
197
+ answer?.kind !== "choice" ||
198
+ !Number.isFinite(answer.confidence) ||
199
+ answer.confidence > 1 ||
200
+ !answer.probabilities ||
201
+ Array.isArray(answer.probabilities) ||
202
+ !Object.hasOwn(answer.probabilities, answer.value) ||
203
+ !Object.values(answer.probabilities).every((value) => Number.isFinite(value) && value >= 0 && value <= 1)
204
+ ) {
208
205
  return undefined;
209
206
  }
210
207
 
@@ -214,7 +211,7 @@ export function choiceValue(answer, system) {
214
211
  }
215
212
 
216
213
  const { first, second } = topTwo(answer.probabilities);
217
- if (answer.probabilities && first - second < limits.margin) {
214
+ if (answer.probabilities[answer.value] !== first || first - second < limits.margin) {
218
215
  return undefined;
219
216
  }
220
217
 
@@ -226,7 +223,14 @@ export function choiceValue(answer, system) {
226
223
  * returned level is the rounded band; callers compare against their own rubric.
227
224
  */
228
225
  export function scoreLevel(answer, system) {
229
- if (answer?.kind !== "score" || typeof answer.confidence !== "number") {
226
+ if (
227
+ answer?.kind !== "score" ||
228
+ !Number.isFinite(answer.confidence) ||
229
+ answer.confidence > 1 ||
230
+ !Number.isFinite(answer.value) ||
231
+ answer.value < 0 ||
232
+ answer.value > 9
233
+ ) {
230
234
  return undefined;
231
235
  }
232
236