@intentic/constants 1.248.0 → 1.250.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
@@ -14,10 +14,13 @@ The ports, paths and image references the daemon, the CLIs and the desktop app a
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
16
  - [src/assertion-measure.mjs](src/assertion-measure.mjs), [src/contract-shrink.mjs](src/contract-shrink.mjs),
17
- [src/control-bytes.mjs](src/control-bytes.mjs) and [src/mirror-roots.mjs](src/mirror-roots.mjs): the four
18
- judgments the repository's checkout gates (`_tools/checks/`) and the daemon both make, kept as one copy each.
17
+ [src/control-bytes.mjs](src/control-bytes.mjs), [src/mirror-roots.mjs](src/mirror-roots.mjs) and
18
+ [src/ci-infra-steps.mjs](src/ci-infra-steps.mjs): the five judgments the repository's scripts and checks
19
+ (`_tools/checks/`, `_tools/scripts/ci/ci-audit.mjs`) and the daemon both make, kept as one copy each.
19
20
  Hand-written JavaScript for the same reason `node.mjs` is: a gate that runs before `pnpm install` imports them
20
- by relative path, and the daemon imports them as subpaths of this package.
21
+ by relative path, and the daemon imports them as subpaths of this package. `assertion-measure.mjs` carries
22
+ three of them, not one: which files are test files (`TEST_FILE`), how strong a TypeScript file's assertions
23
+ are, and how strong a python file's are.
21
24
 
22
25
  ## How it fits
23
26
 
@@ -26,7 +29,10 @@ makes it the home of the few pure judgments a pre-install script and the daemon
26
29
  they could both import would need an install to resolve. `mirror-roots.mjs` is the clearest case of that: the
27
30
  set of directories an isolated turn overlays is the daemon's business (`agents/isolation.ts` mounts them), and
28
31
  whether a build script may `rm -rf` one of them is a checkout gate's business, and the two answers have to be
29
- the same answer or a name added to one is a directory the other stops protecting. A port number that lives
32
+ the same answer or a name added to one is a directory the other stops protecting. `assertion-measure.mjs` is
33
+ the same shape of problem one level up: the push gate refuses an undeclared weakening and the daemon reports one
34
+ at the Stop, so if the two disagreed about which files are tests, or about what a python `assert` is worth, a
35
+ file could be measured by one and ignored by the other while both claimed to hold the same line. A port number that lives
30
36
  in two files is a port number that will eventually be two different numbers, which is the entire argument for
31
37
  this package existing. The same argument covers the directory layouts: `/work`, `/history`, `.intentic`,
32
38
  `/opt/intentic`: which were previously typed out by hand across dozens of files with nothing linking the copies.
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export * from "./provider-logos.js";
2
2
  export declare const WORKSPACE_ROOT = "/work";
3
3
  export declare const HISTORY_ROOT = "/history";
4
4
  export declare const STATE_DIR = ".intentic";
5
+ export declare const MEMORY_FILE = "AGENTS.md";
5
6
  export declare const HOST_STATE_ROOT = "/opt/intentic";
6
7
  export declare const LEGAL_VERSION = "2026-09-03";
7
8
  export declare const LEGAL_CONTACT_EMAIL = "contact@intentic.dev";
@@ -82,4 +83,6 @@ export declare const DAEMON_PORT = 8787;
82
83
  export declare const PREVIEW_PORT = 5173;
83
84
  export declare const LOCAL_PORT = 8788;
84
85
  export declare const TRANSLATOR_PORT = 8789;
