@skein-code/cli 0.3.16 → 0.3.17

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
@@ -45,10 +45,12 @@ inspectable index for code retrieval and no retrieval service dependency.
45
45
  enforcement, changed-file telemetry, and persisted tool results.
46
46
  - **Repository reuse guard:** the first substantive implementation addition gets
47
47
  a warning-only, content-free receipt of current helper candidates and read
48
- evidence. After a successful TS/JS write, Skein also compares newly added or
48
+ evidence. After a successful TS/JS write, Skein compares newly added or
49
49
  significantly expanded functions with the pre-write index generation using
50
- normalized fingerprints. Docs, config, fixtures, generated files, deletions,
51
- small functions, and ordinary local edits stay quiet.
50
+ normalized fingerprints. Calibrated Type-1/2 matches block completion until
51
+ reuse, removal, or exact audited suppression; Type-3 remains warning-only.
52
+ Docs, config, fixtures, generated files, deletions, small functions, and
53
+ ordinary local edits stay quiet.
52
54
  - **Bounded tool context:** large tool results cannot crowd the task out of the
53
55
  model window. Skein keeps a token-budgeted head/tail receipt and, when the
54
56
  producer captured the complete result, retains a redacted session-scoped
package/dist/cli.js CHANGED
@@ -220,7 +220,7 @@ function isSqliteBusy(error) {
220
220
  // package.json
221
221
  var package_default = {
222
222
  name: "@skein-code/cli",
223
- version: "0.3.16",
223
+ version: "0.3.17",
224
224
  description: "A context-first, model-agnostic coding agent with an auditable terminal workspace.",
225
225
  type: "module",
226
226
  license: "MIT",
@@ -236,6 +236,9 @@ var package_default = {
236
236
  },
237
237
  skein: {
238
238
  releaseNotes: [
239
+ "Calibrated Type-1/2 duplication matches now block completion until reuse or exact audited suppression",
240
+ "Type-3 duplication remains warning-only and Type-4 semantic equivalence remains explicitly unsupported",
241
+ "Duplication benchmark fixtures report deterministic recall, precision, false-positive rate, and latency",
239
242
  "Duplication warnings now aggregate into the single completion receipt without changing verified status",
240
243
  "Exact match ids support bounded, reason-coded suppression with audit and credential/code-block safeguards",
241
244
  "Duplication audit tools are disclosed only when active warnings exist and repaired paths clear stale matches",
@@ -274,6 +277,7 @@ var package_default = {
274
277
  "pretest:pty": "npm run build",
275
278
  "test:pty": "sh test/pty/run-visual.sh",
276
279
  "benchmark:context": "tsx scripts/benchmark-local-index.ts",
280
+ "benchmark:duplication": "tsx scripts/benchmark-duplication.ts",
277
281
  "verify:package": "node scripts/verify-package.mjs",
278
282
  "release:verify": "npm run check && npm run verify:package --",
279
283
  typecheck: "tsc --noEmit",
@@ -5674,7 +5678,7 @@ var reuseReceiptSchema = z5.object({
5674
5678
  indexGeneration: z5.string().optional(),
5675
5679
  changeSequence: z5.number().int().nonnegative(),
5676
5680
  status: z5.enum(["warning", "skipped", "unresolved"]),
5677
- warningOnly: z5.literal(true)
5681
+ warningOnly: z5.boolean()
5678
5682
  }).strict();
5679
5683
  var duplicationMatchSchema = z5.object({
5680
5684
  matchId: z5.string().regex(/^[a-f0-9]{24}$/u).optional(),
@@ -5689,7 +5693,8 @@ var duplicationAuditSchema = z5.object({
5689
5693
  baselineGeneration: z5.string().min(1),
5690
5694
  changeSequence: z5.number().int().nonnegative(),
5691
5695
  status: z5.enum(["clear", "warning", "unresolved"]),
5692
- warningOnly: z5.literal(true),
5696
+ warningOnly: z5.boolean(),
5697
+ enforcement: z5.enum(["warning", "blocking"]).optional(),
5693
5698
  checkedFunctions: z5.number().int().nonnegative(),
5694
5699
  skippedSmallFunctions: z5.number().int().nonnegative(),
5695
5700
  matches: z5.array(duplicationMatchSchema).max(8),
@@ -5759,7 +5764,7 @@ var lastRunSchema = z5.object({
5759
5764
  detail: z5.string(),
5760
5765
  mutationTracking: z5.enum(["complete", "unknown"]).optional(),
5761
5766
  duplication: z5.object({
5762
- enforcement: z5.literal("warning"),
5767
+ enforcement: z5.enum(["warning", "blocking"]),
5763
5768
  status: z5.enum(["clear", "warning", "unresolved", "suppressed"]),
5764
5769
  warningCount: z5.number().int().nonnegative(),
5765
5770
  unresolvedCount: z5.number().int().nonnegative(),
@@ -8397,6 +8402,16 @@ function buildRunCompletion(changedFiles, evidence, currentChangeSequence, mutat
8397
8402
  ...duplication ? { duplication } : {}
8398
8403
  };
8399
8404
  }
8405
+ if (duplication?.enforcement === "blocking" && duplication.warningCount > 0) {
8406
+ return {
8407
+ status: "unverified",
8408
+ changedFiles: files,
8409
+ checks: [],
8410
+ detail: "A high-confidence duplicate implementation requires reuse, extension, or an exact audited suppression before completion.",
8411
+ ...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "unverified") } : {},
8412
+ duplication
8413
+ };
8414
+ }
8400
8415
  const latestByCommand = /* @__PURE__ */ new Map();
8401
8416
  for (const item of evidence) {
8402
8417
  if (item.changeSequence === currentChangeSequence) {
@@ -8445,6 +8460,13 @@ function buildRunCompletion(changedFiles, evidence, currentChangeSequence, mutat
8445
8460
  };
8446
8461
  }
8447
8462
  function completionRecoveryDirective(completion) {
8463
+ if (completion.duplication?.enforcement === "blocking" && completion.duplication.warningCount > 0) {
8464
+ const matches = completion.duplication.matches.filter((match) => match.kind === "type-1-or-2").map((match) => `- ${match.matchId}: ${match.changedPath} ${match.changedSymbol} duplicates ${match.candidatePath} ${match.candidateSymbol}`).join("\n");
8465
+ return `<runtime-completion-gate status="duplication_blocking" authorization="none">
8466
+ High-confidence Type-1/2 duplication was detected. Reuse or extend the existing implementation, remove the duplicate, or call the read-only duplication_audit tool to suppress an exact match with an audited reason. Suppression never waives verification, safety, accessibility, error handling, concurrency, or correctness requirements.
8467
+ ${matches || "- Active duplicate match details are unavailable."}
8468
+ </runtime-completion-gate>`;
8469
+ }
8448
8470
  if (completion.acceptance && acceptanceUnresolved(completion.acceptance)) {
8449
8471
  const unresolved2 = completion.acceptance.unresolved.map((item) => `- [${item.status}] ${item.id}: ${item.description}`).join("\n");
8450
8472
  const failed = completion.checks.filter((check) => !check.ok).map((check) => `- ${check.command} (tool call ${check.toolCallId})`).join("\n");
@@ -8885,8 +8907,9 @@ function buildDuplicationCompletion(audit, suppressions = [], runStartedAt) {
8885
8907
  const suppressedIds = new Set(suppressions.filter((item) => activeIds.has(item.matchId)).map((item) => item.matchId));
8886
8908
  const warnings = [...active.values()].filter((match) => !suppressedIds.has(match.matchId));
8887
8909
  const suppressedCount = active.size - warnings.length;
8910
+ const enforcement = warnings.some((match) => match.kind === "type-1-or-2") ? "blocking" : "warning";
8888
8911
  return {
8889
- enforcement: "warning",
8912
+ enforcement,
8890
8913
  status: unresolved2.size ? "unresolved" : warnings.length ? "warning" : suppressedCount ? "suppressed" : "clear",
8891
8914
  warningCount: warnings.length,
8892
8915
  unresolvedCount: unresolved2.size,
@@ -9807,9 +9830,13 @@ function roundScore(value) {
9807
9830
  // src/agent/duplication-audit.ts
9808
9831
  import { createHash as createHash14 } from "node:crypto";
9809
9832
  import { readFile as readFile15 } from "node:fs/promises";
9810
- var NEAR_CLONE_THRESHOLD = 0.55;
9833
+ var DEFAULT_NEAR_CLONE_THRESHOLD = 0.55;
9811
9834
  var MAX_MATCHES = 8;
9812
9835
  async function auditChangedFunctions(input2) {
9836
+ const nearCloneThreshold = input2.nearCloneThreshold ?? DEFAULT_NEAR_CLONE_THRESHOLD;
9837
+ if (!Number.isFinite(nearCloneThreshold) || nearCloneThreshold < 0 || nearCloneThreshold > 1) {
9838
+ throw new Error("nearCloneThreshold must be a finite number between 0 and 1.");
9839
+ }
9813
9840
  const auditableFiles = input2.changedFiles.filter(supportsFunctionFingerprintPath);
9814
9841
  if (!auditableFiles.length) return void 0;
9815
9842
  const reports = [];
@@ -9836,7 +9863,7 @@ async function auditChangedFunctions(input2) {
9836
9863
  }
9837
9864
  if (!input2.baseline) return unresolved(input2.changeSequence);
9838
9865
  const lookup = createBaselineLookup(input2.baseline.functions);
9839
- const previousByCurrent = matchCurrentFunctions(input2.baseline.functions, currentFunctions);
9866
+ const previousByCurrent = matchCurrentFunctions(input2.baseline.functions, currentFunctions, nearCloneThreshold);
9840
9867
  const added = currentFunctions.flatMap((current) => {
9841
9868
  const previous = previousByCurrent.get(locationIdentity(current));
9842
9869
  const recheck = input2.recheckFunctions?.has(identity(current)) || previous !== void 0 && input2.recheckFunctions?.has(identity(previous));
@@ -9849,7 +9876,7 @@ async function auditChangedFunctions(input2) {
9849
9876
  for (const candidate of findCandidateFunctions(lookup, item)) {
9850
9877
  if (previous && locationIdentity(candidate) === locationIdentity(previous)) continue;
9851
9878
  const similarity = fingerprintSimilarity(item, candidate);
9852
- if (similarity < NEAR_CLONE_THRESHOLD || best && best.similarity >= similarity) continue;
9879
+ if (similarity < nearCloneThreshold || best && best.similarity >= similarity) continue;
9853
9880
  best = { candidate, similarity };
9854
9881
  }
9855
9882
  if (!best) continue;
@@ -9865,11 +9892,13 @@ async function auditChangedFunctions(input2) {
9865
9892
  }
9866
9893
  matches.sort((left, right) => right.similarity - left.similarity || left.changedPath.localeCompare(right.changedPath));
9867
9894
  const bounded = matches.slice(0, MAX_MATCHES);
9895
+ const enforcement = bounded.some((match) => match.kind === "type-1-or-2") ? "blocking" : "warning";
9868
9896
  return {
9869
9897
  baselineGeneration: input2.baseline.generation,
9870
9898
  changeSequence: input2.changeSequence,
9871
9899
  status: bounded.length ? "warning" : "clear",
9872
- warningOnly: true,
9900
+ warningOnly: enforcement === "warning",
9901
+ enforcement,
9873
9902
  checkedFunctions: added.length,
9874
9903
  skippedSmallFunctions,
9875
9904
  matches: bounded,
@@ -9882,6 +9911,7 @@ function clear(baselineGeneration, changeSequence, skippedSmallFunctions) {
9882
9911
  changeSequence,
9883
9912
  status: "clear",
9884
9913
  warningOnly: true,
9914
+ enforcement: "warning",
9885
9915
  checkedFunctions: 0,
9886
9916
  skippedSmallFunctions,
9887
9917
  matches: [],
@@ -9931,15 +9961,15 @@ function findCandidateFunctions(lookup, current) {
9931
9961
  }
9932
9962
  return [...candidates.values()];
9933
9963
  }
9934
- function matchCurrentFunctions(baseline, current) {
9964
+ function matchCurrentFunctions(baseline, current, nearCloneThreshold) {
9935
9965
  const matches = /* @__PURE__ */ new Map();
9936
9966
  const available = new Map(baseline.map((item) => [locationIdentity(item), item]));
9937
- assignMatches(current, matches, available, (item) => [...available.values()].filter((candidate) => identity(candidate) === identity(item) && linesOverlap(candidate, item)), false);
9938
- assignMatches(current, matches, available, (item) => [...available.values()].filter((candidate) => identity(candidate) === identity(item)), true);
9939
- assignMatches(current, matches, available, (item) => [...available.values()].filter((candidate) => candidate.path === item.path && linesOverlap(candidate, item)), true);
9967
+ assignMatches(current, matches, available, (item) => [...available.values()].filter((candidate) => identity(candidate) === identity(item) && linesOverlap(candidate, item)), false, nearCloneThreshold);
9968
+ assignMatches(current, matches, available, (item) => [...available.values()].filter((candidate) => identity(candidate) === identity(item)), true, nearCloneThreshold);
9969
+ assignMatches(current, matches, available, (item) => [...available.values()].filter((candidate) => candidate.path === item.path && linesOverlap(candidate, item)), true, nearCloneThreshold);
9940
9970
  return matches;
9941
9971
  }
9942
- function assignMatches(current, matches, available, candidatesFor, requireSimilarity) {
9972
+ function assignMatches(current, matches, available, candidatesFor, requireSimilarity, nearCloneThreshold) {
9943
9973
  for (const item of current) {
9944
9974
  const currentId = locationIdentity(item);
9945
9975
  if (matches.has(currentId)) continue;
@@ -9948,7 +9978,7 @@ function assignMatches(current, matches, available, candidatesFor, requireSimila
9948
9978
  const similarity = fingerprintSimilarity(candidate, item);
9949
9979
  if (!best || similarity > best.similarity) best = { candidate, similarity };
9950
9980
  }
9951
- if (!best || requireSimilarity && best.similarity < NEAR_CLONE_THRESHOLD) continue;
9981
+ if (!best || requireSimilarity && best.similarity < nearCloneThreshold) continue;
9952
9982
  matches.set(currentId, best.candidate);
9953
9983
  available.delete(locationIdentity(best.candidate));
9954
9984
  }
@@ -9962,6 +9992,7 @@ function unresolved(changeSequence, generation = "unavailable") {
9962
9992
  changeSequence,
9963
9993
  status: "unresolved",
9964
9994
  warningOnly: true,
9995
+ enforcement: "warning",
9965
9996
  checkedFunctions: 0,
9966
9997
  skippedSmallFunctions: 0,
9967
9998
  matches: [],
@@ -10588,7 +10619,8 @@ Tool succeeded, but afterTool hook failed: ${afterHookError.message}` : executio
10588
10619
 
10589
10620
  ${completeContent}`;
10590
10621
  if (duplicationAudit?.status === "warning" || duplicationAudit?.status === "unresolved") {
10591
- completeContent = `Duplication audit (warning-only): ${duplicationAudit.rationale}
10622
+ const enforcement = duplicationAudit.enforcement === "blocking" ? "completion-blocking Type-1/2" : "warning-only";
10623
+ completeContent = `Duplication audit (${enforcement}): ${duplicationAudit.rationale}
10592
10624
 
10593
10625
  ${completeContent}`;
10594
10626
  }