@davesheffer/hunch 1.20.0 → 1.20.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.
package/README.md CHANGED
@@ -171,7 +171,7 @@ repository, separate from the code repository. Hunch does not host it. Create a
171
171
  that every teammate can access, install Hunch on team machines and CI, then have one maintainer run:
172
172
 
173
173
  ```bash
174
- npm i -g @davesheffer/hunch@1.20.0
174
+ npm i -g @davesheffer/hunch@1.20.1
175
175
  hunch shared --repo git@github.com:acme/project-hunch-memory.git
176
176
  git add .gitignore .hunch/team.json
177
177
  git commit -m "chore: connect shared Hunch memory"
@@ -186,7 +186,7 @@ printed by Hunch. Omit `--migrate` for a new setup.
186
186
  After the pointer commit lands, teammates need Hunch installed and Git access to the memory repo:
187
187
 
188
188
  ```bash
189
- npm i -g @davesheffer/hunch@1.20.0
189
+ npm i -g @davesheffer/hunch@1.20.1
190
190
  git pull
191
191
  hunch init
192
192
  hunch doctor
@@ -234,7 +234,7 @@ but stops automatic memory commits and pushes. As a team-coordinated rollback, r
234
234
  commit to stop discovery after teammates pull the revert. Existing machines retain their ignored
235
235
  local overlay until they are deliberately disconnected; do not delete the memory repo as part of a
236
236
  rollback. For this rollout, reinstall the previous published package with
237
- `npm i -g @davesheffer/hunch@1.19.0`; the release receipt resolves and records the verified rollback
237
+ `npm i -g @davesheffer/hunch@1.20.0`; the release receipt resolves and records the verified rollback
238
238
  target from the npm registry instead of trusting Git tags. Pause enforcement first as shown above,
239
239
  and keep every team client on the same release before resuming Matrix policy workflows.
240
240
 
package/dist/cli/index.js CHANGED
@@ -83,7 +83,8 @@ import { pendingEscalations, policyEscalations } from "../core/escalations.js";
83
83
  import { premiseEscalations } from "../core/premises.js";
84
84
  import { parseDocAnchors, renderDocGrounding } from "../core/docanchors.js";
85
85
  import { compareCandidates } from "../core/compare.js";
86
- import { planLandscapeAdoption } from "../core/landscapeAdoption.js";
86
+ import { compareCodeUnits } from "../core/canonicalOrder.js";
87
+ import { MAX_LANDSCAPE_REFRESH_REVISIONS, planLandscapeAdoption, } from "../core/landscapeAdoption.js";
87
88
  import { discoverRepositoryLandscape } from "../extractors/landscapeDiscovery.js";
88
89
  import { checkConformance } from "../core/conformance.js";
89
90
  import { ConstitutionService, policyEvaluationEnvelope } from "../constitution/service.js";
@@ -2911,22 +2912,46 @@ landscapeCmd
2911
2912
  .option("--all", "adopt every candidate in the reviewed discovery")
2912
2913
  .option("--candidate <hashes...>", "adopt only these candidate hashes")
2913
2914
  .option("--acknowledge-issues", "confirm that the printed discovery issues were reviewed")
