@cratis/pi 7.84.0 → 7.84.2

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.
@@ -126,6 +126,10 @@ array.should.have.lengthOf(3);
126
126
  (() => throwingFn()).should.throw(ErrorType);
127
127
  ```
128
128
 
129
+ ### Comparing JSON
130
+
131
+ Never assert JSON equality by comparing `JSON.stringify` output: object key order is insertion order, so two equal documents can serialize differently and a spec fails, or passes, for the wrong reason. Compare parsed values with `should.deep.equal`. When a helper must compare serialized JSON (a snapshot, a file on disk, a request body), canonicalize first: sort object keys recursively at every level and keep array order, because array order is data. Review flags raw `JSON.stringify(a) === JSON.stringify(b)` equality in specs and spec helpers.
132
+
129
133
  ## Mocking with Sinon
130
134
 
131
135
  ```typescript
@@ -230,8 +230,8 @@ inside the command (`CRATIS_HOOKS_ALLOW_STORE_MUTATIONS=1 cratis …`) changes n
230
230
 
231
231
  The Bash guard is a no-op unless **both** target-repository opt-in conditions hold: the target is under
232
232
  `github.com/Cratis/`, and its `.github/workflows` contains a reusable Cratis/Workflows
233
- `verify-release-notes` or `verify-semver-label` caller (including the renamed
234
- `verify-release-intent` caller). Installing the corpus or `@cratis/pi` alone does not opt in a
233
+ `verify-pull-request-policy` caller. The legacy `verify-release-notes`, `verify-semver-label` and
234
+ `verify-release-intent` callers it replaced are still recognized. Installing the corpus or `@cratis/pi` alone does not opt in a
235
235
  consumer or personal repository. This scope applies to both Claude Code and Pi; the direct checker
236
236
  remains available explicitly in any repository. As in `gh`, an edit's PR URL takes priority over
237
237
  `--repo` / `-R`, then literal leading or inherited `GH_REPO`, then the current directory's `origin`.
@@ -258,14 +258,17 @@ node .cratis/ai/hooks/scripts/cratis-check-pr.mjs --body-file .ai-work/pr-body.m
258
258
  node .cratis/ai/hooks/scripts/cratis-check-pr.mjs --body-file .ai-work/pr-body.md --pr 123 --strict