86
+ export declare const GOOGLE_CLIENT_ID = "481795963975-cq9msl6higcd91joidrfp8mjlkuq5fk3.apps.googleusercontent.com";
87
+ export declare const GOOGLE_TOKEN_STORAGE_KEY = "intentic.gid.481795963975-cq9msl6higcd91joidrfp8mjlkuq5fk3.apps.googleusercontent.com";
85
88
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,cAAc,qBAAqB,CAAC;AAkBpC,eAAO,MAAM,cAAc,UAAU,CAAC;AAItC,eAAO,MAAM,YAAY,aAAa,CAAC;AAMvC,eAAO,MAAM,SAAS,cAAc,CAAC;AAIrC,eAAO,MAAM,eAAe,kBAAkB,CAAC;AAK/C,eAAO,MAAM,aAAa,eAAe,CAAC;AAC1C,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAW1D,eAAO,MAAM,iBAAiB,uCAAuC,CAAC;AACtE,eAAO,MAAM,oBAAoB,WAAW,CAAC;AAC7C,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAUtC,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAe5C,eAAO,MAAM,mBAAmB,6BAA6B,CAAC;AAK9D,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAe3D,eAAO,MAAM,mBAAmB,8BAA8B,CAAC;AAE/D,eAAO,MAAM,eAAe;;iBAClB,IAAI,EAAE,UAAU;iBAAE,IAAI,EAAE,YAAY;;;iBACnC,IAAI,EAAE,cAAc;iBAAE,IAAI,EAAE,aAAa;;;iBAGtC,IAAI,EAAE,eAAe;iBAAE,IAAI,EAAE,iBAAiB;;;iBAC7C,IAAI,EAAE,mBAAmB;iBAAE,IAAI,EAAE,kBAAkB;;;iBAC/C,IAAI,EAAE,eAAe;iBAAE,IAAI,EAAE,iBAAiB;;;iBAChD,IAAI,EAAE,OAAO;iBAAE,IAAI,EAAE,SAAS;;;iBAC7B,IAAI,EAAE,WAAW;iBAAE,IAAI,EAAE,UAAU;;;iBACrC,IAAI,EAAE,SAAS;iBAAE,IAAI,EAAE,WAAW;;;iBACjC,IAAI,EAAE,aAAa;iBAAE,IAAI,EAAE,YAAY;;;iBACzC,IAAI,EAAE,UAAU;iBAAE,IAAI,EAAE,aAAa;;;iBAClC,IAAI,EAAE,cAAc;iBAAE,IAAI,EAAE,cAAc;;;iBAC9C,IAAI,EAAE,SAAS;iBAAE,IAAI,EAAE,aAAa;;;iBACjC,IAAI,EAAE,aAAa;iBAAE,IAAI,EAAE,cAAc;;;iBAC3C,IAAI,EAAE,UAAU;iBAAE,IAAI,EAAE,YAAY;;;iBACjC,IAAI,EAAE,cAAc;iBAAE,IAAI,EAAE,aAAa;;CACQ,CAAC;AAEpE,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,eAAe,CAAC;AAGzD,eAAO,MAAM,gBAAgB,QAAS,aAAa,KAAG,MAA+D,CAAC;AAGtH,eAAO,MAAM,iBAAiB,QAAS,aAAa,KAAG,MAA+D,CAAC;AAavH,eAAO,MAAM,WAAW,OAAO,CAAC;AAChC,eAAO,MAAM,YAAY,OAAO,CAAC;AASjC,eAAO,MAAM,UAAU,OAAO,CAAC;AAO/B,eAAO,MAAM,eAAe,OAAO,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,cAAc,qBAAqB,CAAC;AAOpC,eAAO,MAAM,cAAc,UAAU,CAAC;AAGtC,eAAO,MAAM,YAAY,aAAa,CAAC;AAGvC,eAAO,MAAM,SAAS,cAAc,CAAC;AAKrC,eAAO,MAAM,WAAW,cAAc,CAAC;AAGvC,eAAO,MAAM,eAAe,kBAAkB,CAAC;AAG/C,eAAO,MAAM,aAAa,eAAe,CAAC;AAC1C,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAI1D,eAAO,MAAM,iBAAiB,uCAAuC,CAAC;AACtE,eAAO,MAAM,oBAAoB,WAAW,CAAC;AAC7C,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAGtC,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAG5C,eAAO,MAAM,mBAAmB,6BAA6B,CAAC;AAG9D,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAI3D,eAAO,MAAM,mBAAmB,8BAA8B,CAAC;AAE/D,eAAO,MAAM,eAAe;;iBAClB,IAAI,EAAE,UAAU;iBAAE,IAAI,EAAE,YAAY;;;iBACnC,IAAI,EAAE,cAAc;iBAAE,IAAI,EAAE,aAAa;;;iBAEtC,IAAI,EAAE,eAAe;iBAAE,IAAI,EAAE,iBAAiB;;;iBAC7C,IAAI,EAAE,mBAAmB;iBAAE,IAAI,EAAE,kBAAkB;;;iBAC/C,IAAI,EAAE,eAAe;iBAAE,IAAI,EAAE,iBAAiB;;;iBAChD,IAAI,EAAE,OAAO;iBAAE,IAAI,EAAE,SAAS;;;iBAC7B,IAAI,EAAE,WAAW;iBAAE,IAAI,EAAE,UAAU;;;iBACrC,IAAI,EAAE,SAAS;iBAAE,IAAI,EAAE,WAAW;;;iBACjC,IAAI,EAAE,aAAa;iBAAE,IAAI,EAAE,YAAY;;;iBACzC,IAAI,EAAE,UAAU;iBAAE,IAAI,EAAE,aAAa;;;iBAClC,IAAI,EAAE,cAAc;iBAAE,IAAI,EAAE,cAAc;;;iBAC9C,IAAI,EAAE,SAAS;iBAAE,IAAI,EAAE,aAAa;;;iBACjC,IAAI,EAAE,aAAa;iBAAE,IAAI,EAAE,cAAc;;;iBAC3C,IAAI,EAAE,UAAU;iBAAE,IAAI,EAAE,YAAY;;;iBACjC,IAAI,EAAE,cAAc;iBAAE,IAAI,EAAE,aAAa;;CACQ,CAAC;AAEpE,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,eAAe,CAAC;AAGzD,eAAO,MAAM,gBAAgB,QAAS,aAAa,KAAG,MAA+D,CAAC;AAGtH,eAAO,MAAM,iBAAiB,QAAS,aAAa,KAAG,MAA+D,CAAC;AAIvH,eAAO,MAAM,WAAW,OAAO,CAAC;AAChC,eAAO,MAAM,YAAY,OAAO,CAAC;AAGjC,eAAO,MAAM,UAAU,OAAO,CAAC;AAG/B,eAAO,MAAM,eAAe,OAAO,CAAC;AAGpC,eAAO,MAAM,gBAAgB,6EAA6E,CAAC;AAG3G,eAAO,MAAM,wBAAwB,0FAAqC,CAAC"}
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ export * from "./provider-logos.js";
2
2
  export const WORKSPACE_ROOT = "/work";
3
3
  export const HISTORY_ROOT = "/history";
4
4
  export const STATE_DIR = ".intentic";
5
+ export const MEMORY_FILE = "AGENTS.md";
5
6
  export const HOST_STATE_ROOT = "/opt/intentic";
6
7
  export const LEGAL_VERSION = "2026-09-03";
7
8
  export const LEGAL_CONTACT_EMAIL = "contact@intentic.dev";
@@ -36,4 +37,6 @@ export const DAEMON_PORT = 8787;
36
37
  export const PREVIEW_PORT = 5173;
37
38
  export const LOCAL_PORT = 8788;
38
39
  export const TRANSLATOR_PORT = 8789;
