@deftai/directive-core 0.98.1 → 0.100.0

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.
Files changed (144) hide show
  1. package/dist/authz/actions.d.ts +6 -0
  2. package/dist/authz/actions.js +3 -0
  3. package/dist/authz/classify.js +401 -75
  4. package/dist/authz/decompose-apply.d.ts +67 -0
  5. package/dist/authz/decompose-apply.js +302 -0
  6. package/dist/authz/index.d.ts +3 -1
  7. package/dist/authz/index.js +3 -1
  8. package/dist/authz/store.d.ts +53 -0
  9. package/dist/authz/store.js +219 -1
  10. package/dist/authz/types.d.ts +19 -2
  11. package/dist/authz/types.js +5 -1
  12. package/dist/consumer-check-contract/evaluate.d.ts +40 -0
  13. package/dist/consumer-check-contract/evaluate.js +188 -3
  14. package/dist/consumer-check-contract/index.d.ts +1 -1
  15. package/dist/consumer-check-contract/index.js +1 -1
  16. package/dist/content-contracts/skills/greptile-detector.d.ts +42 -0
  17. package/dist/content-contracts/skills/greptile-detector.js +202 -4
  18. package/dist/decision/index.d.ts +17 -0
  19. package/dist/decision/index.js +35 -0
  20. package/dist/decision/list.d.ts +47 -0
  21. package/dist/decision/list.js +250 -0
  22. package/dist/decision/schema.d.ts +88 -0
  23. package/dist/decision/schema.js +293 -0
  24. package/dist/decision/write.d.ts +82 -0
  25. package/dist/decision/write.js +427 -0
  26. package/dist/doctor/checks.d.ts +44 -0
  27. package/dist/doctor/checks.js +428 -2
  28. package/dist/doctor/main.d.ts +6 -7
  29. package/dist/doctor/main.js +72 -79
  30. package/dist/doctor/which.d.ts +20 -1
  31. package/dist/doctor/which.js +67 -1
  32. package/dist/eval/report.d.ts +29 -0
  33. package/dist/eval/report.js +69 -0
  34. package/dist/eval/run.d.ts +9 -0
  35. package/dist/eval/run.js +40 -4
  36. package/dist/eval/version-pin.d.ts +99 -0
  37. package/dist/eval/version-pin.js +181 -0
  38. package/dist/finish-loop/pr-finish-loop.d.ts +13 -2
  39. package/dist/finish-loop/pr-finish-loop.js +116 -5
  40. package/dist/finish-loop/types.d.ts +1 -1
  41. package/dist/hooks/dispatcher.d.ts +9 -5
  42. package/dist/hooks/dispatcher.js +23 -15
  43. package/dist/hooks/readonly.d.ts +6 -3
  44. package/dist/hooks/readonly.js +44 -25
  45. package/dist/index.d.ts +2 -0
  46. package/dist/index.js +2 -0
  47. package/dist/init-deposit/agent-hooks.d.ts +20 -0
  48. package/dist/init-deposit/agent-hooks.js +51 -31
  49. package/dist/intake/issue-ingest.js +29 -0
  50. package/dist/lifecycle/completed-consistency.d.ts +60 -0
  51. package/dist/lifecycle/completed-consistency.js +443 -0
  52. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +47 -0
  53. package/dist/lifecycle/completed-tracked-on-delivery.js +415 -0
  54. package/dist/lifecycle/index.d.ts +2 -0
  55. package/dist/lifecycle/index.js +2 -0
  56. package/dist/literal-acceptance/capture.d.ts +52 -0
  57. package/dist/literal-acceptance/capture.js +658 -0
  58. package/dist/literal-acceptance/evaluate.d.ts +47 -0
  59. package/dist/literal-acceptance/evaluate.js +193 -0
  60. package/dist/literal-acceptance/index.d.ts +12 -0
  61. package/dist/literal-acceptance/index.js +12 -0
  62. package/dist/literal-acceptance/run.d.ts +38 -0
  63. package/dist/literal-acceptance/run.js +216 -0
  64. package/dist/literal-acceptance/safety.d.ts +24 -0
  65. package/dist/literal-acceptance/safety.js +251 -0
  66. package/dist/literal-acceptance/types.d.ts +82 -0
  67. package/dist/literal-acceptance/types.js +26 -0
  68. package/dist/platform/host-content-surface.d.ts +74 -0
  69. package/dist/platform/host-content-surface.js +214 -0
  70. package/dist/platform/index.d.ts +1 -0
  71. package/dist/platform/index.js +1 -0
  72. package/dist/policy/ceremony-dial.d.ts +289 -0
  73. package/dist/policy/ceremony-dial.js +980 -0
  74. package/dist/policy/deft-directive-disable.js +12 -2
  75. package/dist/policy/index.d.ts +2 -0
  76. package/dist/policy/index.js +16 -1
  77. package/dist/policy/merge-approval-head.d.ts +129 -0
  78. package/dist/policy/merge-approval-head.js +461 -0
  79. package/dist/policy/require-human-merge.d.ts +5 -0
  80. package/dist/policy/require-human-merge.js +5 -0
  81. package/dist/pr-merge-readiness/ci-gate.d.ts +20 -3
  82. package/dist/pr-merge-readiness/ci-gate.js +65 -5
  83. package/dist/pr-merge-readiness/compute.d.ts +13 -0
  84. package/dist/pr-merge-readiness/compute.js +99 -14
  85. package/dist/pr-merge-readiness/evaluate.js +10 -0
  86. package/dist/pr-merge-readiness/gh.d.ts +49 -0
  87. package/dist/pr-merge-readiness/gh.js +268 -0
  88. package/dist/pr-merge-readiness/index.d.ts +2 -2
  89. package/dist/pr-merge-readiness/index.js +2 -2
  90. package/dist/pr-merge-readiness/mergeability.js +5 -0
  91. package/dist/pr-merge-readiness/output.js +2 -0
  92. package/dist/pr-merge-readiness/parse.js +4 -0
  93. package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +4 -0
  94. package/dist/pr-merge-readiness/types.d.ts +6 -0
  95. package/dist/pr-wait-mergeable/cascade.d.ts +11 -0
  96. package/dist/pr-wait-mergeable/cascade.js +115 -1
  97. package/dist/pr-wait-mergeable/main.d.ts +4 -0
  98. package/dist/pr-wait-mergeable/main.js +4 -0
  99. package/dist/pr-wait-mergeable/types.d.ts +3 -1
  100. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -1
  101. package/dist/pr-wait-mergeable/wrappers.js +7 -1
  102. package/dist/preflight/evaluate.d.ts +15 -1
  103. package/dist/preflight/evaluate.js +33 -2
  104. package/dist/render/framework-commands.js +4 -0
  105. package/dist/scope/acceptance-evidence.d.ts +76 -0
  106. package/dist/scope/acceptance-evidence.js +348 -0
  107. package/dist/scope/coverage-map.d.ts +98 -0
  108. package/dist/scope/coverage-map.js +558 -0
  109. package/dist/scope/decompose.js +125 -11
  110. package/dist/scope/effort-activate-gate.d.ts +28 -0
  111. package/dist/scope/effort-activate-gate.js +64 -0
  112. package/dist/scope/index.d.ts +4 -0
  113. package/dist/scope/index.js +4 -0
  114. package/dist/scope/parent-lineage.d.ts +125 -0
  115. package/dist/scope/parent-lineage.js +626 -0
  116. package/dist/scope/transition.d.ts +8 -0
  117. package/dist/scope/transition.js +69 -2
  118. package/dist/session/active-cli.d.ts +79 -0
  119. package/dist/session/active-cli.js +382 -0
  120. package/dist/session/effort-budget.d.ts +130 -0
  121. package/dist/session/effort-budget.js +334 -0
  122. package/dist/session/index.d.ts +2 -0
  123. package/dist/session/index.js +2 -0
  124. package/dist/session/session-start.d.ts +30 -1
  125. package/dist/session/session-start.js +234 -26
  126. package/dist/session/verify-session-ritual.d.ts +14 -0
  127. package/dist/session/verify-session-ritual.js +33 -0
  128. package/dist/story-ready/evaluate.d.ts +11 -0
  129. package/dist/story-ready/evaluate.js +41 -3
  130. package/dist/swarm/index.d.ts +2 -0
  131. package/dist/swarm/index.js +2 -0
  132. package/dist/swarm/pre-dispatch-cli.d.ts +19 -0
  133. package/dist/swarm/pre-dispatch-cli.js +143 -0
  134. package/dist/swarm/pre-dispatch.d.ts +87 -0
  135. package/dist/swarm/pre-dispatch.js +373 -0
  136. package/dist/triage/actions/candidates-log.d.ts +1 -2
  137. package/dist/triage/actions/candidates-log.js +37 -6
  138. package/dist/vbrief-activate/activate.js +6 -0
  139. package/dist/vbrief-validate/constants.d.ts +2 -0
  140. package/dist/vbrief-validate/constants.js +2 -0
  141. package/dist/vbrief-validate/schema.js +4 -1
  142. package/dist/xbrief-migrate/migrate-project.d.ts +4 -0
  143. package/dist/xbrief-migrate/migrate-project.js +92 -3
  144. package/package.json +19 -3