259
259
  ```
260
260
 
261
- The checker fetches both marked programs from Cratis/Workflows' `verify-release-notes.yml` at
262
- the immutable commit SHA in the repository's own release-notes caller. If there is no such caller,
261
+ The checker fetches both marked programs (`release-notes` and `release-notes-drift`) from
262
+ Cratis/Workflows' `verify-pull-request-policy.yml` at the ref in the repository's own policy caller,
263
+ and ignores the other programs marked in that file. Without a policy caller it fetches them from
264
+ `verify-release-notes.yml` at the ref in a legacy release-notes caller. If there is neither,
263
265
  it uses the reviewed pin `40125b4fcae388e62ed9471edfffd0e991422f0a`. Floating callers such as `@main`
264
266
  fetch the programs at that ref; if the fetch fails, they use the bundled programs from the reviewed
265
267
  pin with a warning, so body validation is never silently skipped. Programs receive only the proposed PR
266
268
  metadata and `PATH`, `HOME`, and `LANG`, not inherited credentials or Node runtime options.
267
269
  The exact programs are cached by workflow blob SHA under
268
- `${XDG_CACHE_HOME:-~/.cache}/cratis/release-notes/<ref>/<sha>.cjs`. Offline, the newest complete
270
+ `${XDG_CACHE_HOME:-~/.cache}/cratis/verify-pull-request-policy/<ref>/<sha>.cjs` for a policy caller and
271
+ `${XDG_CACHE_HOME:-~/.cache}/cratis/release-notes/<ref>/<sha>.cjs` otherwise. Offline, the newest complete
269
272
  cached copy for that same pin is used with a warning. With no cache it reports **unchecked** and
270
273
  exits **3**; the hook passes through with that warning. Successful checks are silent in hook mode;
271
274
  real warnings go to Pi's context or Claude Code's JSON `systemMessage` and `additionalContext`. Exit **0** means no violations; **1** means invalid notes or
@@ -53,8 +53,9 @@ const git = (args, cwd = process.cwd()) => {
53
53
  return result.status === 0 ? result.stdout.trim() : '';
54
54
  };
55
55
 
56
+ // The combined pull-request policy caller opts in, as do the legacy per-check callers it replaced.
56
57
  function workflowCallers(source) {
57
- return [...source.matchAll(/^\s*uses:\s*['"]?Cratis\/Workflows\/\.github\/workflows\/(verify-(?:release-notes|semver-label|release-intent))\.yml@([^\s'"#]+)/gmi)]
58
+ return [...source.matchAll(/^\s*uses:\s*['"]?Cratis\/Workflows\/\.github\/workflows\/(verify-(?:pull-request-policy|release-notes|semver-label|release-intent))\.yml@([^\s'"#]+)/gmi)]
58
59
  .map(match => ({ name: match[1], ref: match[2] }));
59
60
  }
60
61
 
@@ -117,12 +118,27 @@ function programs(workflow) {
117
118
  return bundle;
118
119
  }
119
120
 
121
+ // Workflows that carry the marked release-note programs, in order of preference. Each has its own
122
+ // cache root so a bundle from one file is never offered for the other at the same ref.
123
+ const ruleSources = [
124
+ { caller: 'verify-pull-request-policy', file: 'verify-pull-request-policy.yml', cache: 'verify-pull-request-policy' },
125
+ { caller: 'verify-release-notes', file: 'verify-release-notes.yml', cache: 'release-notes' },
126
+ ];
127
+
128
+ function ruleSource(repositoryCallers) {
129
+ for (const source of ruleSources) {
130
+ const caller = repositoryCallers.find(candidate => candidate.name === source.caller);
131
+ if (caller) return { ...source, ref: caller.ref };
132
+ }
133
+ return { ...ruleSources.find(source => source.caller === 'verify-release-notes'), ref: reviewedRulesRef };
134
+ }
135
+
120
136
  function rules(warn, repositoryCallers) {
121
- const ref = repositoryCallers.find(caller => caller.name === 'verify-release-notes')?.ref || reviewedRulesRef;
137
+ const { file: workflowFile, cache: cacheName, ref } = ruleSource(repositoryCallers);
122
138
  const floating = !/^[a-f0-9]{40}$/i.test(ref);
123
- const cache = join(process.env.XDG_CACHE_HOME || join(homedir(), '.cache'), 'cratis', 'release-notes', ref);
139
+ const cache = join(process.env.XDG_CACHE_HOME || join(homedir(), '.cache'), 'cratis', cacheName, ref);
124
140
  try {
125
- const workflow = gh(['api', '-H', 'Accept: application/vnd.github.raw', `repos/Cratis/Workflows/contents/.github/workflows/verify-release-notes.yml?ref=${encodeURIComponent(ref)}`]);
141
+ const workflow = gh(['api', '-H', 'Accept: application/vnd.github.raw', `repos/Cratis/Workflows/contents/.github/workflows/${workflowFile}?ref=${encodeURIComponent(ref)}`]);
126
142
  const bundle = programs(workflow);
127
143
  const bytes = Buffer.from(workflow);
128
144
  const sha = createHash('sha1').update(`blob ${bytes.length}\0`).update(bytes).digest('hex');
@@ -136,7 +152,7 @@ function rules(warn, repositoryCallers) {
136
152
  if (floating || ref === reviewedRulesRef) {
137
153
  const bundled = fileURLToPath(new URL('./cratis-release-notes-reviewed.cjs', import.meta.url));
138
154
  new Script(readFileSync(bundled, 'utf8'));
139
- warn(`Could not fetch release-note rules at ${ref}; using bundled reviewed ${reviewedRulesRef}. ${error.message}`);
155
+ warn(`Could not fetch release-note rules from ${workflowFile} at ${ref}; using bundled reviewed ${reviewedRulesRef}. ${error.message}`);
140
156
  return bundled;
141
157
  }
142
158
  try {
@@ -148,12 +164,12 @@ function rules(warn, repositoryCallers) {
148
164
  const source = readFileSync(file, 'utf8');
149
165
  if (!source.includes('// cratis:program release-notes\n') || !source.includes('// cratis:program release-notes-drift\n')) continue;
150
166
  new Script(source);
151
- warn(`Could not fetch release-note rules; using cached ${file}. ${error.message}`);
167
+ warn(`Could not fetch release-note rules from ${workflowFile}; using cached ${file}. ${error.message}`);
152
168
  return file;
153
169
  } catch { /* Try the next complete cache entry. */ }
154
170
  }
155
171
  } catch { /* No cache available. */ }
156
- warn(`unchecked: could not fetch release-note rules and nothing is cached. ${error.message}`);
172
+ warn(`unchecked: could not fetch release-note rules from ${workflowFile} at ${ref} and nothing is cached. ${error.message}`);
157
173
  return undefined;
158
174
  }
159
175
  }
@@ -24,7 +24,7 @@ if ! command -v node >/dev/null 2>&1; then
24
24
  exit 0
25
25
  fi
26
26
  root=$(git rev-parse --show-toplevel 2>/dev/null || true)
27
- if ! grep -Eq "^[[:space:]]*uses:[[:space:]]*['\"]?Cratis/Workflows/\\.github/workflows/verify-(release-notes|semver-label|release-intent)\\.yml@" "$root"/.github/workflows/*.yml "$root"/.github/workflows/*.yaml 2>/dev/null; then
27
+ if ! grep -Eq "^[[:space:]]*uses:[[:space:]]*['\"]?Cratis/Workflows/\\.github/workflows/verify-(pull-request-policy|release-notes|semver-label|release-intent)\\.yml@" "$root"/.github/workflows/*.yml "$root"/.github/workflows/*.yaml 2>/dev/null; then
28
28
  exit 0
29
29
  fi
30
30
  # JSON makes fail-open warnings visible to Claude; stderr exposes the same warning to Pi.
@@ -125,6 +125,10 @@ array.should.have.lengthOf(3);
125
125
  (() => throwingFn()).should.throw(ErrorType);
126
126
  ```
127
127
 
128
+ ### Comparing JSON
129
+
130
+ Never assert JSON equality by comparing `JSON.stringify` output: object key order is insertion order, so two equal documents can serialize differently and a spec fails, or passes, for the wrong reason. Compare parsed values with `should.deep.equal`. When a helper must compare serialized JSON (a snapshot, a file on disk, a request body), canonicalize first: sort object keys recursively at every level and keep array order, because array order is data. Review flags raw `JSON.stringify(a) === JSON.stringify(b)` equality in specs and spec helpers.
131
+
128
132
  ## Mocking with Sinon
129
133
 
130
134
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/pi",
3
- "version": "7.84.0",
3
+ "version": "7.84.2",
4
4
  "description": "Configuration-aware Cratis AI integration for Pi",
5
5
  "type": "module",
6
6
  "files": [