40
+ export const GOOGLE_CLIENT_ID = "481795963975-cq9msl6higcd91joidrfp8mjlkuq5fk3.apps.googleusercontent.com";
41
+ export const GOOGLE_TOKEN_STORAGE_KEY = `intentic.gid.${GOOGLE_CLIENT_ID}`;
39
42
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,cAAc,qBAAqB,CAAC;AAkBpC,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC;AAItC,MAAM,CAAC,MAAM,YAAY,GAAG,UAAU,CAAC;AAMvC,MAAM,CAAC,MAAM,SAAS,GAAG,WAAW,CAAC;AAIrC,MAAM,CAAC,MAAM,eAAe,GAAG,eAAe,CAAC;AAK/C,MAAM,CAAC,MAAM,aAAa,GAAG,YAAY,CAAC;AAC1C,MAAM,CAAC,MAAM,mBAAmB,GAAG,sBAAsB,CAAC;AAW1D,MAAM,CAAC,MAAM,iBAAiB,GAAG,oCAAoC,CAAC;AACtE,MAAM,CAAC,MAAM,oBAAoB,GAAG,QAAQ,CAAC;AAC7C,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,CAAC;AACvC,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAUtC,MAAM,CAAC,MAAM,yBAAyB,GAAG,EAAE,CAAC;AAe5C,MAAM,CAAC,MAAM,mBAAmB,GAAG,0BAA0B,CAAC;AAK9D,MAAM,CAAC,MAAM,oBAAoB,GAAG,sBAAsB,CAAC;AAe3D,MAAM,CAAC,MAAM,mBAAmB,GAAG,2BAA2B,CAAC;AAE/D,MAAM,CAAC,MAAM,eAAe,GAAG;IAC3B,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE;IAC5C,GAAG,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,aAAa,EAAE;IAGlD,MAAM,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,iBAAiB,EAAE;IAC1D,OAAO,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,kBAAkB,EAAE;IAChE,WAAW,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,iBAAiB,EAAE;IAC/D,SAAS,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE;IAC7C,UAAU,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,EAAE;IACnD,QAAQ,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE;IAChD,SAAS,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,YAAY,EAAE;IACtD,OAAO,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,EAAE;IAClD,UAAU,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,cAAc,EAAE;IAC1D,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,EAAE;IAChD,SAAS,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,cAAc,EAAE;IACxD,OAAO,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE;IACjD,UAAU,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,aAAa,EAAE;CACM,CAAC;AAKpE,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAAkB,EAAU,EAAE,CAAC,GAAG,oBAAoB,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AAGtH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,GAAkB,EAAU,EAAE,CAAC,GAAG,mBAAmB,IAAI,eAAe,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AAavH,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,CAAC;AAChC,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC;AASjC,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,CAAC;AAO/B,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,cAAc,qBAAqB,CAAC;AAOpC,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC;AAGtC,MAAM,CAAC,MAAM,YAAY,GAAG,UAAU,CAAC;AAGvC,MAAM,CAAC,MAAM,SAAS,GAAG,WAAW,CAAC;AAKrC,MAAM,CAAC,MAAM,WAAW,GAAG,WAAW,CAAC;AAGvC,MAAM,CAAC,MAAM,eAAe,GAAG,eAAe,CAAC;AAG/C,MAAM,CAAC,MAAM,aAAa,GAAG,YAAY,CAAC;AAC1C,MAAM,CAAC,MAAM,mBAAmB,GAAG,sBAAsB,CAAC;AAI1D,MAAM,CAAC,MAAM,iBAAiB,GAAG,oCAAoC,CAAC;AACtE,MAAM,CAAC,MAAM,oBAAoB,GAAG,QAAQ,CAAC;AAC7C,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,CAAC;AACvC,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAGtC,MAAM,CAAC,MAAM,yBAAyB,GAAG,EAAE,CAAC;AAG5C,MAAM,CAAC,MAAM,mBAAmB,GAAG,0BAA0B,CAAC;AAG9D,MAAM,CAAC,MAAM,oBAAoB,GAAG,sBAAsB,CAAC;AAI3D,MAAM,CAAC,MAAM,mBAAmB,GAAG,2BAA2B,CAAC;AAE/D,MAAM,CAAC,MAAM,eAAe,GAAG;IAC3B,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE;IAC5C,GAAG,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,aAAa,EAAE;IAElD,MAAM,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,iBAAiB,EAAE;IAC1D,OAAO,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,kBAAkB,EAAE;IAChE,WAAW,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,iBAAiB,EAAE;IAC/D,SAAS,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE;IAC7C,UAAU,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,EAAE;IACnD,QAAQ,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE;IAChD,SAAS,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,YAAY,EAAE;IACtD,OAAO,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,EAAE;IAClD,UAAU,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,cAAc,EAAE;IAC1D,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,EAAE;IAChD,SAAS,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,cAAc,EAAE;IACxD,OAAO,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE;IACjD,UAAU,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,aAAa,EAAE;CACM,CAAC;AAKpE,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAAkB,EAAU,EAAE,CAAC,GAAG,oBAAoB,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AAGtH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,GAAkB,EAAU,EAAE,CAAC,GAAG,mBAAmB,IAAI,eAAe,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AAIvH,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,CAAC;AAChC,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC;AAGjC,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,CAAC;AAG/B,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAC;AAGpC,MAAM,CAAC,MAAM,gBAAgB,GAAG,0EAA0E,CAAC;AAG3G,MAAM,CAAC,MAAM,wBAAwB,GAAG,gBAAgB,gBAAgB,EAAE,CAAC"}
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"provider-logos.d.ts","sourceRoot":"","sources":["../src/provider-logos.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB;aAC7B,MAAM,EAAE,qxDAAqxD;aAC7xD,KAAK,EAAE,s7CAAs7C;aAC77C,IAAI,EAAE,8iBAA8iB;aAEpjB,IAAI,EAAE,iDAAiD;aACvD,MAAM,EAAE,8RAA8R;aAEtS,MAAM,EAAE,0VAA0V;aAElW,IAAI,EAAE,ksCAAksC;aAExsC,GAAG,EAAE,yHAAyH;CACxH,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,oBAAoB,CAAC;AAK9D,eAAO,MAAM,gBAAgB,UAAW,aAAa,KAAG,SAAS,GAAG,SAAgE,CAAC"}
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"provider-logos.js","sourceRoot":"","sources":["../src/provider-logos.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAChC,MAAM,EAAE,qxDAAqxD;IAC7xD,KAAK,EAAE,s7CAAs7C;IAC77C,IAAI,EAAE,8iBAA8iB;IAEpjB,IAAI,EAAE,iDAAiD;IACvD,MAAM,EAAE,8RAA8R;IAEtS,MAAM,EAAE,0VAA0V;IAElW,IAAI,EAAE,ksCAAksC;IAExsC,GAAG,EAAE,yHAAyH;CACxH,CAAC;AAMX,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.248.0",
3
+ "version": "1.250.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",
@@ -21,7 +21,9 @@
21
21
  "src/control-bytes.mjs",
22
22
  "src/control-bytes.d.mts",
23
23
  "src/mirror-roots.mjs",
24
- "src/mirror-roots.d.mts"
24
+ "src/mirror-roots.d.mts",
25
+ "src/ci-infra-steps.mjs",
26
+ "src/ci-infra-steps.d.mts"
25
27
  ],
