halfcycle 0.3.17 → 0.3.18

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "halfcycle",
3
- "version": "0.3.17",
3
+ "version": "0.3.18",
4
4
  "description": "Halfcycle Method bundle — resolution-stub slash commands, remote method-delivery registration, and governance-hook wiring for a Halfcycle engagement repo. It ships NO worker role files: a project's roles are authored from that project's own recorded decisions at orchestration kickoff (FX-2, W3-F-27).",
5
5
  "commands": [
6
6
  {
@@ -14594,6 +14594,80 @@ function date4(params) {
14594
14594
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
14595
14595
  config(en_default());
14596
14596
 
14597
+ // ../events/dist/evaluation.js
14598
+ var envRefSchema = external_exports.object({
14599
+ name: external_exports.string(),
14600
+ argType: external_exports.enum(["build", "runtime", "both"])
14601
+ }).strict();
14602
+ var sqlWriteSchema = external_exports.object({
14603
+ kind: external_exports.enum(["insert", "update", "upsert", "delete"]),
14604
+ table: external_exports.string(),
14605
+ whereColumns: external_exports.array(external_exports.string())
14606
+ }).strict();
14607
+ var schemaConstraintSchema = external_exports.object({
14608
+ table: external_exports.string(),
14609
+ kind: external_exports.enum(["unique", "check", "foreignKey"]),
14610
+ constraintName: external_exports.string().optional()
14611
+ }).strict();
14612
+ var numericColumnSchema = external_exports.object({
14613
+ table: external_exports.string(),
14614
+ column: external_exports.string(),
14615
+ pgType: external_exports.enum(["numeric", "bigint", "decimal"]),
14616
+ wireField: external_exports.string().optional(),
14617
+ hasRegisteredCoercion: external_exports.boolean().optional()
14618
+ }).strict();
14619
+ var thirdPartyEndpointSchema = external_exports.object({
14620
+ signature: external_exports.string(),
14621
+ provider: external_exports.string().optional(),
14622
+ hasCapturedFixture: external_exports.boolean()
14623
+ }).strict();
14624
+ var sourceExcerptSchema = external_exports.object({
14625
+ /** The declared construct the excerpt was cut around (a derived-fact anchor). */
14626
+ construct: external_exports.enum(["sqlWrite", "thirdPartyEndpoint", "envRef"]),
14627
+ /** 1-based start line in the changed file (best-effort). */
14628
+ startLine: external_exports.number().int().nonnegative(),
14629
+ /** The bounded excerpt text — capped CLIENT-SIDE in the runner before send. */
14630
+ text: external_exports.string()
14631
+ }).strict();
14632
+ var diffMetadataSchema = external_exports.object({
14633
+ path: external_exports.string(),
14634
+ astShapes: external_exports.array(external_exports.string()),
14635
+ envRefs: external_exports.array(envRefSchema),
14636
+ sqlWrites: external_exports.array(sqlWriteSchema),
14637
+ endpointSigs: external_exports.array(external_exports.string()),
14638
+ schemaConstraints: external_exports.array(schemaConstraintSchema),
14639
+ numericColumns: external_exports.array(numericColumnSchema),
14640
+ thirdPartyEndpoints: external_exports.array(thirdPartyEndpointSchema),
14641
+ // Present only when you have turned source excerpts on. Left out
14642
+ // entirely otherwise — never sent as an empty list.
14643
+ sourceExcerpts: external_exports.array(sourceExcerptSchema).optional()
14644
+ }).strict();
14645
+ var changeSetSchema = external_exports.object({
14646
+ files: external_exports.array(diffMetadataSchema)
14647
+ }).strict();
14648
+ var NON_CAPABILITY_DESCRIPTOR_FIELDS = [
14649
+ "path",
14650
+ "sourceExcerpts"
14651
+ ];
14652
+ var CLIENT_CAPABILITY_VOCABULARY = Object.keys(diffMetadataSchema.shape).filter((field) => !NON_CAPABILITY_DESCRIPTOR_FIELDS.includes(field));
14653
+ function isClientCapability(value) {
14654
+ return CLIENT_CAPABILITY_VOCABULARY.includes(value);
14655
+ }
14656
+ var evaluationRequestSchema = external_exports.object({
14657
+ engagementId: external_exports.string(),
14658
+ changeSet: changeSetSchema,
14659
+ /**
14660
+ * What this client can supply: the names of the descriptor kinds it fills in
14661
+ * for each changed file. A check that needs a kind this client does not send
14662
+ * is reported by name as not run, with the missing kind named, instead of
14663
+ * quietly reporting nothing.
14664
+ *
14665
+ * Omitting the field means "unknown", never "everything" — a client that
14666
+ * says nothing is treated as an older one, not a capable one.
14667
+ */
14668
+ clientCapabilities: external_exports.array(external_exports.string()).optional()
14669
+ }).strict();
14670
+
14597
14671
  // ../core/dist/guard-record.js
14598
14672
  var severitySchema = external_exports.enum(["info", "warn", "block"]);
14599
14673
  var channelSchema = external_exports.enum(["stable", "candidate", "rented"]);
@@ -14641,11 +14715,26 @@ var firedGuardSchema = external_exports.object({
14641
14715
  */
14642
14716
  path: external_exports.string().optional()
14643
14717
  }).strict();
14718
+ var notRunCheckSchema = external_exports.object({
14719
+ /** Which check did not run, by the same reference a fired check carries. */
14720
+ patternRef: external_exports.string(),
14721
+ /**
14722
+ * Why it could not run, in plain words — what this client did not send, or
14723
+ * that the check's kind is not implemented yet. Never the check's own rule.
14724
+ */
14725
+ reason: external_exports.string()
14726
+ }).strict();
14644
14727
  var resultEnvelopeSchema = external_exports.object({
14645
14728
  guardsFired: external_exports.array(firedGuardSchema),
14646
14729
  severity: severitySchema.nullable(),
14647
14730
  blocking: external_exports.boolean(),
14648
- explanation: external_exports.string()
14731
+ explanation: external_exports.string(),
14732
+ /**
14733
+ * The checks that could not run on this edit, each named with the reason.
14734
+ * Absent when every check ran — so an evaluation where nothing was skipped
14735
+ * looks exactly as it always has, and an absent list is never an empty one.
14736
+ */
14737
+ notRun: external_exports.array(notRunCheckSchema).optional()
14649
14738
  }).strict();
14650
14739
  var wireErrorSchema = external_exports.object({
14651
14740
  statusCode: external_exports.number(),
@@ -14653,6 +14742,60 @@ var wireErrorSchema = external_exports.object({
14653
14742
  message: external_exports.string()
14654
14743
  }).strict();
14655
14744
 
14745
+ // ../events/dist/phase-identity.js
14746
+ var PHASE_SEGMENT_PREFIX = "phase-";
14747
+ function isValidPhaseIdentity(identity) {
14748
+ if (typeof identity === "number")
14749
+ return Number.isInteger(identity) && identity >= 0;
14750
+ if (typeof identity !== "string")
14751
+ return false;
14752
+ if (identity === "")
14753
+ return false;
14754
+ if (identity.includes("/"))
14755
+ return false;
14756
+ if (identity.includes("\\"))
14757
+ return false;
14758
+ if (identity.includes("\0"))
14759
+ return false;
14760
+ if (identity.startsWith("."))
14761
+ return false;
14762
+ return true;
14763
+ }
14764
+ var InvalidPhaseIdentityError = class extends Error {
14765
+ /** The value that was refused, exactly as supplied. */
14766
+ identity;
14767
+ constructor(identity, allowedLocation) {
14768
+ const subject = allowedLocation === void 0 ? "a file" : "a Build Record";
14769
+ const where = allowedLocation === void 0 ? "" : `
14770
+ Records are written only into ${allowedLocation.replace(/\\/g, "/")}/. Nothing was written.`;
14771
+ super(`that phase identity cannot name ${subject}.
14772
+ identity: ${describeIdentity(identity)}` + where);
14773
+ this.name = "InvalidPhaseIdentityError";
14774
+ this.identity = identity;
14775
+ }
14776
+ };
14777
+ function renderPhaseSegment(identity) {
14778
+ if (!isValidPhaseIdentity(identity)) {
14779
+ throw new InvalidPhaseIdentityError(identity);
14780
+ }
14781
+ return `${PHASE_SEGMENT_PREFIX}${identity}`;
14782
+ }
14783
+ function describeIdentity(identity) {
14784
+ if (typeof identity === "string")
14785
+ return identity;
14786
+ if (identity === null)
14787
+ return "null";
14788
+ if (identity === void 0)
14789
+ return "none supplied";
14790
+ if (typeof identity === "number" || typeof identity === "boolean")
14791
+ return String(identity);
14792
+ if (Array.isArray(identity))
14793
+ return "(a list)";
14794
+ if (typeof identity === "object")
14795
+ return "(an object)";
14796
+ return `(a ${typeof identity})`;
14797
+ }
14798
+
14656
14799
  // dist/client.js
14657
14800
  var REQUEST_TIMEOUT_MS = 1e4;
14658
14801
  async function evaluate(url2, token, request, fetchFn = globalThis.fetch) {
@@ -15780,6 +15923,16 @@ function buildChangeSet(files, context = {}) {
15780
15923
  });
15781
15924
  return { files: fileMetadata };
15782
15925
  }
15926
+ function deriveClientCapabilities(changeSet) {
15927
+ const seen = /* @__PURE__ */ new Set();
15928
+ for (const file2 of changeSet.files) {
15929
+ for (const key of Object.keys(file2)) {
15930
+ if (isClientCapability(key))
15931
+ seen.add(key);
15932
+ }
15933
+ }
15934
+ return [...seen];
15935
+ }
15783
15936
  function produceExcerpts(diffContent, descriptors, caps) {
15784
15937
  const excerpts = [];
15785
15938
  const seen = /* @__PURE__ */ new Set();
@@ -15911,26 +16064,22 @@ async function emitTelemetry(run, config2, logger, fetchFn = globalThis.fetch) {
15911
16064
  await postTelemetry(run, config2.controlTelemetryUrl, config2.controlTelemetryToken, logger, fetchFn);
15912
16065
  }
15913
16066
  }
15914
- function normalizePhaseBaseName(phase) {
15915
- if (phase === void 0)
15916
- return null;
15917
- const trimmed = phase.trim();
15918
- if (trimmed === "")
15919
- return null;
15920
- const match = /^(?:phase-)?(\d+)$/.exec(trimmed);
15921
- if (!match)
15922
- return null;
15923
- return `phase-${match[1]}`;
15924
- }
15925
16067
  function localLogPath(run, config2) {
15926
16068
  const logDir = config2.guardEvalLogDir ?? join4(homedir(), ".halfcycle", "guard-eval-log");
15927
- const base = normalizePhaseBaseName(run.phase);
15928
- const fileName = base !== null ? `${base}.jsonl` : "unphased.jsonl";
16069
+ const phase = run.phase;
16070
+ const fileName = phase === void 0 || phase === "" ? "unphased.jsonl" : `${renderPhaseSegment(phase)}.jsonl`;
15929
16071
  return join4(logDir, fileName);
15930
16072
  }
15931
16073
  function appendToLocalLog(run, config2, logger) {
16074
+ let logPath;
16075
+ try {
16076
+ logPath = localLogPath(run, config2);
16077
+ } catch (err) {
16078
+ const msg = err instanceof Error ? err.message : String(err);
16079
+ logger.warn(`[Halfcycle telemetry] Phase name refused, nothing written (dropped): ${msg}`);
16080
+ return;
16081
+ }
15932
16082
  try {
15933
- const logPath = localLogPath(run, config2);
15934
16083
  mkdirSync2(dirname2(logPath), { recursive: true });
15935
16084
  appendFileSync(logPath, JSON.stringify(run) + "\n", "utf-8");
15936
16085
  } catch (err) {
@@ -16104,6 +16253,14 @@ function emitCredentialRejected(statusCode, serviceMessage, exitCode) {
16104
16253
  writeSync(2, credentialRejectedMessage(statusCode, serviceMessage, HOOK_REMEDY));
16105
16254
  process.exitCode = exitCode;
16106
16255
  }
16256
+ function notRunReport(notRun, label = "[Halfcycle]") {
16257
+ if (!notRun || notRun.length === 0)
16258
+ return "";
16259
+ const count = notRun.length;
16260
+ const noun = count === 1 ? "check" : "checks";
16261
+ const lines = notRun.map((entry) => ` [NOT RUN] ${entry.patternRef}: ${entry.reason}`);
16262
+ return [`${label} ${count} ${noun} did not run on this change:`, ...lines].join("\n");
16263
+ }
16107
16264
 
16108
16265
  // dist/ci.js
16109
16266
  async function runCi() {
@@ -16154,7 +16311,8 @@ async function runCi() {
16154
16311
  const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
16155
16312
  const clientResult = await evaluate(guardServiceUrl, guardServiceToken, {
16156
16313
  engagementId: guardEngagementId,
16157
- changeSet
16314
+ changeSet,
16315
+ clientCapabilities: deriveClientCapabilities(changeSet)
16158
16316
  });
16159
16317
  if (!clientResult.ok) {
16160
16318
  const failure = classifyFailure(clientResult);
@@ -16183,9 +16341,9 @@ async function runCi() {
16183
16341
  const run = buildGuardEvalRun({
16184
16342
  engagementId: guardEngagementId,
16185
16343
  runType: "ci",
16186
- // Phase source (D6, HALFCYCLE_PHASE) — emit.ts normalises this to the exact
16187
- // phase-<N>.jsonl filename the Build-Record reader expects (#61 seam). Absent
16188
- // at CI time → the run is unphased (lands in unphased.jsonl).
16344
+ // The phase this run belongs to, as the client named it — it names the
16345
+ // guard-eval log file the run is appended to. Absent at CI time → the run
16346
+ // is unphased and lands in unphased.jsonl.
16189
16347
  phase: telemetryConfig.phase,
16190
16348
  outcome: "evaluated",
16191
16349
  changeSet,
@@ -16202,6 +16360,9 @@ async function runCi() {
16202
16360
  return actOnEnvelope(clientResult.envelope);
16203
16361
  }
16204
16362
  function actOnEnvelope(envelope) {
16363
+ const notRun = notRunReport(envelope.notRun, "[Halfcycle CI]");
16364
+ if (notRun)
16365
+ process.stdout.write(notRun + "\n");
16205
16366
  if (envelope.guardsFired.length === 0) {
16206
16367
  return 0;
16207
16368
  }
@@ -16275,7 +16436,8 @@ async function runPostToolUse() {
16275
16436
  const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
16276
16437
  const clientResult = await evaluate(guardServiceUrl, guardServiceToken, {
16277
16438
  engagementId: guardEngagementId,
16278
- changeSet
16439
+ changeSet,
16440
+ clientCapabilities: deriveClientCapabilities(changeSet)
16279
16441
  });
16280
16442
  if (!clientResult.ok) {
16281
16443
  const failure = classifyFailure(clientResult);
@@ -16301,8 +16463,8 @@ async function runPostToolUse() {
16301
16463
  const run = buildGuardEvalRun({
16302
16464
  engagementId: guardEngagementId,
16303
16465
  runType: "hook",
16304
- // Phase source (D6, HALFCYCLE_PHASE) — emit.ts normalises this to the exact
16305
- // phase-<N>.jsonl filename the Build-Record reader expects (#61 seam).
16466
+ // The phase this run belongs to, as the client named it — it names the
16467
+ // guard-eval log file the run is appended to. Absent → the run is unphased.
16306
16468
  phase: telemetryConfig.phase,
16307
16469
  outcome: "evaluated",
16308
16470
  changeSet,
@@ -16319,18 +16481,23 @@ async function runPostToolUse() {
16319
16481
  actOnEnvelope2(clientResult.envelope);
16320
16482
  }
16321
16483
  function actOnEnvelope2(envelope) {
16484
+ const notRun = notRunReport(envelope.notRun);
16485
+ const notRunBlock = notRun ? `
16486
+ ${notRun}` : "";
16322
16487
  if (envelope.guardsFired.length === 0) {
16488
+ if (notRun)
16489
+ emitWarning(notRun);
16323
16490
  return;
16324
16491
  }
16325
16492
  if (envelope.blocking) {
16326
16493
  const guardLines = envelope.guardsFired.map((g) => ` [${g.severity.toUpperCase()}] ${g.patternRef}: ${g.explanation}`).join("\n");
16327
16494
  const reason = `Guard blocked: ${envelope.explanation}
16328
- ${guardLines}`;
16495
+ ${guardLines}${notRunBlock}`;
16329
16496
  emitBlock(reason);
16330
16497
  } else {
16331
16498
  const guardLines = envelope.guardsFired.map((g) => ` [${g.severity.toUpperCase()}] ${g.patternRef}: ${g.explanation}`).join("\n");
16332
16499
  emitWarning(`[Halfcycle] Guard warning: ${envelope.explanation}
16333
- ${guardLines}`);
16500
+ ${guardLines}${notRunBlock}`);
16334
16501
  }
16335
16502
  }
16336
16503
  function readStdin() {
@@ -16535,7 +16702,8 @@ async function runSessionDiff(input, kind) {
16535
16702
  const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
16536
16703
  const clientResult = await evaluate(guardServiceUrl, guardServiceToken, {
16537
16704
  engagementId: guardEngagementId,
16538
- changeSet
16705
+ changeSet,
16706
+ clientCapabilities: deriveClientCapabilities(changeSet)
16539
16707
  });
16540
16708
  if (!clientResult.ok) {
16541
16709
  const failure = classifyFailure(clientResult);
@@ -16579,20 +16747,26 @@ async function runSessionDiff(input, kind) {
16579
16747
  actOnEnvelope3(clientResult.envelope, kind, input.stop_hook_active === true);
16580
16748
  }
16581
16749
  function actOnEnvelope3(envelope, kind, neverBlock) {
16582
- if (envelope.guardsFired.length === 0)
16750
+ const notRun = notRunReport(envelope.notRun);
16751
+ const notRunBlock = notRun ? `
16752
+ ${notRun}` : "";
16753
+ if (envelope.guardsFired.length === 0) {
16754
+ if (notRun)
16755
+ emitWarning(notRun);
16583
16756
  return;
16757
+ }
16584
16758
  const where = kind === "stop" ? "session stop" : "session stop";
16585
16759
  const guardLines = envelope.guardsFired.map((g) => ` [${g.severity.toUpperCase()}] ${g.patternRef}: ${g.explanation}`).join("\n");
16586
16760
  if (envelope.blocking && !neverBlock) {
16587
16761
  emitBlock(`Guard blocked at ${where}: ${envelope.explanation}
16588
- ${guardLines}`);
16762
+ ${guardLines}${notRunBlock}`);
16589
16763
  } else if (envelope.blocking && neverBlock) {
16590
16764
  emitWarning(`[Halfcycle] Guard would block at ${where} but the stop was already continued (stop_hook_active) \u2014 surfacing as a warning:
16591
16765
  ${envelope.explanation}
16592
- ${guardLines}`);
16766
+ ${guardLines}${notRunBlock}`);
16593
16767
  } else {
16594
16768
  emitWarning(`[Halfcycle] Guard warning at ${where}: ${envelope.explanation}
16595
- ${guardLines}`);
16769
+ ${guardLines}${notRunBlock}`);
16596
16770
  }
16597
16771
  }
16598
16772