@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.
- package/README.md +78 -61
- package/dist/cli/index.js +99 -2
- package/dist/core/delivery.js +213 -13
- package/dist/core/landscapeAdoption.js +271 -0
- package/dist/core/landscapeDelivery.js +281 -0
- package/dist/core/types.js +128 -1
- package/dist/extractors/git.js +2 -2
- package/dist/extractors/landscapeDiscovery.js +2217 -280
- package/dist/mcp/server.js +45 -4
- package/dist/store/hunchStore.js +7 -0
- package/dist/synthesis/synthesize.js +22 -7
- package/package.json +3 -2
- package/server.json +2 -2
package/dist/core/types.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* tool inputs. Every record carries `provenance` so nothing is a blind assertion.
|
|
7
7
|
*/
|
|
8
8
|
import { z } from "zod";
|
|
9
|
-
import {
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { findingId, resourceId, resourceRelationshipId } from "./ids.js";
|
|
10
11
|
/** Where a fact came from and how much to trust it. Confidence tiers (DESIGN §4):
|
|
11
12
|
* inferred < extracted < llm_draft < llm_draft+human_confirmed/derived. */
|
|
12
13
|
export const ProvenanceSchema = z.object({
|
|
@@ -485,6 +486,132 @@ export const FindingSchema = z.object({
|
|
|
485
486
|
resolved_commit: z.string().nullable().default(null).describe("the commit that fixed it (set when triage becomes resolved)"),
|
|
486
487
|
provenance: ProvenanceSchema,
|
|
487
488
|
});
|
|
489
|
+
export const LANDSCAPE_DRIFT_CANDIDATE_SCHEMA_VERSION = "hunch.landscape-drift-candidate/1";
|
|
490
|
+
const LANDSCAPE_DRIFT_HASH = /^sha256:[a-f0-9]{64}$/;
|
|
491
|
+
const LANDSCAPE_DRIFT_RECEIPT_ID = /^[a-z][a-z0-9_:-]{2,127}$/;
|
|
492
|
+
/**
|
|
493
|
+
* An external observer's content-addressed mismatch claim. It is intake evidence
|
|
494
|
+
* for an advisory Finding only: it cannot update a Resource, relationship,
|
|
495
|
+
* currentness, decision, constraint, or execution policy.
|
|
496
|
+
*/
|
|
497
|
+
export const LandscapeDriftCandidateSchema = z.object({
|
|
498
|
+
schema: z.literal(LANDSCAPE_DRIFT_CANDIDATE_SCHEMA_VERSION),
|
|
499
|
+
candidateId: z.string().regex(/^ldf_[a-f0-9]{24}$/),
|
|
500
|
+
classification: z.literal("repository_identity_mismatch"),
|
|
501
|
+
resourceId: z.string().min(3).max(512),
|
|
502
|
+
declaredRepositoryId: z.string().min(3).max(512),
|
|
503
|
+
observedRepositoryId: z.string().min(3).max(512),
|
|
504
|
+
observation: z.object({
|
|
505
|
+
providerId: z.string().regex(/^[a-z][a-z0-9-]{0,63}$/),
|
|
506
|
+
observedAt: z.string().min(1).max(64),
|
|
507
|
+
providerReceiptId: z.string().regex(LANDSCAPE_DRIFT_RECEIPT_ID),
|
|
508
|
+
providerReceiptHash: z.string().regex(LANDSCAPE_DRIFT_HASH),
|
|
509
|
+
resolutionReceiptId: z.string().regex(LANDSCAPE_DRIFT_RECEIPT_ID),
|
|
510
|
+
resolutionReceiptHash: z.string().regex(LANDSCAPE_DRIFT_HASH),
|
|
511
|
+
resolutionSetHash: z.string().regex(LANDSCAPE_DRIFT_HASH),
|
|
512
|
+
}).strict(),
|
|
513
|
+
authority: z.literal("finding_candidate"),
|
|
514
|
+
contentHash: z.string().regex(LANDSCAPE_DRIFT_HASH),
|
|
515
|
+
}).strict().superRefine((candidate, ctx) => {
|
|
516
|
+
if (!Number.isFinite(Date.parse(candidate.observation.observedAt))) {
|
|
517
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["observation", "observedAt"], message: "landscape drift observation timestamp is invalid" });
|
|
518
|
+
}
|
|
519
|
+
for (const [field, value] of [
|
|
520
|
+
["resourceId", candidate.resourceId],
|
|
521
|
+
["declaredRepositoryId", candidate.declaredRepositoryId],
|
|
522
|
+
["observedRepositoryId", candidate.observedRepositoryId],
|
|
523
|
+
]) {
|
|
524
|
+
if (!isCredentialFreeText(value)) {
|
|
525
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: [field], message: "landscape drift candidate contains credential material" });
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (candidate.declaredRepositoryId === candidate.observedRepositoryId) {
|
|
529
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "landscape drift candidate must describe a real identity mismatch" });
|
|
530
|
+
}
|
|
531
|
+
const unsigned = landscapeDriftCandidateUnsigned(candidate);
|
|
532
|
+
const contentHash = landscapeDriftCandidateHash(unsigned);
|
|
533
|
+
if (candidate.contentHash !== contentHash || candidate.candidateId !== `ldf_${contentHash.slice(7, 31)}`) {
|
|
534
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "landscape drift candidate seal is invalid" });
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
function landscapeDriftCanonical(value) {
|
|
538
|
+
if (Array.isArray(value))
|
|
539
|
+
return `[${value.map(landscapeDriftCanonical).join(",")}]`;
|
|
540
|
+
if (value && typeof value === "object") {
|
|
541
|
+
return `{${Object.entries(value)
|
|
542
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
543
|
+
.map(([key, child]) => `${JSON.stringify(key)}:${landscapeDriftCanonical(child)}`)
|
|
544
|
+
.join(",")}}`;
|
|
545
|
+
}
|
|
546
|
+
return JSON.stringify(value) ?? "null";
|
|
547
|
+
}
|
|
548
|
+
function landscapeDriftCandidateHash(value) {
|
|
549
|
+
return `sha256:${createHash("sha256").update(landscapeDriftCanonical(value)).digest("hex")}`;
|
|
550
|
+
}
|
|
551
|
+
function landscapeDriftCandidateUnsigned(candidate) {
|
|
552
|
+
return {
|
|
553
|
+
schema: candidate.schema,
|
|
554
|
+
classification: candidate.classification,
|
|
555
|
+
resourceId: candidate.resourceId,
|
|
556
|
+
declaredRepositoryId: candidate.declaredRepositoryId,
|
|
557
|
+
observedRepositoryId: candidate.observedRepositoryId,
|
|
558
|
+
observation: { ...candidate.observation },
|
|
559
|
+
authority: candidate.authority,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
export function createLandscapeDriftCandidate(input) {
|
|
563
|
+
const unsigned = landscapeDriftCandidateUnsigned({
|
|
564
|
+
schema: LANDSCAPE_DRIFT_CANDIDATE_SCHEMA_VERSION,
|
|
565
|
+
classification: "repository_identity_mismatch",
|
|
566
|
+
resourceId: input.resourceId,
|
|
567
|
+
declaredRepositoryId: input.declaredRepositoryId,
|
|
568
|
+
observedRepositoryId: input.observedRepositoryId,
|
|
569
|
+
observation: { ...input.observation },
|
|
570
|
+
authority: "finding_candidate",
|
|
571
|
+
});
|
|
572
|
+
const contentHash = landscapeDriftCandidateHash(unsigned);
|
|
573
|
+
return LandscapeDriftCandidateSchema.parse({
|
|
574
|
+
...unsigned,
|
|
575
|
+
candidateId: `ldf_${contentHash.slice(7, 31)}`,
|
|
576
|
+
contentHash,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
export function assertLandscapeDriftCandidate(value) {
|
|
580
|
+
LandscapeDriftCandidateSchema.parse(value);
|
|
581
|
+
}
|
|
582
|
+
/** Convert one valid external observation into advisory Hunch memory, never graph authority. */
|
|
583
|
+
export function landscapeDriftCandidateFinding(value) {
|
|
584
|
+
const candidate = LandscapeDriftCandidateSchema.parse(value);
|
|
585
|
+
const title = `External repository identity drift: ${candidate.resourceId}`;
|
|
586
|
+
const evidence = [
|
|
587
|
+
`candidate:${candidate.candidateId}`,
|
|
588
|
+
`candidate-content:${candidate.contentHash}`,
|
|
589
|
+
`provider-receipt:${candidate.observation.providerReceiptId}@${candidate.observation.providerReceiptHash}`,
|
|
590
|
+
`resolution-receipt:${candidate.observation.resolutionReceiptId}@${candidate.observation.resolutionReceiptHash}`,
|
|
591
|
+
`resolution-set:${candidate.observation.resolutionSetHash}`,
|
|
592
|
+
];
|
|
593
|
+
return FindingSchema.parse({
|
|
594
|
+
id: findingId(title),
|
|
595
|
+
title,
|
|
596
|
+
observation: `Declared repository ${candidate.declaredRepositoryId} was observed as ${candidate.observedRepositoryId}; review the external reference before changing the landscape.`,
|
|
597
|
+
evidence,
|
|
598
|
+
method: null,
|
|
599
|
+
severity: "medium",
|
|
600
|
+
triage: "open",
|
|
601
|
+
affected_files: [],
|
|
602
|
+
affected_symbols: [candidate.resourceId],
|
|
603
|
+
violates_constraint: null,
|
|
604
|
+
spawned_decision: null,
|
|
605
|
+
observed_at: candidate.observation.observedAt,
|
|
606
|
+
resolved_commit: null,
|
|
607
|
+
provenance: {
|
|
608
|
+
source: "orc_observed+candidate",
|
|
609
|
+
confidence: 0.8,
|
|
610
|
+
evidence,
|
|
611
|
+
last_verified: candidate.observation.observedAt,
|
|
612
|
+
},
|
|
613
|
+
});
|
|
614
|
+
}
|
|
488
615
|
/** The entity collections, keyed by their on-disk directory name. */
|
|
489
616
|
export const ENTITY_KINDS = ["components", "resources", "edges", "symbols", "decisions", "bugs", "constraints", "runbooks", "findings"];
|
|
490
617
|
export const SCHEMAS = {
|
package/dist/extractors/git.js
CHANGED
|
@@ -77,7 +77,7 @@ function gitSafeWithoutReplacements(args, cwd, maxBuffer = 64 * 1024 * 1024) {
|
|
|
77
77
|
return execFileSync("git", args, {
|
|
78
78
|
cwd,
|
|
79
79
|
encoding: "utf8",
|
|
80
|
-
env: { ...process.env, GIT_NO_REPLACE_OBJECTS: "1" },
|
|
80
|
+
env: { ...foreignRepoEnv(process.env), GIT_NO_REPLACE_OBJECTS: "1" },
|
|
81
81
|
maxBuffer,
|
|
82
82
|
stdio: ["ignore", "pipe", "ignore"],
|
|
83
83
|
}).trim();
|
|
@@ -483,7 +483,7 @@ export function stableRepositoryName(root) {
|
|
|
483
483
|
const digest = createHash("sha256").update(fetchRemote, "utf8").digest("hex");
|
|
484
484
|
return `git-remote:sha256:${digest}`;
|
|
485
485
|
}
|
|
486
|
-
const shallow =
|
|
486
|
+
const shallow = gitSafeIsolated(["rev-parse", "--is-shallow-repository"], root) === "true";
|
|
487
487
|
if (shallow)
|
|
488
488
|
return basename(mainWorktreeRoot(root));
|
|
489
489
|
const roots = gitSafeWithoutReplacements(["rev-list", "--max-parents=0", "HEAD"], root)
|