carrick 0.3.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/LICENSE.md +99 -0
  2. package/README.md +79 -0
  3. package/bin/carrick.mjs +168 -0
  4. package/dist/channel.d.ts +16 -0
  5. package/dist/channel.js +37 -0
  6. package/dist/channel.js.map +1 -0
  7. package/dist/cli.d.ts +29 -0
  8. package/dist/cli.js +60 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/contract.d.ts +154 -0
  11. package/dist/contract.js +141 -0
  12. package/dist/contract.js.map +1 -0
  13. package/dist/diagnostics.d.ts +54 -0
  14. package/dist/diagnostics.js +199 -0
  15. package/dist/diagnostics.js.map +1 -0
  16. package/dist/hook/post-edit.d.ts +13 -0
  17. package/dist/hook/post-edit.js +76 -0
  18. package/dist/hook/post-edit.js.map +1 -0
  19. package/dist/hook/session-start.d.ts +2 -0
  20. package/dist/hook/session-start.js +44 -0
  21. package/dist/hook/session-start.js.map +1 -0
  22. package/dist/init/identity.d.ts +20 -0
  23. package/dist/init/identity.js +60 -0
  24. package/dist/init/identity.js.map +1 -0
  25. package/dist/init/repos.d.ts +19 -0
  26. package/dist/init/repos.js +46 -0
  27. package/dist/init/repos.js.map +1 -0
  28. package/dist/init/run.d.ts +11 -0
  29. package/dist/init/run.js +228 -0
  30. package/dist/init/run.js.map +1 -0
  31. package/dist/init/settings.d.ts +50 -0
  32. package/dist/init/settings.js +138 -0
  33. package/dist/init/settings.js.map +1 -0
  34. package/dist/log.d.ts +2 -0
  35. package/dist/log.js +35 -0
  36. package/dist/log.js.map +1 -0
  37. package/dist/native.d.ts +48 -0
  38. package/dist/native.js +127 -0
  39. package/dist/native.js.map +1 -0
  40. package/dist/render.d.ts +65 -0
  41. package/dist/render.js +334 -0
  42. package/dist/render.js.map +1 -0
  43. package/dist/root.d.ts +30 -0
  44. package/dist/root.js +82 -0
  45. package/dist/root.js.map +1 -0
  46. package/dist/server.d.ts +2 -0
  47. package/dist/server.js +255 -0
  48. package/dist/server.js.map +1 -0
  49. package/dist/templates.d.ts +18 -0
  50. package/dist/templates.js +66 -0
  51. package/dist/templates.js.map +1 -0
  52. package/package.json +72 -0
  53. package/plugin/.claude-plugin/plugin.json +6 -0
  54. package/plugin/.lsp.json +14 -0
  55. package/plugin/hooks/hooks.json +27 -0
  56. package/sidecar/dist/src/bundler.d.ts +141 -0
  57. package/sidecar/dist/src/bundler.js +680 -0
  58. package/sidecar/dist/src/capture/anchors.d.ts +61 -0
  59. package/sidecar/dist/src/capture/anchors.js +1132 -0
  60. package/sidecar/dist/src/capture/api.d.ts +378 -0
  61. package/sidecar/dist/src/capture/api.js +10 -0
  62. package/sidecar/dist/src/capture/augmentations.d.ts +20 -0
  63. package/sidecar/dist/src/capture/augmentations.js +60 -0
  64. package/sidecar/dist/src/capture/check-classify.d.ts +58 -0
  65. package/sidecar/dist/src/capture/check-classify.js +189 -0
  66. package/sidecar/dist/src/capture/check-deep.d.ts +32 -0
  67. package/sidecar/dist/src/capture/check-deep.js +91 -0
  68. package/sidecar/dist/src/capture/check-poison.d.ts +40 -0
  69. package/sidecar/dist/src/capture/check-poison.js +155 -0
  70. package/sidecar/dist/src/capture/check-probe.d.ts +70 -0
  71. package/sidecar/dist/src/capture/check-probe.js +147 -0
  72. package/sidecar/dist/src/capture/check-scrub.d.ts +31 -0
  73. package/sidecar/dist/src/capture/check-scrub.js +71 -0
  74. package/sidecar/dist/src/capture/check-workspace.d.ts +52 -0
  75. package/sidecar/dist/src/capture/check-workspace.js +218 -0
  76. package/sidecar/dist/src/capture/check.d.ts +39 -0
  77. package/sidecar/dist/src/capture/check.js +450 -0
  78. package/sidecar/dist/src/capture/deep-walk.d.ts +62 -0
  79. package/sidecar/dist/src/capture/deep-walk.js +243 -0
  80. package/sidecar/dist/src/capture/index.d.ts +36 -0
  81. package/sidecar/dist/src/capture/index.js +477 -0
  82. package/sidecar/dist/src/capture/lockfile.d.ts +48 -0
  83. package/sidecar/dist/src/capture/lockfile.js +490 -0
  84. package/sidecar/dist/src/capture/machinery.d.ts +59 -0
  85. package/sidecar/dist/src/capture/machinery.js +160 -0
  86. package/sidecar/dist/src/capture/node-builder.d.ts +37 -0
  87. package/sidecar/dist/src/capture/node-builder.js +123 -0
  88. package/sidecar/dist/src/capture/paths-rewrite.d.ts +34 -0
  89. package/sidecar/dist/src/capture/paths-rewrite.js +100 -0
  90. package/sidecar/dist/src/capture/self-check.d.ts +38 -0
  91. package/sidecar/dist/src/capture/self-check.js +317 -0
  92. package/sidecar/dist/src/capture/specifiers.d.ts +39 -0
  93. package/sidecar/dist/src/capture/specifiers.js +56 -0
  94. package/sidecar/dist/src/definition-resolver.d.ts +57 -0
  95. package/sidecar/dist/src/definition-resolver.js +153 -0
  96. package/sidecar/dist/src/index.d.ts +14 -0
  97. package/sidecar/dist/src/index.js +564 -0
  98. package/sidecar/dist/src/monorepo-builder.d.ts +129 -0
  99. package/sidecar/dist/src/monorepo-builder.js +584 -0
  100. package/sidecar/dist/src/project-loader.d.ts +130 -0
  101. package/sidecar/dist/src/project-loader.js +399 -0
  102. package/sidecar/dist/src/type-inferrer.d.ts +940 -0
  103. package/sidecar/dist/src/type-inferrer.js +3540 -0
  104. package/sidecar/dist/src/type-structural-expander.d.ts +61 -0
  105. package/sidecar/dist/src/type-structural-expander.js +283 -0
  106. package/sidecar/dist/src/type-text-canonicalizer.d.ts +40 -0
  107. package/sidecar/dist/src/type-text-canonicalizer.js +297 -0
  108. package/sidecar/dist/src/types.d.ts +669 -0
  109. package/sidecar/dist/src/types.js +5 -0
  110. package/sidecar/dist/src/validators.d.ts +2214 -0
  111. package/sidecar/dist/src/validators.js +336 -0
  112. package/sidecar/package.json +6 -0
  113. package/templates/carrick.json +7 -0
  114. package/templates/carrick.yml +45 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/init/settings.ts"],"names":[],"mappings":"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"}