26
28
  "exports": {
27
29
  ".": {
@@ -51,6 +53,10 @@
51
53
  "types": "./src/control-bytes.d.mts",
52
54
  "default": "./src/control-bytes.mjs"
53
55
  },
56
+ "./ci-infra-steps": {
57
+ "types": "./src/ci-infra-steps.d.mts",
58
+ "default": "./src/ci-infra-steps.mjs"
59
+ },
54
60
  "./mirror-roots": {
55
61
  "types": "./src/mirror-roots.d.mts",
56
62
  "default": "./src/mirror-roots.mjs"
@@ -7,8 +7,13 @@ export interface AssertionMeasure {
7
7
  }
8
8
  export type Weakening = "downgrade" | "narrowing";
9
9
  export const NARROWING: number;
10
+ export const TEST_FILE: RegExp;
10
11
  export const EXACT: readonly string[];
11
12
  export const LOOSE: readonly string[];
13
+ export const PY_EXACT: readonly string[];
14
+ export const PY_LOOSE: readonly string[];
12
15
  export function measure(source: string): AssertionMeasure;
16
+ export function measurePython(source: string): AssertionMeasure;
17
+ export function measureFile(source: string, path: string): AssertionMeasure;
13
18
  export function weakened(before: AssertionMeasure | undefined, after: AssertionMeasure): Weakening | undefined;
14
19
  export function describeWeakening(path: string, shape: Weakening, before: AssertionMeasure, after: AssertionMeasure): string;
@@ -1,63 +1,14 @@
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/verify/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. */
1
+ // Three numbers per test file (exact matchers, loose matchers, asserted-text characters) that flag a weaker second
2
+ // version; shared by the push gate and the daemon's turn-ending check so both agree. Regex over source, not an AST,
3
+ // since the push gate runs before install.
53
4
 
54
5
  // Asserted text that shrinks past this fraction of what it was, with no test removed, is a narrowing.
55
6
  export const NARROWING = 0.75;
56
7
 
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. */
8
+ // Test file naming this ratchet knows: `foo.test.ts`/`foo.spec.tsx`, or pytest's `test_foo.py`/`foo_test.py`.
9
+ export const TEST_FILE = /(\.(test|spec)\.[cm]?[jt]sx?|(^|\/)(test_[^/]+|[^/]+_test)\.py)$/;
10
+
11
+ // toThrow/toHaveProperty count as neither (an argument makes them exact); expect.any always counts as loose.
61
12
  export const EXACT = [
62
13
  "toBe",
63
14
  "toEqual",
@@ -105,23 +56,9 @@ const ASYMMETRIC = /\bexpect\.(any|anything|stringContaining|stringMatching|obje
105
56
  const MATCHER = /\.(to[A-Z][A-Za-z]*)\s*\(/g;
106
57
  const TEST_CASE = /^\s*(?:test|it)(?:\.(?:each|skip|only|concurrent|todo|fails|skipIf|runIf))?\s*\(/gm;
107
58
 
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.
59
+ // Text a matcher's argument list pins, walked by hand (args can be multi-line, nested calls). Comments are stripped
60
+ // first so a comment's apostrophe can't read as an open quote; a template's static runs count, its `${…}` does not.
61
+ // Skips past a template's `${…}` by brace depth; the expression inside is computed, not asserted text.
125
62
  const pastInterpolation = (source, from) => {
126
63
  let braces = 1;
127
64
  let i = from + 2;
@@ -132,7 +69,7 @@ const pastInterpolation = (source, from) => {
132
69
  return i;
133
70
  };
134
71
 
135
- // A template literal, from its opening backtick: the characters of its static runs, and where it ends.
72
+ // A template literal from its opening backtick: the character count of its static runs, and where it ends.
136
73
  const templateChars = (source, from) => {
137
74
  let chars = 0;
138
75
  let run = from + 1;
@@ -190,7 +127,7 @@ const assertedChars = (source, from) => {
190
127
  chars += template.chars;
191
128
  i = template.end;
192
129
  } else if (ch === "/" && /[(,\s=]/.test(source[i - 1] ?? "(")) {
193
- // A regex literal in argument position: its source is asserted text like a string's.
130
+ // A regex literal in argument position; its source counts as asserted text like a string's.
194
131
  let j = i + 1;
195
132
  for (; j < source.length && source[j] !== "/" && source[j] !== "\n"; j += 1) {
196
133
  if (source[j] === "\\") {
@@ -223,13 +160,173 @@ export const measure = (source) => {
223
160
  return { exact: exactCount, loose: looseCount, chars, tests };
224
161
  };
225
162
 
226
- // Weaker, in either of the two shapes the header names. `before` absent (a new file) can only be stronger.
163
+ // Same three numbers over Python's `assert` statements and unittest methods, mapped onto the same vocabulary (`==`/`is`
164
+ // exact, `in`/comparisons/bare assert loose, `approx(...)` loosens). A compound assert counts toward both exact and
165
+ // loose. assertRaises counts as neither, like toThrow.
166
+ export const PY_EXACT = [
167
+ "assertEqual",
168
+ "assertNotEqual",
169
+ "assertIs",
170
+ "assertIsNot",
171
+ "assertIsNone",
172
+ "assertIsNotNone",
173
+ "assertListEqual",
174
+ "assertDictEqual",
175
+ "assertSetEqual",
176
+ "assertTupleEqual",
177
+ "assertSequenceEqual",
178
+ "assertMultiLineEqual",
179
+ "assertCountEqual",
180
+ "assertAlmostEqual",
181
+ ];
182
+ export const PY_LOOSE = [
183
+ "assertTrue",
184
+ "assertFalse",
185
+ "assertIn",
186
+ "assertNotIn",
187
+ "assertIsInstance",
188
+ "assertNotIsInstance",
189
+ "assertGreater",
190
+ "assertGreaterEqual",
191
+ "assertLess",
192
+ "assertLessEqual",
193
+ "assertRegex",
194
+ "assertNotRegex",
195
+ "assertWarns",
196
+ "assertLogs",
197
+ ];
198
+ const pyExact = new Set(PY_EXACT);
199
+ const pyLoose = new Set(PY_LOOSE);
200
+
201
+ const PY_EXACT_OP = /==|!=|\bis\b/;
202
+ const PY_LOOSE_OP = /\bin\b|<=|>=|<|>|\bisinstance\s*\(|\bapprox\s*\(/;
203
+ const PY_ASSERT = /^[ \t]*assert\b/gm;
204
+ const PY_METHOD = /\bassert[A-Z][A-Za-z]*\s*\(/g;
205
+ const PY_TEST_CASE = /^[ \t]*(?:async\s+)?def\s+test\w*\s*\(/gm;
206
+
207
+ // One pass blanks every comment and string's contents (same length as source, so offsets line up) and counts each
208
+ // string's characters by position. Must be one pass: a `#` inside a string is not a comment and a quote inside a
209
+ // comment doesn't open a string; triple-quoted strings are tracked since a docstring spans lines.
210
+ // Blanks one string literal in `code`, counts its characters in `text`, and returns where it ends. A single-quoted
211
+ // string stops at the next newline so one stray quote can't blank the rest of the file.
212
+ const blankString = (source, code, text, start) => {
213
+ const ch = source[start];
214
+ const quote = source.startsWith(ch.repeat(3), start) ? ch.repeat(3) : ch;
215
+ let i = start + quote.length;
216
+ while (i < source.length && !source.startsWith(quote, i)) {
217
+ if (quote.length === 1 && source[i] === "\n") {
218
+ return i;
219
+ }
220
+ text[i] = 1;
221
+ code[i] = " ";
222
+ i += source[i] === "\\" ? 2 : 1;
223
+ }
224
+ return Math.min(i + quote.length - 1, source.length - 1);
225
+ };
226
+
227
+ const blankComment = (source, code, start) => {
228
+ let i = start;
229
+ while (i < source.length && source[i] !== "\n") {
230
+ code[i] = " ";
231
+ i += 1;
232
+ }
233
+ return i;
234
+ };
235
+
236
+ const blankPython = (source) => {
237
+ const code = [...source];
238
+ const text = new Uint8Array(source.length);
239
+ for (let i = 0; i < source.length; i += 1) {
240
+ const ch = source[i];
241
+ if (ch === "#") {
242
+ i = blankComment(source, code, i);
243
+ } else if (ch === '"' || ch === "'") {
244
+ i = blankString(source, code, text, i);
245
+ }
246
+ }
247
+ return { code: code.join(""), text };
248
+ };
249
+
250
+ const OPENERS = new Set(["(", "[", "{"]);
251
+ const CLOSERS = new Set([")", "]", "}"]);
252
+
253
+ // End of the statement starting at `from`: first newline at bracket depth zero, not escaped by a trailing backslash.
254
+ // Reads the blanked code, so a bracket or backslash inside a string can't extend it.
255
+ const statementEnd = (code, from) => {
256
+ let depth = 0;
257
+ for (let i = from; i < code.length; i += 1) {
258
+ const ch = code[i];
259
+ depth += OPENERS.has(ch) ? 1 : 0;
260
+ depth -= CLOSERS.has(ch) ? 1 : 0;
261
+ const ends = ch === "\n" && depth <= 0 && code[i - 1] !== "\\";
262
+ if (ends) {
263
+ return i;
264
+ }
265
+ }
266
+ return code.length;
267
+ };
268
+
269
+ const charsIn = (text, from, to) => {
270
+ let chars = 0;
271
+ for (let i = from; i < to; i += 1) {
272
+ chars += text[i];
273
+ }
274
+ return chars;
275
+ };
276
+
277
+ // Classifies each `assert` statement by the operators it uses.
278
+ const assertStatements = (code, text) => {
279
+ const totals = { exact: 0, loose: 0, chars: 0 };
280
+ for (const match of code.matchAll(PY_ASSERT)) {
281
+ const end = statementEnd(code, match.index);
282
+ const statement = code.slice(match.index, end);
283
+ const isExact = PY_EXACT_OP.test(statement);
284
+ // No operator at all is a bare truthiness assert; it admits anything that is not falsy.
285
+ totals.exact += isExact ? 1 : 0;
286
+ totals.loose += PY_LOOSE_OP.test(statement) || !isExact ? 1 : 0;
287
+ totals.chars += charsIn(text, match.index, end);
288
+ }
289
+ return totals;
290
+ };
291
+
292
+ // unittest's assertion methods, classified by name like a matcher.
293
+ const assertMethods = (code, text) => {
294
+ const totals = { exact: 0, loose: 0, chars: 0 };
295
+ for (const match of code.matchAll(PY_METHOD)) {
296
+ const name = match[0].slice(0, match[0].search(/\s*\(/));
297
+ // A name in neither set (assertRaises, a project helper) counts as neither, and its text is not counted.
298
+ if (!pyExact.has(name) && !pyLoose.has(name)) {
299
+ continue;
300
+ }
301
+ totals.exact += pyExact.has(name) ? 1 : 0;
302
+ totals.loose += pyLoose.has(name) ? 1 : 0;
303
+ totals.chars += charsIn(text, match.index, statementEnd(code, match.index));
304
+ }
305
+ return totals;
306
+ };
307
+
308
+ export const measurePython = (source) => {
309
+ const { code, text } = blankPython(source);
310
+ const statements = assertStatements(code, text);
311
+ const methods = assertMethods(code, text);
312
+ return {
313
+ exact: statements.exact + methods.exact,
314
+ loose: statements.loose + methods.loose,
315
+ chars: statements.chars + methods.chars,
316
+ tests: [...code.matchAll(PY_TEST_CASE)].length,
317
+ };
318
+ };
319
+
320
+ // Decides which measure a file gets, by extension, so both readers agree. An unrecognized file falls to the TypeScript
321
+ // measure, which reads it as zero of everything, so it's silently unmeasured rather than wrongly flagged.
322
+ export const measureFile = (source, path) => (path.endsWith('.py') ? measurePython(source) : measure(source));
323
+
324
+ // Weaker, in either of the two shapes measured. A new file, with no before, can only be stronger.
227
325
  export const weakened = (before, after) => {
228
326
  if (before === undefined) {
229
327
  return undefined;
230
328
  }
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.
329
+ // More text pinned than before rules out a toEqual→toMatchObject move, whatever the matcher counts did.
233
330
  if (after.exact < before.exact && after.loose > before.loose && after.chars <= before.chars) {
234
331
  return "downgrade";
235
332
  }
@@ -239,6 +336,6 @@ export const weakened = (before, after) => {
239
336
  return undefined;
240
337
  };
241
338
 
242
- // One line per weakened file, the numbers a reader needs to judge the heuristic for themselves.
339
+ // One line per weakened file, with the numbers a reader needs to judge the heuristic themselves.
243
340
  export const describeWeakening = (path, shape, before, after) =>
244
341
  `${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 ci-infra-steps.mjs, the one list of runner-owned job steps the CI board and the audit script share.
2
+ export const INFRA_STEP: RegExp;
3
+ export function isInfraStep(name: string): boolean;
@@ -0,0 +1,6 @@
1
+ // The steps of a CI job the runner owns, not the repository: a failure in one says the fleet died before any step of
2
+ // the checkout ran, so it is nothing an agent on the code can fix. One list, read by the daemon's CI board and by
3
+ // _tools/scripts/ci/ci-audit.mjs.
4
+ export const INFRA_STEP = /^(Set up job|Set up runner|Initialize containers|Stop containers|Complete job|Post .*)$/;
5
+
6
+ export const isInfraStep = (name) => INFRA_STEP.test(name);
@@ -1,30 +1,8 @@
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). */
1
+ // What base offers that head no longer does, as dotted paths; shared by the push gate and the commit drafter so both
2
+ // draw the same verdict. Growth never appears in the result; array elements match unordered, so a moved element passes.
3
+ // `description` is skipped only as a schema keyword, not when it names an actual field.
24
4
 
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`.
5
+ // JSON Schema keywords whose value maps NAME to schema; inside one a key is a field, elsewhere a keyword.
28
6
  const NAME_MAPS = new Set(["properties", "patternProperties", "$defs", "definitions"]);
29
7
 
30
8
  const dotted = (at, key) => (at === "" ? key : `${at}.${key}`);
@@ -64,9 +42,7 @@ export const shrunkSurfaces = (base, head, at = "", out = [], named = true) => {
64
42
  return out;
65
43
  };
66
44
 
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.
45
+ // Same comparison over two lock-file texts; either side failing to parse yields no shrink rather than a throw.
70
46
  export const lockShrinkage = (baseText, headText) => {
71
47
  try {
72
48
  return shrunkSurfaces(JSON.parse(baseText), JSON.parse(headText));
@@ -1,16 +1,8 @@
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. */
1
+ // Bytes that make a text file binary, checked once here for the checkout gate and the daemon's per-edit reader. A NUL
2
+ // reads as binary everywhere (git, grep, diff viewers); its backslash-u-0000 escape is the same code point at runtime
3
+ // but stays text.
10
4
 
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.
5
+ // Extensions allowed arbitrary bytes, skipped by name not sniffed: binary-looking source is the bug.
14
6
  export const BINARY_EXTENSIONS = new Set([
15
7
  "png",
16
8
  "jpg",
@@ -43,13 +35,11 @@ export const BINARY_EXTENSIONS = new Set([
43
35
 
44
36
  export const isBinaryPath = (path) => BINARY_EXTENSIONS.has(path.split(".").pop()?.toLowerCase() ?? "");
45
37
 
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.
38
+ // C0 controls minus tab/newline/CR; DEL rides along, equally invisible and out of place in source.
48
39
  export const isForbiddenByte = (byte) => byte <= 0x08 || byte === 0x0b || byte === 0x0c || (byte >= 0x0e && byte <= 0x1f) || byte === 0x7f;
49
40
 
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.
41
+ // First forbidden byte as {offset, line, column, byte}, or undefined if none; slices only once a bad byte is found, so
42
+ // a clean file costs one linear scan.
53
43
  export const firstForbiddenByte = (bytes) => {
54
44
  for (let at = 0; at < bytes.length; at++) {
55
45
  if (isForbiddenByte(bytes[at])) {
@@ -1,102 +1,34 @@
1
- /* THE DIRECTORIES AN ISOLATED TURN MOUNTS OVER, and the one thing the main checkout must never do to them.
2
- *
3
- * A worktree holds TRACKED files only, so the two trees a package's dependents resolve THROUGH — its installed
4
- * tree (`node_modules`) and its build output (`dist`, `generated`) — cannot be checked out and have to come
5
- * from the main tree. The daemon supplies them as overlayfs mounts, one per directory, the MAIN checkout's copy
6
- * as the lowerdir and a per-conversation upper layer for whatever the turn writes
7
- * (_sandbox/sandbox/src/agents/isolation.ts, which imports this set rather than keeping its own).
8
- *
9
- * AN OVERLAY RESOLVES ITS LOWERDIR ONCE, AT MOUNT TIME, and holds that dentry for the life of the mount.
10
- * Rewriting the FILES inside it is harmless, and that is the whole reason mirroring a build directory works at
11
- * all: measured on this image (ext4 lower, kernel 6.18), unlinking every file in the lower root and writing new
12
- * ones is picked up by the merged view immediately, entry for entry. REPLACING THE DIRECTORY ITSELF is not.
13
- * `rm -rf dist` followed by a `mkdir` gives that path a new inode; the mount keeps pointing at the old one, and
14
- * the merged directory then reads as COMPLETELY EMPTY — not even the entries in the turn's own upper layer,
15
- * though `stat` on any of those upper files still succeeds, which is what makes the symptom so hard to read.
16
- * `mount -o remount` does not repair it. Only umount/mount does, and nothing inside the turn can do either: the
17
- * mount root is the one lower directory a turn cannot shadow with a write of its own.
18
- *
19
- * That is not a hazard someone imagined. It is what `_platform/prisma`'s build script did: `rm -rf ./generated
20
- * ./dist ./.cache` ahead of `prisma generate`. Run on the main tree by `turbo run build` — the push gate's third
21
- * tier, the image-tree prep, an owner typing `pnpm build` — it replaced the lowerdir of every live agent
22
- * worktree's `_platform/prisma/generated` overlay at once. Each of those turns was then holding a directory with
23
- * a freshly generated `client.ts` in it that `readdir` reported as empty, so the `"include": ["./generated/**"]`
24
- * glob in that package's tsconfig matched nothing and the declarations emit died with
25
- *
26
- * _platform/prisma/client.ts(1,15): error TS6307: File '.../generated/client.ts' is not listed within the
27
- * file list of project '.../_platform/prisma/tsconfig.json'
28
- *
29
- * on the turn-ending check of every conversation, whatever the turn had actually changed. A gate that is red for
30
- * a reason no turn caused is the failure mode docs/ci-failure-audit.md exists to hunt, and it teaches everyone
31
- * reading it that a red check is background noise.
32
- *
33
- * WHY THE RULE IS ABOUT THE MOUNT ROOT AND NOT ABOUT EVERY DIRECTORY UNDER IT. `prisma generate` does the same
34
- * remove-and-recreate to `generated/models` and `generated/internal` on every run, and no rule here could stop
35
- * it — that is a third-party generator's business. It does not have to be stopped: a turn's own generate rmdirs
36
- * those same subdirectories through the MERGED view first, which leaves an opaque upper directory the stale
37
- * lower can no longer reach. Only the mount root has no such repair, which is exactly where this rule sits.
38
- *
39
- * So: EMPTY A MIRRORED DIRECTORY, NEVER REPLACE IT. _tools/scripts/build/clean-outputs.mjs is what does that, and
40
- * _tools/checks/mirror-roots.mjs refuses the shape wherever a shell command in this repository spells it.
41
- *
42
- * Hand-written JavaScript rather than compiled TypeScript for the reason node.mjs gives: the checkout gate that
43
- * enforces this imports it by relative path from a clone that has never installed, and the daemon imports the
44
- * same file as `@intentic/constants/mirror-roots`. */
1
+ // Directories an isolated turn's overlay mounts over (node_modules, .venv, dist, generated): emptying them is safe, but
2
+ // replacing the directory (rm -rf then mkdir) orphans the mount at its old inode, and only umount/mount repairs it.
3
+ // _tools/checks/mirror-roots.mjs refuses the replacing shape; clean-outputs.mjs does the emptying instead.
45
4
 
46
- // The directory NAMES a turn overlays, discovered by name wherever they appear in the tree (isolation.ts walks
47
- // for them). Caches are deliberately absent, and that absence is load-bearing: see the MIRRORED_DIRS comment in
48
- // isolation.ts for why a mirrored `.cache` would hand a turn the main checkout's idea of what its dist was
49
- // built from. Nothing mounts a `.cache`, so it is free to be removed outright, and the build scripts fixed for
50
- // this still do exactly that to theirs.
51
- export const MIRRORED_DIRS = new Set(["node_modules", "dist", "generated"]);
5
+ // Overlaid directory names; `.cache` is deliberately excluded and `.venv` is python's node_modules.
6
+ export const MIRRORED_DIRS = new Set(["node_modules", ".venv", "dist", "generated"]);
52
7
 
53
- // A path's last segment, with quotes and trailing slashes taken off. `"$PKG/dist"` and `./generated/` both name
54
- // a mirror root; `node_modules/.pnpm/onnxruntime-web@*` does not, and neither does `dist/*`, which removes the
55
- // CONTENTS and leaves the inode alone.
8
+ // Last path segment, quotes and trailing slash stripped; `dist/*` (contents) is not a match, only the directory itself
9
+ // is.
56
10
  const lastSegment = (token) => {
57
11
  const bare = token.replace(/^['"]|['"]$/g, "").replace(/\/+$/, "");
58
12
  return bare.slice(bare.lastIndexOf("/") + 1);
59
13
  };
60
14
 
61
- /* One shell word at a time, quotes kept so `lastSegment` can strip them and a quoted `'{}'` still reads as the
62
- * find placeholder it is. Not a shell parser and not trying to be: what this has to recognize is a removal
63
- * someone WROTE, and every one of those in this repository is a plain sequence of words. */
15
+ // Splits into shell words, keeping quotes so lastSegment can strip them (and `'{}'` reads as find's placeholder); not a
16
+ // shell parser.
64
17
  const tokenize = (segment) => segment.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
65
18
 
66
- /* The verbs that can remove a directory, and the words that may stand in front of one. `docker rm -f <name>`
67
- * removes a container and `find … -exec rm -rf {} +` removes files, so the preceding word is what tells them
68
- * apart: a removal is a COMMAND here, or the thing an exec/sudo/xargs runs, never an argument to something
69
- * else. (It costs nothing to be wrong about `docker rm` anyway — it is never recursive — but a check that
70
- * reports a container by name would be read as noise, and a noisy gate gets switched off.) */
19
+ // Verbs that remove a directory, and words that may precede one; the word before tells command from argument.
71
20
  const REMOVERS = new Set(["rm", "rmdir", "rimraf"]);
72
21
  const RUNNERS = new Set(["exec", "-exec", "-execdir", "sudo", "xargs", "then", "do", "else", "{", "(", "npx", "pnpm", "bunx", "yarn"]);
73
- // `-rf`, `-fr`, `-Rf`, `-r`, `--recursive`. A non-recursive `rm` cannot take a directory at all, so it can
74
- // never be the operation this is about: `rm -f dist.zip` is fine and must stay unreported. (It is also what
75
- // keeps `pnpm rm <package>`, an uninstall, out of this: nothing there is recursive.)
22
+ // Recursive rm flags (-rf, -fr, -Rf, -r, --recursive); a non-recursive rm can't take a directory at all.
76
23
  const RECURSIVE = /^(?:--recursive$|-[a-zA-Z]*[rR])/;
77
- // Where a `find -exec` command ends. Everything after it belongs to the find again.
24
+ // Where a find -exec command ends; everything after belongs to find again.
78
25
  const EXEC_END = new Set([";", "\\;", "+"]);
79
26
  const PLACEHOLDER = /^['"]?\{\}['"]?$/;
80
- // The find predicates that NAME what will be removed, and the one that makes a find safe: `-mindepth 1` never
81
- // yields the directory it started from, which is precisely how you empty a tree without replacing its root.
27
+ // find predicates naming what will be removed.
82
28
  const NAME_PREDICATES = new Set(["-name", "-iname", "-path", "-wholename", "-ipath"]);
83
29
 
84
- /* WHICH MIRROR ROOTS A SHELL COMMAND WOULD REPLACE, as the operands were written, so a report can quote them.
85
- *
86
- * Two shapes, because those are the two ways this repository has ever spelled it:
87
- * · a literal removal — `rm -rf ./generated ./dist ./.cache`, `rm -rf "$PKG/dist"`
88
- * · a find that removes what it names — `find . \( -name 'node_modules' -o -name 'dist' \) -prune -exec rm
89
- * -rf '{}' +`, where the removal's own operand is a placeholder and the find's predicates say what it hits.
90
- *
91
- * Split on the separators that end one command, so `a && rm -rf dist` is two commands and a `find … -exec rm …`
92
- * stays one: only inside a single command do a find's predicates describe that removal's operands.
93
- *
94
- * A FIND IS READ WHOLE, AND THAT ROUNDS TOWARDS REFUSING. `find . -name node_modules -prune -o -name dist
95
- * -prune -exec rm -rf {} +` removes only the second name; the first is pruned past. Telling them apart means
96
- * implementing find's expression grammar — `-o`, `-a`, `-prune` and their precedence — for a distinction that
97
- * changes nothing about the answer, since the safe rewrite is the same either way and neither name may be
98
- * REMOVED by a command running in the checkout. So every `-name` in a removing find is reported, and the fix
99
- * for a false one is the fix for a true one. */
30
+ // Mirror roots a shell command would replace, split into commands on &&/;/|. Recognizes a literal removal and a find
31
+ // whose predicates name what -exec/-delete removes; over-reports a pruned name rather than parsing -o/-prune.
100
32
  export const replacedMirrorRoots = (command) => {
101
33
  const found = [];
102
34
  for (const segment of command.split(/\|\||&&|[;|\n]/)) {
@@ -107,7 +39,7 @@ export const replacedMirrorRoots = (command) => {
107
39
  continue;
108
40
  }
109
41
  const named = tokens.flatMap((token, at) => (NAME_PREDICATES.has(token) && tokens[at + 1] !== undefined ? [tokens[at + 1]] : []));
110
- // `find -delete` removes what the predicates name, with no `rm` anywhere in the line to notice.
42
+ // `find -delete` removes what the predicates name, with no `rm` on the line to notice.
111
43
  if (tokens.includes("-delete")) {
112
44
  found.push(...named.filter((token) => MIRRORED_DIRS.has(lastSegment(token))));
113
45
  }
@@ -132,7 +64,7 @@ export const replacedMirrorRoots = (command) => {
132
64
  continue;
133
65
  }
134
66
  for (const operand of operands) {
135
- // A `{}` is the find's placeholder: what it stands for is whatever the predicates named.
67
+ // A `{}` is find's placeholder: it stands for whatever the predicates named.
136
68
  const targets = PLACEHOLDER.test(operand) ? named : [operand];
137
69
  found.push(...targets.filter((target) => MIRRORED_DIRS.has(lastSegment(target))));
138
70
  }
package/src/node.mjs CHANGED
@@ -2,57 +2,24 @@ import { existsSync, statSync } from "node:fs";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
5
- /* FINDING A ROOT BY LOOKING FOR IT, INSTEAD OF COUNTING THE WAY UP TO IT.
6
- *
7
- * Two dozen files used to work out where the monorepo root was by counting how deep they sat: `../..` from a
8
- * package's own directory, `../../..` from its `src/`, `../../../..` from the installer scripts. Every one of
9
- * those numbers is correct only for the file's CURRENT depth, and nothing anywhere checks it. Move the file
10
- * one directory and it silently resolves somewhere else: no import fails, no type breaks; you find out when
11
- * something reads the wrong .env at runtime, or reads nothing and falls back to a default.
12
- *
13
- * Walking up until a marker appears has none of that coupling. A file can sit at any depth, move between
14
- * packages, or be symlinked in, and still get the same answer.
15
- *
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. `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
- * helper those three had to import from `dist/` would be a helper they could not import on a clean checkout,
20
- * which is exactly how the second copy of this walk gets written. Plain .mjs with a hand-written .d.mts beside
21
- * it is importable at every point in the build, so there only has to be one.
22
- *
23
- * A caller that runs before `pnpm install` has one more constraint: `@intentic/constants/node` is a BARE
24
- * specifier and bare specifiers resolve through node_modules, which a bare checkout has none of. Those callers
25
- * (`_tools/checks/*.mjs`, run by the pre-push hook and by CI's preflight job) import THIS FILE by relative path
26
- * instead. Still one walk; only the way in differs.
27
- *
28
- * NOT EXPORTED FROM THE PACKAGE INDEX, and that is deliberate: the index is imported by browser code
29
- * (Setup.vue reads PLATFORM_WEB_ORIGIN) and must never pull in node:fs. Path VALUES live there; path DISCOVERY
30
- * lives here, behind the `@intentic/constants/node` subpath. */
5
+ // Finds the monorepo root by walking up to a marker (pnpm-workspace.yaml), not by counting `../..`, which broke
6
+ // silently whenever a file moved. Kept out of the package index (browser-safe, no node:fs) and hand-written as plain
7
+ // .mjs so callers can import it before install or build.
31
8
 
32
- // The marker that identifies the monorepo root. pnpm-workspace.yaml rather than package.json or .git: a
33
- // package.json sits in all 78 packages, and .git is absent in an agent worktree's checkout and present in
34
- // unrelated parents. This file exists exactly once, at exactly the directory everyone means by "the root".
9
+ // Root marker; not package.json (in every package) or .git (absent in a worktree, present in other parents).
35
10
  const REPO_MARKER = "pnpm-workspace.yaml";
36
11
 
37
- /* Where the caller is, from whichever of the three things it has in hand. `import.meta.url` is a file:// URL
38
- * and needs converting; `import.meta.dirname` and a bare path are already paths. Accepting all three is what
39
- * stops a caller reaching for `fileURLToPath(new URL("...", import.meta.url))` out of habit and reintroducing
40
- * a relative segment on the way in. */
12
+ // Caller's directory from a file:// URL, import.meta.dirname, or a bare path; accepting all three avoids callers
13
+ // composing their own relative path.
41
14
  const startDir = (from) => {
42
15
  const path = from.startsWith("file:") ? fileURLToPath(from) : resolve(from);
43
- /* A URL or path naming a FILE has to become its directory; one naming a directory is already there. Asked
44
- * of the filesystem rather than guessed from the shape, because `.../src/lib` and `.../lib/version.js` are
45
- * indistinguishable as strings.
46
- *
47
- * A path that does not exist is treated as a directory, and costs nothing when it isn't: the walk simply
48
- * finds no marker at that level and moves to the parent, which is where a non-existent file's directory
49
- * was all along. */
16
+ // Resolved via the filesystem, not guessed from the string; a path that doesn't exist is treated as a directory at
17
+ // no cost.
50
18
  return statSync(path, { throwIfNoEntry: false })?.isDirectory() === false ? dirname(path) : path;
51
19
  };
52
20
 
53
- // Walk up from `dir` until `marker` is found beside us, or we run out of parents. Returns "" for not-found so
54
- // each caller decides whether that is fatal: the installers treat it as "not run from a checkout" and carry
55
- // on, while everything inside the repo treats it as impossible and throws.
21
+ // Walks up from `dir` until `marker` appears beside it, or parents run out (returns ""); callers decide whether that's
22
+ // fatal.
56
23
  const walkUp = (dir, marker) => {
57
24
  let current = dir;
58
25
  for (;;) {
@@ -67,12 +34,8 @@ const walkUp = (dir, marker) => {
67
34
  }
68
35
  };
69
36
 
70
- /* THE MONOREPO ROOT, from anywhere inside it. Pass `import.meta.url`: the caller's own location is the only
71
- * thing this needs, and it is the one thing every module already knows about itself.
72
- *
73
- * Throws when the marker is nowhere above the caller, which inside this repo means the checkout is broken. The
74
- * throw is the point: the counting version's failure mode was to return a WRONG directory confidently, and a
75
- * wrong directory is how a config loader silently reads no .env and every credential arrives empty. */
37
+ // Monorepo root from anywhere inside it (pass import.meta.url). Throws if the marker is nowhere above, rather than
38
+ // resolving to a confidently wrong directory.
76
39
  export const repoRoot = (from) => {
77
40
  const found = walkUp(startDir(from), REPO_MARKER);
78
41
  if (found === "") {
@@ -81,11 +44,8 @@ export const repoRoot = (from) => {
81
44
  return found;
82
45
  };
83
46
 
84
- /* THE CALLING PACKAGE'S OWN ROOT: the directory its package.json sits in. The other thing the dot-counting
85
- * was reaching for: `createRequire(import.meta.url)("../../package.json")` in three different version.ts files,
86
- * each with a different number of dots because each sat at a different depth, all of them meaning "mine".
87
- *
88
- * Stops at the FIRST package.json above the caller, so a package's own manifest always wins over the root's. */
47
+ // Caller's own package root: the directory nearest above it holding a package.json. Stops at the first match, so a
48
+ // package's own manifest always wins over the root's.
89
49
  export const packageRoot = (from) => {
90
50
  const found = walkUp(startDir(from), "package.json");
91
51
  if (found === "") {