@davesheffer/hunch 1.19.0 → 1.20.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,6 +28,7 @@ import { collectCorrectionStageSources } from "../extractors/correctionSources.j
28
28
  import { buildDeliveryEnvelope } from "../core/delivery.js";
29
29
  import { armExecutionObligations, loadPipelineState, savePipelineState } from "../core/pipeline.js";
30
30
  import { recordServed } from "../core/served.js";
31
+ import { EdgeSchema, ResourceSchema } from "../core/types.js";
31
32
  import { compareCandidates } from "../core/compare.js";
32
33
  import { checkConformance } from "../core/conformance.js";
33
34
  import { ConstitutionService, policyEvaluationEnvelope } from "../constitution/service.js";
@@ -144,13 +145,45 @@ const EXECUTION_OBLIGATION_SCHEMA = z.object({
144
145
  output_excludes: z.array(z.string()).optional(),
145
146
  }),
146
147
  });
148
+ const LANDSCAPE_SELECTION_RECEIPT_SCHEMA = z.object({
149
+ selectionReason: z.enum(["exact-target", "task-match", "orientation-root", "graph-neighbor", "graph-connection"]),
150
+ selectionRank: z.number().int().positive(),
151
+ rank: z.number().int().positive(),
152
+ deliveryReason: z.literal("ranked"),
153
+ required: z.literal(false),
154
+ blocking: z.literal(false),
155
+ provenanceStatus: z.literal("current"),
156
+ tokenCost: z.number().int().positive(),
157
+ });
158
+ const LANDSCAPE_FRAGMENT_SCHEMA = z.object({
159
+ schema: z.literal("hunch.landscape-fragment/1"),
160
+ authority: z.literal("human_confirmed"),
161
+ target: z.string(),
162
+ resources: z.array(LANDSCAPE_SELECTION_RECEIPT_SCHEMA.extend({ record: ResourceSchema })),
163
+ relationships: z.array(LANDSCAPE_SELECTION_RECEIPT_SCHEMA.extend({
164
+ selectionReason: z.literal("graph-connection"),
165
+ record: EdgeSchema,
166
+ })),
167
+ omitted: z.array(z.object({
168
+ kind: z.enum(["resources", "relationships"]),
169
+ recordId: z.string(),
170
+ reason: z.enum(["budget", "stale-provenance", "endpoint-not-delivered", "landscape-cap"]),
171
+ detail: z.string(),
172
+ })),
173
+ reviewIds: z.array(z.string()),
174
+ discoveryHashes: z.array(z.string()),
175
+ sourceRevisions: z.array(z.string()),
176
+ fragmentHash: z.string(),
177
+ });
147
178
  /** Public MCP shape for the canonical delivery envelope. Keeping the schema on
148
179
  * the tool means orchestrators can consume receipt facts without scraping the
149
180
  * backward-compatible text block. */