@@ -1,22 +1,18 @@
1
1
  import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
- import { VBRIEF_VERSION } from "@deftai/directive-types";
4
3
  import { evaluate as evaluateAgentsMdAdvisory } from "../agents-md-advisory/evaluate.js";
5
4
  import { evaluateConsumerGateIntegrity, formatConsumerGateIntegrityFailure, } from "../check/consumer-gate-integrity.js";
6
5
  import { contentRoot } from "../content-root.js";
7
- import { resolveProjectDefinitionPath } from "../layout/resolve.js";
8
6
  import { DEFT_DIRECTIVE_DISABLE_FLAG_NAME, DEFT_DIRECTIVE_DISABLE_STATUS, DEFT_DIRECTIVE_DISABLE_TRACKED_WARNING, detectDeftDirectiveDisable, formatDeftDirectiveDisableMessage, isDeftDirectiveDisableActive, } from "../policy/deft-directive-disable.js";
9
7
  import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_FLAG_NAME, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY, } from "../policy/no-deft-directive.js";
10
8
  import { describeShadowedPlanExtension, detectShadowedPlanExtensions, } from "../policy/plan-extensions.js";
11
9
  import { loadProjectDefinition } from "../policy/resolve.js";
12
10
  import { checkLocalEngineIntegrity, classify, detectPackageManager, evaluateSkew, reconcileVersions, plan as resolvePlan, } from "../resolution/index.js";
13
- import { classifyXbriefSchemaDistance } from "../staleness-tickler/probe-xbrief.js";
14
11
  import { resolveUserMdPath } from "../user-config/resolve-user-md.js";
