@intentic/constants 1.239.0 → 1.242.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.
package/README.md CHANGED
@@ -13,10 +13,17 @@ The ports, paths and image references the daemon, the CLIs and the desktop app a
13
13
  values, and the install-script table. Isomorphic, imported by browser code, so nothing here may touch `node:fs`.
14
14
  - [src/node.mjs](src/node.mjs): `repoRoot()` and `packageRoot()`, behind the `@intentic/constants/node`
15
15
  subpath. Node-only, and hand-written JavaScript rather than compiled TypeScript.
16
+ - [src/assertion-measure.mjs](src/assertion-measure.mjs), [src/contract-shrink.mjs](src/contract-shrink.mjs) and
17
+ [src/control-bytes.mjs](src/control-bytes.mjs): the three judgments the repository's checkout gates
18
+ (`_tools/checks/`) and the daemon both make, kept as one copy each. Hand-written JavaScript for the same
19
+ reason `node.mjs` is: a gate that runs before `pnpm install` imports them by relative path, and the daemon
20
+ imports them as subpaths of this package.
16
21
 
17
22
  ## How it fits
18
23
 
19
- The bottom of the dependency graph: it imports nothing and almost everything imports it. A port number that lives
24
+ The bottom of the dependency graph: it imports nothing and almost everything imports it. That is also what
25
+ makes it the home of the few pure judgments a pre-install script and the daemon have to share: anything else
26
+ they could both import would need an install to resolve. A port number that lives
20
27
  in two files is a port number that will eventually be two different numbers, which is the entire argument for
21
28
  this package existing. The same argument covers the directory layouts: `/work`, `/history`, `.intentic`,
22
29
  `/opt/intentic`: which were previously typed out by hand across dozens of files with nothing linking the copies.
@@ -41,7 +48,7 @@ by nothing. Walking up to a marker has no such coupling, so a file can move anyw
41
48
  gets written. It is also why the root `package.json` depends on this package: without that link, scripts under
42
49
  `_tools/scripts/` cannot resolve it by name.
43
50
  - **The name only resolves once `pnpm install` has run**, because a bare specifier is looked up through
44
- `node_modules`. The two callers that run before any install: `prepass.mjs --checks-only`, which the `pre-push`
51
+ `node_modules`. The two callers that run before any install: `_tools/checks/run.mjs`, which the `pre-push`
45
52
  hook and the CI `preflight` job invoke on a bare checkout: therefore import `../constants/src/node.mjs` by
46
53
  path. Same file, same single copy of the walk, no install required. Everything that runs after the install
47
54
  imports it by name.
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export declare const WORKSPACE_ROOT = "/work";
3
3
  export declare const HISTORY_ROOT = "/history";
4
4
  export declare const STATE_DIR = ".intentic";
5
5
  export declare const HOST_STATE_ROOT = "/opt/intentic";
6
- export declare const LEGAL_VERSION = "2026-08-13";
6
+ export declare const LEGAL_VERSION = "2026-09-03";
7
7
  export declare const LEGAL_CONTACT_EMAIL = "contact@intentic.dev";
8
8
  export declare const LEGAL_ENTITY_NAME = "Artur Kurowski, trading as radarsu";
9
9
  export declare const LEGAL_ENTITY_COUNTRY = "Poland";
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ export const WORKSPACE_ROOT = "/work";
3
3
  export const HISTORY_ROOT = "/history";
4
4
  export const STATE_DIR = ".intentic";
5
5
  export const HOST_STATE_ROOT = "/opt/intentic";
6
- export const LEGAL_VERSION = "2026-08-13";
6
+ export const LEGAL_VERSION = "2026-09-03";
7
7
  export const LEGAL_CONTACT_EMAIL = "contact@intentic.dev";
8
8
  export const LEGAL_ENTITY_NAME = "Artur Kurowski, trading as radarsu";
9
9
  export const LEGAL_ENTITY_COUNTRY = "Poland";
@@ -5,6 +5,8 @@ export declare const PROVIDER_BRAND_PATHS: {
5
5
  readonly kimi: `M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z`;
6
6
  readonly gemini: `M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81`;
7
7
  readonly cursor: `M11.503.131 1.891 5.678a.84.84 0 0 0-.42.726v11.188c0 .3.162.575.42.724l9.609 5.55a1 1 0 0 0 .998 0l9.61-5.55a.84.84 0 0 0 .42-.724V6.404a.84.84 0 0 0-.42-.726L12.497.131a1.01 1.01 0 0 0-.996 0M2.657 6.338h18.55c.263 0 .43.287.297.515L12.23 22.918c-.062.107-.229.064-.229-.06V12.335a.59.59 0 0 0-.295-.51l-9.11-5.257c-.109-.063-.064-.23.061-.23`;
8
+ readonly meta: `M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z`;
9
+ readonly zai: `M12.105 2L9.927 4.953H.653L2.83 2h9.276zM23.254 19.048L21.078 22h-9.242l2.174-2.952h9.244zM24 2L9.264 22H0L14.736 2H24z`;
8
10
  };