2915
+ .option("--refresh-reviewed", "replace only byte-proven records from an older reviewed exact revision")
2914
2916
  .action((opts) => {
2915
2917
  if (opts.all && opts.candidate?.length)
2916
2918
  return fail("choose either --all or --candidate, not both");
2917
2919
  if (!opts.all && !opts.candidate?.length)
2918
2920
  return fail("choose --all or name reviewed hashes with --candidate <hashes...>");
2921
+ if (opts.refreshReviewed && !opts.all)
2922
+ return fail("--refresh-reviewed requires --all so the complete prior review can be proven");
2919
2923
  const { store, root } = storeFor();
2920
2924
  try {
2921
2925
  const discovery = discoverRepositoryLandscape(root, opts.ref);
2926
+ const existingResources = store.recs("resources");
2927
+ const existingRelationships = store.recs("edges");
2928
+ const selectedHashes = new Set(opts.all
2929
+ ? [...discovery.resources, ...discovery.relationships].map((candidate) => candidate.candidateHash)
2930
+ : opts.candidate);
2931
+ const selectedIds = new Set([...discovery.resources, ...discovery.relationships]
2932
+ .filter((candidate) => selectedHashes.has(candidate.candidateHash))
2933
+ .map((candidate) => candidate.record.id));
2934
+ const previousRevisions = opts.refreshReviewed
2935
+ ? [...new Set([...existingResources, ...existingRelationships]
2936
+ .filter((record) => selectedIds.has(record.id))
2937
+ .map((record) => record.currentness?.source_revision)
2938
+ .filter((revision) => Boolean(revision) && revision !== discovery.sourceRevision))]
2939
+ .sort(compareCodeUnits)
2940
+ : [];
2941
+ if (previousRevisions.length > MAX_LANDSCAPE_REFRESH_REVISIONS) {
2942
+ return fail(`reviewed refresh needs ${previousRevisions.length} prior revisions; the safe limit is ${MAX_LANDSCAPE_REFRESH_REVISIONS}`);
2943
+ }
2944
+ const previousDiscoveries = previousRevisions.map((revision) => discoverRepositoryLandscape(root, revision));
2922
2945
  const plan = planLandscapeAdoption({
2923
2946
  discovery,
2924
2947
  expectedDiscoveryHash: opts.expected,
2925
2948
  reviewer: opts.reviewedBy,
2926
2949
  candidateHashes: opts.all ? "all" : opts.candidate,
2927
2950
  acknowledgeIssues: opts.acknowledgeIssues,
2928
- existingResources: store.recs("resources"),
2929
- existingRelationships: store.recs("edges"),
2951
+ existingResources,
2952
+ existingRelationships,
2953
+ refreshReviewed: opts.refreshReviewed,
2954
+ previousDiscoveries,
2930
2955
  });
2931
2956
  for (const resource of plan.resourcesToWrite)
2932
2957
  store.putCapture("resources", resource);
@@ -2937,7 +2962,8 @@ landscapeCmd
2937
2962
  pumpMemoryHome(store, root, store.captureHome(false), "hunch: adopt reviewed Engineering Landscape candidates");
2938
2963
  }
2939
2964
  console.log(JSON.stringify(plan.receipt, null, 2));
2940
- console.log(`✓ accepted ${plan.receipt.acceptedResourceIds.length} resource(s) and ${plan.receipt.acceptedRelationshipIds.length} relationship(s); wrote ${plan.resourcesToWrite.length + plan.relationshipsToWrite.length}, reused ${plan.receipt.reusedResourceIds.length + plan.receipt.reusedRelationshipIds.length}.`);
2965
+ const refreshed = plan.refreshedResourceIds.length + plan.refreshedRelationshipIds.length;
2966
+ console.log(`✓ accepted ${plan.receipt.acceptedResourceIds.length} resource(s) and ${plan.receipt.acceptedRelationshipIds.length} relationship(s); wrote ${plan.resourcesToWrite.length + plan.relationshipsToWrite.length} (${refreshed} refreshed), reused ${plan.receipt.reusedResourceIds.length + plan.receipt.reusedRelationshipIds.length}.`);
2941
2967
  }
2942
2968
  catch (error) {
2943
2969
  fail(error instanceof Error ? error.message : String(error));
@@ -5,6 +5,7 @@ export const LANDSCAPE_REVIEW_SCHEMA_VERSION = "hunch.landscape-review/1";
5
5
  export const LANDSCAPE_ADOPTION_RECEIPT_SCHEMA_VERSION = "hunch.landscape-adoption-receipt/1";
6
6
  const SHA256 = /^sha256:[a-f0-9]{64}$/;
7
7
  const MAX_SELECTIONS = 4_096;
8
+ export const MAX_LANDSCAPE_REFRESH_REVISIONS = 16;
8
9
  function sortedUnique(values, label) {
9
10
  if (values.length > MAX_SELECTIONS)
10
11
  throw new Error(`landscape ${label} exceeds the bounded selection limit`);
@@ -168,6 +169,71 @@ function sameAcceptedCandidate(record, candidate, discoveryHash) {
168
169
  // when the entire reviewed record still equals the candidate-derived value.
169
170
  return landscapeContentHash(record) === landscapeContentHash(expected);
170
171
  }
172
+ function previousDiscoveriesForRefresh(input) {
173
+ const previous = input.previousDiscoveries ?? [];
174
+ if (!input.refreshReviewed) {
175
+ if (previous.length > 0)
176
+ throw new Error("landscape previous discoveries require explicit reviewed refresh authority");
177
+ return new Map();
178
+ }
179
+ if (input.candidateHashes !== "all") {
180
+ throw new Error("landscape reviewed refresh requires the complete candidate set (--all)");
181
+ }
182
+ if (previous.length > MAX_LANDSCAPE_REFRESH_REVISIONS) {
183
+ throw new Error(`landscape reviewed refresh exceeds the bounded prior-revision proof limit (${MAX_LANDSCAPE_REFRESH_REVISIONS})`);
184
+ }
185
+ const byRevision = new Map();
186
+ for (const discovery of previous) {
187
+ assertLandscapeDiscoveryIntegrity(discovery);
188
+ if (discovery.repositoryRootIdentity !== input.discovery.repositoryRootIdentity) {
189
+ throw new Error("landscape reviewed refresh prior discovery belongs to a different repository");
190
+ }
191
+ if (discovery.sourceRevision === input.discovery.sourceRevision) {
192
+ throw new Error("landscape reviewed refresh requires an older exact discovery revision");
193
+ }
194
+ if (byRevision.has(discovery.sourceRevision)) {
195
+ throw new Error(`landscape reviewed refresh contains duplicate prior revision: ${discovery.sourceRevision}`);
196
+ }
197
+ byRevision.set(discovery.sourceRevision, discovery);
198
+ }
199
+ return byRevision;
200
+ }
201
+ function isProvenPreviousAdoption(record, previousDiscoveries) {
202
+ const sourceRevision = record.currentness?.source_revision;
203
+ if (!sourceRevision)
204
+ return false;
205
+ const previous = previousDiscoveries.get(sourceRevision);
206
+ if (!previous)
207
+ return false;
208
+ const candidates = record.schema === "hunch.resource/1"
209
+ ? previous.resources
210
+ : previous.relationships;
211
+ const candidate = candidates.find((value) => value.record.id === record.id);
212
+ if (!candidate || !sameAcceptedCandidate(record, candidate, previous.discoveryHash))
213
+ return false;
214
+ const reviewer = record.metadata.landscape_reviewed_by;
215
+ const reviewedAt = record.metadata.landscape_reviewed_at;
216
+ const reviewId = record.metadata.landscape_review_id;
217
+ if (typeof reviewer !== "string" || typeof reviewedAt !== "string" || typeof reviewId !== "string")
218
+ return false;
219
+ try {
220
+ const allCandidateHashes = [...previous.resources, ...previous.relationships]
221
+ .map((value) => value.candidateHash)
222
+ .sort(compareCodeUnits);
223
+ const reconstructed = reviewFor({
224
+ discovery: previous,
225
+ expectedDiscoveryHash: previous.discoveryHash,
226
+ reviewer,
227
+ reviewedAt,
228
+ candidateHashes: "all",
229
+ acknowledgeIssues: previous.issues.length > 0,
230
+ }, allCandidateHashes);
231
+ return reconstructed.reviewId === reviewId;
232
+ }
233
+ catch {
234
+ return false;
235
+ }
236
+ }
171
237
  /**
172
238
  * Convert an exact candidate discovery into a prevalidated write plan.
173
239
  *
@@ -208,6 +274,7 @@ export function planLandscapeAdoption(input) {
208
274
  }
209
275
  }
210
276
  const review = reviewFor(input, selectedCandidateHashes);
277
+ const previousDiscoveries = previousDiscoveriesForRefresh(input);
211
278
  const existingResources = new Map();
212
279
  for (const value of input.existingResources ?? []) {
213
280
  const record = ResourceSchema.parse(value);
@@ -226,13 +293,21 @@ export function planLandscapeAdoption(input) {
226
293
  const relationshipsToWrite = [];
227
294
  const reusedResourceIds = [];
228
295
  const reusedRelationshipIds = [];
296
+ const refreshedResourceIds = [];
297
+ const refreshedRelationshipIds = [];
229
298
  for (const candidate of selectedResources) {
230
299
  const existing = existingResources.get(candidate.record.id);
231
300
  if (existing) {
232
- if (!sameAcceptedCandidate(existing, candidate, input.discovery.discoveryHash)) {
301
+ if (sameAcceptedCandidate(existing, candidate, input.discovery.discoveryHash)) {
302
+ reusedResourceIds.push(existing.id);
303
+ }
304
+ else if (input.refreshReviewed && isProvenPreviousAdoption(existing, previousDiscoveries)) {
305
+ resourcesToWrite.push(acceptedResource(candidate, input.discovery.discoveryHash, review));
306
+ refreshedResourceIds.push(existing.id);
307
+ }
308
+ else {
233
309
  throw new Error(`landscape resource ${candidate.record.id} already exists with different reviewed content`);
234
310
  }
235
- reusedResourceIds.push(existing.id);
236
311
  }
237
312
  else {
238
313
  resourcesToWrite.push(acceptedResource(candidate, input.discovery.discoveryHash, review));
@@ -241,10 +316,16 @@ export function planLandscapeAdoption(input) {
241
316
  for (const candidate of selectedRelationships) {
242
317
  const existing = existingRelationships.get(candidate.record.id);
243
318
  if (existing) {
244
- if (!sameAcceptedCandidate(existing, candidate, input.discovery.discoveryHash)) {
319
+ if (sameAcceptedCandidate(existing, candidate, input.discovery.discoveryHash)) {
320
+ reusedRelationshipIds.push(existing.id);
321
+ }
322
+ else if (input.refreshReviewed && isProvenPreviousAdoption(existing, previousDiscoveries)) {
323
+ relationshipsToWrite.push(acceptedRelationship(candidate, input.discovery.discoveryHash, review));
324
+ refreshedRelationshipIds.push(existing.id);
325
+ }
326
+ else {
245
327
  throw new Error(`landscape relationship ${candidate.record.id} already exists with different reviewed content`);
246
328
  }
247
- reusedRelationshipIds.push(existing.id);
248
329
  }
249
330
  else {
250
331
  relationshipsToWrite.push(acceptedRelationship(candidate, input.discovery.discoveryHash, review));
@@ -266,6 +347,8 @@ export function planLandscapeAdoption(input) {
266
347
  receipt: { ...receiptUnsigned, receiptId },
267
348
  resourcesToWrite,
268
349
  relationshipsToWrite,
350
+ refreshedResourceIds: refreshedResourceIds.sort(compareCodeUnits),
351
+ refreshedRelationshipIds: refreshedRelationshipIds.sort(compareCodeUnits),
269
352
  };
270
353
  }
271
354
  //# sourceMappingURL=landscapeAdoption.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "1.20.0",
3
+ "version": "1.20.1",
4
4
  "mcpName": "io.github.davesheffer/hunch",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
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.20.0",
10
+ "version": "1.20.1",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@davesheffer/hunch",
16
- "version": "1.20.0",
16
+ "version": "1.20.1",
17
17
  "runtimeHint": "npx",
18
18
  "packageArguments": [
19
19
  {