15
12
  import { evaluateAgentHooks } from "../verify-env/agent-hooks.js";
16
13
  import { probeAgentHooksLive } from "../verify-env/agent-hooks-live-probe.js";
17
- import { readDeclaredArtifactVersion } from "../xbrief-migrate/transforms.js";
18
14
  import { agentsRefreshPlan, hasV3ManagedMarker } from "./agents-md.js";
19
- import { runChecks } from "./checks.js";
15
+ import { checkXbriefEnvelopeMajorVersion, runChecks, XBRIEF_ENVELOPE_MAJOR_CHECK, XBRIEF_ENVELOPE_MIGRATE_COMMAND, } from "./checks.js";
20
16
  import { CONSUMER_FRAMEWORK_DIRS, EXPECTED_CONTENT_DIRS, EXPECTED_FRAMEWORK_DIRS, NETWORK_DISCLOSURE_LINE, PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE, TASKFILE_INCLUDE_SNIPPET, UV_INSTALL_URL, } from "./constants.js";
21
17
  import { decideThrottle, formatIsoZ, readState, renderDoctorStatusLine, writeState, } from "./doctor-state.js";
22
18
  import { formatAllowedFlagsHint, formatUnknownFlagsError, parseDoctorFlags } from "./flags.js";
@@ -33,8 +29,6 @@ import { runLocalSignpostChecks } from "./signpost-checks.js";
33
29
  import { classifyTaskfileInclude, formatGatesSurfaceDualRemediation, formatMissingIncludeSnippet, GATES_SURFACE_DEFT_REMEDIATION, includesBlockHasDeftTaskfile, resolveConsumerTaskfile, } from "./taskfile.js";
34
30
  import { defaultWhich } from "./which.js";
35
31
  const DEFAULT_RESOLUTION_PLATFORMS = ["linux", "darwin", "win32"];
36
- /** Next-action command for project-envelope behind-major (#2971). */
37
- const XBRIEF_ENVELOPE_MIGRATE_COMMAND = "deft migrate:xbrief";
38
32
  /**
39
33
  * Read the `packageManager` field (Corepack) from a project package.json, or
40
34
  * null when absent/unreadable. Lets the doctor detect a pnpm project that has
@@ -653,6 +647,11 @@ function runInstallIntegrityChecks(projectRoot, sink, addFinding, seams) {
653
647
  const name = String(entry.name ?? "install-integrity");
654
648
  const status = String(entry.status ?? "");
655
649
  const detail = String(entry.detail ?? "");
650
+ // Authoritative envelope-major emission is runXbriefEnvelopeVersionCheck
651
+ // (runs for framework repo + consumers). Skip install-integrity duplicate.
652
+ if (name === XBRIEF_ENVELOPE_MAJOR_CHECK) {
653
+ continue;
654
+ }
656
655
  if (status === "pass") {
657
656
  sink.success(`${name}: pass`);
658
657
  continue;
@@ -954,105 +953,99 @@ function runTaskfileIncludeCheck(projectRoot, fixMode, jsonMode, sink, addFindin
954
953
  });
955
954
  }
956
955
  /**
957
- * Fail closed when PROJECT-DEFINITION under an xbrief/ layout still declares
958
- * envelope 0.6 while the framework schema is 0.8 (#2971). Greenfield (no
959
- * project definition yet) and current 0.8 envelopes pass. Unreadable paths
960
- * (test seams / permission) skip rather than false-positive fail. Behind-major
961
- * records `deft migrate:xbrief` as the next action — layout rename alone is not
962
- * enough. Distinct from deposited-schema (`stale-xbrief-schema-deposit`) which
963
- * must NOT route to migrate:xbrief when only framework schema files are stale.
956
+ * Fail closed when any scanned project xbrief lifecycle `*.xbrief.json` envelope
957
+ * declares a schema major behind the framework target (#2971 / #3243). Greenfield
958
+ * (no envelopes) skips. Behind-major records `deft migrate:xbrief` as the next
959
+ * action (hybrid in-place rewrite on already-xbrief trees #3236). Distinct from
960
+ * deposited-schema (`stale-xbrief-schema-deposit`) which must NOT route to
961
+ * migrate:xbrief when only framework schema files are stale.
964
962
  */
