@davesheffer/hunch 1.8.2 → 1.8.3

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 CHANGED
@@ -13,6 +13,9 @@ relevant context before it makes a change.
13
13
  Memory starts **advisory**. Nothing blocks until you explicitly trust a precise rule and choose
14
14
  strict enforcement.
15
15
 
16
+ **Memory is the input. The product boundary is the receipt:** relevant evidence before an edit,
17
+ then a deterministic check of the change against the rules your team has explicitly trusted.
18
+
16
19
  ## Start in five minutes
17
20
 
18
21
  Requires Node 22.13+ and a git repository.
@@ -104,5 +107,6 @@ Local tools see the combined graph; public CI and committed documentation stay p
104
107
  - [VS Code extension](vscode-extension/README.md)
105
108
  - [Contributing](CONTRIBUTING.md)
106
109
  - [Architecture benchmark](bench/architectural-conformance.md)
110
+ - [Competitive landscape (dated; re-verify before quoting)](docs/competitive-landscape.md)
107
111
 
108
112
  Apache-2.0
package/dist/cli/index.js CHANGED
@@ -1957,6 +1957,23 @@ experimentCmd
1957
1957
  store.close();
1958
1958
  }
1959
1959
  });
1960
+ experimentCmd
1961
+ .command("qualify")
1962
+ .description("Record a passing excluded comprehension check before an EXP-03 revision-2 timed review.")
1963
+ .argument("<file>", "reviewer qualification JSON")
1964
+ .action((file) => {
1965
+ const { store, root } = storeFor();
1966
+ try {
1967
+ const input = JSON.parse(readFileSync(resolve(file), "utf8"));
1968
+ console.log(JSON.stringify(new ConstitutionService(store, root).qualifyExperimentReviewer(input), null, 2));
1969
+ }
1970
+ catch (e) {
1971
+ fail(e.message);
1972
+ }
1973
+ finally {
1974
+ store.close();
1975
+ }
1976
+ });
1960
1977
  experimentCmd
1961
1978
  .command("next")
1962
1979
  .description("Start or resume the next randomized EXP-03 human review and return only its assigned treatment.")
@@ -245,6 +245,46 @@ export function assignmentTreatment(bank, run, assignment) {
245
245
  throw new Error(`assignment ${assignment.id} treatment hash mismatch`);
246
246
  return treatment;
247
247
  }