package/dist/log.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export type Logger = (...parts: unknown[]) => void;
2
+ export declare function createLogger(prefix: string, env?: NodeJS.ProcessEnv): Logger;
package/dist/log.js ADDED
@@ -0,0 +1,35 @@
1
+ // Logging that can never reach a protocol stream.
2
+ //
3
+ // The language server owns stdout for JSON-RPC and the hooks own stdout for the
4
+ // context they print, so every line here goes to stderr, and to a file when
5
+ // CARRICK_LOG names one. Failing to log is not an error worth raising.
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ export function createLogger(prefix, env = process.env) {
9
+ const file = env["CARRICK_LOG"] ?? null;
10
+ const quiet = env["CARRICK_LOG_QUIET"] === "1";
11
+ return (...parts) => {
12
+ const text = parts
13
+ .map((part) => (typeof part === "string" ? part : JSON.stringify(part)))
14
+ .join(" ");
15
+ const line = `[${new Date().toISOString()}] ${prefix}: ${text}\n`;
16
+ if (file) {
17
+ try {
18
+ fs.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
19
+ fs.appendFileSync(file, line);
20
+ }
21
+ catch {
22
+ // A log that cannot be written is not a reason to fail an edit.
23
+ }
24
+ }
25
+ if (quiet)
26
+ return;
27
+ try {
28
+ process.stderr.write(line);
29
+ }
30
+ catch {
31
+ // Same.
32
+ }
33
+ };
34
+ }
35
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,EAAE;AACF,gFAAgF;AAChF,4EAA4E;AAC5E,uEAAuE;AAEvE,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAI7B,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,MAAyB,OAAO,CAAC,GAAG;IAC/E,MAAM,IAAI,GAAG,GAAG,CAAC,aAAa,CAAC,IAAI,IAAI,CAAC;IACxC,MAAM,KAAK,GAAG,GAAG,CAAC,mBAAmB,CAAC,KAAK,GAAG,CAAC;IAC/C,OAAO,CAAC,GAAG,KAAgB,EAAQ,EAAE;QACnC,MAAM,IAAI,GAAG,KAAK;aACf,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;aACvE,IAAI,CAAC,GAAG,CAAC,CAAC;QACb,MAAM,IAAI,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC;QAClE,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,CAAC;gBACH,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBACpE,EAAE,CAAC,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAChC,CAAC;YAAC,MAAM,CAAC;gBACP,gEAAgE;YAClE,CAAC;QACH,CAAC;QACD,IAAI,KAAK;YAAE,OAAO;QAClB,IAAI,CAAC;YACH,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC7B,CAAC;QAAC,MAAM,CAAC;YACP,QAAQ;QACV,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,48 @@
1
+ /** Where the binary for a platform is published. */
2
+ export declare function platformPackage(platform?: string, arch?: string): string;
3
+ /** Every platform this package publishes a binary for. */
4
+ export declare const PLATFORMS: Array<{
5
+ platform: string;
6
+ arch: string;
7
+ }>;
8
+ /** The file name inside a platform package. */
9
+ export declare function binaryName(platform?: string): string;
10
+ /** This package's own root, whatever the cwd is. */
11
+ export declare function packageRoot(): string;
12
+ export type NativeLookup = {
13
+ /** Absolute path to the scanner binary, or null when nothing resolved. */
14
+ binary: string | null;
15
+ /** Why there is none, ready to print. Null when there is one. */
16
+ problem: string | null;
17
+ /** How it was found, for the log and for the tests. */
18
+ source: "env" | "platform_package" | "none";
19
+ };
20
+ export type ResolveOptions = {
21
+ env?: NodeJS.ProcessEnv;
22
+ platform?: string;
23
+ arch?: string;
24
+ /** Injectable for tests: resolves a package's `package.json` to a path. */
25
+ resolveManifest?: (specifier: string) => string;
26
+ exists?: (target: string) => boolean;
27
+ };
28
+ /**
29
+ * The scanner binary this machine should run.
30
+ *
31
+ * CARRICK_NATIVE_BINARY wins, so a developer can point the shipped CLI at a
32
+ * `cargo build` and the smokes can pin one. Otherwise the platform package
33
+ * that npm installed carries it. A missing platform package is a message, not
34
+ * a crash: the caller decides whether that is fatal (running a scan) or merely
35
+ * silent (a hook that must never fail an edit).
36
+ */
37
+ export declare function resolveNativeBinary(options?: ResolveOptions): NativeLookup;
38
+ /**
39
+ * The sidecar directory to hand the binary, or null when this checkout has
40
+ * none built. Null is not an error here: the binary's own discovery covers a
41
+ * source checkout, and the read-only commands never start a sidecar at all.
42
+ */
43
+ export declare function resolveSidecarDir(options?: ResolveOptions): string | null;
44
+ /**
45
+ * The environment to run the binary with: this package's sidecar, and nothing
46
+ * else changed.
47
+ */
48
+ export declare function nativeEnv(options?: ResolveOptions): NodeJS.ProcessEnv;
package/dist/native.js ADDED
@@ -0,0 +1,127 @@
1
+ // Finding the scanner binary, and the sidecar it needs, inside an npm install.
2
+ //
3
+ // The Rust binary cannot ship in this package: npm would put five platforms'
4
+ // worth of it on every machine. It ships in `@carrick-tools/cli-<platform>`
5
+ // packages listed as `optionalDependencies`, so npm installs exactly the one
6
+ // this machine can run and skips the rest. Nothing here runs at install time —
7
+ // no postinstall, no download — which is why `npm install --ignore-scripts`
8
+ // still produces a working `carrick`.
9
+ //
10
+ // The sidecar goes the other way round: it is TypeScript, it is the same on
11
+ // every platform, and it needs `ts-morph` and `zod` resolvable from where it
12
+ // sits. So it ships in THIS package, beside `node_modules/`, and the binary is
13
+ // told where it is with CARRICK_SIDECAR_DIR (src/main.rs reads it first).
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import { createRequire } from "node:module";
17
+ import { fileURLToPath } from "node:url";
18
+ /** Where the binary for a platform is published. */
19
+ export function platformPackage(platform = process.platform, arch = process.arch) {
20
+ return `@carrick-tools/cli-${platform}-${arch}`;
21
+ }
22
+ /** Every platform this package publishes a binary for. */
23
+ export const PLATFORMS = [
24
+ { platform: "darwin", arch: "arm64" },
25
+ { platform: "darwin", arch: "x64" },
26
+ { platform: "linux", arch: "x64" },
27
+ { platform: "linux", arch: "arm64" },
28
+ { platform: "win32", arch: "x64" },
29
+ ];
30
+ /** The file name inside a platform package. */
31
+ export function binaryName(platform = process.platform) {
32
+ return platform === "win32" ? "carrick.exe" : "carrick";
33
+ }
34
+ /** This package's own root, whatever the cwd is. */
35
+ export function packageRoot() {
36
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
37
+ }
38
+ function defaultResolveManifest(specifier) {
39
+ // Resolved from this file, so the lookup follows the install that owns this
40
+ // copy of the package rather than the caller's cwd.
41
+ return createRequire(import.meta.url).resolve(specifier);
42
+ }
43
+ function defaultExists(target) {
44
+ try {
45
+ return fs.existsSync(target);
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ /**
52
+ * The scanner binary this machine should run.
53
+ *
54
+ * CARRICK_NATIVE_BINARY wins, so a developer can point the shipped CLI at a
55
+ * `cargo build` and the smokes can pin one. Otherwise the platform package
56
+ * that npm installed carries it. A missing platform package is a message, not
57
+ * a crash: the caller decides whether that is fatal (running a scan) or merely
58
+ * silent (a hook that must never fail an edit).
59
+ */
60
+ export function resolveNativeBinary(options = {}) {
61
+ const env = options.env ?? process.env;
62
+ const exists = options.exists ?? defaultExists;
63
+ const override = env["CARRICK_NATIVE_BINARY"];
64
+ if (override) {
65
+ if (exists(override)) {
66
+ return { binary: override, problem: null, source: "env" };
67
+ }
68
+ return {
69
+ binary: null,
70
+ source: "env",
71
+ problem: `CARRICK_NATIVE_BINARY is set to ${override}, which is not a file on this machine.`,
72
+ };
73
+ }
74
+ const platform = options.platform ?? process.platform;
75
+ const arch = options.arch ?? process.arch;
76
+ const specifier = platformPackage(platform, arch);
77
+ const resolveManifest = options.resolveManifest ?? defaultResolveManifest;
78
+ let manifest;
79
+ try {
80
+ manifest = resolveManifest(`${specifier}/package.json`);
81
+ }
82
+ catch {
83
+ const supported = PLATFORMS.some((entry) => entry.platform === platform && entry.arch === arch);
84
+ return {
85
+ binary: null,
86
+ source: "none",
87
+ problem: supported
88
+ ? `The carrick binary for ${platform}-${arch} is not installed. It ships as ${specifier}, an optional dependency of this package. Reinstall with 'npm install carrick', or set CARRICK_NATIVE_BINARY to a binary you built.`
89
+ : `Carrick publishes no binary for ${platform}-${arch}. Supported: ${PLATFORMS.map((entry) => `${entry.platform}-${entry.arch}`).join(", ")}. Set CARRICK_NATIVE_BINARY to a binary you built to use it anyway.`,
90
+ };
91
+ }
92
+ const binary = path.join(path.dirname(manifest), "bin", binaryName(platform));
93
+ if (!exists(binary)) {
94
+ return {
95
+ binary: null,
96
+ source: "none",
97
+ problem: `${specifier} is installed but holds no binary at ${binary}. Reinstall it, or set CARRICK_NATIVE_BINARY.`,
98
+ };
99
+ }
100
+ return { binary, problem: null, source: "platform_package" };
101
+ }
102
+ /**
103
+ * The sidecar directory to hand the binary, or null when this checkout has
104
+ * none built. Null is not an error here: the binary's own discovery covers a
105
+ * source checkout, and the read-only commands never start a sidecar at all.
106
+ */
107
+ export function resolveSidecarDir(options = {}) {
108
+ const env = options.env ?? process.env;
109
+ const exists = options.exists ?? defaultExists;
110
+ const configured = env["CARRICK_SIDECAR_DIR"];
111
+ if (configured)
112
+ return configured;
113
+ const bundled = path.join(packageRoot(), "sidecar");
114
+ return exists(path.join(bundled, "dist", "src", "index.js")) ? bundled : null;
115
+ }
116
+ /**
117
+ * The environment to run the binary with: this package's sidecar, and nothing
118
+ * else changed.
119
+ */
120
+ export function nativeEnv(options = {}) {
121
+ const env = { ...(options.env ?? process.env) };
122
+ const sidecar = resolveSidecarDir(options);
123
+ if (sidecar)
124
+ env["CARRICK_SIDECAR_DIR"] = sidecar;
125
+ return env;
126
+ }
127
+ //# sourceMappingURL=native.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native.js","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,EAAE;AACF,6EAA6E;AAC7E,4EAA4E;AAC5E,6EAA6E;AAC7E,+EAA+E;AAC/E,4EAA4E;AAC5E,sCAAsC;AACtC,EAAE;AACF,4EAA4E;AAC5E,6EAA6E;AAC7E,+EAA+E;AAC/E,0EAA0E;AAE1E,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,oDAAoD;AACpD,MAAM,UAAU,eAAe,CAC7B,WAAmB,OAAO,CAAC,QAAQ,EACnC,OAAe,OAAO,CAAC,IAAI;IAE3B,OAAO,sBAAsB,QAAQ,IAAI,IAAI,EAAE,CAAC;AAClD,CAAC;AAED,0DAA0D;AAC1D,MAAM,CAAC,MAAM,SAAS,GAA8C;IAClE,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE;IACrC,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE;IACnC,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE;IAClC,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE;IACpC,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE;CACnC,CAAC;AAEF,+CAA+C;AAC/C,MAAM,UAAU,UAAU,CAAC,WAAmB,OAAO,CAAC,QAAQ;IAC5D,OAAO,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC;AAC1D,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,WAAW;IACzB,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;AAC1E,CAAC;AAoBD,SAAS,sBAAsB,CAAC,SAAiB;IAC/C,4EAA4E;IAC5E,oDAAoD;IACpD,OAAO,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAC3D,CAAC;AAED,SAAS,aAAa,CAAC,MAAc;IACnC,IAAI,CAAC;QACH,OAAO,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CAAC,UAA0B,EAAE;IAC9D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACvC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,aAAa,CAAC;IAC/C,MAAM,QAAQ,GAAG,GAAG,CAAC,uBAAuB,CAAC,CAAC;IAC9C,IAAI,QAAQ,EAAE,CAAC;QACb,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YACrB,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QAC5D,CAAC;QACD,OAAO;YACL,MAAM,EAAE,IAAI;YACZ,MAAM,EAAE,KAAK;YACb,OAAO,EAAE,mCAAmC,QAAQ,wCAAwC;SAC7F,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC;IACtD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAC1C,MAAM,SAAS,GAAG,eAAe,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAClD,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,IAAI,sBAAsB,CAAC;IAC1E,IAAI,QAAgB,CAAC;IACrB,IAAI,CAAC;QACH,QAAQ,GAAG,eAAe,CAAC,GAAG,SAAS,eAAe,CAAC,CAAC;IAC1D,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,SAAS,GAAG,SAAS,CAAC,IAAI,CAC9B,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,CAC9D,CAAC;QACF,OAAO;YACL,MAAM,EAAE,IAAI;YACZ,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,SAAS;gBAChB,CAAC,CAAC,0BAA0B,QAAQ,IAAI,IAAI,kCAAkC,SAAS,qIAAqI;gBAC5N,CAAC,CAAC,mCAAmC,QAAQ,IAAI,IAAI,gBAAgB,SAAS,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,qEAAqE;SACnN,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC9E,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QACpB,OAAO;YACL,MAAM,EAAE,IAAI;YACZ,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,GAAG,SAAS,wCAAwC,MAAM,+CAA+C;SACnH,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;AAC/D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAA0B,EAAE;IAC5D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACvC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,aAAa,CAAC;IAC/C,MAAM,UAAU,GAAG,GAAG,CAAC,qBAAqB,CAAC,CAAC;IAC9C,IAAI,UAAU;QAAE,OAAO,UAAU,CAAC;IAClC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,SAAS,CAAC,CAAC;IACpD,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AAChF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,UAA0B,EAAE;IACpD,MAAM,GAAG,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;IAChD,MAAM,OAAO,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAC3C,IAAI,OAAO;QAAE,GAAG,CAAC,qBAAqB,CAAC,GAAG,OAAO,CAAC;IAClD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,65 @@
1
+ import { type Boundary, type CheckItem, type CheckResult, type StatusResult, type StatusService } from "./contract.ts";
2
+ /** How many item lines one hook message carries before it says how many are left. */
3
+ export declare const MAX_ITEM_LINES = 12;
4
+ /** How many counterpart sites one item line names before the same. */
5
+ export declare const MAX_COUNTERPARTS = 4;
6
+ export declare function shortHash(commit: string | undefined): string;
7
+ /**
8
+ * The boundary as the CLI prints it: one line per thing the scan could not
9
+ * classify, and nothing for the ones it classified all of.
10
+ */
11
+ export declare function boundaryLines(boundary: Boundary | undefined, service: string | undefined): string[];
12
+ /**
13
+ * The boundary for one payload: the CLI's own lines when it sent them,
14
+ * otherwise this port of `ServiceBoundary::lines`.
15
+ *
16
+ * `boundary_lines` is printed exactly as it arrives, so the hook, the
17
+ * diagnostic and `carrick check` in a terminal say the same sentence about the
18
+ * same count. Nothing here reformats or re-orders it.
19
+ */
20
+ export declare function boundaryFor(result: CheckResult): string[];
21
+ /** True when the lines came from the CLI rather than from the port above. */
22
+ export declare function boundaryIsPreRendered(result: CheckResult): boolean;
23
+ /**
24
+ * The verdict state in words. It is the type layer's word and only that: it
25
+ * says nothing about freshness, which `stale` and `changed_since_index` carry.
26
+ */
27
+ export declare function stateWord(state: string | undefined): string;
28
+ /** One line for one route or call: where it is, what it is, what is known about it. */
29
+ export declare function itemLine(item: CheckItem, file: string | undefined): string;
30
+ /** Problems first, then everything that names another service, both by line. */
31
+ export declare function reportableItems(result: CheckResult): CheckItem[];
32
+ /**
33
+ * The PostToolUse context, or `null` when the index has nothing to say at all.
34
+ *
35
+ * A file with no indexed route or call still gets the boundary, because that is
36
+ * the difference between "nothing crosses a service here" and "nothing here was
37
+ * classified". Only an error payload and a payload with neither items nor a
38
+ * boundary are silent.
39
+ */
40
+ export declare function renderPostToolUse(result: CheckResult, displayFile?: string): string | null;
41
+ /**
42
+ * The boundary for one service in a status answer, same preference as a check.
43
+ */
44
+ export declare function serviceBoundary(service: StatusService): string[];
45
+ /** How many stale files one service line names before it says how many are left. */
46
+ export declare const MAX_STALE_FILES = 5;
47
+ /**
48
+ * One line per service: what it holds, at which commit, and how far its repo
49
+ * has moved since.
50
+ *
51
+ * Services of one repo share a commit and a changed-file count, so the count is
52
+ * stated on the first service of each repo and the others point at it. Saying
53
+ * it once is the difference between one repo with three services and three
54
+ * repos that all happen to have moved by the same number of files.
55
+ */
56
+ export declare function serviceLine(service: StatusService, sharesRepoWith: string | null): string;
57
+ /**
58
+ * The SessionStart line, from `carrick status --json`.
59
+ *
60
+ * `check` and `touch` each take exactly one file, so this is the only read that
61
+ * answers for a workspace. It carries no verdict: `status` states what is
62
+ * indexed and what each service could not classify, and nothing about whether a
63
+ * contract holds.
64
+ */
65
+ export declare function renderSessionStart(status: StatusResult): string;
package/dist/render.js ADDED
@@ -0,0 +1,334 @@
1
+ // Turning a check payload into lines a model reads.
2
+ //
3
+ // Shape, from the design (§4.4) and carried by both hooks: locations first,
4
+ // boundary last, one line per item. The boundary wording is a port of
5
+ // `ServiceBoundary::lines` in src/boundary.rs, so the CLI, the PR comment and
6
+ // this hook say the same sentence about the same count.
7
+ import { connectedItems, problemItems, } from "./contract.js";
8
+ /** How many item lines one hook message carries before it says how many are left. */
9
+ export const MAX_ITEM_LINES = 12;
10
+ /** How many counterpart sites one item line names before the same. */
11
+ export const MAX_COUNTERPARTS = 4;
12
+ export function shortHash(commit) {
13
+ if (!commit)
14
+ return "an unknown commit";
15
+ return commit.slice(0, 7);
16
+ }
17
+ function firstReason(count) {
18
+ const first = count.reasons?.[0];
19
+ if (!first)
20
+ return "";
21
+ return count.total > 1 ? ` (e.g. ${first})` : ` (${first})`;
22
+ }
23
+ /**
24
+ * The boundary as the CLI prints it: one line per thing the scan could not
25
+ * classify, and nothing for the ones it classified all of.
26
+ */
27
+ export function boundaryLines(boundary, service) {
28
+ if (!boundary)
29
+ return [];
30
+ const who = service ?? "this workspace";
31
+ // Local mode dispatches nothing to the analyzer, so the count is 0 on every
32
+ // laptop index and the clause would be noise on every edit. The commit is
33
+ // the part of the header that always says something.
34
+ const attempted = boundary.files_attempted ?? 0;
35
+ const out = [
36
+ attempted > 0
37
+ ? `${who} at ${shortHash(boundary.commit_hash)}: ${attempted} file(s) sent to the analyzer`
38
+ : `${who} at ${shortHash(boundary.commit_hash)}`,
39
+ ];
40
+ const push = (count, what) => {
41
+ if (!count || count.total === 0)
42
+ return;
43
+ out.push(` ${count.total} ${what}${firstReason(count)}`);
44
+ };
45
+ push(boundary.files_lost, "file(s) the analyzer never answered for");
46
+ push(boundary.consumers_not_resolved, "call site(s) that named a client member and did not resolve to it");
47
+ push(boundary.sdk_unresolved, "SDK call(s) that produced no edge");
48
+ push(boundary.unknown_call_paths, "indexed call(s) whose path no producer may claim");
49
+ push(boundary.routes_without_response_type, "route(s) with no resolved response type");
50
+ push(boundary.calls_without_expected_type, "call(s) with no resolved expected type");
51
+ if ((boundary.unemitted_literal_candidates ?? 0) > 0) {
52
+ out.push(` ${boundary.unemitted_literal_candidates} bare route-literal call site(s) left unclassified`);
53
+ }
54
+ if ((boundary.model_only_rows ?? 0) > 0) {
55
+ out.push(` ${boundary.model_only_rows} row(s) the model alone states (${boundary.model_rows_joined ?? 0} joined a deterministic row)`);
56
+ }
57
+ if ((boundary.model_endpoints_discarded_in_claimed_modules ?? 0) > 0) {
58
+ out.push(` ${boundary.model_endpoints_discarded_in_claimed_modules} model endpoint(s) dropped in modules a routing convention claims`);
59
+ }
60
+ if (boundary.types_degraded) {
61
+ out.push(` types degraded at ${boundary.types_degraded.stage ?? "an unnamed stage"}: ${boundary.types_degraded.detail ?? "no detail"}`);
62
+ }
63
+ if (boundary.bare_checkout) {
64
+ out.push(" types captured on a bare checkout: anything through a dependency is `any`");
65
+ }
66
+ return out;
67
+ }
68
+ /**
69
+ * The boundary for one payload: the CLI's own lines when it sent them,
70
+ * otherwise this port of `ServiceBoundary::lines`.
71
+ *
72
+ * `boundary_lines` is printed exactly as it arrives, so the hook, the
73
+ * diagnostic and `carrick check` in a terminal say the same sentence about the
74
+ * same count. Nothing here reformats or re-orders it.
75
+ */
76
+ export function boundaryFor(result) {
77
+ if (result.boundary_lines?.length)
78
+ return result.boundary_lines;
79
+ const counts = boundaryLines(result.boundary, result.service);
80
+ // `boundary_note` is the sentence about what a local index cannot hold at
81
+ // all, and it leads the CLI's own lines, so it leads these too.
82
+ if (result.boundary_note)
83
+ return [result.boundary_note, ...counts];
84
+ return counts;
85
+ }
86
+ /** True when the lines came from the CLI rather than from the port above. */
87
+ export function boundaryIsPreRendered(result) {
88
+ return Boolean(result.boundary_lines?.length);
89
+ }
90
+ function counterpartText(counterparts) {
91
+ const shown = counterparts.slice(0, MAX_COUNTERPARTS);
92
+ const rendered = shown
93
+ .map((counterpart) => {
94
+ const where = counterpart.file
95
+ ? `${counterpart.file}${counterpart.line ? `:${counterpart.line}` : ""}`
96
+ : "an unnamed file";
97
+ return counterpart.service ? `${counterpart.service} ${where}` : where;
98
+ })
99
+ .join(", ");
100
+ const rest = counterparts.length - shown.length;
101
+ return rest > 0 ? `${rendered}, and ${rest} more` : rendered;
102
+ }
103
+ /**
104
+ * What to call the other side. A `peer` is a shared external contract, so it
105
+ * gets no producer/consumer word: the locations are the whole answer.
106
+ */
107
+ function roleLabel(counterparts) {
108
+ const roles = new Set(counterparts.map((counterpart) => counterpart.role));
109
+ if (roles.size === 1 && roles.has("consumer"))
110
+ return "Consumers";
111
+ if (roles.size === 1 && roles.has("producer"))
112
+ return "Producers";
113
+ if (roles.size === 1 && roles.has("peer"))
114
+ return "Same contract in";
115
+ return "Counterparts";
116
+ }
117
+ /** `[fact]` or `[candidate, model]`: R1's label, plus what produced the row. */
118
+ function sourceLabel(item) {
119
+ const parts = [];
120
+ if (item.source)
121
+ parts.push(item.source);
122
+ if (item.resolution_source)
123
+ parts.push(item.resolution_source);
124
+ return parts.length ? ` [${parts.join(", ")}]` : "";
125
+ }
126
+ /**
127
+ * The verdict state in words. It is the type layer's word and only that: it
128
+ * says nothing about freshness, which `stale` and `changed_since_index` carry.
129
+ */
130
+ export function stateWord(state) {
131
+ if (state === "not_checked")
132
+ return "no type verdict";
133
+ if (state === "unresolved")
134
+ return "no usable type on one side, so nothing is claimed";
135
+ if (state === "resolved")
136
+ return "compiler-compared";
137
+ return state ?? "";
138
+ }
139
+ /**
140
+ * What is known about the contract at this row.
141
+ *
142
+ * `result` is null wherever the state is the whole statement, so there is no
143
+ * result word to print and the state carries the line. Where both are present
144
+ * the state qualifies the result, because `method_mismatch` with no type
145
+ * verdict behind it is a different claim from a compiler-compared mismatch.
146
+ */
147
+ function verdictText(item) {
148
+ if (!item.verdict)
149
+ return "";
150
+ const { state, result, detail } = item.verdict;
151
+ const head = result == null
152
+ ? stateWord(state)
153
+ : state === "resolved"
154
+ ? result
155
+ : `${result} (${stateWord(state)})`;
156
+ if (!head)
157
+ return detail ? ` ${detail}` : "";
158
+ return detail ? ` ${head}: ${detail}` : ` ${head}`;
159
+ }
160
+ /** One line for one route or call: where it is, what it is, what is known about it. */
161
+ export function itemLine(item, file) {
162
+ const where = `${file ?? "this file"}:${item.line ?? 1}:${item.col ?? 1}`;
163
+ const operation = [item.method, item.path].filter(Boolean).join(" ");
164
+ const counterparts = item.counterparts ?? [];
165
+ const segments = [
166
+ `- ${where} ${item.kind}${operation ? ` ${operation}` : ""}${sourceLabel(item)}${verdictText(item)}`,
167
+ ];
168
+ if (item.evidence)
169
+ segments.push(`Read off ${item.evidence}`);
170
+ if (counterparts.length) {
171
+ segments.push(`${roleLabel(counterparts)}: ${counterpartText(counterparts)}`);
172
+ }
173
+ return segments.map((segment) => segment.replace(/\.+$/, "")).join(". ");
174
+ }
175
+ /** Problems first, then everything that names another service, both by line. */
176
+ export function reportableItems(result) {
177
+ const byLine = (a, b) => (a.line ?? 0) - (b.line ?? 0);
178
+ const problems = problemItems(result).sort(byLine);
179
+ const connected = connectedItems(result)
180
+ .filter((item) => !problems.includes(item))
181
+ .sort(byLine);
182
+ return [...problems, ...connected];
183
+ }
184
+ function deletedLine(result) {
185
+ if (!result.deleted)
186
+ return null;
187
+ const consumers = (result.items ?? []).reduce((total, item) => total + (item.counterparts ?? []).length, 0);
188
+ return `This file is gone from disk and the index still holds ${(result.items ?? []).length} row(s) for it, with ${consumers} counterpart(s) still on the other side.`;
189
+ }
190
+ /**
191
+ * Append the boundary to a rendered message.
192
+ *
193
+ * The port's first line is labelled, because on its own it reads as a bare
194
+ * count. The CLI's own lines are appended untouched: a label glued to the front
195
+ * of them would no longer be the bytes the CLI printed.
196
+ */
197
+ function pushBoundary(lines, result, boundary) {
198
+ if (!boundary.length)
199
+ return;
200
+ if (boundaryIsPreRendered(result)) {
201
+ for (const line of boundary)
202
+ lines.push(line);
203
+ return;
204
+ }
205
+ lines.push(`Boundary: ${boundary[0]}`);
206
+ for (const line of boundary.slice(1))
207
+ lines.push(line);
208
+ }
209
+ function staleLine(result) {
210
+ if (!result.stale)
211
+ return null;
212
+ const changed = result.changed_since_index;
213
+ const suffix = typeof changed === "number"
214
+ ? ` ${changed} file(s) in the workspace have changed since ${shortHash(result.index_commit)}.`
215
+ : "";
216
+ return `This file has changed since the index, so these verdicts describe the indexed version.${suffix}`;
217
+ }
218
+ /**
219
+ * The PostToolUse context, or `null` when the index has nothing to say at all.
220
+ *
221
+ * A file with no indexed route or call still gets the boundary, because that is
222
+ * the difference between "nothing crosses a service here" and "nothing here was
223
+ * classified". Only an error payload and a payload with neither items nor a
224
+ * boundary are silent.
225
+ */
226
+ export function renderPostToolUse(result, displayFile) {
227
+ if (result.error)
228
+ return null;
229
+ const items = reportableItems(result);
230
+ const boundary = boundaryFor(result);
231
+ // The boundary is never dropped: the local index holds no bare-receiver route
232
+ // and no `fetch` call, because both need a model, so an empty answer without
233
+ // the boundary beside it reads as "there is nothing here" when it means
234
+ // "nothing here was classified".
235
+ if (items.length === 0 && boundary.length === 0)
236
+ return null;
237
+ // `result.file` is relative to the repo that owns it, so the caller's own
238
+ // workspace-relative path is the one a reader can open; it wins when given.
239
+ const where = displayFile ?? result.file;
240
+ const service = result.service ? `${result.service}, ` : "";
241
+ const lines = [
242
+ `Carrick checked ${where ?? "this file"} against the workspace index (${service}indexed at ${shortHash(result.index_commit)}).`,
243
+ ];
244
+ for (const item of items.slice(0, MAX_ITEM_LINES))
245
+ lines.push(itemLine(item, where));
246
+ const hidden = items.length - Math.min(items.length, MAX_ITEM_LINES);
247
+ if (hidden > 0) {
248
+ lines.push(`- and ${hidden} more route(s) or call(s) in this file, from \`carrick check\`.`);
249
+ }
250
+ const deleted = deletedLine(result);
251
+ if (deleted)
252
+ lines.push(deleted);
253
+ const stale = staleLine(result);
254
+ if (stale)
255
+ lines.push(stale);
256
+ pushBoundary(lines, result, boundary);
257
+ return lines.join("\n");
258
+ }
259
+ /**
260
+ * The boundary for one service in a status answer, same preference as a check.
261
+ */
262
+ export function serviceBoundary(service) {
263
+ if (service.boundary_lines?.length)
264
+ return service.boundary_lines;
265
+ const counts = boundaryLines(service.boundary, service.service);
266
+ if (service.boundary_note)
267
+ return [service.boundary_note, ...counts];
268
+ return counts;
269
+ }
270
+ /** How many stale files one service line names before it says how many are left. */
271
+ export const MAX_STALE_FILES = 5;
272
+ function staleText(service) {
273
+ const total = service.stale_files_total ?? service.changed_since_index;
274
+ const shown = (service.stale_files ?? []).slice(0, MAX_STALE_FILES);
275
+ if (!shown.length)
276
+ return "";
277
+ const rest = total - shown.length;
278
+ const listed = rest > 0 ? `${shown.join(", ")}, +${rest} more` : shown.join(", ");
279
+ return ` (${listed})`;
280
+ }
281
+ /**
282
+ * One line per service: what it holds, at which commit, and how far its repo
283
+ * has moved since.
284
+ *
285
+ * Services of one repo share a commit and a changed-file count, so the count is
286
+ * stated on the first service of each repo and the others point at it. Saying
287
+ * it once is the difference between one repo with three services and three
288
+ * repos that all happen to have moved by the same number of files.
289
+ */
290
+ export function serviceLine(service, sharesRepoWith) {
291
+ const head = `- ${service.service} at ${shortHash(service.index_commit)}: ${service.routes} route(s), ${service.calls} call(s)`;
292
+ if (sharesRepoWith) {
293
+ return `${head}. Same repo as ${sharesRepoWith}, so the same ${service.changed_since_index} changed file(s)`;
294
+ }
295
+ return `${head}, changed since index: ${service.changed_since_index}${staleText(service)}`;
296
+ }
297
+ /**
298
+ * The SessionStart line, from `carrick status --json`.
299
+ *
300
+ * `check` and `touch` each take exactly one file, so this is the only read that
301
+ * answers for a workspace. It carries no verdict: `status` states what is
302
+ * indexed and what each service could not classify, and nothing about whether a
303
+ * contract holds.
304
+ */
305
+ export function renderSessionStart(status) {
306
+ if (status.error === "not_indexed") {
307
+ return "Carrick has no index for this workspace, so nothing in this session is checked against the other services. `carrick index --workspace <dir>` builds one.";
308
+ }
309
+ if (status.error) {
310
+ return `Carrick could not read its index for this workspace (${status.error}).`;
311
+ }
312
+ if (!status.services.length) {
313
+ return `Carrick has an index at ${status.workspace ?? "this workspace"} and it holds no services.`;
314
+ }
315
+ const version = status.scanner_version ? `, scanner ${status.scanner_version}` : "";
316
+ const where = status.workspace ? ` in ${status.workspace}` : "";
317
+ const when = status.indexed_at ? ` at ${status.indexed_at}` : "";
318
+ const lines = [
319
+ `Carrick indexed ${status.services.length} service(s)${where}${when}${version}.`,
320
+ ];
321
+ const firstOfRepo = new Map();
322
+ for (const service of status.services) {
323
+ const shared = firstOfRepo.get(service.repo) ?? null;
324
+ if (!shared)
325
+ firstOfRepo.set(service.repo, service.service);
326
+ lines.push(serviceLine(service, shared));
327
+ }
328
+ for (const service of status.services) {
329
+ for (const line of serviceBoundary(service))
330
+ lines.push(line);
331
+ }
332
+ return lines.join("\n");
333
+ }
334
+ //# sourceMappingURL=render.js.map