965
963
  export function runXbriefEnvelopeVersionCheck(projectRoot, sink, addFinding, seams) {
966
- const checkName = "xbrief-envelope-version";
967
- const isFile = seams.isFile ?? ((p) => existsSync(p));
968
- const readText = seams.readText ?? readTextSafe;
969
- const targetVersion = VBRIEF_VERSION;
964
+ const checkName = XBRIEF_ENVELOPE_MAJOR_CHECK;
970
965
  if (seams.probeXbriefEnvelope) {
971
966
  const probe = seams.probeXbriefEnvelope(projectRoot);
972
967
  emitXbriefEnvelopeFinding(checkName, probe, sink, addFinding);
973
968
  return;
974
969
  }
975
- let definitionPath;
976
- try {
977
- definitionPath = resolveProjectDefinitionPath(projectRoot);
978
- }
979
- catch {
980
- // Pure vbrief/-only trees are already covered by layout migrate signposts.
981
- const skipMessage = `${checkName}: skip -- legacy-only layout (use layout migrate first)`;
982
- sink.info(skipMessage);
983
- addFinding({
984
- severity: "skip",
985
- message: skipMessage,
986
- check: checkName,
987
- status: "skip",
988
- reason: "legacy-only-layout",
989
- });
990
- return;
991
- }
992
- if (!isFile(definitionPath)) {
993
- const skipMessage = `${checkName}: skip -- no PROJECT-DEFINITION yet (greenfield)`;
994
- sink.info(skipMessage);
995
- addFinding({
996
- severity: "skip",
997
- message: skipMessage,
998
- check: checkName,
999
- status: "skip",
1000
- reason: "no-project-definition",
1001
- });
1002
- return;
1003
- }
1004
- const text = readText(definitionPath);
1005
- if (text === null) {
1006
- // Unreadable path (permissions or injectable seams) is not proof of 0.6.
1007
- const skipMessage = `${checkName}: skip -- PROJECT-DEFINITION unreadable`;
970
+ const result = checkXbriefEnvelopeMajorVersion(projectRoot, {
971
+ ...(seams.readText ? { readText: seams.readText } : {}),
972
+ ...(seams.isFile ? { isFile: seams.isFile } : {}),
973
+ ...(seams.isDir ? { isDir: seams.isDir } : {}),
974
+ });
975
+ const data = (result.data ?? {});
976
+ const targetVersion = typeof data.target_version === "string"
977
+ ? data.target_version
978
+ : String(data.target_version ?? "");
979
+ const declaredFromData = data.declared_versions;
980
+ const declaredVersion = Array.isArray(declaredFromData)
981
+ ? (declaredFromData.find((v) => typeof v === "string") ?? null)
982
+ : null;
983
+ if (result.status === "skip") {
984
+ const skipMessage = `${checkName}: skip -- ${result.detail}`;
1008
985
  sink.info(skipMessage);
1009
986
  addFinding({
1010
987
  severity: "skip",
1011
988
  message: skipMessage,
1012
989
  check: checkName,
1013
990
  status: "skip",
1014
- reason: "unreadable",
991
+ reason: typeof data.reason === "string" ? data.reason : "skip",
992
+ target_version: targetVersion || undefined,
1015
993
  });
1016
994
  return;
1017
995
  }
1018
- let declaredVersion = null;
1019
- try {
1020
- const parsed = JSON.parse(text);
1021
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1022
- declaredVersion = readDeclaredArtifactVersion(parsed);
1023
- }
1024
- else {
1025
- const skipMessage = `${checkName}: skip -- PROJECT-DEFINITION is not a JSON object`;
1026
- sink.info(skipMessage);
996
+ if (result.status === "pass") {
997
+ const status = typeof data.status === "string" ? data.status : "current";
998
+ if (status === "behind-minor") {
999
+ // Major-only check (#3243) does not fail closed, but surface an advisory
1000
+ // when the shared scan observed behind-minor distance.
1001
+ const warnMessage = `${checkName}: behind-minor -- ${result.detail}. Prefer \`${XBRIEF_ENVELOPE_MIGRATE_COMMAND}\` when convenient.`;
1002
+ sink.warn(warnMessage);
1027
1003
  addFinding({
1028
- severity: "skip",
1029
- message: skipMessage,
1004
+ severity: "warning",
1005
+ message: warnMessage,
1030
1006
  check: checkName,
1031
- status: "skip",
1032
- reason: "invalid-json-shape",
1007
+ status: "behind-minor",
1008
+ suggestion: XBRIEF_ENVELOPE_MIGRATE_COMMAND,
1009
+ target_version: targetVersion || undefined,
1010
+ next_command: XBRIEF_ENVELOPE_MIGRATE_COMMAND,
1033
1011
  });
1034
1012
  return;
1035
1013
  }
1036
- }
1037
- catch {
1038
- const skipMessage = `${checkName}: skip -- PROJECT-DEFINITION JSON parse failed`;
1039
- sink.info(skipMessage);
1014
+ const okMessage = `${checkName}: ${result.detail}`;
1015
+ sink.success(okMessage);
1040
1016
  addFinding({
1041
1017
  severity: "skip",
1042
- message: skipMessage,
1018
+ message: okMessage,
1043
1019
  check: checkName,
1044
- status: "skip",
1045
- reason: "parse-error",
1020
+ status: "current",
1021
+ target_version: targetVersion || undefined,
1046
1022
  });
1047
1023
  return;
1048
1024
  }
1049
- const distance = classifyXbriefSchemaDistance(declaredVersion, targetVersion);
1050
- emitXbriefEnvelopeFinding(checkName, {
1051
- declaredVersion,
1052
- targetVersion,
1053
- distance,
1054
- stale: distance !== "current",
1055
- }, sink, addFinding);
1025
+ // fail / error → behind-major fail closed (migratable 0.6 or non-migratable)
1026
+ const status = typeof data.status === "string" ? data.status : "behind-major";
1027
+ const nextCommand = typeof data.next_command === "string"
1028
+ ? data.next_command
1029
+ : data.next_command === null
1030
+ ? null
1031
+ : XBRIEF_ENVELOPE_MIGRATE_COMMAND;
1032
+ const suggestion = typeof data.suggestion === "string"
1033
+ ? data.suggestion
1034
+ : (nextCommand ?? `set xBRIEFInfo.version to framework target`);
1035
+ const message = `${checkName}: ${result.detail}`;
1036
+ sink.error(message);
1037
+ addFinding({
1038
+ severity: "error",
1039
+ message,
1040
+ check: checkName,
1041
+ status,
1042
+ suggestion,
1043
+ declared_version: declaredVersion,
1044
+ target_version: targetVersion || undefined,
1045
+ next_command: nextCommand,
1046
+ sample_paths: data.sample_paths,
1047
+ behind_major_count: data.behind_major_count,
1048
+ });
1056
1049
  }
1057
1050
  function emitXbriefEnvelopeFinding(checkName, probe, sink, addFinding) {
1058
1051
  if (probe.distance === "current") {
@@ -1,3 +1,22 @@
1
- /** Default PATH lookup mirroring Python `shutil.which`. */
1
+ export interface WhichAllOptions {
2
+ readonly env?: NodeJS.ProcessEnv;
3
+ readonly platform?: NodeJS.Platform;
4
+ readonly exists?: (path: string) => boolean;
5
+ /** Optional executable predicate (defaults to file + X_OK on posix). */
6
+ readonly isExecutable?: (path: string) => boolean;
7
+ }
8
+ /**
9
+ * Enumerate every PATH match for `name` (PATH order, first = highest precedence).
10
+ * Pure PATH scan — no shell locator — so tests and win32/posix stay hermetic and
11
+ * gated ritual never executes a PATH-substituted `which`/`where` (#3233).
12
+ * Only files (posix: executable bit) are accepted — not bare directories.
13
+ */
14
+ export declare function whichAllFromPath(name: string, options?: WhichAllOptions): string[];
15
+ /**
16
+ * All PATH matches for `name` without shelling out (#3233 security).
17
+ * Alias of {@link whichAllFromPath} — never runs bare `which` / `where`.
18
+ */
19
+ export declare function defaultWhichAll(name: string, options?: WhichAllOptions): string[];
20
+ /** Default PATH lookup mirroring Python `shutil.which` (first match only). */
2
21
  export declare function defaultWhich(name: string): string | null;
3
22
  //# sourceMappingURL=which.d.ts.map
@@ -1,5 +1,71 @@
1
1
  import { execFileSync } from "node:child_process";
2
- /** Default PATH lookup mirroring Python `shutil.which`. */
2
+ import { accessSync, constants, existsSync, statSync } from "node:fs";
3
+ import { posix, win32 } from "node:path";
4
+ function defaultIsExecutable(path, platform) {
5
+ try {
6
+ const st = statSync(path);
7
+ // Directories named `deft` must not shadow later executables (#3233 Greptile).
8
+ if (st.isDirectory())
9
+ return false;
10
+ if (!st.isFile() && !st.isSymbolicLink())
11
+ return false;
12
+ if (platform === "win32") {
13
+ return true;
14
+ }
15
+ accessSync(path, constants.X_OK);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ /**
23
+ * Enumerate every PATH match for `name` (PATH order, first = highest precedence).
24
+ * Pure PATH scan — no shell locator — so tests and win32/posix stay hermetic and
25
+ * gated ritual never executes a PATH-substituted `which`/`where` (#3233).
26
+ * Only files (posix: executable bit) are accepted — not bare directories.
27
+ */
28
+ export function whichAllFromPath(name, options = {}) {
29
+ const env = options.env ?? process.env;
30
+ const platform = options.platform ?? process.platform;
31
+ const exists = options.exists ?? existsSync;
32
+ const isExecutable = options.isExecutable ?? ((p) => defaultIsExecutable(p, platform));
33
+ const pathValue = env.PATH ?? env.Path ?? "";
34
+ if (pathValue === "") {
35
+ return [];
36
+ }
37
+ const isWindows = platform === "win32";
38
+ const exts = isWindows ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";") : [""];
39
+ const sep = isWindows ? ";" : ":";
40
+ const joinPath = isWindows ? win32.join : posix.join;
41
+ const seen = new Set();
42
+ const out = [];
43
+ for (const dir of pathValue.split(sep)) {
44
+ if (dir === "")
45
+ continue;
46
+ for (const ext of exts) {
47
+ const candidate = joinPath(dir, `${name}${ext}`);
48
+ const key = isWindows ? candidate.toLowerCase() : candidate;
49
+ if (seen.has(key))
50
+ continue;
51
+ if (exists(candidate) && isExecutable(candidate)) {
52
+ seen.add(key);
53
+ out.push(candidate);
54
+ // One match per PATH directory (first PATHEXT hit wins on win32).
55
+ break;
56
+ }
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+ /**
62
+ * All PATH matches for `name` without shelling out (#3233 security).
63
+ * Alias of {@link whichAllFromPath} — never runs bare `which` / `where`.
64
+ */
65
+ export function defaultWhichAll(name, options = {}) {
66
+ return whichAllFromPath(name, options);
67
+ }
68
+ /** Default PATH lookup mirroring Python `shutil.which` (first match only). */
3
69
  export function defaultWhich(name) {
4
70
  const locator = process.platform === "win32" ? "where" : "which";
5
71
  try {
@@ -1,4 +1,5 @@
1
1
  import { type GoldenRunRecord } from "./run.js";
2
+ import { type CellVersionPurity, type MixedVersionPolicy } from "./version-pin.js";
2
3
  export declare const REPORT_SCHEMA_VERSION: 1;
3
4
  /** Metric delta with a simple two-proportion significance test (#896). */
4
5
  export interface MetricDelta {
@@ -17,6 +18,15 @@ export interface HoldoutTripwire {
17
18
  readonly primaryDelta: number;
18
19
  readonly holdoutDelta: number;
19
20
  }
21
+ /** Version purity evidence for report consumers (#3215). */
22
+ export interface VersionPurityEvidence {
23
+ readonly pure: boolean;
24
+ readonly summary: string;
25
+ readonly cells: readonly CellVersionPurity[];
26
+ readonly championCellAllowed: boolean;
27
+ readonly challengerCellAllowed: boolean;
28
+ readonly policy: MixedVersionPolicy;
29
+ }
20
30
  /** Version-diff report between champion and challenger golden runs. */
21
31
  export interface GoldenEvalReport {
22
32
  readonly schemaVersion: typeof REPORT_SCHEMA_VERSION;
@@ -27,12 +37,19 @@ export interface GoldenEvalReport {
27
37
  readonly challengerRunId: string;
28
38
  readonly deltas: readonly MetricDelta[];
29
39
  readonly holdoutTripwire: HoldoutTripwire;
40
+ /** Cell-level framework version purity for the reported model (#3215). */
41
+ readonly versionPurity: VersionPurityEvidence;
30
42
  }
31
43
  export interface ReportGoldenEvalOptions {
32
44
  readonly projectRoot?: string;
33
45
  readonly championVersion: string;
34
46
  readonly challengerVersion: string;
35
47
  readonly model: string;
48
+ /**
49
+ * Mixed-version cell policy (#3215). Default `refuse` fails the report when
50
+ * ledger runs for this model disagree on framework version within a treatment.
51
+ */
52
+ readonly mixedVersionPolicy?: MixedVersionPolicy;
36
53
  }
37
54
  export interface ReportGoldenEvalResult {
38
55
  readonly code: 0 | 1 | 2;
@@ -48,6 +65,18 @@ export declare function twoProportionZTest(passedA: number, totalA: number, pass
48
65
  };
49
66
  /** Detect holdout tripwire: primary improves while holdout regresses (#1703). */
50
67
  export declare function evaluateHoldoutTripwire(champion: GoldenRunRecord, challenger: GoldenRunRecord): HoldoutTripwire;
68
+ /**
69
+ * Map a golden-run ledger row to the #3215 versioned-run identity.
70
+ * Treatment is model@harness@version so cross-version champion/challenger
71
+ * comparisons stay distinct cells; pin disagreements under one version refuse.
72
+ */
73
+ export declare function goldenRunToVersionedRun(record: GoldenRunRecord): {
74
+ frameworkVersion: string;
75
+ treatment: string;
76
+ model: string;
77
+ harness: string;
78
+ runId: string;
79
+ };
51
80
  /** Diff two directive versions with metric deltas and significance (#1703 Tier 2). */
52
81
  export declare function reportGoldenEval(options: ReportGoldenEvalOptions): ReportGoldenEvalResult;
53
82
  //# sourceMappingURL=report.d.ts.map
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { resolveEvalPath } from "../layout/resolve.js";
3
3
  import { GOLDEN_RUNS_HISTORY_REL } from "./run.js";
4
+ import { aggregateCellWithVersionPurity, evaluateLedgerVersionPurity, } from "./version-pin.js";
4
5
  export const REPORT_SCHEMA_VERSION = 1;
5
6
  function readGoldenRuns(projectRoot) {
6
7
  const path = resolveEvalPath(projectRoot, GOLDEN_RUNS_HISTORY_REL);
@@ -89,6 +90,21 @@ export function evaluateHoldoutTripwire(champion, challenger) {
89
90
  function formatPercent(value) {
90
91
  return `${(value * 100).toFixed(1)}%`;
91
92
  }
93
+ /**
94
+ * Map a golden-run ledger row to the #3215 versioned-run identity.
95
+ * Treatment is model@harness@version so cross-version champion/challenger
96
+ * comparisons stay distinct cells; pin disagreements under one version refuse.
97
+ */
98
+ export function goldenRunToVersionedRun(record) {
99
+ const pin = record.frameworkVersionPin?.frameworkVersion ?? record.directiveVersion;
100
+ return {
101
+ frameworkVersion: pin,
102
+ treatment: `${record.model}@${record.harness}@${record.directiveVersion}`,
103
+ model: record.model,
104
+ harness: record.harness,
105
+ runId: record.runId,
106
+ };
107
+ }
92
108
  /** Diff two directive versions with metric deltas and significance (#1703 Tier 2). */
93
109
  export function reportGoldenEval(options) {
94
110
  if (!options.championVersion.trim() || !options.challengerVersion.trim()) {
@@ -102,7 +118,48 @@ export function reportGoldenEval(options) {
102
118
  return { code: 2, report: null, message: "eval:report: --model is required" };
103
119
  }
104
120
  const projectRoot = options.projectRoot ?? process.cwd();
121
+ const policy = options.mixedVersionPolicy ?? "refuse";
105
122
  const records = readGoldenRuns(projectRoot);
123
+ const modelRecords = records.filter((r) => r.model === options.model);
124
+ // Within each version×model×harness cell, refuse/flag if pins disagree (#3215).
125
+ // Cross-version champion vs challenger is intentional and uses distinct cells.
126
+ const championCellRuns = modelRecords
127
+ .filter((r) => r.directiveVersion === options.championVersion)
128
+ .map(goldenRunToVersionedRun);
129
+ const challengerCellRuns = modelRecords
130
+ .filter((r) => r.directiveVersion === options.challengerVersion)
131
+ .map(goldenRunToVersionedRun);
132
+ const championAgg = aggregateCellWithVersionPurity({
133
+ runs: championCellRuns,
134
+ treatment: `champion@${options.model}@${options.championVersion}`,
135
+ policy,
136
+ });
137
+ const challengerAgg = aggregateCellWithVersionPurity({
138
+ runs: challengerCellRuns,
139
+ treatment: `challenger@${options.model}@${options.challengerVersion}`,
140
+ policy,
141
+ });
142
+ const ledgerPurity = evaluateLedgerVersionPurity(modelRecords.map(goldenRunToVersionedRun));
143
+ if (!championAgg.allowed || !challengerAgg.allowed) {
144
+ const versionPurity = {
145
+ pure: false,
146
+ summary: [
147
+ !championAgg.allowed ? championAgg.purity.message : null,
148
+ !challengerAgg.allowed ? challengerAgg.purity.message : null,
149
+ ]
150
+ .filter((line) => line !== null)
151
+ .join(" "),
152
+ cells: [championAgg.purity, challengerAgg.purity],
153
+ championCellAllowed: championAgg.allowed,
154
+ challengerCellAllowed: challengerAgg.allowed,
155
+ policy,
156
+ };
157
+ return {
158
+ code: 1,
159
+ report: null,
160
+ message: `eval:report: mixed framework versions in treatment cell(s) — aggregation refused (#3215)\n ${versionPurity.summary}`,
161
+ };
162
+ }
106
163
  const champion = findLatestGoldenRun(records, options.championVersion, options.model);
107
164
  const challenger = findLatestGoldenRun(records, options.challengerVersion, options.model);
108
165
  if (champion === null) {
@@ -133,6 +190,14 @@ export function reportGoldenEval(options) {
133
190
  metricDelta("overallPassRate", champion.summary.passRate, challenger.summary.passRate, countPasses(champion.results).passed, countPasses(champion.results).total, countPasses(challenger.results).passed, countPasses(challenger.results).total),
134
191
  ];
135
192
  const holdoutTripwire = evaluateHoldoutTripwire(champion, challenger);
193
+ const versionPurity = {
194
+ pure: championAgg.purity.pure && challengerAgg.purity.pure && ledgerPurity.pure,
195
+ summary: `${championAgg.purity.message} ${challengerAgg.purity.message}`,
196
+ cells: [championAgg.purity, challengerAgg.purity, ...ledgerPurity.cells],
197
+ championCellAllowed: championAgg.allowed,
198
+ challengerCellAllowed: challengerAgg.allowed,
199
+ policy,
200
+ };
136
201
  const report = {
137
202
  schemaVersion: REPORT_SCHEMA_VERSION,
138
203
  championVersion: options.championVersion,
@@ -142,6 +207,7 @@ export function reportGoldenEval(options) {
142
207
  challengerRunId: challenger.runId,
143
208
  deltas,
144
209
  holdoutTripwire,
210
+ versionPurity,
145
211
  };
146
212
  const lines = [
147
213
  `eval:report champion=v${options.championVersion} challenger=v${options.challengerVersion} model=${options.model}`,
@@ -154,6 +220,9 @@ export function reportGoldenEval(options) {
154
220
  return ` ${d.metric}: ${formatPercent(d.champion)} -> ${formatPercent(d.challenger)} (delta ${(d.delta * 100).toFixed(1)}pp, ${sig})`;
155
221
  }),
156
222
  ` ${holdoutTripwire.summary}`,
223
+ ` version purity: ${versionPurity.pure ? "ok" : "mixed"} — ${versionPurity.summary}`,
224
+ ` champion cell pin: v${champion.frameworkVersionPin?.frameworkVersion ?? champion.directiveVersion}`,
225
+ ` challenger cell pin: v${challenger.frameworkVersionPin?.frameworkVersion ?? challenger.directiveVersion}`,
157
226
  ];
158
227
  const code = holdoutTripwire.triggered ? 1 : 0;
159
228
  return { code, report, message: lines.join("\n") };
@@ -1,3 +1,4 @@
1
+ import { type FrameworkVersionPin } from "./version-pin.js";
1
2
  export declare const GOLDEN_RUNS_HISTORY_REL = "results/golden-runs.jsonl";
2
3
  export declare const GOLDEN_RUN_SCHEMA_VERSION: 1;
3
4
  export declare const GOLDEN_CORPUS_VERSION = "2026-07-05-tier2-fixture-v1";
@@ -32,7 +33,13 @@ export interface GoldenTaskResult {
32
33
  export interface GoldenRunRecord {
33
34
  readonly schemaVersion: typeof GOLDEN_RUN_SCHEMA_VERSION;
34
35
  readonly runId: string;
36
+ /** Framework version pin for this run (alias of frameworkVersionPin.frameworkVersion). */
35
37
  readonly directiveVersion: string;
38
+ /**
39
+ * Resolved framework version pin captured at run start (#3215).
40
+ * Optional for ledger rows written before the purity gate shipped.
41
+ */
42
+ readonly frameworkVersionPin?: FrameworkVersionPin;
36
43
  readonly model: string;
37
44
  readonly harness: string;
38
45
  readonly seeds: readonly number[];
@@ -63,6 +70,8 @@ export interface RunGoldenEvalResult {
63
70
  readonly code: 0 | 1 | 2;
64
71
  readonly record: GoldenRunRecord | null;
65
72
  readonly message: string;
73
+ /** #1584 shared-benchmark manifest after version pin wire, when present on disk. */
74
+ readonly sharedBenchmarkManifest?: Record<string, unknown> | null;
66
75
  }
67
76
  /** Fixed golden corpus with objective graders (#1703 Tier 2). */
68
77
  export declare const GOLDEN_CORPUS: readonly GoldenTaskDefinition[];
package/dist/eval/run.js CHANGED
@@ -2,11 +2,11 @@ import { createHash } from "node:crypto";
2
2
  import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join, resolve } from "node:path";
5
- import { readCorePackageVersion } from "../engine-version.js";
6
5
  import { containedWrite } from "../fs/contained-write.js";
7
6
  import { resolveEvalPath } from "../layout/resolve.js";
8
7
  import { BYTE_DIFF_WHOLE_FILE_THRESHOLD, InstrumentedVbriefCrud } from "./crud-telemetry.js";
9
8
  import { evaluateHealth } from "./health.js";
9
+ import { applyVersionPinToSharedBenchmark, resolveFrameworkVersionPin, } from "./version-pin.js";
10
10
  export const GOLDEN_RUNS_HISTORY_REL = "results/golden-runs.jsonl";
11
11
  export const GOLDEN_RUN_SCHEMA_VERSION = 1;
12
12
  export const GOLDEN_CORPUS_VERSION = "2026-07-05-tier2-fixture-v1";
@@ -250,10 +250,15 @@ export function runGoldenEval(options) {
250
250
  return { code: 2, record: null, message: "eval:run: --model is required" };
251
251
  }
252
252
  const projectRoot = options.projectRoot ?? process.cwd();
253
- const directiveVersion = options.directiveVersion ?? readCorePackageVersion();
253
+ const now = options.now ?? (() => new Date());
254
+ // #3215: resolve framework version once at run start (package.json pin or override).
255
+ const frameworkVersionPin = resolveFrameworkVersionPin({
256
+ override: options.directiveVersion,
257
+ now,
258
+ });
259
+ const directiveVersion = frameworkVersionPin.frameworkVersion;
254
260
  const harness = options.harness ?? "deterministic-fixture";
255
261
  const seeds = options.seeds ?? [1, 2, 3];
256
- const now = options.now ?? (() => new Date());
257
262
  const persist = options.persist ?? true;
258
263
  if (seeds.length === 0) {
259
264
  return { code: 2, record: null, message: "eval:run: at least one seed is required" };
@@ -293,6 +298,7 @@ export function runGoldenEval(options) {
293
298
  schemaVersion: GOLDEN_RUN_SCHEMA_VERSION,
294
299
  runId: runIdFor(directiveVersion, options.model, harness, seeds),
295
300
  directiveVersion,
301
+ frameworkVersionPin,
296
302
  model: options.model,
297
303
  harness,
298
304
  seeds,
@@ -303,6 +309,8 @@ export function runGoldenEval(options) {
303
309
  results,
304
310
  summary,
305
311
  };
312
+ // Persist ledger first so a failed golden-run write never leaves a dangling
313
+ // shared-benchmark pin without a matching run row (#3215 Greptile).
306
314
  if (persist) {
307
315
  try {
308
316
  persistGoldenRun(projectRoot, record);
@@ -312,16 +320,44 @@ export function runGoldenEval(options) {
312
320
  code: 2,
313
321
  record,
314
322
  message: `eval:run: failed to persist golden run: ${String(err)}`,
323
+ sharedBenchmarkManifest: null,
315
324
  };
316
325
  }
317
326
  }
327
+ // #1584 / #3215: wire (+ persist when ledger was written) shared-benchmark pin.
328
+ let sharedBenchmark;
329
+ try {
330
+ sharedBenchmark = applyVersionPinToSharedBenchmark(projectRoot, frameworkVersionPin, {
331
+ persist,
332
+ });
333
+ }
334
+ catch (err) {
335
+ return {
336
+ code: 2,
337
+ record,
338
+ message: `eval:run: failed to wire shared-benchmark pin: ${String(err)}`,
339
+ sharedBenchmarkManifest: null,
340
+ };
341
+ }
318
342
  const lines = [
319
343
  `eval:run v${record.directiveVersion} model=${record.model} harness=${record.harness} seeds=[${record.seeds.join(",")}]`,
344
+ ` framework version pin: ${frameworkVersionPin.frameworkVersion} (source=${frameworkVersionPin.source})`,
320
345
  ` primary pass rate: ${(summary.primaryPassRate * 100).toFixed(1)}% (${summary.primaryTotal} trials)`,
321
346
  ` holdout pass rate: ${(summary.holdoutPassRate * 100).toFixed(1)}% (${summary.holdoutTotal} trials)`,
322
347
  ` rotating holdout task: ${rotatingHoldout?.id ?? "none"}`,
323
348
  ` runId=${record.runId}`,
324
349
  ];
325
- return { code: 0, record, message: lines.join("\n") };
350
+ if (sharedBenchmark.persisted) {
351
+ lines.push(" shared-benchmark manifest: frameworkVersion pin persisted (#1584 / #3215)");
352
+ }
353
+ else if (sharedBenchmark.applied) {
354
+ lines.push(" shared-benchmark manifest: frameworkVersion wired in-memory (#1584 / #3215)");
355
+ }
356
+ return {
357
+ code: 0,
358
+ record,
359
+ message: lines.join("\n"),
360
+ sharedBenchmarkManifest: sharedBenchmark.manifest,
361
+ };
326
362
  }
327
363
  //# sourceMappingURL=run.js.map