248
+ /** Human-facing help is deliberately outside the hash-bound treatment. It may
249
+ * explain the review task, but must never interpret the assigned evidence. */
250
+ export function experimentReviewGuide(arm) {
251
+ const notEnough = {
252
+ value: "uncompilable",
253
+ label: "Not enough information",
254
+ use_when: "The requirement does not clearly say which code relationship must always hold.",
255
+ };
256
+ if (arm === "A") {
257
+ return {
258
+ title: "Write one code rule from the requirement",
259
+ question: "Can one exact code rule be written from the requirement without guessing?",
260
+ action: "If yes, write one sentence naming the code elements, required or forbidden relationship, direct or transitive meaning, and file scope. If no, choose Not enough information.",
261
+ answer_template: "<subject> must <directly or transitively> <required or forbidden relationship> <target>; scope: <file or component>",
262
+ warning: "Use only the stated requirement. Current code may confirm names, but cannot add intent.",
263
+ choices: [
264
+ { value: "accepted_precise", label: "Rule written", use_when: "Your sentence expresses one exact rule fully supported by the requirement." },
265
+ notEnough,
266
+ ],
267
+ };
268
+ }
269
+ const choices = [
270
+ { value: "accepted_precise", label: "Yes — exact match", use_when: "The proposed rule says exactly what the requirement says." },
271
+ { value: "accepted_edited", label: "Needs editing", use_when: "The requirement supports one rule, but the proposal needs a specific correction." },
272
+ { value: "rejected", label: "Unsupported", use_when: "The proposal adds or changes meaning that the requirement does not support." },
273
+ notEnough,
274
+ ];
275
+ return {
276
+ title: arm === "C" ? "Check the proposed rule and its proof card" : "Check the proposed rule",
277
+ question: "Does the proposed rule say exactly what the requirement says?",
278
+ action: arm === "C"
279
+ ? "Compare the requirement with the proposed rule, then use the proof card only to check that the named code targets are bound correctly."
280
+ : "Compare the requirement with the proposed rule. Check the relationship, direction, direct or transitive meaning, and scope.",
281
+ answer_template: "Choose one plain-language option; include corrected rule text only for Needs editing.",
282
+ warning: arm === "C"
283
+ ? "The proof card can verify code bindings, but it cannot add intent missing from the requirement."
284
+ : "Do not infer intent from the current implementation or from nearby code.",
285
+ choices,
286
+ };
287
+ }
248
288
  export function experimentRunContentHash(run) {
249
289
  const { id: _id, content_hash: _hash, ...body } = run;
250
290
  return canonicalHash(body);
@@ -537,6 +577,38 @@ export function compileExperimentReviewStart(run, assignment, reviewer, opts = {
537
577
  const contentHash = canonicalHash(body);
538
578
  return ExperimentReviewStartSchema.parse({ id: `expreview_${shortHash(contentHash)}`, content_hash: contentHash, ...body });
539
579
  }
580
+ export const ExperimentReviewerQualificationSchema = z.object({
581
+ id: z.string().regex(/^expreviewqual_[a-f0-9]{10}$/),
582
+ content_hash: z.string().regex(HASH),
583
+ preregistration_id: z.string().regex(/^expreg_[a-f0-9]{10}$/),
584
+ preregistration_hash: z.string().regex(HASH),
585
+ reviewer: z.string().regex(/^human:[^\s]+$/i),
586
+ protocol: z.literal("exp03-plain-language-comprehension-v2"),
587
+ cases_hash: z.string().regex(HASH),
588
+ passed: z.literal(true),
589
+ reason: z.string().trim().min(1).max(4000),
590
+ data_class: z.literal("private"),
591
+ authority: z.literal("none"),
592
+ recorded_at: z.string().datetime({ offset: true }),
593
+ }).strict();
594
+ export function experimentReviewerQualificationContentHash(record) {
595
+ const { id: _id, content_hash: _hash, ...body } = record;
596
+ return canonicalHash(body);
597
+ }
598
+ export function compileExperimentReviewerQualification(input, preregistration, opts = {}) {
599
+ if (preregistration.experiment !== "EXP-03" || preregistration.revision < 2)
600
+ throw new Error("plain-language reviewer qualification requires EXP-03 revision 2 or later");
601
+ if (input.preregistration_id !== preregistration.id || input.preregistration_hash !== preregistration.content_hash)
602
+ throw new Error("reviewer qualification must bind the exact current preregistration");
603
+ const body = {
604
+ ...input,
605
+ data_class: "private",
606
+ authority: "none",
607
+ recorded_at: opts.now ?? new Date().toISOString(),
608
+ };
609
+ const contentHash = canonicalHash(body);
610
+ return ExperimentReviewerQualificationSchema.parse({ id: `expreviewqual_${shortHash(contentHash)}`, content_hash: contentHash, ...body });
611
+ }
540
612
  export const ExperimentFollowupSchema = z.object({
541
613
  id: z.string().regex(/^expfollow_[a-f0-9]{10}$/),
542
614
  content_hash: z.string().regex(HASH),
@@ -943,6 +1015,24 @@ export class ExperimentRepository {
943
1015
  this.put("experiment-review-starts", parsed.id, parsed);
944
1016
  return parsed;
945
1017
  }
1018
+ listReviewerQualifications() {
1019
+ return this.load("experiment-review-qualifications", "expreviewqual_", (raw) => {
1020
+ const parsed = ExperimentReviewerQualificationSchema.parse(raw);
1021
+ if (experimentReviewerQualificationContentHash(parsed) !== parsed.content_hash || parsed.id !== `expreviewqual_${shortHash(parsed.content_hash)}`)
1022
+ throw new Error(`experiment reviewer qualification ${parsed.id} content hash mismatch`);
1023
+ return parsed;
1024
+ }).sort((a, b) => a.id.localeCompare(b.id));
1025
+ }
1026
+ putReviewerQualification(record) {
1027
+ const parsed = ExperimentReviewerQualificationSchema.parse(record);
1028
+ if (experimentReviewerQualificationContentHash(parsed) !== parsed.content_hash || parsed.id !== `expreviewqual_${shortHash(parsed.content_hash)}`)
1029
+ throw new Error(`experiment reviewer qualification ${parsed.id} content hash mismatch`);
1030
+ const incumbent = this.listReviewerQualifications().find((item) => item.preregistration_id === parsed.preregistration_id && item.reviewer === parsed.reviewer);
1031
+ if (incumbent)
1032
+ return incumbent;
1033
+ this.put("experiment-review-qualifications", parsed.id, parsed);
1034
+ return parsed;
1035
+ }
946
1036
  listFollowups() {
947
1037
  const records = this.load("experiment-followups", "expfollow_", (raw) => {
948
1038
  const parsed = ExperimentFollowupSchema.parse(raw);
@@ -31,7 +31,7 @@ import { evaluateExecutableBehaviorPolicy } from "./behaviorEvaluator.js";
31
31
  import { executeG2OperationalDrill } from "./g2Drills.js";
32
32
  import { G3_REQUIRED_EXPERIMENTS, G3EvidenceRepository, compileExperimentPreregistration, compileG3Plan, compileProofReviewMeasurement, scoreG3Readiness, } from "./g3.js";
33
33
  import { executeG3AdapterConformance, g3ConformanceSourceHash } from "./g3Conformance.js";
34
- import { ExperimentRepository, assignmentTreatment, buildExperimentReport, compileExperimentCaseBank, compileExperimentFollowup, compileExperimentOutcome, compileExperimentReviewStart, compileExperimentRun, compileExperimentStop, compileExp03ReviewResponse, currentExperimentOutcomes, normalizedEditDistance, } from "./experiment.js";
34
+ import { ExperimentRepository, assignmentTreatment, buildExperimentReport, compileExperimentCaseBank, compileExperimentFollowup, compileExperimentOutcome, compileExperimentReviewStart, compileExperimentReviewerQualification, compileExperimentRun, compileExperimentStop, compileExp03ReviewResponse, currentExperimentOutcomes, experimentReviewGuide, normalizedEditDistance, } from "./experiment.js";
35
35
  import { executeExp01Assignment } from "./experimentRunner.js";
36
36
  function relationSummary(policy) {
37
37
  return {
@@ -492,11 +492,22 @@ export class ConstitutionService {
492
492
  const bank = this.experimentRepository.listCaseBanks().find((item) => item.id === run.case_bank_id);
493
493
  if (!bank)
494
494
  throw new Error(`run ${run.id} is missing exact case bank ${run.case_bank_id}`);
495
+ const preregistration = this.g3Repository.listExperiments().find((item) => item.id === run.preregistration_id && item.content_hash === run.preregistration_hash);
496
+ const usesPlainLanguageReview = bank.cases.some((item) => "required_relationship" in item);
497
+ if (usesPlainLanguageReview && !preregistration)
498
+ throw new Error(`run ${run.id} is missing exact preregistration ${run.preregistration_id}`);
499
+ if (preregistration && preregistration.revision >= 2) {
500
+ const qualification = this.experimentRepository.listReviewerQualifications().find((item) => item.preregistration_id === preregistration.id && item.preregistration_hash === preregistration.content_hash && item.reviewer === reviewer);
501
+ if (!qualification)
502
+ throw new Error(`${reviewer} must pass the excluded plain-language comprehension check before a revision-${preregistration.revision} timed review`);
503
+ const targetReviewers = new Set(bank.cases.map((item) => item.strata.target_reviewer).filter(Boolean));
504
+ if (targetReviewers.has(reviewer) || targetReviewers.has(reviewer.replace(/^human:/i, "")))
505
+ throw new Error(`${reviewer} cannot perform timed reviews because the same actor labeled revision-${preregistration.revision} targets`);
506
+ }
495
507
  // Single-operator mitigation (expreg_9c9617cd13, revision >= 3): at least 48 hours
496
508
  // must separate the case-bank lock from the FIRST review start — enforced, not
497
509
  // merely auditable, so a violation is impossible rather than post-hoc visible.
498
- const prereg = this.g3Repository.listExperiments().find((item) => item.id === run.preregistration_id);
499
- if (prereg && prereg.revision >= 3) {
510
+ if (preregistration && preregistration.revision >= 3) {
500
511
  const elapsed = Date.parse(opts.now ?? new Date().toISOString()) - Date.parse(bank.locked_at);
501
512
  const hasStart = this.experimentRepository.listReviewStarts().some((item) => item.run_id === run.id);
502
513
  if (!hasStart && elapsed < 48 * 3_600_000) {
@@ -518,7 +529,13 @@ export class ConstitutionService {
518
529
  if (!assignment)
519
530
  throw new Error(`no unreviewed EXP-03 assignment is available for ${reviewer}`);
520
531
  const start = existing ?? this.experimentRepository.putReviewStart(compileExperimentReviewStart(run, assignment, reviewer, opts));
521
- return { start, assignment, treatment: assignmentTreatment(bank, run, assignment) };
532
+ return { start, assignment, treatment: assignmentTreatment(bank, run, assignment), review_guide: experimentReviewGuide(assignment.arm) };
533
+ }
534
+ qualifyExperimentReviewer(input, opts = {}) {
535
+ const preregistration = this.g3Repository.currentExperiments().find((item) => item.experiment === "EXP-03");
536
+ if (!preregistration)
537
+ throw new Error("no current EXP-03 preregistration");
538
+ return this.experimentRepository.putReviewerQualification(compileExperimentReviewerQualification(input, preregistration, opts));
522
539
  }
523
540
  /** Resolve an EXP-03 run/assignment/case triple (the shared lookup for both
524
541
  * review-submission dialects). */
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "1.8.2",
3
+ "version": "1.8.3",
4
4
  "license": "Apache-2.0",
5
5
  "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
6
- "description": "Architectural Conformance for AI-generated code: a git-native graph that deterministically blocks AI changes which break your architecture — the semantic invariants (layering, must-reach, dependency direction) pattern-SAST can't express grounded in the decisions and bugs behind each rule, across any MCP assistant (Claude Code, Cursor, Copilot, Windsurf, Antigravity, Codex).",
6
+ "description": "Engineering memory and a deterministic Change Gate for AI-assisted codebases: decisions, rejected approaches, constraints, and bug lineage become portable context and opt-in enforcement for every MCP assistant.",
7
7
  "homepage": "https://hunch-pi.vercel.app",
8
8
  "repository": {
9
9
  "type": "git",
@@ -19,6 +19,7 @@
19
19
  "files": [
20
20
  "dist/**/*.js",
21
21
  "bench/constitution-exp03-v1.json",
22
+ "tooling/competitive-watch.mjs",
22
23
  "LICENSE",
23
24
  "NOTICE"
24
25
  ],
@@ -33,7 +34,12 @@
33
34
  "windsurf",
34
35
  "antigravity",
35
36
  "mcp",
37
+ "coding-agents",
38
+ "agent-memory",
36
39
  "engineering-memory",
40
+ "architectural-conformance",
41
+ "change-gate",
42
+ "code-governance",
37
43
  "knowledge-graph",
38
44
  "code-intelligence",
39
45
  "ai",
@@ -52,6 +58,7 @@
52
58
  "rehearse:constitution": "npm run build && node tooling/constitution-clean-rehearsal.mjs",
53
59
  "gate:release": "node tooling/release-gate.mjs",
54
60
  "site:proof": "npm run build && node tooling/generate-public-proof.mjs",
61
+ "research:competitors": "node tooling/competitive-watch.mjs",
55
62
  "prepublishOnly": "npm run build"
56
63
  },
57
64
  "dependencies": {
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+
3
+ const repos = [
4
+ "davesheffer/hunch",
5
+ "gitmem-dev/gitmem",
6
+ "ismaelkedir/knowit",
7
+ "weigibbor/mnemo",
8
+ "oldskultxo/aictx",
9
+ "riponcm/projectmem",
10
+ "Cranot/roam-code",
11
+ "blackwell-systems/knowing",
12
+ ];
13
+
14
+ const distinctivePhrases = [
15
+ "Causal Merge Verdict",
16
+ "corrections become enforced",
17
+ "content-matched constraints",
18
+ "deterministic Change Gate for AI-assisted codebases",
19
+ ];
20
+
21
+ const token = process.env.GITHUB_TOKEN?.trim();
22
+ const headers = {
23
+ Accept: "application/vnd.github+json",
24
+ "User-Agent": "hunch-competitive-watch",
25
+ "X-GitHub-Api-Version": "2022-11-28",
26
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
27
+ };
28
+
29
+ async function github(path) {
30
+ const response = await fetch(`https://api.github.com${path}`, { headers });
31
+ if (!response.ok) {
32
+ const detail = (await response.text()).slice(0, 300).replaceAll("\n", " ");
33
+ throw new Error(`GitHub ${response.status} for ${path}: ${detail}`);
34
+ }
35
+ return response.json();
36
+ }
37
+
38
+ async function repoSnapshot(repo) {
39
+ const data = await github(`/repos/${repo}`);
40
+ return {
41
+ repo,
42
+ created: data.created_at,
43
+ pushed: data.pushed_at,
44
+ stars: data.stargazers_count,
45
+ forks: data.forks_count,
46
+ issues: data.open_issues_count,
47
+ url: data.html_url,
48
+ };
49
+ }
50
+
51
+ async function phraseSnapshot(phrase) {
52
+ const query = encodeURIComponent(`\"${phrase}\"`);
53
+ const data = await github(`/search/code?q=${query}&per_page=100`);
54
+ const external = data.items
55
+ .filter((item) => !item.repository.full_name.startsWith("davesheffer/"))
56
+ .map((item) => ({
57
+ repo: item.repository.full_name,
58
+ path: item.path,
59
+ url: item.html_url,
60
+ }));
61
+ return { phrase, total: data.total_count, external };
62
+ }
63
+
64
+ function render(snapshot, phrases) {
65
+ const lines = [
66
+ `# Competitive watch — ${new Date().toISOString()}`,
67
+ "",
68
+ "## Public repository signals",
69
+ "",
70
+ "| Repository | Created | Last push | Stars | Forks | Open issues |",
71
+ "| --- | --- | --- | ---: | ---: | ---: |",
72
+ ];
73
+
74
+ for (const item of snapshot) {
75
+ lines.push(
76
+ `| [${item.repo}](${item.url}) | ${item.created.slice(0, 10)} | ${item.pushed.slice(0, 10)} | ${item.stars} | ${item.forks} | ${item.issues} |`,
77
+ );
78
+ }
79
+
80
+ lines.push("", "## Distinctive phrase search", "");
81
+ if (!token) {
82
+ lines.push("Skipped: set `GITHUB_TOKEN` to enable authenticated GitHub code search.");
83
+ } else {
84
+ for (const result of phrases) {
85
+ lines.push(`- **${result.phrase}** — ${result.external.length} external indexed match(es)`);
86
+ for (const match of result.external) {
87
+ lines.push(` - [${match.repo} · ${match.path}](${match.url})`);
88
+ }
89
+ }
90
+ }
91
+
92
+ lines.push(
93
+ "",
94
+ "> Signals are leads, not copying findings. Re-check chronology and substantial similarity before drawing a conclusion.",
95
+ );
96
+ return `${lines.join("\n")}\n`;
97
+ }
98
+
99
+ try {
100
+ const snapshot = await Promise.all(repos.map(repoSnapshot));
101
+ const phrases = token
102
+ ? await Promise.all(distinctivePhrases.map(phraseSnapshot))
103
+ : [];
104
+ process.stdout.write(render(snapshot, phrases));
105
+ } catch (error) {
106
+ process.stderr.write(`competitive-watch: ${error instanceof Error ? error.message : String(error)}\n`);
107
+ process.exitCode = 1;
108
+ }