150
181
  const DELIVERY_OUTPUT_SCHEMA = z.object({
182
+ schema_version: z.literal("hunch.delivery-envelope/1"),
183
+ receipt_id: z.string().regex(/^hdr_[a-f0-9]{24}$/),
151
184
  text: z.string(),
152
185
  delivered: z.array(z.object({
153
- kind: z.enum(["constraints", "decisions", "bugs", "findings"]),
186
+ kind: z.enum(["constraints", "decisions", "bugs", "findings", "resources", "relationships"]),
154
187
  record_id: z.string(),
155
188
  rank: z.number().int().positive(),
156
189
  delivery_reason: z.enum(["ranked", "blocking-reserved"]),
@@ -178,13 +211,15 @@ const DELIVERY_OUTPUT_SCHEMA = z.object({
178
211
  token_cost: z.number().int().nonnegative(),
179
212
  })),
180
213
  omitted: z.array(z.object({
181
- kind: z.enum(["constraints", "decisions", "bugs", "findings"]),
214
+ kind: z.enum(["constraints", "decisions", "bugs", "findings", "resources", "relationships"]),
182
215
  record_id: z.string(),
183
- reason: z.enum(["budget", "stale-provenance", "retired", "actionability-cap", "low-confidence", "insufficient-context", "low-relevance"]),
216
+ reason: z.enum(["budget", "stale-provenance", "retired", "actionability-cap", "endpoint-not-delivered", "landscape-cap", "low-confidence", "insufficient-context", "low-relevance"]),
184
217
  detail: z.string(),
185
218
  })),
219
+ landscape: LANDSCAPE_FRAGMENT_SCHEMA.nullable(),
186
220
  budget_tokens: z.number().int().nonnegative(),
187
221
  used_chars: z.number().int().nonnegative(),
222
+ accounted_chars: z.number().int().nonnegative(),
188
223
  blocking_overflow: z.boolean(),
189
224
  abstention: z.object({
190
225
  active: z.boolean(),
@@ -775,7 +810,13 @@ export function buildServerWithRootControl(initialRoot) {
775
810
  // Task-phrase input ("improve retrieval ranking") resolves no file/symbol and
776
811
  // used to return an empty brief while the graph held the answer — fall back to
777
812
  // FTS so the assistant always leaves with the closest matches, not a shrug.
778
- const empty = !ctx.constraints.length && !ctx.decisions.length && !ctx.bugs.length && !ctx.blast_radius.length;
813
+ const empty = !ctx.constraints.length &&
814
+ !ctx.decisions.length &&
815
+ !ctx.bugs.length &&
816
+ !ctx.blast_radius.length &&
817
+ !ctx.findings.length &&
818
+ !ctx.landscape?.resources.length &&
819
+ !ctx.landscape?.relationships.length;
779
820
  if (empty && !asOf) {
780
821
  const hits = store.search(target, 8);
781
822
  if (hits.length) {
@@ -25,6 +25,7 @@ import { edgeId } from "../core/ids.js";
25
25
  import { isStrictBlocker, isVetoBlocker } from "../core/strictgate.js";
26
26
  import { effectiveForbids, matchForbids } from "../core/constraintmatch.js";
27
27
  import { analyzeDiff } from "../extractors/diff.js";
28
+ import { selectReviewedLandscape, } from "../core/landscapeDelivery.js";
28
29
  /** Git cannot resolve repository identity from a cwd that does not exist yet.
29
30
  * Probe the nearest real directory so a planned nested overlay cannot evade the
30
31
  * public-repository boundary merely by deferring mkdir until its first write. */
@@ -1666,6 +1667,12 @@ export class HunchStore {
1666
1667
  blast_radius: [...blast.values()].sort((a, b) => a.depth - b.depth).slice(0, 12),
1667
1668
  components: w.components,
1668
1669
  findings: this.liveFindingsFor(target).slice(0, 8),
1670
+ // Landscape records do not yet carry a valid-time window. A historical
1671
+ // query therefore withholds them instead of mixing current graph state
1672
+ // into an as-of memory envelope.
1673
+ landscape: opts.asOf
1674
+ ? undefined
1675
+ : selectReviewedLandscape(this.recs("resources"), this.recs("edges"), target),
1669
1676
  budget_tokens: budget,
1670
1677
  };
1671
1678
  return ctx;
@@ -121,10 +121,15 @@ export async function syncCommit(store, root, sha, opts = {}) {
121
121
  // grounding. No-ops when no assistant CLI is available; never raises trust (dec_9a2f2fe72a).
122
122
  if (wantVerify)
123
123
  draft = await verifyDecisionSafe(await selectVerifier({ root }), input, draft);
124
+ // The draft's own drafter, not merely the one selected — draftDecisionSafe sets
125
+ // fellBackTo when the selected provider threw and it silently substituted the
126
+ // deterministic heuristic. Every telemetry/return below must report THIS, since
127
+ // backfill's LLM-vs-heuristic tally reads it directly off the return value.
128
+ const actualProvider = draft.fellBackTo ?? provider.name;
124
129
  // Advisory synthesis telemetry for `hunch review` — which provider ran, how many drafts
125
130
  // were reconciled, their agreement, and the verifier's grounding. Rides in `evidence`
126
131
  // (no schema change → respects forward-migration invariant con_947c578b2c).
127
- const synthBits = [`provider=${provider.name}`];
132
+ const synthBits = [`provider=${actualProvider}`];
128
133
  if (draft.samples)
129
134
  synthBits.push(`samples=${draft.samples}`);
130
135
  if (draft.agreement != null)
@@ -215,7 +220,7 @@ export async function syncCommit(store, root, sha, opts = {}) {
215
220
  store.putPrivate("decisions", decision);
216
221
  else
217
222
  store.json.put("decisions", decision);
218
- return { status: "written", decision, provider: provider.name };
223
+ return { status: "written", decision, provider: actualProvider };
219
224
  }
220
225
  function putBugInHome(store, bug, home) {
221
226
  return home === "private" ? store.putPrivate("bugs", bug) : store.json.put("bugs", bug);
@@ -244,6 +249,9 @@ export async function recordFailure(store, root, failure, opts = {}) {
244
249
  suspects: suspects.map((s) => `${s.name} @ ${s.file}`),
245
250
  };
246
251
  const draft = await draftBugSafe(provider, input);
252
+ // The draft's own drafter, not merely the one selected — see the identical
253
+ // comment in syncCommit above.
254
+ const actualProvider = draft.fellBackTo ?? provider.name;
247
255
  // Seed the id from the test id (stable), not the LLM title — one bug per test.
248
256
  const id = bugId(failure.test);
249
257
  // recurrence = a DIFFERENT prior bug with a similar symptom (not this same one).
@@ -284,7 +292,7 @@ export async function recordFailure(store, root, failure, opts = {}) {
284
292
  putBugInHome(store, bug, home);
285
293
  }
286
294
  raiseFragility(store, affectedFiles, home);
287
- return { status: "written", bug, constraint, provider: provider.name, touchedHomes: [home] };
295
+ return { status: "written", bug, constraint, provider: actualProvider, touchedHomes: [home] };
288
296
  }
289
297
  /** Orchestrate one `hunch test` run into graph writes: capture each failing test
290
298
  * as a Bug (recordFailure → suspects / recurrence / Constraint promotion), and
@@ -506,19 +514,26 @@ export async function draftDecisionSafe(provider, input) {
506
514
  try {
507
515
  return await provider.draftDecision(input);
508
516
  }
509
- catch {
517
+ catch (error) {
510
518
  // A provider failure (network, bad creds, CLI crash, unparseable output) must
511
519
  // never abort the learning loop — fall back to the deterministic heuristic
512
520
  // draft, which is honestly labeled ("inferred") rather than a hollow llm_draft.
513
- return new DeterministicProvider().draftDecision(input);
521
+ // fellBackTo/fallbackReason tell the caller what ACTUALLY drafted this, not
522
+ // merely what was selected — see syncCommit, which must report this instead
523
+ // of the originally-selected provider's name.
524
+ const draft = await new DeterministicProvider().draftDecision(input);
525
+ return { ...draft, fellBackTo: "deterministic", fallbackReason: error instanceof Error ? error.message : String(error) };
514
526
  }
515
527
  }
516
528
  export async function draftBugSafe(provider, input) {
517
529
  try {
518
530
  return await provider.draftBug(input);
519
531
  }
520
- catch {
521
- return new DeterministicProvider().draftBug(input);
532
+ catch (error) {
533
+ // See draftDecisionSafe above — the same fallback contract applies to bugs,
534
+ // and recordFailure must report the actual (fallback) provider likewise.
535
+ const draft = await new DeterministicProvider().draftBug(input);
536
+ return { ...draft, fellBackTo: "deterministic", fallbackReason: error instanceof Error ? error.message : String(error) };
522
537
  }
523
538
  }
524
539
  //# sourceMappingURL=synthesize.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "1.19.0",
3
+ "version": "1.20.0-rc.2",
4
4
  "mcpName": "io.github.davesheffer/hunch",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
@@ -58,7 +58,7 @@
58
58
  "build": "npm run clean && tsc -p tsconfig.json",
59
59
  "dev": "tsx src/cli/index.ts",
60
60
  "hunch": "tsx src/cli/index.ts",
61
- "test": "tsx --test --test-concurrency=2 test/*.test.ts",
61
+ "test": "tsx --test --test-concurrency=4 test/*.test.ts",
62
62
  "test:e2e:md1": "tsx --test test/md1-e2e.test.ts",
63
63
  "test:e2e:team-matrix": "tsx --test test/team-matrix-e2e.test.ts",
64
64
  "verify:matrix": "node tooling/matrix-release-verification.mjs",
@@ -68,6 +68,7 @@
68
68
  "site:proof": "npm run build && node tooling/generate-public-proof.mjs",
69
69
  "bench:md1": "node tooling/md1-benchmark.mjs",
70
70
  "research:competitors": "node tooling/competitive-watch.mjs",
71
+ "outreach": "node tooling/outreach-pipeline.mjs",
71
72
  "prepublishOnly": "npm run build"
72
73
  },
73
74
  "dependencies": {
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "source": "github"
8
8
  },
9
9
  "websiteUrl": "https://hunch-pi.vercel.app",
10
- "version": "1.19.0",
10
+ "version": "1.20.0-rc.2",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@davesheffer/hunch",
16
- "version": "1.19.0",
16
+ "version": "1.20.0-rc.2",
17
17
  "runtimeHint": "npx",
18
18
  "packageArguments": [
19
19
  {