@davesheffer/hunch 1.20.0-rc.2 → 1.20.0-rc.4
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 +5 -2
- package/dist/cli/index.js +111 -9
- package/dist/constitution/delta.js +1 -1
- package/dist/constitution/schema.js +1 -1
- package/dist/core/correctionStage.js +8 -5
- package/dist/core/escalations.js +21 -0
- package/dist/core/importReview.js +192 -0
- package/dist/core/pipeline.js +24 -12
- package/dist/core/types.js +1 -1
- package/dist/extractors/adrImport.js +99 -35
- package/dist/extractors/correctionSources.js +3 -3
- package/dist/extractors/diff.js +6 -0
- package/dist/extractors/indexer.js +138 -29
- package/dist/extractors/languages.js +65 -1
- package/dist/extractors/nativeTreeSitter.js +4 -2
- package/dist/extractors/parse.js +42 -2
- package/dist/extractors/php.js +169 -0
- package/dist/extractors/repoSource.js +66 -1
- package/dist/integrations/claudemd.js +1 -1
- package/dist/mcp/server.js +45 -2
- package/dist/store/hunchStore.js +71 -26
- package/package.json +2 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -58,11 +58,13 @@ existing configuration.
|
|
|
58
58
|
passes, needs attention, or should be blocked.
|
|
59
59
|
- **Past bugs stay useful** — see which old incident a piece of code fixed before accidentally
|
|
60
60
|
undoing it.
|
|
61
|
-
- **Understands how code connects** — for TypeScript, JavaScript, Python, Go, YAML, and Helm, Hunch
|
|
61
|
+
- **Understands how code connects** — for TypeScript, JavaScript, Python, Go, PHP, YAML, and Helm, Hunch
|
|
62
62
|
can see what calls or depends on the code you are about to change. Its memory works with any
|
|
63
63
|
language.
|
|
64
64
|
- **Works with existing decision documents** — import your architecture decision records into
|
|
65
|
-
Hunch, or export Hunch decisions back to a standard format other tools can read.
|
|
65
|
+
Hunch, or export Hunch decisions back to a standard format other tools can read. Imported ADRs
|
|
66
|
+
start as useful advisory memory; during normal work your assistant asks you to approve or decline
|
|
67
|
+
one exact ADR at a time. Silence never grants authority, and changed ADR text is asked again.
|
|
66
68
|
|
|
67
69
|
The source of truth is readable JSON in `.hunch/`. A local SQLite index makes retrieval fast but
|
|
68
70
|
is always rebuildable.
|
|
@@ -98,6 +100,7 @@ Most memory work happens automatically after commits. These commands cover the c
|
|
|
98
100
|
| `hunch check --working` | Check current changes against the decisions and rules your team trusts |
|
|
99
101
|
| `hunch log` | See what Hunch remembered and undo a memory change if needed |
|
|
100
102
|
| `hunch escalations` | See the rare questions that need a human answer |
|
|
103
|
+
| `hunch review` | Answer the current imported-ADR approve/decline question from the terminal |
|
|
101
104
|
| `hunch doctor` | Diagnose setup problems |
|
|
102
105
|
|
|
103
106
|
<details>
|
package/dist/cli/index.js
CHANGED
|
@@ -31,7 +31,7 @@ import { assertCompleteRepoScan, indexRepo, scanRepo } from "../extractors/index
|
|
|
31
31
|
import { syncCommit, recordFailure, captureTestRun } from "../synthesis/synthesize.js";
|
|
32
32
|
import { parseTestReport } from "../extractors/testreport.js";
|
|
33
33
|
import { readSynthesisPreference, resolveSynthesisProvider, selectProvider, SYNTH_PREFERENCES, writeSynthesisPreference, normalizeProviderName, } from "../synthesis/provider.js";
|
|
34
|
-
import { isGitRepo, isGitRepoRoot, sameGitPublication, sameRemoteUrl, canonicalRemoteUrl, repositoryUsesRemote, headSha, isolatedHeadSha, logSince, lastChangeDate, stagedFiles, workingFiles, commitFiles, asOfDate, stagedDiff, workingDiff, commitDiff, rangeFiles, rangeDiff, rangeSubjects, revExists, revParse, commitAndPushHunch, pullHunchStatus, syncExistingHunch, gitUntrackCached, gitCommonDir, hooksDir, isLinkedWorktree, mainWorktreeRoot, gitMemoryLog, memoryMoveDiff, revertMemoryMove, pushCurrentBranch, commitChanges } from "../extractors/git.js";
|
|
34
|
+
import { isGitRepo, isGitRepoRoot, sameGitPublication, sameRemoteUrl, canonicalRemoteUrl, repositoryUsesRemote, headSha, isolatedHeadSha, logSince, lastChangeDate, firstCommitForFile, stagedFiles, workingFiles, commitFiles, asOfDate, stagedDiff, workingDiff, commitDiff, rangeFiles, rangeDiff, rangeSubjects, revExists, revParse, commitAndPushHunch, pullHunchStatus, syncExistingHunch, gitUntrackCached, gitCommonDir, hooksDir, isLinkedWorktree, mainWorktreeRoot, gitMemoryLog, memoryMoveDiff, revertMemoryMove, pushCurrentBranch, commitChanges } from "../extractors/git.js";
|
|
35
35
|
import { parseMemoryLog } from "../core/memorylog.js";
|
|
36
36
|
import { renamesOf, planRepair, repairDecision, repairConstraint } from "../core/repair.js";
|
|
37
37
|
import { planPolicyRepair, repairPolicySpec } from "../constitution/repairPolicies.js";
|
|
@@ -75,6 +75,7 @@ import { generateWiki, wikiStatus, wikiPrompt, publicHome, privateHome, readWiki
|
|
|
75
75
|
import { adoptProsePrompt } from "../wiki/adopt.js";
|
|
76
76
|
import { topicCollisions, isInForce, liveForTopic } from "../core/topics.js";
|
|
77
77
|
import { ADR_DIR_CANDIDATES, ADR_FILE_RE, mapAdrCorpus } from "../extractors/adrImport.js";
|
|
78
|
+
import { applyImportedAdrReview, carryImportedAdrReview, importedAdrReviewHash, importedAdrSourceHash, isImportedAdrDecision, pendingImportedAdrReviews } from "../core/importReview.js";
|
|
78
79
|
import { exportMadrCorpus, isRegenerableMadr } from "../integrations/madrExport.js";
|
|
79
80
|
import { buildMadrManifest, writeMadrManifest, refreshMadrCorpus } from "../integrations/madrManifest.js";
|
|
80
81
|
import { pendingEscalations, policyEscalations } from "../core/escalations.js";
|
|
@@ -217,6 +218,10 @@ program
|
|
|
217
218
|
const res = indexRepo(store, root, { source: { kind: "commit", ref: "HEAD" } });
|
|
218
219
|
store.reindex();
|
|
219
220
|
console.log(` ✓ indexed committed HEAD (${res.files} files) → ${res.symbols} symbols, ${res.edges} edges, ${res.components} components`);
|
|
221
|
+
for (const item of res.coverage) {
|
|
222
|
+
const reasons = Object.entries(item.reasons).map(([reason, count]) => `${reason}:${count}`).join(", ");
|
|
223
|
+
console.log(` coverage ${item.language}: ${item.eligible} eligible · ${item.parsed} parsed · ${item.skipped} skipped${reasons ? ` (${reasons})` : ""}`);
|
|
224
|
+
}
|
|
220
225
|
if (res.skipped)
|
|
221
226
|
console.log(` ⚠ ${res.skipped} file(s) could not be parsed (skipped)`);
|
|
222
227
|
}
|
|
@@ -351,6 +356,10 @@ program
|
|
|
351
356
|
const healed = shouldAutoCommit ? [] : refreshExistingGrounding(root, store);
|
|
352
357
|
console.log(`Indexed ${res.files} files:`);
|
|
353
358
|
console.log(` ${counts.symbols} symbols, ${counts.edges} edges, ${counts.components} components`);
|
|
359
|
+
for (const item of res.coverage) {
|
|
360
|
+
const reasons = Object.entries(item.reasons).map(([reason, count]) => `${reason}:${count}`).join(", ");
|
|
361
|
+
console.log(` coverage ${item.language}: ${item.eligible} eligible · ${item.parsed} parsed · ${item.skipped} skipped${reasons ? ` (${reasons})` : ""}`);
|
|
362
|
+
}
|
|
354
363
|
if (correctionSweep.scanned) {
|
|
355
364
|
console.log(` correction reviews: ${correctionSweep.proved} proved · ${correctionSweep.already_proved} current · ${correctionSweep.pending} pending · ${correctionSweep.legacy_only} legacy-only · ${correctionSweep.conflicted} conflicted · ${correctionSweep.failed.length} failed; authority none`);
|
|
356
365
|
}
|
|
@@ -3110,7 +3119,16 @@ program
|
|
|
3110
3119
|
const files = readdirSync(join(root, dir)).filter((f) => ADR_FILE_RE.test(f)).sort();
|
|
3111
3120
|
if (!files.length)
|
|
3112
3121
|
return fail(`no NNNN-slug.md ADR files in ${dir}`);
|
|
3113
|
-
const sources = files.map((f) =>
|
|
3122
|
+
const sources = files.map((f) => {
|
|
3123
|
+
const relPath = `${dir}/${f}`;
|
|
3124
|
+
const sourceRevision = firstCommitForFile(relPath, root) || null;
|
|
3125
|
+
return {
|
|
3126
|
+
relPath,
|
|
3127
|
+
text: readFileSync(join(root, dir, f), "utf8"),
|
|
3128
|
+
sourceDate: sourceRevision ? (asOfDate(sourceRevision, root) ?? null) : null,
|
|
3129
|
+
sourceRevision,
|
|
3130
|
+
};
|
|
3131
|
+
});
|
|
3114
3132
|
const { decisions, warnings } = mapAdrCorpus(sources);
|
|
3115
3133
|
for (const w of warnings)
|
|
3116
3134
|
console.log(` ⚠ ${w}`);
|
|
@@ -3136,8 +3154,12 @@ program
|
|
|
3136
3154
|
}
|
|
3137
3155
|
store.json.ensureDirs();
|
|
3138
3156
|
let created = 0, updated = 0;
|
|
3139
|
-
for (
|
|
3140
|
-
|
|
3157
|
+
for (let index = 0; index < decisions.length; index++) {
|
|
3158
|
+
const candidate = decisions[index];
|
|
3159
|
+
const previous = opts.private ? store.getRec("decisions", candidate.id) : store.json.get("decisions", candidate.id);
|
|
3160
|
+
const d = carryImportedAdrReview(previous, candidate);
|
|
3161
|
+
decisions[index] = d;
|
|
3162
|
+
if (previous)
|
|
3141
3163
|
updated++;
|
|
3142
3164
|
else
|
|
3143
3165
|
created++;
|
|
@@ -3150,6 +3172,18 @@ program
|
|
|
3150
3172
|
const flush = flushCapture(store, hunchPaths(root).hunch, !!opts.private, `hunch: import ${decisions.length} ADR(s) from ${dir}`);
|
|
3151
3173
|
const live = decisions.filter((d) => d.status === "accepted").length;
|
|
3152
3174
|
console.log(`✓ imported ${decisions.length} ADR(s) from ${dir} (${created} new, ${updated} updated; ${live} live, ${decisions.length - live} historical)${opts.private ? " [private overlay]" : ""}`);
|
|
3175
|
+
const pending = pendingImportedAdrReviews(decisions);
|
|
3176
|
+
if (pending.length) {
|
|
3177
|
+
const first = pending[0];
|
|
3178
|
+
const sourceHash = importedAdrSourceHash(first);
|
|
3179
|
+
const reviewHash = importedAdrReviewHash(first);
|
|
3180
|
+
console.log(`\n⚖ HUMAN REVIEW ${pending.length} imported live ADR(s) are advisory until you answer.`);
|
|
3181
|
+
console.log(` “${first.title}” (${first.id})`);
|
|
3182
|
+
console.log(" Approve it as human-confirmed project authority, or decline and keep it advisory?");
|
|
3183
|
+
console.log(` Approve: hunch review --approve-import ${first.id} --expected-source-hash ${sourceHash} --expected-review-hash ${reviewHash} --reviewed-by <you>`);
|
|
3184
|
+
console.log(` Decline: hunch review --decline-import ${first.id} --expected-source-hash ${sourceHash} --expected-review-hash ${reviewHash} --reviewed-by <you>`);
|
|
3185
|
+
console.log(" Your coding assistant will also ask this inline in the next Hunch session; silence never approves it.");
|
|
3186
|
+
}
|
|
3153
3187
|
if (flush === "pushed")
|
|
3154
3188
|
console.log(" ↳ private memory committed + pushed");
|
|
3155
3189
|
}
|
|
@@ -3952,6 +3986,7 @@ program
|
|
|
3952
3986
|
const precise = blocking.filter((c) => !!effectiveForbids(c));
|
|
3953
3987
|
const scopeOnly = blocking.filter((c) => !effectiveForbids(c));
|
|
3954
3988
|
const drafts = store.json.loadAll("decisions").filter(isReviewDraft);
|
|
3989
|
+
const importedReviews = pendingImportedAdrReviews(store.json.loadAll("decisions"));
|
|
3955
3990
|
const { ready, scrutiny } = partitionReview(drafts, READY_MIN_GROUNDED);
|
|
3956
3991
|
const stale = store.staleness((f) => lastChangeDate(f, root)).filter((s) => s.kind === "constraint");
|
|
3957
3992
|
const fnote = {
|
|
@@ -3970,6 +4005,9 @@ program
|
|
|
3970
4005
|
if (ready.length || scrutiny.length) {
|
|
3971
4006
|
console.log(`\n ⏳ TO CONFIRM ${ready.length} ready · ${scrutiny.length} need scrutiny → hunch review${ready.length ? " --accept-verified" : ""}`);
|
|
3972
4007
|
}
|
|
4008
|
+
if (importedReviews.length) {
|
|
4009
|
+
console.log(`\n ⚖ ADR REVIEW ${importedReviews.length} imported live ADR(s) need an approve/decline answer → hunch review`);
|
|
4010
|
+
}
|
|
3973
4011
|
if (stale.length) {
|
|
3974
4012
|
console.log(`\n ♻ STALE ${stale.length} rule(s) whose guarded code moved since last verified → re-confirm to keep the teeth`);
|
|
3975
4013
|
}
|
|
@@ -4573,9 +4611,14 @@ program
|
|
|
4573
4611
|
});
|
|
4574
4612
|
program
|
|
4575
4613
|
.command("review")
|
|
4576
|
-
.description("
|
|
4614
|
+
.description("Answer hash-bound imported-ADR questions, or triage deliberate proposed drafts.")
|
|
4577
4615
|
.option("--accept <id>", "promote a decision to accepted/human-confirmed (confirms its tripwires)")
|
|
4578
4616
|
.option("--reject <id>", "reject a draft decision with a durable lifecycle tombstone")
|
|
4617
|
+
.option("--approve-import <id>", "approve one exact imported ADR as human-confirmed authority")
|
|
4618
|
+
.option("--decline-import <id>", "record that one exact imported ADR was reviewed and must stay advisory")
|
|
4619
|
+
.option("--expected-source-hash <hash>", "exact sha256 source hash printed with the imported-ADR question")
|
|
4620
|
+
.option("--expected-review-hash <hash>", "exact sha256 mapped-meaning hash printed with the imported-ADR question")
|
|
4621
|
+
.option("--reviewed-by <label>", "credential-free human reviewer label for an imported-ADR answer")
|
|
4579
4622
|
.option("--accept-verified", "batch-accept every Critic-verified, well-grounded draft (>= --min-grounded)")
|
|
4580
4623
|
.option("--reject-duplicates", "batch-reject drafts that near-duplicate an accepted record (deterministic term+file similarity — hygiene, not judgment)")
|
|
4581
4624
|
.option("--min-grounded <n>", "grounded-ness threshold for the ready group / --accept-verified", String(READY_MIN_GROUNDED))
|
|
@@ -4587,15 +4630,60 @@ program
|
|
|
4587
4630
|
store.close();
|
|
4588
4631
|
return fail("--private needs HUNCH_PRIVATE_DIR set to a private store");
|
|
4589
4632
|
}
|
|
4633
|
+
const actionCount = [opts.accept, opts.reject, opts.approveImport, opts.declineImport, opts.acceptVerified, opts.rejectDuplicates].filter(Boolean).length;
|
|
4634
|
+
if (actionCount > 1) {
|
|
4635
|
+
store.close();
|
|
4636
|
+
return fail("choose exactly one review action at a time");
|
|
4637
|
+
}
|
|
4590
4638
|
const decisions = () => opts.private ? store.recs("decisions") : store.json.loadAll("decisions");
|
|
4591
4639
|
let publicGroundingChanged = false;
|
|
4592
4640
|
const touchedHomes = new Set();
|
|
4593
|
-
if (opts.
|
|
4641
|
+
if (opts.approveImport || opts.declineImport) {
|
|
4642
|
+
const id = opts.approveImport ?? opts.declineImport;
|
|
4643
|
+
if (!opts.expectedSourceHash || !opts.expectedReviewHash || !opts.reviewedBy) {
|
|
4644
|
+
store.close();
|
|
4645
|
+
return fail("an imported-ADR answer requires --expected-source-hash <hash>, --expected-review-hash <hash>, and --reviewed-by <label>");
|
|
4646
|
+
}
|
|
4647
|
+
const d = opts.private ? store.getRec("decisions", id) : store.json.get("decisions", id);
|
|
4648
|
+
if (!d) {
|
|
4649
|
+
store.close();
|
|
4650
|
+
return fail(`decision ${id} not found`);
|
|
4651
|
+
}
|
|
4652
|
+
try {
|
|
4653
|
+
const reviewed = applyImportedAdrReview(d, {
|
|
4654
|
+
disposition: opts.approveImport ? "approve" : "decline",
|
|
4655
|
+
expectedSourceHash: opts.expectedSourceHash,
|
|
4656
|
+
expectedReviewHash: opts.expectedReviewHash,
|
|
4657
|
+
reviewer: opts.reviewedBy,
|
|
4658
|
+
});
|
|
4659
|
+
const home = opts.private ? decisionMemoryHome(store, id) : "public";
|
|
4660
|
+
putDecisionInHome(store, reviewed, home);
|
|
4661
|
+
touchedHomes.add(home);
|
|
4662
|
+
store.reindex();
|
|
4663
|
+
if (home === "public") {
|
|
4664
|
+
publicGroundingChanged = true;
|
|
4665
|
+
if (!store.autoCommit)
|
|
4666
|
+
refreshExistingGrounding(root, store);
|
|
4667
|
+
}
|
|
4668
|
+
console.log(opts.approveImport
|
|
4669
|
+
? `✓ approved ${id} for exact source ${opts.expectedSourceHash} (now human-confirmed authority)`
|
|
4670
|
+
: `✓ declined authority for ${id} at exact source ${opts.expectedSourceHash} (reviewed; remains advisory)`);
|
|
4671
|
+
}
|
|
4672
|
+
catch (error) {
|
|
4673
|
+
store.close();
|
|
4674
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
4675
|
+
}
|
|
4676
|
+
}
|
|
4677
|
+
else if (opts.accept) {
|
|
4594
4678
|
const d = opts.private ? store.getRec("decisions", opts.accept) : store.json.get("decisions", opts.accept);
|
|
4595
4679
|
if (!d) {
|
|
4596
4680
|
store.close();
|
|
4597
4681
|
return fail(`decision ${opts.accept} not found`);
|
|
4598
4682
|
}
|
|
4683
|
+
if (isImportedAdrDecision(d)) {
|
|
4684
|
+
store.close();
|
|
4685
|
+
return fail(`imported ADR ${d.id} requires the hash-bound --approve-import flow shown by hunch review`);
|
|
4686
|
+
}
|
|
4599
4687
|
const { source, armed, home } = acceptDecision(store, d, opts.private ? decisionMemoryHome(store, d.id) : "public");
|
|
4600
4688
|
touchedHomes.add(home);
|
|
4601
4689
|
store.reindex();
|
|
@@ -4668,12 +4756,26 @@ program
|
|
|
4668
4756
|
}
|
|
4669
4757
|
}
|
|
4670
4758
|
else {
|
|
4671
|
-
const
|
|
4759
|
+
const allDecisions = decisions();
|
|
4760
|
+
const imported = pendingImportedAdrReviews(allDecisions);
|
|
4761
|
+
const drafts = allDecisions.filter(isReviewDraft);
|
|
4672
4762
|
const { ready, scrutiny } = partitionReview(drafts, minGrounded);
|
|
4673
|
-
if (!ready.length && !scrutiny.length) {
|
|
4763
|
+
if (!ready.length && !scrutiny.length && !imported.length) {
|
|
4674
4764
|
console.log("✓ No drafts awaiting review — captured memory auto-trusts (advisory) the moment it lands.");
|
|
4675
4765
|
}
|
|
4676
4766
|
else {
|
|
4767
|
+
if (imported.length) {
|
|
4768
|
+
const d = imported[0];
|
|
4769
|
+
const sourceHash = importedAdrSourceHash(d);
|
|
4770
|
+
const reviewHash = importedAdrReviewHash(d);
|
|
4771
|
+
console.log(`⚖ ${imported.length} imported live ADR(s) need a human answer (one at a time):\n`);
|
|
4772
|
+
console.log(` ${d.id} ${d.title}`);
|
|
4773
|
+
console.log(` ${d.related_files[0] ?? "source unknown"} source ${sourceHash} review ${reviewHash}`);
|
|
4774
|
+
console.log(` ${d.decision.slice(0, 180)}`);
|
|
4775
|
+
console.log("\n Approve as human-confirmed authority, or decline and keep advisory?");
|
|
4776
|
+
console.log(` Approve: hunch review --approve-import ${d.id} --expected-source-hash ${sourceHash} --expected-review-hash ${reviewHash} --reviewed-by <you>`);
|
|
4777
|
+
console.log(` Decline: hunch review --decline-import ${d.id} --expected-source-hash ${sourceHash} --expected-review-hash ${reviewHash} --reviewed-by <you>\n`);
|
|
4778
|
+
}
|
|
4677
4779
|
if (ready.length) {
|
|
4678
4780
|
console.log(`✓ ${ready.length} ready to confirm — Critic-verified, grounded ≥ ${minGrounded} (best first):\n`);
|
|
4679
4781
|
for (const it of ready)
|
|
@@ -4905,7 +5007,7 @@ program
|
|
|
4905
5007
|
// ---- escalations (the inline "ask the human" surface) ---------------------
|
|
4906
5008
|
program
|
|
4907
5009
|
.command("escalations")
|
|
4908
|
-
.description("The decisions a human must make NOW — surfaced to be asked INLINE
|
|
5010
|
+
.description("The decisions a human must make NOW — surfaced to be asked INLINE, never inferred: one exact imported ADR at a time, topic conflicts, stale premises, and Constitution activation calls. Normally empty. Exits non-zero when any are open.")
|
|
4909
5011
|
.option("--json", "emit the escalation entries as JSON (the VS Code panel's data source)")
|
|
4910
5012
|
.action(async (opts) => {
|
|
4911
5013
|
const { store, root } = storeFor();
|
|
@@ -4,7 +4,7 @@ import { commitChanges, fileAtRef, firstParent, revParse } from "../extractors/g
|
|
|
4
4
|
import { attributeCalls, parseSource } from "../extractors/parse.js";
|
|
5
5
|
import { canonicalHash } from "./canonical.js";
|
|
6
6
|
import { StructuralDeltaSchema, } from "./schema.js";
|
|
7
|
-
const CODE_EXT = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/;
|
|
7
|
+
const CODE_EXT = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|php)$/;
|
|
8
8
|
const SKIP_SEGMENTS = new Set(["node_modules", ".git", ".hunch", "dist", "build", "coverage", ".next", "out", "vendor"]);
|
|
9
9
|
const MAX_CODE_FILES = 64;
|
|
10
10
|
const MAX_SOURCE_BYTES = 8 * 1024 * 1024;
|
|
@@ -14,7 +14,7 @@ export const DataClassSchema = z.enum(["public", "private", "secret"]);
|
|
|
14
14
|
export const StructuralSymbolRefSchema = z.object({
|
|
15
15
|
file: z.string().min(1),
|
|
16
16
|
name: z.string().min(1),
|
|
17
|
-
kind: z.enum(["function", "method", "class", "interface", "type", "variable", "file"]),
|
|
17
|
+
kind: z.enum(["function", "method", "class", "interface", "trait", "enum", "type", "variable", "file"]),
|
|
18
18
|
});
|
|
19
19
|
export const StructuralCallRefSchema = z.object({
|
|
20
20
|
file: z.string().min(1),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { compareCodeUnits } from "./canonicalOrder.js";
|
|
2
3
|
import { rankIssueImplementationOwners, } from "./pipeline.js";
|
|
3
4
|
import { compileVerifiedEvidenceMap, } from "./evidenceMap.js";
|
|
4
5
|
import { buildFileFirstDeclarationClusters, buildProgressiveDeclarationPlan, } from "./declarationClusters.js";
|
|
@@ -109,8 +110,10 @@ function terms(value) {
|
|
|
109
110
|
* but runtime declarations win ties in the selected layer. */
|
|
110
111
|
function runtimeDeclarationOwners(sources) {
|
|
111
112
|
const owners = new Set();
|
|
112
|
-
const declaration = /^(?:export\s+)?(?:default\s+)?(?:declare\s+)?(?:async\s+)?(?:function|class|enum|const|let|var)\s+([$A-Za-z_][$\w]*)/gm;
|
|
113
113
|
for (const source of sources) {
|
|
114
|
+
const declaration = source.path.endsWith(".php")
|
|
115
|
+
? /^(?:(?:#\[[^\r\n]*\])\r?\n)*(?:(?:abstract|final|readonly)\s+)*(?:class|interface|trait|enum|function)\s+&?\s*([A-Za-z_][A-Za-z0-9_]*)/gm
|
|
116
|
+
: /^(?:export\s+)?(?:default\s+)?(?:declare\s+)?(?:async\s+)?(?:function|class|enum|const|let|var)\s+([$A-Za-z_][$\w]*)/gm;
|
|
114
117
|
for (const match of source.content.matchAll(declaration)) {
|
|
115
118
|
owners.add(`${source.path}::${match[1]}`);
|
|
116
119
|
}
|
|
@@ -149,7 +152,7 @@ export function rankIssueCorrectionStageCandidates(issueValue, sources) {
|
|
|
149
152
|
|| Number(a.type_scaffolding) - Number(b.type_scaffolding)
|
|
150
153
|
|| b.symbol_overlap - a.symbol_overlap
|
|
151
154
|
|| b.lexical_score - a.lexical_score
|
|
152
|
-
|| a.owner
|
|
155
|
+
|| compareCodeUnits(a.owner, b.owner));
|
|
153
156
|
// Overloads and repeated declarations can produce the same owner more than
|
|
154
157
|
// once. A shortlist must spend each slot on a distinct correction candidate.
|
|
155
158
|
const seen = new Set();
|
|
@@ -264,7 +267,7 @@ export function selectGuardedExecutionBridge(baselineOwnersValue, rankedOwnersVa
|
|
|
264
267
|
: [];
|
|
265
268
|
}).sort((left, right) => right.execution_ratio - left.execution_ratio
|
|
266
269
|
|| left.static_rank - right.static_rank
|
|
267
|
-
|| left.owner
|
|
270
|
+
|| compareCodeUnits(left.owner, right.owner))[0];
|
|
268
271
|
if (direct)
|
|
269
272
|
return [direct];
|
|
270
273
|
const maxRatio = evidence.reduce((best, entry) => Math.max(best, entry.ratio), 0);
|
|
@@ -283,7 +286,7 @@ export function selectGuardedExecutionBridge(baselineOwnersValue, rankedOwnersVa
|
|
|
283
286
|
- Number(left.strategy === "direct-high-contrast-execution")
|
|
284
287
|
|| right.execution_ratio - left.execution_ratio
|
|
285
288
|
|| left.static_rank - right.static_rank
|
|
286
|
-
|| left.owner
|
|
289
|
+
|| compareCodeUnits(left.owner, right.owner));
|
|
287
290
|
return choices[0] ?? null;
|
|
288
291
|
}
|
|
289
292
|
export function reserveExecutionGuidedFileOwner(baselineOwnersValue, rankedOwnersValue, evidenceMap, requestedLimit = CORRECTION_STAGE_CANDIDATE_LIMIT) {
|
|
@@ -352,7 +355,7 @@ export function rankIssueAdaptiveCorrectionCandidates(issueValue, sources) {
|
|
|
352
355
|
|| b.path_overlap - a.path_overlap
|
|
353
356
|
|| b.symbol_overlap - a.symbol_overlap
|
|
354
357
|
|| Number(b.runtime_declaration) - Number(a.runtime_declaration)
|
|
355
|
-
|| a.owner
|
|
358
|
+
|| compareCodeUnits(a.owner, b.owner));
|
|
356
359
|
const deeper = ranked.filter((candidate) => !candidate.invoked_entrance
|
|
357
360
|
&& (candidate.path_overlap > 0 || candidate.symbol_overlap > 0));
|
|
358
361
|
return deeper.length ? ranked.filter((candidate) => !candidate.invoked_entrance) : ranked;
|
package/dist/core/escalations.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { topicCollisions } from "./topics.js";
|
|
2
|
+
import { importedAdrReviewHash, importedAdrSourceHash, pendingImportedAdrReviews } from "./importReview.js";
|
|
2
3
|
/** The decisions a human must make NOW, to be asked INLINE. Empty in a healthy graph. */
|
|
3
4
|
export function pendingEscalations(decisions) {
|
|
4
5
|
const out = [];
|
|
@@ -12,6 +13,26 @@ export function pendingEscalations(decisions) {
|
|
|
12
13
|
resolution: `supersede the others: re-record the chosen one with supersedes:<other-id>, or split the topic.`,
|
|
13
14
|
});
|
|
14
15
|
}
|
|
16
|
+
// Imported ADRs are immediately useful as advisory memory, but reading a file
|
|
17
|
+
// cannot mint human authority. Ask about exactly one at a time in ordinary
|
|
18
|
+
// session orientation; once answered, the next one naturally surfaces.
|
|
19
|
+
const imported = pendingImportedAdrReviews(decisions);
|
|
20
|
+
const next = imported[0];
|
|
21
|
+
if (next) {
|
|
22
|
+
const sourceHash = importedAdrSourceHash(next);
|
|
23
|
+
const reviewHash = importedAdrReviewHash(next);
|
|
24
|
+
const sourcePath = next.related_files[0] ?? "its ADR source";
|
|
25
|
+
const remaining = imported.length - 1;
|
|
26
|
+
const clip = (value, max) => value.length > max ? value.slice(0, max - 1).trimEnd() + "…" : value;
|
|
27
|
+
out.push({
|
|
28
|
+
kind: "imported-adr-review",
|
|
29
|
+
topic: next.topic ?? next.id,
|
|
30
|
+
decisionIds: [next.id],
|
|
31
|
+
question: `I imported ADR “${clip(next.title, 90)}” (${next.id}) as advisory memory. Approve it as human-confirmed project authority, or decline and keep it advisory?`,
|
|
32
|
+
detail: `${sourcePath} · source ${sourceHash} · review ${reviewHash} · ${clip(next.decision, 180)}${remaining ? ` · ${remaining} more imported ADR(s) will follow one at a time` : ""}`,
|
|
33
|
+
resolution: `after the human answers, call hunch_review_imported_adr with decision_id=${next.id}, expected_source_hash=${sourceHash}, expected_review_hash=${reviewHash}, and disposition=approve|decline; CLI: hunch review --approve-import|--decline-import ${next.id} --expected-source-hash ${sourceHash} --expected-review-hash ${reviewHash} --reviewed-by <you>`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
15
36
|
return out;
|
|
16
37
|
}
|
|
17
38
|
/** The Constitution's genuine human moments (§59.5.3), framed as inline questions:
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hash-bound human review for live ADRs imported from MADR/Nygard documents.
|
|
3
|
+
*
|
|
4
|
+
* Import gives Hunch useful advisory memory immediately, but importing a file is
|
|
5
|
+
* not a human countersign. This module owns the narrow trust-elevation boundary:
|
|
6
|
+
* an explicit approve/decline answer applies only to the exact source bytes the
|
|
7
|
+
* person saw. Re-importing unchanged bytes preserves the answer; changed bytes
|
|
8
|
+
* discard it and become reviewable again.
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { isCredentialFreeText } from "./types.js";
|
|
12
|
+
const SOURCE_HASH = /^sha256:[a-f0-9]{64}$/;
|
|
13
|
+
const REVIEW_RECEIPT = /^adr-review:(approved|declined):(sha256:[a-f0-9]{64})$/;
|
|
14
|
+
const REVIEW_HASH_PREFIX = "adr-review-candidate:";
|
|
15
|
+
const REVIEWER_PREFIX = "adr-reviewer:";
|
|
16
|
+
const REVIEWED_AT_PREFIX = "adr-reviewed-at:";
|
|
17
|
+
function sourceTokens(source) {
|
|
18
|
+
return source.split("+").filter(Boolean);
|
|
19
|
+
}
|
|
20
|
+
function withoutHumanConfirmation(source) {
|
|
21
|
+
return sourceTokens(source).filter((token) => token !== "human_confirmed").join("+");
|
|
22
|
+
}
|
|
23
|
+
function withHumanConfirmation(source) {
|
|
24
|
+
const tokens = sourceTokens(source);
|
|
25
|
+
if (!tokens.includes("human_confirmed"))
|
|
26
|
+
tokens.push("human_confirmed");
|
|
27
|
+
return tokens.join("+");
|
|
28
|
+
}
|
|
29
|
+
export function isImportedAdrDecision(decision) {
|
|
30
|
+
return sourceTokens(decision.provenance.source).includes("imported:madr");
|
|
31
|
+
}
|
|
32
|
+
export function importedAdrSourceHash(decision) {
|
|
33
|
+
return decision.provenance.evidence.find((item) => SOURCE_HASH.test(item)) ?? null;
|
|
34
|
+
}
|
|
35
|
+
/** Hash the complete mapped meaning, not just the ADR source bytes. This prevents
|
|
36
|
+
* a later importer/parser change from carrying old authority onto new semantics
|
|
37
|
+
* even when the Markdown file itself did not change. */
|
|
38
|
+
export function importedAdrReviewHash(decision) {
|
|
39
|
+
const evidence = decision.provenance.evidence.filter((item) => !REVIEW_RECEIPT.test(item)
|
|
40
|
+
&& !item.startsWith(REVIEW_HASH_PREFIX)
|
|
41
|
+
&& !item.startsWith(REVIEWER_PREFIX)
|
|
42
|
+
&& !item.startsWith(REVIEWED_AT_PREFIX));
|
|
43
|
+
const canonical = {
|
|
44
|
+
id: decision.id,
|
|
45
|
+
title: decision.title,
|
|
46
|
+
topic: decision.topic,
|
|
47
|
+
status: decision.status,
|
|
48
|
+
context: decision.context,
|
|
49
|
+
decision: decision.decision,
|
|
50
|
+
consequences: decision.consequences,
|
|
51
|
+
alternatives_rejected: decision.alternatives_rejected,
|
|
52
|
+
rejected_tripwires: decision.rejected_tripwires,
|
|
53
|
+
related_components: decision.related_components,
|
|
54
|
+
related_files: decision.related_files,
|
|
55
|
+
supersedes: decision.supersedes,
|
|
56
|
+
superseded_by: decision.superseded_by,
|
|
57
|
+
caused_by_bug: decision.caused_by_bug,
|
|
58
|
+
commit: decision.commit,
|
|
59
|
+
valid_from: decision.valid_from ?? null,
|
|
60
|
+
valid_to: decision.valid_to,
|
|
61
|
+
retired: decision.retired,
|
|
62
|
+
date: decision.date,
|
|
63
|
+
import_evidence: evidence,
|
|
64
|
+
};
|
|
65
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(canonical)).digest("hex")}`;
|
|
66
|
+
}
|
|
67
|
+
function currentReviewReceipt(decision) {
|
|
68
|
+
const sourceHash = importedAdrSourceHash(decision);
|
|
69
|
+
if (!sourceHash)
|
|
70
|
+
return null;
|
|
71
|
+
const reviewHash = importedAdrReviewHash(decision);
|
|
72
|
+
const recordedReviewHash = decision.provenance.evidence.find((value) => value.startsWith(REVIEW_HASH_PREFIX))?.slice(REVIEW_HASH_PREFIX.length);
|
|
73
|
+
if (recordedReviewHash !== reviewHash)
|
|
74
|
+
return null;
|
|
75
|
+
for (const item of decision.provenance.evidence) {
|
|
76
|
+
const match = REVIEW_RECEIPT.exec(item);
|
|
77
|
+
if (!match || match[2] !== sourceHash)
|
|
78
|
+
continue;
|
|
79
|
+
return {
|
|
80
|
+
disposition: match[1] === "approved" ? "approve" : "decline",
|
|
81
|
+
sourceHash,
|
|
82
|
+
reviewHash,
|
|
83
|
+
reviewer: decision.provenance.evidence.find((value) => value.startsWith(REVIEWER_PREFIX))?.slice(REVIEWER_PREFIX.length) ?? null,
|
|
84
|
+
reviewedAt: decision.provenance.evidence.find((value) => value.startsWith(REVIEWED_AT_PREFIX))?.slice(REVIEWED_AT_PREFIX.length) ?? null,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
export function importedAdrReview(decision) {
|
|
90
|
+
const receipt = currentReviewReceipt(decision);
|
|
91
|
+
if (receipt)
|
|
92
|
+
return receipt;
|
|
93
|
+
const sourceHash = importedAdrSourceHash(decision);
|
|
94
|
+
if (sourceHash && sourceTokens(decision.provenance.source).includes("human_confirmed")) {
|
|
95
|
+
// Backward-compatible with an imported record countersigned before receipts
|
|
96
|
+
// existed. Its exact source hash still prevents carrying it onto changed bytes.
|
|
97
|
+
return {
|
|
98
|
+
disposition: "approve",
|
|
99
|
+
sourceHash,
|
|
100
|
+
reviewHash: importedAdrReviewHash(decision),
|
|
101
|
+
reviewer: null,
|
|
102
|
+
reviewedAt: decision.provenance.last_verified ?? null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
export function isPendingImportedAdrReview(decision) {
|
|
108
|
+
return isImportedAdrDecision(decision)
|
|
109
|
+
&& decision.status === "accepted"
|
|
110
|
+
&& !decision.superseded_by
|
|
111
|
+
&& !decision.valid_to
|
|
112
|
+
&& !!importedAdrSourceHash(decision)
|
|
113
|
+
&& importedAdrReview(decision) === null;
|
|
114
|
+
}
|
|
115
|
+
export function pendingImportedAdrReviews(decisions) {
|
|
116
|
+
return decisions
|
|
117
|
+
.filter(isPendingImportedAdrReview)
|
|
118
|
+
.sort((a, b) => (a.date || "").localeCompare(b.date || "") || a.id.localeCompare(b.id));
|
|
119
|
+
}
|
|
120
|
+
function validateReviewer(reviewer) {
|
|
121
|
+
const normalized = reviewer.trim();
|
|
122
|
+
if (!normalized || normalized.length > 128 || /[\r\n]/.test(normalized) || !isCredentialFreeText(normalized)) {
|
|
123
|
+
throw new Error("ADR reviewer must be a credential-free label of 1-128 characters");
|
|
124
|
+
}
|
|
125
|
+
return normalized;
|
|
126
|
+
}
|
|
127
|
+
/** Apply one explicit answer to one exact live imported ADR. Never changes the
|
|
128
|
+
* ADR lifecycle or content: decline means "reviewed, keep advisory", not delete. */
|
|
129
|
+
export function applyImportedAdrReview(decision, input) {
|
|
130
|
+
if (!isImportedAdrDecision(decision))
|
|
131
|
+
throw new Error(`${decision.id} is not an imported MADR/Nygard decision`);
|
|
132
|
+
if (decision.status !== "accepted" || decision.superseded_by || decision.valid_to) {
|
|
133
|
+
throw new Error(`${decision.id} is no longer a live accepted ADR; refresh the review question`);
|
|
134
|
+
}
|
|
135
|
+
if (!SOURCE_HASH.test(input.expectedSourceHash))
|
|
136
|
+
throw new Error("expected ADR source hash must be sha256:<64 lowercase hex characters>");
|
|
137
|
+
const actualHash = importedAdrSourceHash(decision);
|
|
138
|
+
if (!actualHash || actualHash !== input.expectedSourceHash) {
|
|
139
|
+
throw new Error(`ADR source changed after the question was shown (expected ${input.expectedSourceHash}, current ${actualHash ?? "missing"}); review the current ADR instead`);
|
|
140
|
+
}
|
|
141
|
+
if (!SOURCE_HASH.test(input.expectedReviewHash))
|
|
142
|
+
throw new Error("expected ADR review hash must be sha256:<64 lowercase hex characters>");
|
|
143
|
+
const actualReviewHash = importedAdrReviewHash(decision);
|
|
144
|
+
if (actualReviewHash !== input.expectedReviewHash) {
|
|
145
|
+
throw new Error(`imported ADR meaning changed after the question was shown (expected ${input.expectedReviewHash}, current ${actualReviewHash}); review the current ADR instead`);
|
|
146
|
+
}
|
|
147
|
+
const reviewer = validateReviewer(input.reviewer);
|
|
148
|
+
const reviewedAt = input.reviewedAt ?? new Date().toISOString();
|
|
149
|
+
if (reviewedAt.length > 64 || !Number.isFinite(Date.parse(reviewedAt)))
|
|
150
|
+
throw new Error("ADR review timestamp must be ISO-compatible");
|
|
151
|
+
const evidence = decision.provenance.evidence.filter((item) => !REVIEW_RECEIPT.test(item) && !item.startsWith(REVIEW_HASH_PREFIX) && !item.startsWith(REVIEWER_PREFIX) && !item.startsWith(REVIEWED_AT_PREFIX));
|
|
152
|
+
evidence.push(`adr-review:${input.disposition === "approve" ? "approved" : "declined"}:${actualHash}`, `${REVIEW_HASH_PREFIX}${actualReviewHash}`, `${REVIEWER_PREFIX}${reviewer}`, `${REVIEWED_AT_PREFIX}${reviewedAt}`);
|
|
153
|
+
return {
|
|
154
|
+
...decision,
|
|
155
|
+
provenance: {
|
|
156
|
+
...decision.provenance,
|
|
157
|
+
source: input.disposition === "approve"
|
|
158
|
+
? withHumanConfirmation(decision.provenance.source)
|
|
159
|
+
: withoutHumanConfirmation(decision.provenance.source),
|
|
160
|
+
confidence: input.disposition === "approve" ? Math.max(decision.provenance.confidence, 0.95) : Math.min(decision.provenance.confidence, 0.75),
|
|
161
|
+
evidence,
|
|
162
|
+
last_verified: reviewedAt,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** Carry a review across idempotent re-import only when the source bytes match.
|
|
167
|
+
* A changed hash deliberately returns the clean new import, reopening review. */
|
|
168
|
+
export function carryImportedAdrReview(previous, next) {
|
|
169
|
+
if (!previous || !isImportedAdrDecision(previous) || !isImportedAdrDecision(next))
|
|
170
|
+
return next;
|
|
171
|
+
const previousHash = importedAdrSourceHash(previous);
|
|
172
|
+
const nextHash = importedAdrSourceHash(next);
|
|
173
|
+
if (!previousHash || previousHash !== nextHash)
|
|
174
|
+
return next;
|
|
175
|
+
const review = importedAdrReview(previous);
|
|
176
|
+
if (!review || review.reviewHash !== importedAdrReviewHash(next))
|
|
177
|
+
return next;
|
|
178
|
+
const receiptEvidence = previous.provenance.evidence.filter((item) => REVIEW_RECEIPT.test(item) || item.startsWith(REVIEW_HASH_PREFIX) || item.startsWith(REVIEWER_PREFIX) || item.startsWith(REVIEWED_AT_PREFIX));
|
|
179
|
+
return {
|
|
180
|
+
...next,
|
|
181
|
+
provenance: {
|
|
182
|
+
...next.provenance,
|
|
183
|
+
source: review.disposition === "approve"
|
|
184
|
+
? withHumanConfirmation(next.provenance.source)
|
|
185
|
+
: withoutHumanConfirmation(next.provenance.source),
|
|
186
|
+
confidence: review.disposition === "approve" ? Math.max(next.provenance.confidence, 0.95) : next.provenance.confidence,
|
|
187
|
+
evidence: [...next.provenance.evidence, ...receiptEvidence],
|
|
188
|
+
last_verified: previous.provenance.last_verified,
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=importReview.js.map
|
package/dist/core/pipeline.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
26
26
|
import { tmpdir } from "node:os";
|
|
27
27
|
import { join } from "node:path";
|
|
28
|
+
import { compareCodeUnits } from "./canonicalOrder.js";
|
|
28
29
|
export const EXECUTION_OBLIGATION_CATEGORIES = [
|
|
29
30
|
"evidence",
|
|
30
31
|
"behavior",
|
|
@@ -34,6 +35,21 @@ export const EXECUTION_OBLIGATION_CATEGORIES = [
|
|
|
34
35
|
"other",
|
|
35
36
|
];
|
|
36
37
|
export const CONTRACT_AXES = ["runtime", "static", "serialization", "compatibility"];
|
|
38
|
+
function ownerSourcePath(path) {
|
|
39
|
+
return /^[A-Za-z0-9._/-]+\.(?:tsx?|php)$/.test(path)
|
|
40
|
+
&& !/(?:^|\/)(?:tests?|__tests__)(?:\/|$)|\.test\.(?:tsx?|php)$/.test(path);
|
|
41
|
+
}
|
|
42
|
+
/** Extract top-level declarations for the bounded correction diagnostic. PHP
|
|
43
|
+
* declarations deliberately require column-zero PSR-style layout so methods or
|
|
44
|
+
* nested conditional declarations cannot masquerade as repository owners. */
|
|
45
|
+
function ownerDeclarations(path, content) {
|
|
46
|
+
const declaration = path.endsWith(".php")
|
|
47
|
+
? /^(?:(?:#\[[^\r\n]*\])\r?\n)*(?:(abstract|final|readonly)\s+)*(class|interface|trait|enum|function)\s+&?\s*([A-Za-z_][A-Za-z0-9_]*)/gm
|
|
48
|
+
: /(?:^|\n)\s*(?:export\s+)?(?:declare\s+)?(interface|class|function|const|type)\s+([$A-Za-z_][$\w]*)/g;
|
|
49
|
+
return [...content.matchAll(declaration)].map((match) => path.endsWith(".php")
|
|
50
|
+
? { kind: match[2], symbol: match[3], index: match.index ?? 0 }
|
|
51
|
+
: { kind: match[1], symbol: match[2], index: match.index ?? 0 });
|
|
52
|
+
}
|
|
37
53
|
export const emptyState = () => ({
|
|
38
54
|
turn: 0,
|
|
39
55
|
soulInjected: false,
|
|
@@ -408,17 +424,14 @@ export function rankContractAxisRiskOwners(closure, sourceValue) {
|
|
|
408
424
|
const content = typeof item.content === "string" && item.content.length <= 1_000_000 ? item.content : null;
|
|
409
425
|
if (!path || !content || seen.has(path) || path.startsWith("/") || path.split("/").includes(".."))
|
|
410
426
|
continue;
|
|
411
|
-
if (
|
|
427
|
+
if (!ownerSourcePath(path))
|
|
412
428
|
continue;
|
|
413
429
|
seen.add(path);
|
|
414
430
|
sources.push({ path, content });
|
|
415
431
|
}
|
|
416
432
|
const ranked = [];
|
|
417
|
-
const declaration = /(?:^|\n)\s*(?:export\s+)?(?:declare\s+)?(interface|class|function|const|type)\s+([$A-Za-z_][$\w]*)/g;
|
|
418
433
|
for (const source of sources) {
|
|
419
|
-
for (const
|
|
420
|
-
const kind = match[1];
|
|
421
|
-
const symbol = match[2];
|
|
434
|
+
for (const { kind, symbol } of ownerDeclarations(source.path, source.content)) {
|
|
422
435
|
const normalized = symbol.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
423
436
|
for (const [anchor, evidenceWeight] of candidates) {
|
|
424
437
|
if (!normalized.includes(anchor))
|
|
@@ -446,7 +459,7 @@ export function rankContractAxisRiskOwners(closure, sourceValue) {
|
|
|
446
459
|
}
|
|
447
460
|
}
|
|
448
461
|
}
|
|
449
|
-
ranked.sort((a, b) => b.score - a.score || a.owner
|
|
462
|
+
ranked.sort((a, b) => b.score - a.score || compareCodeUnits(a.owner, b.owner));
|
|
450
463
|
const ownerScores = new Map();
|
|
451
464
|
for (const item of ranked) {
|
|
452
465
|
if (!ownerScores.has(item.owner))
|
|
@@ -534,19 +547,18 @@ export function rankIssueImplementationOwners(issueValue, sourceValue, candidate
|
|
|
534
547
|
const content = typeof item.content === "string" && item.content.length <= 1_000_000 ? item.content : null;
|
|
535
548
|
if (!path || !content || seen.has(path) || path.startsWith("/") || path.split("/").includes(".."))
|
|
536
549
|
continue;
|
|
537
|
-
if (
|
|
550
|
+
if (!ownerSourcePath(path))
|
|
538
551
|
continue;
|
|
539
552
|
seen.add(path);
|
|
540
553
|
sources.push({ path, content });
|
|
541
554
|
}
|
|
542
555
|
const declarations = [];
|
|
543
|
-
const declaration = /^(?:export\s+)?(?:declare\s+)?(?:interface|class|function|const|type)\s+([$A-Za-z_][$\w]*)/gm;
|
|
544
556
|
for (const source of sources) {
|
|
545
|
-
const matches =
|
|
557
|
+
const matches = ownerDeclarations(source.path, source.content);
|
|
546
558
|
for (let index = 0; index < matches.length; index++) {
|
|
547
559
|
const match = matches[index];
|
|
548
|
-
const symbol = match
|
|
549
|
-
const start = match.index
|
|
560
|
+
const symbol = match.symbol;
|
|
561
|
+
const start = match.index;
|
|
550
562
|
const end = matches[index + 1]?.index ?? source.content.length;
|
|
551
563
|
const text = `${symbol} ${symbol} ${source.content.slice(start, end)}`.slice(0, 80_000);
|
|
552
564
|
const tokens = implementationOwnerTokens(text);
|
|
@@ -598,7 +610,7 @@ export function rankIssueImplementationOwners(issueValue, sourceValue, candidate
|
|
|
598
610
|
symbol_disclosed: symbolDisclosed,
|
|
599
611
|
path_disclosed: pathDisclosed,
|
|
600
612
|
};
|
|
601
|
-
}).sort((a, b) => b.score - a.score || a.owner
|
|
613
|
+
}).sort((a, b) => b.score - a.score || compareCodeUnits(a.owner, b.owner));
|
|
602
614
|
const limit = Number.isSafeInteger(candidateLimit) ? Math.max(1, Math.min(4_000, candidateLimit)) : 20;
|
|
603
615
|
return { candidates: candidates.slice(0, limit) };
|
|
604
616
|
}
|
package/dist/core/types.js
CHANGED
|
@@ -254,7 +254,7 @@ export const EdgeSchema = z.object({
|
|
|
254
254
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["provenance"], message: "resource relationship fields must remain bounded" });
|
|
255
255
|
}
|
|
256
256
|
});
|
|
257
|
-
export const SymbolKind = z.enum(["function", "method", "class", "interface", "type", "variable", "file"]);
|
|
257
|
+
export const SymbolKind = z.enum(["function", "method", "class", "interface", "trait", "enum", "type", "variable", "file"]);
|
|
258
258
|
export const SymbolMetricsSchema = z.object({
|
|
259
259
|
loc: z.number().default(0),
|
|
260
260
|
churn_90d: z.number().default(0).describe("times changed in last 90 days"),
|