9
11
  export type ProviderBrand = keyof typeof PROVIDER_BRAND_PATHS;
10
12
  export declare const providerFillRule: (brand: ProviderBrand) => "evenodd" | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"provider-logos.d.ts","sourceRoot":"","sources":["../src/provider-logos.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,oBAAoB;aAC7B,MAAM,EAAE,qxDAAqxD;aAC7xD,KAAK,EAAE,s7CAAs7C;aAC77C,IAAI,EAAE,8iBAA8iB;aAGpjB,IAAI,EAAE,iDAAiD;aACvD,MAAM,EAAE,8RAA8R;aAGtS,MAAM,EAAE,0VAA0V;CAC5V,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,oBAAoB,CAAC;AAG9D,eAAO,MAAM,gBAAgB,UAAW,aAAa,KAAG,SAAS,GAAG,SAAuD,CAAC"}
1
+ {"version":3,"file":"provider-logos.d.ts","sourceRoot":"","sources":["../src/provider-logos.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,oBAAoB;aAC7B,MAAM,EAAE,qxDAAqxD;aAC7xD,KAAK,EAAE,s7CAAs7C;aAC77C,IAAI,EAAE,8iBAA8iB;aAGpjB,IAAI,EAAE,iDAAiD;aACvD,MAAM,EAAE,8RAA8R;aAGtS,MAAM,EAAE,0VAA0V;aAGlW,IAAI,EAAE,ksCAAksC;aAIxsC,GAAG,EAAE,yHAAyH;CACxH,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,oBAAoB,CAAC;AAS9D,eAAO,MAAM,gBAAgB,UAAW,aAAa,KAAG,SAAS,GAAG,SAAgE,CAAC"}
@@ -5,6 +5,9 @@ export const PROVIDER_BRAND_PATHS = {
5
5
  kimi: `M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z`,
6
6
  gemini: `M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81`,
7
7
  cursor: `M11.503.131 1.891 5.678a.84.84 0 0 0-.42.726v11.188c0 .3.162.575.42.724l9.609 5.55a1 1 0 0 0 .998 0l9.61-5.55a.84.84 0 0 0 .42-.724V6.404a.84.84 0 0 0-.42-.726L12.497.131a1.01 1.01 0 0 0-.996 0M2.657 6.338h18.55c.263 0 .43.287.297.515L12.23 22.918c-.062.107-.229.064-.229-.06V12.335a.59.59 0 0 0-.295-.51l-9.11-5.257c-.109-.063-.064-.23.061-.23`,
8
+ meta: `M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z`,
9
+ zai: `M12.105 2L9.927 4.953H.653L2.83 2h9.276zM23.254 19.048L21.078 22h-9.242l2.174-2.952h9.244zM24 2L9.264 22H0L14.736 2H24z`,
8
10
  };
9
- export const providerFillRule = (brand) => (brand === `grok` ? `evenodd` : undefined);
11
+ const EVENODD_BRANDS = new Set([`grok`, `zai`]);
12
+ export const providerFillRule = (brand) => (EVENODD_BRANDS.has(brand) ? `evenodd` : undefined);
10
13
  //# sourceMappingURL=provider-logos.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"provider-logos.js","sourceRoot":"","sources":["../src/provider-logos.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAChC,MAAM,EAAE,qxDAAqxD;IAC7xD,KAAK,EAAE,s7CAAs7C;IAC77C,IAAI,EAAE,8iBAA8iB;IAGpjB,IAAI,EAAE,iDAAiD;IACvD,MAAM,EAAE,8RAA8R;IAGtS,MAAM,EAAE,0VAA0V;CAC5V,CAAC;AAKX,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,KAAoB,EAAyB,EAAE,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC"}
1
+ {"version":3,"file":"provider-logos.js","sourceRoot":"","sources":["../src/provider-logos.ts"],"names":[],"mappings":"AAiBA,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAChC,MAAM,EAAE,qxDAAqxD;IAC7xD,KAAK,EAAE,s7CAAs7C;IAC77C,IAAI,EAAE,8iBAA8iB;IAGpjB,IAAI,EAAE,iDAAiD;IACvD,MAAM,EAAE,8RAA8R;IAGtS,MAAM,EAAE,0VAA0V;IAGlW,IAAI,EAAE,ksCAAksC;IAIxsC,GAAG,EAAE,yHAAyH;CACxH,CAAC;AAUX,MAAM,cAAc,GAA+B,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;AAC5E,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,KAAoB,EAAyB,EAAE,CAAC,CAAC,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentic/constants",
3
- "version": "1.239.0",
3
+ "version": "1.242.0",
4
4
  "description": "Shared constants for the intentic packages, ports, paths, and image references the daemon, CLIs and desktop app all agree on",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -13,7 +13,13 @@
13
13
  "files": [
14
14
  "dist",
15
15
  "src/node.mjs",
16
- "src/node.d.mts"
16
+ "src/node.d.mts",
17
+ "src/assertion-measure.mjs",
18
+ "src/assertion-measure.d.mts",
19
+ "src/contract-shrink.mjs",
20
+ "src/contract-shrink.d.mts",
21
+ "src/control-bytes.mjs",
22
+ "src/control-bytes.d.mts"
17
23
  ],
18
24
  "exports": {
19
25
  ".": {
@@ -30,6 +36,18 @@
30
36
  "./node": {
31
37
  "types": "./src/node.d.mts",
32
38
  "default": "./src/node.mjs"
39
+ },
40
+ "./assertion-measure": {
41
+ "types": "./src/assertion-measure.d.mts",
42
+ "default": "./src/assertion-measure.mjs"
43
+ },
44
+ "./contract-shrink": {
45
+ "types": "./src/contract-shrink.d.mts",
46
+ "default": "./src/contract-shrink.mjs"
47
+ },
48
+ "./control-bytes": {
49
+ "types": "./src/control-bytes.d.mts",
50
+ "default": "./src/control-bytes.mjs"
33
51
  }
34
52
  },
35
53
  "devDependencies": {
@@ -0,0 +1,14 @@
1
+ // Types for assertion-measure.mjs, so the daemon's agreement test (agent-tests.test.ts) can import it by path.
2
+ export interface AssertionMeasure {
3
+ readonly exact: number;
4
+ readonly loose: number;
5
+ readonly chars: number;
6
+ readonly tests: number;
7
+ }
8
+ export type Weakening = "downgrade" | "narrowing";
9
+ export const NARROWING: number;
10
+ export const EXACT: readonly string[];
11
+ export const LOOSE: readonly string[];
12
+ export function measure(source: string): AssertionMeasure;
13
+ export function weakened(before: AssertionMeasure | undefined, after: AssertionMeasure): Weakening | undefined;
14
+ export function describeWeakening(path: string, shape: Weakening, before: AssertionMeasure, after: AssertionMeasure): string;
@@ -0,0 +1,244 @@
1
+ /* HOW STRONG A TEST FILE'S ASSERTIONS ARE, as three numbers, and whether a second version of the file is weaker.
2
+ *
3
+ * The pure half of the assertion ratchet, shared by the push gate (_tools/scripts/assertion-ratchet.mjs, which
4
+ * runs it over a commit range or the working tree, importing this file by relative path because a pre-push hook
5
+ * runs on a clone that may never have installed) and by the daemon's turn-ending check
6
+ * (_sandbox/sandbox/src/agent/agent-tests.ts, importing it as @intentic/constants/assertion-measure), which asks
7
+ * the same question of the test files a turn touched and tells the model while it can still act. One copy, so
8
+ * the two readers cannot disagree.
9
+ *
10
+ * WHY. On 2026-08-31 eight commits in fifty minutes "relaxed" about 180 test files. `toEqual({ …, message:
11
+ * "Reached Example, authenticated as ada." })` became `toMatchObject({ … })` plus `toContain("ada")`; `"9 of 12
12
+ * files still in your workspace"` became `toContain("9")` and `toContain("12")`; `"Start your first agent"`
13
+ * became `"first agent"`. Every suite stayed green, every gate said yes, and each of those tests can now barely
14
+ * fail. AGENTS.md forbids exactly this ("an assertion that cannot fail is worse than no test") and no linter can
15
+ * enforce it: the shape of a weak `toContain` is the shape of a strong one. What CAN be seen is the direction
16
+ * of travel between two versions of the same file, which is what this measures.
17
+ *
18
+ * THREE NUMBERS PER FILE: EXACT matchers (toBe, toEqual, toStrictEqual, toHaveLength, toHaveBeenCalledWith,
19
+ * snapshots…), LOOSE matchers (toContain, toMatch, toMatchObject, toBeTruthy, toBeGreaterThan, expect.any…), and
20
+ * the characters of literal text the assertions pin down (every string, regex and template run inside a
21
+ * matcher's argument list, comments excluded). A file is weaker in either of two shapes:
22
+ *
23
+ * · a DOWNGRADE: fewer exact matchers and more loose ones AND no more asserted text than before, the
24
+ * `toEqual` → `toMatchObject` move. That third clause is what tells the move from its opposite. The move
25
+ * always SHEDS pinned text — it replaces a whole expected object with a fragment of one — so a file that
26
+ * ends up pinning more text than it did is doing something else, whatever its matcher mix did. Without the
27
+ * clause the rule read absolute counts with no sense of scale, and a suite that grew by five tests and 247
28
+ * characters of expectation was refused for turning one `toEqual({})` — an exact matcher asserting that a
29
+ * result is EMPTY — into `toMatchObject({ permissionDecision: "deny" })`, which pins a value the old
30
+ * assertion could not see. One matcher moved from the exact column to the loose one and the file got
31
+ * stronger. Over the 400 commits before this clause was written it changes exactly one verdict, and that
32
+ * one was wrong.
33
+ * · a NARROWING: the asserted text shrinks by more than a quarter while the file keeps as many tests as it had,
34
+ * the "first agent" move. Tests removed with their text are not a narrowing, and the test count says so.
35
+ * Deliberately left on absolute ratio with no floor and no exemption for a file whose matcher mix improved:
36
+ * both were tried against the same 400 commits and both cost more than they bought. A floor big enough to
37
+ * excuse an 85→52 character file exempts 60% of the repository's test files, because the median test file
38
+ * pins only 110 characters; and exempting "the exact count went up while the loose count went down" lets a
39
+ * commit gut six text assertions and buy the exemption with one added `toBe`, which is a real commit
40
+ * (daf77486) this would then have missed. Of the 62 narrowings in that range, 60 sit on commits whose own
41
+ * subject says they relaxed assertions. The two that do not are a `toEqual({…})` replaced by
42
+ * `toBeUndefined()` and a 33-character trim — both worth a reviewer's eye, which is all a flag asks for.
43
+ *
44
+ * A HEURISTIC, AND SAID TO BE ONE. A refactor that replaces twenty `toBe` lines with one `toEqual` of a whole
45
+ * object reads as fewer exact matchers; a suite that switches from asserting prose to asserting structure reads
46
+ * as narrowing. Both are legitimate, and both are exactly the changes a reviewer should be told about, which is
47
+ * why the gate refuses only an UNDECLARED weakening and the turn-ending check reports rather than refuses.
48
+ *
49
+ * Deliberately regex over source, not an AST: this runs from a pre-push hook on a clone that may never have
50
+ * installed, so it can import nothing, and the matchers it counts are names, which a regex reads as well as a
51
+ * parser does. It cannot see a matcher called through a helper (`expectRow(row).toBe(…)` counts, `check(row)`
52
+ * does not), which is the direction of error that under-reports rather than nags. */
53
+
54
+ // Asserted text that shrinks past this fraction of what it was, with no test removed, is a narrowing.
55
+ export const NARROWING = 0.75;
56
+
57
+ /* The vocabulary. Exact matchers pin a value; loose ones admit a family of them. `toThrow` and `toHaveProperty`
58
+ * are both depending on their arguments (a message or a value makes them exact) and are counted as neither, so
59
+ * a file that trades between them moves no number. Asymmetric matchers (`expect.any`, `objectContaining`) loosen
60
+ * whatever exact matcher they sit inside, so each one counts as loose. */
61
+ export const EXACT = [
62
+ "toBe",
63
+ "toEqual",
64
+ "toStrictEqual",
65
+ "toHaveLength",
66
+ "toHaveBeenCalledWith",
67
+ "toHaveBeenLastCalledWith",
68
+ "toHaveBeenNthCalledWith",
69
+ "toHaveBeenCalledTimes",
70
+ "toHaveBeenCalledOnce",
71
+ "toHaveReturnedWith",
72
+ "toHaveLastReturnedWith",
73
+ "toMatchInlineSnapshot",
74
+ "toMatchSnapshot",
75
+ "toMatchFileSnapshot",
76
+ "toThrowErrorMatchingInlineSnapshot",
77
+ "toThrowErrorMatchingSnapshot",
78
+ "toBeNull",
79
+ "toBeUndefined",
80
+ "toBeNaN",
81
+ "toBeCloseTo",
82
+ ];
83
+ export const LOOSE = [
84
+ "toContain",
85
+ "toContainEqual",
86
+ "toMatch",
87
+ "toMatchObject",
88
+ "toBeTruthy",
89
+ "toBeFalsy",
90
+ "toBeDefined",
91
+ "toBeGreaterThan",
92
+ "toBeGreaterThanOrEqual",
93
+ "toBeLessThan",
94
+ "toBeLessThanOrEqual",
95
+ "toBeInstanceOf",
96
+ "toBeTypeOf",
97
+ "toSatisfy",
98
+ "toHaveBeenCalled",
99
+ "toHaveReturned",
100
+ "toBeOneOf",
101
+ ];
102
+ const exact = new Set(EXACT);
103
+ const loose = new Set(LOOSE);
104
+ const ASYMMETRIC = /\bexpect\.(any|anything|stringContaining|stringMatching|objectContaining|arrayContaining|closeTo)\s*\(/g;
105
+ const MATCHER = /\.(to[A-Z][A-Za-z]*)\s*\(/g;
106
+ const TEST_CASE = /^\s*(?:test|it)(?:\.(?:each|skip|only|concurrent|todo|fails|skipIf|runIf))?\s*\(/gm;
107
+
108
+ /* The literal text a matcher's argument list pins down: from the `(` that opens it to the `)` that closes it,
109
+ * every string literal's characters, every regex's source, and every static run of a template. Walked by hand
110
+ * because a matcher's argument is routinely a multi-line object with nested calls, which no single regex can
111
+ * bound.
112
+ *
113
+ * COMMENTS ARE SKIPPED FIRST, and that is not tidiness. Assertions here are routinely commented one by one,
114
+ * the prose says "the owner's" and "the agent's", and to a walker that reads an apostrophe as an opening quote
115
+ * that comment is a string running to the next apostrophe — over the `)` that closes the matcher, over the
116
+ * tests below it, to the end of the file. The number that came back was not an overcount of one file's text so
117
+ * much as a coin flip on how many apostrophes the prose happened to hold, and editing a comment landed a
118
+ * "narrowing" on a file whose assertions nobody had touched.
119
+ *
120
+ * A TEMPLATE'S STATIC RUNS COUNT, only its `${…}` does not. `${STATE_DIR}/config/safety.md` pins seventeen
121
+ * characters of path and one interpolation, and reading the whole literal as computed made every assertion in
122
+ * a repository that composes its paths from constants — which this one requires, see _tools/checks/path-literals.mjs —
123
+ * look like an assertion about nothing. */
124
+ // Past a template's `${…}`, by brace depth: the expression inside is computed, so none of it is asserted text.
125
+ const pastInterpolation = (source, from) => {
126
+ let braces = 1;
127
+ let i = from + 2;
128
+ for (; i < source.length && braces > 0; i += 1) {
129
+ braces += source[i] === "{" ? 1 : 0;
130
+ braces -= source[i] === "}" ? 1 : 0;
131
+ }
132
+ return i;
133
+ };
134
+
135
+ // A template literal, from its opening backtick: the characters of its static runs, and where it ends.
136
+ const templateChars = (source, from) => {
137
+ let chars = 0;
138
+ let run = from + 1;
139
+ let i = run;
140
+ while (i < source.length && source[i] !== "`") {
141
+ if (source[i] === "\\") {
142
+ i += 2;
143
+ } else if (source[i] === "$" && source[i + 1] === "{") {
144
+ chars += i - run;
145
+ i = pastInterpolation(source, i);
146
+ run = i;
147
+ } else {
148
+ i += 1;
149
+ }
150
+ }
151
+ return { chars: chars + Math.min(i, source.length) - run, end: i };
152
+ };
153
+
154
+ const assertedChars = (source, from) => {
155
+ let depth = 0;
156
+ let chars = 0;
157
+ for (let i = from; i < source.length; i += 1) {
158
+ const ch = source[i];
159
+ if (ch === "/" && source[i + 1] === "/") {
160
+ const end = source.indexOf("\n", i);
161
+ if (end === -1) {
162
+ return chars;
163
+ }
164
+ i = end;
165
+ } else if (ch === "/" && source[i + 1] === "*") {
166
+ const end = source.indexOf("*/", i + 2);
167
+ if (end === -1) {
168
+ return chars;
169
+ }
170
+ i = end + 1;
171
+ } else if (ch === "(") {
172
+ depth += 1;
173
+ } else if (ch === ")") {
174
+ depth -= 1;
175
+ if (depth === 0) {
176
+ return chars;
177
+ }
178
+ } else if (ch === '"' || ch === "'") {
179
+ const quote = ch;
180
+ let j = i + 1;
181
+ for (; j < source.length && source[j] !== quote; j += 1) {
182
+ if (source[j] === "\\") {
183
+ j += 1;
184
+ }
185
+ }
186
+ chars += j - i - 1;
187
+ i = j;
188
+ } else if (ch === "`") {
189
+ const template = templateChars(source, i);
190
+ chars += template.chars;
191
+ i = template.end;
192
+ } else if (ch === "/" && /[(,\s=]/.test(source[i - 1] ?? "(")) {
193
+ // A regex literal in argument position: its source is asserted text like a string's.
194
+ let j = i + 1;
195
+ for (; j < source.length && source[j] !== "/" && source[j] !== "\n"; j += 1) {
196
+ if (source[j] === "\\") {
197
+ j += 1;
198
+ }
199
+ }
200
+ chars += j - i - 1;
201
+ i = j;
202
+ }
203
+ }
204
+ return chars;
205
+ };
206
+
207
+ // The three numbers, and the test count that tells a narrowing from a deletion.
208
+ export const measure = (source) => {
209
+ let exactCount = 0;
210
+ let looseCount = 0;
211
+ let chars = 0;
212
+ for (const match of source.matchAll(MATCHER)) {
213
+ const name = match[1];
214
+ if (exact.has(name)) {
215
+ exactCount += 1;
216
+ } else if (loose.has(name)) {
217
+ looseCount += 1;
218
+ }
219
+ chars += assertedChars(source, match.index + match[0].length - 1);
220
+ }
221
+ looseCount += [...source.matchAll(ASYMMETRIC)].length;
222
+ const tests = [...source.matchAll(TEST_CASE)].length;
223
+ return { exact: exactCount, loose: looseCount, chars, tests };
224
+ };
225
+
226
+ // Weaker, in either of the two shapes the header names. `before` absent (a new file) can only be stronger.
227
+ export const weakened = (before, after) => {
228
+ if (before === undefined) {
229
+ return undefined;
230
+ }
231
+ // The third clause is the scale the first two have none of: see the header. A file that pins MORE text than
232
+ // it did is not making the `toEqual` → `toMatchObject` move, whichever way its matcher counts went.
233
+ if (after.exact < before.exact && after.loose > before.loose && after.chars <= before.chars) {
234
+ return "downgrade";
235
+ }
236
+ if (before.chars > 0 && after.chars < before.chars * NARROWING && after.tests >= before.tests) {
237
+ return "narrowing";
238
+ }
239
+ return undefined;
240
+ };
241
+
242
+ // One line per weakened file, the numbers a reader needs to judge the heuristic for themselves.
243
+ export const describeWeakening = (path, shape, before, after) =>
244
+ `${path}: ${shape} (exact ${before.exact}→${after.exact}, loose ${before.loose}→${after.loose}, asserted chars ${before.chars}→${after.chars}, tests ${before.tests}→${after.tests})`;
@@ -0,0 +1,3 @@
1
+ // Types for contract-shrink.mjs, the wire-contract shrink comparison the checkout gate and the daemon share.
2
+ export function shrunkSurfaces(base: unknown, head: unknown, at?: string, out?: string[], named?: boolean): string[];
3
+ export function lockShrinkage(baseText: string, headText: string): string[];
@@ -0,0 +1,76 @@
1
+ /* WHAT A WIRE-CONTRACT LOCK LOST, as one comparison shared by everything that asks it.
2
+ *
3
+ * contract.lock.json is the sandbox-contract package's exported schemas as one comparable document
4
+ * (_sandbox/sandbox-contract/src/contract-lock.ts explains the pair). Three readers ask what a newer lock no
5
+ * longer offers: the checkout gate that refuses an undeclared shrink at the push (_tools/checks/contract-shrink.mjs),
6
+ * the landing drafter that forces the `!` and the Breaking-Note into the commit message
7
+ * (_sandbox/sandbox/src/git/contract-shrink.ts), and the tests that hold both to the same judgment. One
8
+ * implementation here, hand-written JavaScript rather than compiled TypeScript for the same reason node.mjs
9
+ * beside it is: the gate runs before `pnpm install`, so it imports this file by relative path and nothing else.
10
+ *
11
+ * ADDITIONS NEVER APPEAR IN THE RESULT. Every reader of the wire parses loosely, so growth breaks nobody, and a
12
+ * detector that flagged growth would put a false `!` on ordinary work.
13
+ *
14
+ * ARRAYS ARE THE SCHEMA'S COLLECTIONS (`oneOf` alternatives, `enum` values, `required` names) and the lock
15
+ * writer keeps them in declaration order rather than sorting, so a position means nothing on its own: every
16
+ * base element must be matched by SOME head element, and extras pass in silence exactly like a new property
17
+ * does. An element that merely changed reads as removed: the same verdict either way.
18
+ *
19
+ * `description` AS A KEYWORD IS PROSE, AND PROSE IS NOT A PROMISE. zod's `.describe()` rides into the lock
20
+ * beside the shape, so re-wording a help sentence used to read as "a surface changed" and demand a `!` commit.
21
+ * Nothing on the wire moves when it does. It is skipped ONLY as a keyword: 78 schemas in this lock carry a
22
+ * real field NAMED `description`, and losing one of those is a genuine break, so the walk tracks whether the
23
+ * object it is reading is a name map (keys are fields) or a schema (keys are keywords). */
24
+
25
+ // The JSON Schema keywords whose value is a map of NAME to schema: inside one a key is a field the wire
26
+ // carries, everywhere else a key is a keyword. The lock's own root is one too (its keys are the exported
27
+ // schema names), which is why the walk starts `named`.
28
+ const NAME_MAPS = new Set(["properties", "patternProperties", "$defs", "definitions"]);
29
+
30
+ const dotted = (at, key) => (at === "" ? key : `${at}.${key}`);
31
+
32
+ // Every surface `base` offers that `head` no longer does, as dotted paths.
33
+ export const shrunkSurfaces = (base, head, at = "", out = [], named = true) => {
34
+ if (Array.isArray(base) || Array.isArray(head)) {
35
+ if (!Array.isArray(base) || !Array.isArray(head)) {
36
+ out.push(at);
37
+ return out;
38
+ }
39
+ for (const [index, item] of base.entries()) {
40
+ const itemAt = typeof item === "object" && item !== null ? `${at}[${index}]` : `${at} ${JSON.stringify(item)}`;
41
+ const offered = head.some((candidate) => shrunkSurfaces(item, candidate, itemAt, [], false).length === 0);
42
+ if (!offered) {
43
+ out.push(itemAt);
44
+ }
45
+ }
46
+ return out;
47
+ }
48
+ if (typeof base !== "object" || base === null || typeof head !== "object" || head === null) {
49
+ if (JSON.stringify(base) !== JSON.stringify(head)) {
50
+ out.push(at);
51
+ }
52
+ return out;
53
+ }
54
+ for (const key of Object.keys(base)) {
55
+ if (!named && key === "description") {
56
+ continue;
57
+ }
58
+ if (key in head) {
59
+ shrunkSurfaces(base[key], head[key], dotted(at, key), out, !named && NAME_MAPS.has(key));
60
+ } else {
61
+ out.push(dotted(at, key));
62
+ }
63
+ }
64
+ return out;
65
+ };
66
+
67
+ // The same comparison over the two texts of a lock file. Either side failing to parse yields NO shrink rather
68
+ // than a throw: one reader feeds a commit-message draft, and a mangled lock is the contract-lock test's failure
69
+ // to report, not a reason to draft nothing.
70
+ export const lockShrinkage = (baseText, headText) => {
71
+ try {
72
+ return shrunkSurfaces(JSON.parse(baseText), JSON.parse(headText));
73
+ } catch {
74
+ return [];
75
+ }
76
+ };
@@ -0,0 +1,7 @@
1
+ // Types for control-bytes.mjs, the byte-level text invariant every control-character reader shares.
2
+ export const BINARY_EXTENSIONS: ReadonlySet<string>;
3
+ export function isBinaryPath(path: string): boolean;
4
+ export function isForbiddenByte(byte: number): boolean;
5
+ export function firstForbiddenByte(bytes: Uint8Array): { readonly offset: number; readonly line: number; readonly column: number; readonly byte: number } | undefined;
6
+ export function byteName(byte: number): string;
7
+ export function escapeFor(byte: number): string;
@@ -0,0 +1,65 @@
1
+ /* THE BYTES THAT MAKE A TEXT FILE BINARY, named once for everything that polices them: the checkout gate
2
+ * (_tools/checks/control-chars.mjs, every tracked file), and the daemon's per-edit reader (the `bytes-edit`
3
+ * rule's script), which asks the same question of one file the moment it is written.
4
+ *
5
+ * A NUL typed straight into a string literal is invisible in an editor and decisive everywhere else: git, grep,
6
+ * `file`, code review and every diff viewer sniff for one and call the whole file binary. The escape
7
+ * (backslash-u-0000) is the same code point at runtime and leaves the file text, so every reader here asks for
8
+ * it and nothing else. Hand-written JavaScript rather than compiled TypeScript so a pre-push hook on a clone
9
+ * that never installed can import it by relative path. */
10
+
11
+ // What is allowed to hold arbitrary bytes. Extensions, not paths: an image is an image wherever it lands.
12
+ // Genuinely binary content is skipped by extension rather than by sniffing, because sniffing is exactly the
13
+ // thing that goes wrong here: a source file that LOOKS binary is the bug, not the exemption.
14
+ export const BINARY_EXTENSIONS = new Set([
15
+ "png",
16
+ "jpg",
17
+ "jpeg",
18
+ "gif",
19
+ "webp",
20
+ "avif",
21
+ "ico",
22
+ "icns",
23
+ "pdf",
24
+ "woff",
25
+ "woff2",
26
+ "ttf",
27
+ "otf",
28
+ "eot",
29
+ "zip",
30
+ "gz",
31
+ "tgz",
32
+ "br",
33
+ "wasm",
34
+ "mp4",
35
+ "webm",
36
+ "mp3",
37
+ "wav",
38
+ "bin",
39
+ "node",
40
+ "keystore",
41
+ "jks",
42
+ ]);
43
+
44
+ export const isBinaryPath = (path) => BINARY_EXTENSIONS.has(path.split(".").pop()?.toLowerCase() ?? "");
45
+
46
+ // C0 controls minus the three every text file legitimately contains (tab, newline, carriage return). DEL rides
47
+ // along: it is as invisible as the rest and has no business in source either.
48
+ export const isForbiddenByte = (byte) => byte <= 0x08 || byte === 0x0b || byte === 0x0c || (byte >= 0x0e && byte <= 0x1f) || byte === 0x7f;
49
+
50
+ // The first forbidden byte in a buffer as `{ offset, line, column, byte }`, or undefined for a clean file.
51
+ // One report per file is enough to send someone to it. Sliced only for a byte already known to be bad, so
52
+ // the clean file, the overwhelmingly common case, pays for one linear scan and nothing else.
53
+ export const firstForbiddenByte = (bytes) => {
54
+ for (let at = 0; at < bytes.length; at++) {
55
+ if (isForbiddenByte(bytes[at])) {
56
+ const upto = bytes.subarray(0, at).toString("utf8");
57
+ return { offset: at, line: upto.split("\n").length, column: upto.length - upto.lastIndexOf("\n"), byte: bytes[at] };
58
+ }
59
+ }
60
+ return undefined;
61
+ };
62
+
63
+ // How a byte is named in a report, and how it should be spelled in the file instead.
64
+ export const byteName = (byte) => (byte === 0x00 ? "NUL" : `0x${byte.toString(16).padStart(2, "0")}`);
65
+ export const escapeFor = (byte) => `\\u${byte.toString(16).padStart(4, "0")}`;
package/src/node.mjs CHANGED
@@ -14,15 +14,15 @@ import { fileURLToPath } from "node:url";
14
14
  * packages, or be symlinked in, and still get the same answer.
15
15
  *
16
16
  * WHY THIS FILE IS HAND-WRITTEN JAVASCRIPT rather than TypeScript compiled to dist, in a package where
17
- * everything else is compiled: the earliest callers run BEFORE anything is built. `prepass.mjs` is the script
18
- * that performs the build, and the byte check and the path guard both run ahead of it in `pnpm check`. A
17
+ * everything else is compiled: the earliest callers run BEFORE anything is built. `emit-declarations.mjs` is the script
18
+ * that performs the build, and the byte check and the path guard both run ahead of it in `pnpm checks`. A
19
19
  * helper those three had to import from `dist/` would be a helper they could not import on a clean checkout,
20
20
  * which is exactly how the second copy of this walk gets written. Plain .mjs with a hand-written .d.mts beside
21
21
  * it is importable at every point in the build, so there only has to be one.
22
22
  *
23
23
  * A caller that runs before `pnpm install` has one more constraint: `@intentic/constants/node` is a BARE
24
24
  * specifier and bare specifiers resolve through node_modules, which a bare checkout has none of. Those callers
25
- * (`prepass.mjs`, run by the pre-push hook and by CI's preflight job) import THIS FILE by relative path
25
+ * (`_tools/checks/*.mjs`, run by the pre-push hook and by CI's preflight job) import THIS FILE by relative path
26
26
  * instead. Still one walk; only the way in differs.
27
27
  *
28
28
  * NOT EXPORTED FROM THE PACKAGE INDEX, and that is deliberate: the index is imported by browser code