@illuminis/comprism 0.1.4 → 0.1.6

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 (116) hide show
  1. package/out/agent/command.js +5 -5
  2. package/out/agent/render.d.ts +39 -6
  3. package/out/agent/render.js +287 -28
  4. package/out/agent/session.d.ts +21 -1
  5. package/out/agent/session.js +81 -17
  6. package/out/commands/agents.js +3 -3
  7. package/out/commands/ask.js +20 -11
  8. package/out/commands/codemap.d.ts +1 -1
  9. package/out/commands/codemap.js +3 -3
  10. package/out/commands/commands-thin.js +4 -4
  11. package/out/commands/config.js +1 -1
  12. package/out/commands/cost.js +4 -4
  13. package/out/commands/hooks.js +1 -1
  14. package/out/commands/install.d.ts +1 -1
  15. package/out/commands/install.js +3 -2
  16. package/out/commands/instructions.js +1 -1
  17. package/out/commands/integrations.js +4 -4
  18. package/out/commands/keys.js +6 -6
  19. package/out/commands/login.js +97 -27
  20. package/out/commands/permissions.js +2 -2
  21. package/out/commands/plugins.js +3 -3
  22. package/out/commands/privacy.js +2 -2
  23. package/out/commands/repl.d.ts +4 -0
  24. package/out/commands/repl.js +563 -101
  25. package/out/commands/review.js +6 -6
  26. package/out/commands/settings.js +30 -123
  27. package/out/commands/skills.js +2 -2
  28. package/out/commands/unattended.js +6 -6
  29. package/out/commands/update.js +1 -1
  30. package/out/commands/welcome.d.ts +25 -1
  31. package/out/commands/welcome.js +65 -38
  32. package/out/commands/worktrees.d.ts +1 -1
  33. package/out/graph/sync.d.ts +1 -1
  34. package/out/graph/sync.js +6 -6
  35. package/out/lib/attach.js +4 -3
  36. package/out/lib/commandlist.d.ts +1 -1
  37. package/out/lib/commandlist.js +2 -2
  38. package/out/lib/config.d.ts +3 -2
  39. package/out/lib/config.js +7 -8
  40. package/out/lib/connection.js +1 -1
  41. package/out/lib/gateway.d.ts +9 -5
  42. package/out/lib/keyboard.d.ts +3 -0
  43. package/out/lib/keyboard.js +7 -0
  44. package/out/lib/lastlogin.d.ts +14 -0
  45. package/out/lib/lastlogin.js +105 -0
  46. package/out/lib/machine.js +1 -1
  47. package/out/lib/picker.d.ts +1 -0
  48. package/out/lib/picker.js +158 -0
  49. package/out/lib/project-ops.d.ts +1 -1
  50. package/out/lib/project-ops.js +4 -4
  51. package/out/lib/prompt.d.ts +3 -0
  52. package/out/lib/prompt.js +21 -0
  53. package/out/lib/queue.js +1 -1
  54. package/out/lib/readiness.d.ts +3 -30
  55. package/out/lib/readiness.js +13 -82
  56. package/out/lib/servicecommand.d.ts +14 -0
  57. package/out/lib/servicecommand.js +74 -0
  58. package/out/lib/sessions.js +3 -3
  59. package/out/lib/startcache.d.ts +13 -0
  60. package/out/lib/startcache.js +78 -0
  61. package/out/lib/ui.d.ts +11 -0
  62. package/out/lib/ui.js +28 -0
  63. package/out/lib/voice.d.ts +36 -0
  64. package/out/lib/voice.js +137 -2
  65. package/out/lib/words.d.ts +20 -0
  66. package/out/lib/words.js +27 -0
  67. package/out/machine/codemap/build.d.ts +45 -0
  68. package/out/machine/codemap/build.js +91 -0
  69. package/out/machine/codemap/facts.d.ts +47 -0
  70. package/out/machine/codemap/facts.js +12 -0
  71. package/out/machine/codemap/files.d.ts +45 -0
  72. package/out/machine/codemap/files.js +207 -0
  73. package/out/machine/codemap/read-locales.d.ts +29 -0
  74. package/out/machine/codemap/read-locales.js +246 -0
  75. package/out/machine/codemap/read-python.d.ts +11 -0
  76. package/out/machine/codemap/read-python.js +116 -0
  77. package/out/machine/codemap/read-typescript.d.ts +16 -0
  78. package/out/machine/codemap/read-typescript.js +292 -0
  79. package/out/machine/executor/browser.d.ts +14 -0
  80. package/out/machine/executor/browser.js +270 -0
  81. package/out/machine/executor/diagnostics.d.ts +2 -0
  82. package/out/machine/executor/diagnostics.js +181 -0
  83. package/out/machine/executor/documents.d.ts +40 -0
  84. package/out/machine/executor/documents.js +170 -0
  85. package/out/machine/executor/files.d.ts +2 -0
  86. package/out/machine/executor/files.js +590 -0
  87. package/out/machine/executor/git.d.ts +48 -0
  88. package/out/machine/executor/git.js +145 -0
  89. package/out/machine/executor/hooks.d.ts +51 -0
  90. package/out/machine/executor/hooks.js +154 -0
  91. package/out/machine/executor/index.d.ts +45 -0
  92. package/out/machine/executor/index.js +367 -0
  93. package/out/machine/executor/notebook.d.ts +2 -0
  94. package/out/machine/executor/notebook.js +147 -0
  95. package/out/machine/executor/paths.d.ts +20 -0
  96. package/out/machine/executor/paths.js +154 -0
  97. package/out/machine/executor/sandbox.d.ts +40 -0
  98. package/out/machine/executor/sandbox.js +299 -0
  99. package/out/machine/executor/shell.d.ts +86 -0
  100. package/out/machine/executor/shell.js +582 -0
  101. package/out/machine/executor/toolservers.d.ts +20 -0
  102. package/out/machine/executor/toolservers.js +189 -0
  103. package/out/machine/executor/worktree.d.ts +9 -0
  104. package/out/machine/executor/worktree.js +119 -0
  105. package/out/machine/folder/project.d.ts +28 -0
  106. package/out/machine/folder/project.js +114 -0
  107. package/out/machine/runtime/home.d.ts +2 -0
  108. package/out/machine/runtime/home.js +48 -0
  109. package/out/machine/runtime/needs.d.ts +30 -0
  110. package/out/machine/runtime/needs.js +89 -0
  111. package/out/machine/runtime/self.d.ts +23 -0
  112. package/out/machine/runtime/self.js +124 -0
  113. package/out/native/dictate +0 -0
  114. package/out/providers/index.js +2 -1
  115. package/out/thin.js +13 -12
  116. package/package.json +5 -4
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Turn a folder of source files into code map facts, on the machine that holds
3
+ * them.
4
+ *
5
+ * Reading is the only part of the map that happens here. The joining, the
6
+ * storing and the answering all happen in the service, because a client that
7
+ * joined the map would be a client holding an opinion, and because a map that
8
+ * lived on one laptop could not be read by the same person's editor an hour
9
+ * later.
10
+ */
11
+ import type { FactSet } from "./facts";
12
+ import { IndexedFile } from "./files";
13
+ export interface ReadResult {
14
+ /** One entry per file successfully read. */
15
+ facts: Record<string, FactSet>;
16
+ failures: {
17
+ file: string;
18
+ reason: string;
19
+ }[];
20
+ /** Files present in the project that no reader on this machine can read. */
21
+ unread: {
22
+ python: number;
23
+ typescript: number;
24
+ };
25
+ seconds: number;
26
+ }
27
+ /** What this machine can read at all, said plainly and once. */
28
+ export declare function readers(): {
29
+ python: boolean;
30
+ typescript: boolean;
31
+ };
32
+ /** Read the named files. Everything when `rels` is absent. */
33
+ export declare function readFacts(root: string, rels?: string[], knownNames?: string[]): ReadResult;
34
+ export interface ProjectState {
35
+ files: IndexedFile[];
36
+ stamps: Record<string, string>;
37
+ fingerprint: {
38
+ hash: string;
39
+ files: number;
40
+ };
41
+ commit: string;
42
+ }
43
+ /** What the project looks like right now: which files, and one short string
44
+ * that changes the moment any of them does. Costs about ten milliseconds. */
45
+ export declare function projectState(root: string): ProjectState;
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readers = readers;
4
+ exports.readFacts = readFacts;
5
+ exports.projectState = projectState;
6
+ const files_1 = require("./files");
7
+ const read_locales_1 = require("./read-locales");
8
+ const read_python_1 = require("./read-python");
9
+ const read_typescript_1 = require("./read-typescript");
10
+ /** What this machine can read at all, said plainly and once. */
11
+ function readers() {
12
+ return {
13
+ python: Boolean(require("./read-python").findPython()),
14
+ typescript: Boolean((0, read_typescript_1.loadCompiler)()),
15
+ };
16
+ }
17
+ /**
18
+ * Drop the calls that could never resolve to anything in this project.
19
+ *
20
+ * Two thirds of the raw facts read from a real 1,816 file project were calls to
21
+ * names the project does not define: `len`, `push`, `map`, and every library
22
+ * function anybody imported. The service discards every one of them when it
23
+ * joins the map, so sending and storing them is pure waste, measured at 17.8
24
+ * megabytes of 26.6.
25
+ *
26
+ * `known` is what the project already defines, which the service sends back
27
+ * with the file stamps, plus whatever the files just read define themselves.
28
+ * Nothing is dropped that the service would have kept: the join applies the same
29
+ * rule, only later. Correctness never depends on this, only size.
30
+ */
31
+ function prune(facts, known) {
32
+ const droppable = new Set(["calls", "references"]);
33
+ for (const rel of Object.keys(facts)) {
34
+ const entry = facts[rel];
35
+ if (!entry)
36
+ continue;
37
+ entry.links = entry.links.filter((link) => {
38
+ if (!droppable.has(link.kind))
39
+ return true;
40
+ const leaf = String(link.to_name ?? "").split(".").pop() ?? "";
41
+ return known.has(leaf);
42
+ });
43
+ }
44
+ }
45
+ /** Read the named files. Everything when `rels` is absent. */
46
+ function readFacts(root, rels, knownNames) {
47
+ const started = Date.now();
48
+ const all = (0, files_1.indexedFiles)(root);
49
+ const wanted = rels ? new Set(rels) : null;
50
+ const chosen = wanted ? all.filter((f) => wanted.has(f.rel)) : all;
51
+ const py = chosen.filter((f) => f.lang === "python").map((f) => f.rel);
52
+ const ts = chosen.filter((f) => f.lang === "typescript").map((f) => f.rel);
53
+ const locales = chosen.filter((f) => f.lang === "translations").map((f) => f.rel);
54
+ const fromPython = (0, read_python_1.readPython)(root, py);
55
+ const fromTs = (0, read_typescript_1.readTypeScript)(root, ts);
56
+ // Translation files need no compiler and no interpreter, so they are always
57
+ // read: a machine missing Python still gets its labels mapped.
58
+ const fromLocales = (0, read_locales_1.readLocales)(root, locales);
59
+ const facts = { ...fromPython.facts, ...fromTs.facts, ...fromLocales.facts };
60
+ // Everything the files just read define, plus everything the service already
61
+ // knows this project defines. A name on neither list cannot resolve.
62
+ const known = new Set(knownNames ?? []);
63
+ for (const entry of Object.values(facts)) {
64
+ for (const object of entry.objects) {
65
+ if (object.kind === "file" || object.kind === "route" || object.kind === "table")
66
+ continue;
67
+ const leaf = String(object.name).split(".").pop();
68
+ if (leaf)
69
+ known.add(leaf);
70
+ }
71
+ }
72
+ prune(facts, known);
73
+ return {
74
+ facts,
75
+ failures: [...fromPython.failures, ...fromTs.failures, ...fromLocales.failures],
76
+ unread: {
77
+ python: fromPython.available ? 0 : py.length,
78
+ typescript: fromTs.available ? 0 : ts.length,
79
+ },
80
+ seconds: Math.round((Date.now() - started) / 100) / 10,
81
+ };
82
+ }
83
+ /** What the project looks like right now: which files, and one short string
84
+ * that changes the moment any of them does. Costs about ten milliseconds. */
85
+ function projectState(root) {
86
+ const files = (0, files_1.indexedFiles)(root);
87
+ const stamps = {};
88
+ for (const file of files)
89
+ stamps[file.rel] = (0, files_1.stampOf)(file);
90
+ return { files, stamps, fingerprint: (0, files_1.fingerprint)(files), commit: (0, files_1.headCommit)(root) };
91
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The one shape both readers emit, and the only thing that ever leaves the
3
+ * machine.
4
+ *
5
+ * An object and a link, each carrying a file and a line. Nothing else. No file
6
+ * contents, no source text, no string a person wrote. That is what makes
7
+ * sending the map to the service a different act from sending the code: the
8
+ * service learns that a function called `price_of` exists at line 40 of a named
9
+ * file and what calls it, and it never learns what that function says.
10
+ */
11
+ export interface GraphObject {
12
+ /** Unique within a project. A file is its own path; a symbol is `file::name`. */
13
+ id: string;
14
+ /** file, function, method, class, component, model, table, route. */
15
+ kind: string;
16
+ name: string;
17
+ file: string;
18
+ line: number;
19
+ [extra: string]: unknown;
20
+ }
21
+ export interface GraphLink {
22
+ from: string;
23
+ /** imports, defines, calls, renders, inherits, references, maps_to,
24
+ * handled_by, declares, calls_endpoint, browser_route. */
25
+ kind: string;
26
+ /** Set when the reader could resolve the far end itself. */
27
+ to?: string;
28
+ /** Set when it could not, and the service joins it up. */
29
+ to_name?: string;
30
+ [extra: string]: unknown;
31
+ }
32
+ export interface FactSet {
33
+ objects: GraphObject[];
34
+ links: GraphLink[];
35
+ /** A file that declares routers, and what each is mounted under. Python only:
36
+ * an endpoint's path is written relative to a router that is mounted with a
37
+ * prefix in a different file, and without both halves the front end and the
38
+ * back end never join up. */
39
+ routers?: {
40
+ prefixes: Record<string, string>;
41
+ mounts: {
42
+ target: string;
43
+ prefix: string;
44
+ line: number;
45
+ }[];
46
+ };
47
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ /**
3
+ * The one shape both readers emit, and the only thing that ever leaves the
4
+ * machine.
5
+ *
6
+ * An object and a link, each carrying a file and a line. Nothing else. No file
7
+ * contents, no source text, no string a person wrote. That is what makes
8
+ * sending the map to the service a different act from sending the code: the
9
+ * service learns that a function called `price_of` exists at line 40 of a named
10
+ * file and what calls it, and it never learns what that function says.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,45 @@
1
+ /** How many files one project may have indexed. A ceiling, not a guess: a
2
+ * monorepo of forty thousand files would take minutes to read and produce a
3
+ * map too large to be worth sending, and half a map that claims to be whole is
4
+ * the failure this whole feature exists to avoid. Past this the map says
5
+ * plainly that it covers part of the project. */
6
+ export declare const MAX_FILES = 6000;
7
+ /** How large a single file may be before it is skipped. A generated bundle or
8
+ * a checked-in minified file is not code anybody navigates. */
9
+ export declare const MAX_FILE_BYTES = 1500000;
10
+ export declare const PY_EXT: Set<string>;
11
+ export declare const TS_EXT: Set<string>;
12
+ export interface IndexedFile {
13
+ /** Path relative to the project root, forward slashes, always. */
14
+ rel: string;
15
+ size: number;
16
+ mtimeMs: number;
17
+ lang: "python" | "typescript" | "translations";
18
+ }
19
+ /** Every file the map covers, in a stable order.
20
+ *
21
+ * Sorted, and that is not cosmetic: the fingerprint below is a hash over this
22
+ * list, so an unstable order would report the code had moved every time the
23
+ * filesystem felt like returning entries differently. */
24
+ export declare function indexedFiles(root: string): IndexedFile[];
25
+ /** One short string per file that changes whenever that file does.
26
+ *
27
+ * Size and modified time rather than a hash of the contents: reading six
28
+ * thousand files to find out whether any of them changed costs more than
29
+ * rebuilding the map. A file edited back to its original contents within the
30
+ * same millisecond would be missed, which is a case that does not occur in
31
+ * practice and is corrected by the next ordinary edit. */
32
+ export declare function stampOf(file: IndexedFile): string;
33
+ /** One short string for the whole project. Saying the code has not moved is a
34
+ * claim, and this is the evidence for it. */
35
+ export declare function fingerprint(files: IndexedFile[]): {
36
+ hash: string;
37
+ files: number;
38
+ };
39
+ /** The commit the project is standing on, when it is a repository at all.
40
+ *
41
+ * Read with a plain file read rather than by running git, because this is
42
+ * called at the start of every job and spawning a process there is a cost paid
43
+ * on work that has nothing to do with the map. A project with no git still
44
+ * gets a usable map; it simply cannot stamp an answer with a commit. */
45
+ export declare function headCommit(root: string): string;
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.TS_EXT = exports.PY_EXT = exports.MAX_FILE_BYTES = exports.MAX_FILES = void 0;
37
+ exports.indexedFiles = indexedFiles;
38
+ exports.stampOf = stampOf;
39
+ exports.fingerprint = fingerprint;
40
+ exports.headCommit = headCommit;
41
+ /**
42
+ * What the code map covers, and how to tell in milliseconds whether the code
43
+ * has moved since it was last read.
44
+ *
45
+ * Everything here runs on the person's own machine, because it is the one place
46
+ * their source files exist. Nothing in this folder decides, prices or
47
+ * classifies anything: it reads files and produces facts. That is the division
48
+ * the whole product rests on, and a function here that formed an opinion would
49
+ * be the first crack in it.
50
+ */
51
+ const crypto = __importStar(require("crypto"));
52
+ const fs = __importStar(require("fs"));
53
+ const path = __importStar(require("path"));
54
+ const read_locales_1 = require("./read-locales");
55
+ /** Folders never worth reading: somebody else's code, or build output. */
56
+ const SKIP_DIRS = new Set([
57
+ "node_modules", ".git", "__pycache__", ".venv", "venv", "env",
58
+ "site-packages", "dist", "build", ".pytest_cache", ".mypy_cache",
59
+ "htmlcov", ".ruff_cache", "coverage", ".vite", ".next", ".turbo",
60
+ "playwright-report", "test-results", "vendor", "target", ".cache",
61
+ "scratchpad", ".comprism", ".completionprism",
62
+ ]);
63
+ /** Folder names that are build output only when they sit beside a package
64
+ * manifest. `out` is where TypeScript writes its JavaScript by convention, but
65
+ * it is also a perfectly ordinary name for a source folder, so skipping it
66
+ * everywhere would quietly remove real code from somebody's map. Next to a
67
+ * package.json it is almost always generated.
68
+ *
69
+ * Found by mapping this repository and getting 83 changed files from one edit:
70
+ * the terminal client's own compiled output was in the map, so every rebuild
71
+ * looked like eighty three source files moving. */
72
+ const BUILT_BESIDE_MANIFEST = new Set(["out", "output", "lib", "es", "esm"]);
73
+ /** How many files one project may have indexed. A ceiling, not a guess: a
74
+ * monorepo of forty thousand files would take minutes to read and produce a
75
+ * map too large to be worth sending, and half a map that claims to be whole is
76
+ * the failure this whole feature exists to avoid. Past this the map says
77
+ * plainly that it covers part of the project. */
78
+ exports.MAX_FILES = 6000;
79
+ /** How large a single file may be before it is skipped. A generated bundle or
80
+ * a checked-in minified file is not code anybody navigates. */
81
+ exports.MAX_FILE_BYTES = 1_500_000;
82
+ exports.PY_EXT = new Set([".py"]);
83
+ exports.TS_EXT = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
84
+ function isCode(rel, name) {
85
+ if (name.endsWith(".d.ts"))
86
+ return null;
87
+ if (name.endsWith(".min.js"))
88
+ return null;
89
+ const ext = path.extname(name);
90
+ if (exports.PY_EXT.has(ext))
91
+ return "python";
92
+ if (exports.TS_EXT.has(ext))
93
+ return "typescript";
94
+ // Translation files. Not code, and in the map for a reason no code parser can
95
+ // serve: the label a person sees is a key here and a sentence in ten language
96
+ // files, and "which languages are missing this one" is the question asked
97
+ // about it. See `read-locales.ts`.
98
+ if ((0, read_locales_1.looksLikeTranslations)(rel))
99
+ return "translations";
100
+ return null;
101
+ }
102
+ /** Every file the map covers, in a stable order.
103
+ *
104
+ * Sorted, and that is not cosmetic: the fingerprint below is a hash over this
105
+ * list, so an unstable order would report the code had moved every time the
106
+ * filesystem felt like returning entries differently. */
107
+ function indexedFiles(root) {
108
+ const out = [];
109
+ const walk = (dir) => {
110
+ if (out.length >= exports.MAX_FILES)
111
+ return;
112
+ let entries;
113
+ try {
114
+ entries = fs.readdirSync(dir, { withFileTypes: true });
115
+ }
116
+ catch {
117
+ return;
118
+ }
119
+ for (const entry of entries) {
120
+ if (out.length >= exports.MAX_FILES)
121
+ return;
122
+ const full = path.join(dir, entry.name);
123
+ if (entry.isDirectory()) {
124
+ if (entry.name.startsWith(".") && entry.name !== ".github")
125
+ continue;
126
+ if (SKIP_DIRS.has(entry.name))
127
+ continue;
128
+ if (BUILT_BESIDE_MANIFEST.has(entry.name)
129
+ && fs.existsSync(path.join(dir, "package.json")))
130
+ continue;
131
+ walk(full);
132
+ continue;
133
+ }
134
+ if (!entry.isFile())
135
+ continue;
136
+ const rel = path.relative(root, full).split(path.sep).join("/");
137
+ const lang = isCode(rel, entry.name);
138
+ if (!lang)
139
+ continue;
140
+ let stat;
141
+ try {
142
+ stat = fs.statSync(full);
143
+ }
144
+ catch {
145
+ continue;
146
+ }
147
+ if (stat.size > exports.MAX_FILE_BYTES)
148
+ continue;
149
+ out.push({
150
+ rel,
151
+ size: stat.size,
152
+ mtimeMs: Math.round(stat.mtimeMs),
153
+ lang,
154
+ });
155
+ }
156
+ };
157
+ walk(root);
158
+ out.sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
159
+ return out;
160
+ }
161
+ /** One short string per file that changes whenever that file does.
162
+ *
163
+ * Size and modified time rather than a hash of the contents: reading six
164
+ * thousand files to find out whether any of them changed costs more than
165
+ * rebuilding the map. A file edited back to its original contents within the
166
+ * same millisecond would be missed, which is a case that does not occur in
167
+ * practice and is corrected by the next ordinary edit. */
168
+ function stampOf(file) {
169
+ return `${file.size}:${file.mtimeMs}`;
170
+ }
171
+ /** One short string for the whole project. Saying the code has not moved is a
172
+ * claim, and this is the evidence for it. */
173
+ function fingerprint(files) {
174
+ const hash = crypto.createHash("sha1");
175
+ for (const file of files)
176
+ hash.update(`${file.rel}:${stampOf(file)}\n`);
177
+ return { hash: hash.digest("hex").slice(0, 16), files: files.length };
178
+ }
179
+ /** The commit the project is standing on, when it is a repository at all.
180
+ *
181
+ * Read with a plain file read rather than by running git, because this is
182
+ * called at the start of every job and spawning a process there is a cost paid
183
+ * on work that has nothing to do with the map. A project with no git still
184
+ * gets a usable map; it simply cannot stamp an answer with a commit. */
185
+ function headCommit(root) {
186
+ try {
187
+ const head = fs.readFileSync(path.join(root, ".git", "HEAD"), "utf8").trim();
188
+ if (!head.startsWith("ref:"))
189
+ return head.slice(0, 40);
190
+ const ref = head.slice(4).trim();
191
+ const direct = path.join(root, ".git", ref);
192
+ if (fs.existsSync(direct))
193
+ return fs.readFileSync(direct, "utf8").trim().slice(0, 40);
194
+ const packed = path.join(root, ".git", "packed-refs");
195
+ if (fs.existsSync(packed)) {
196
+ for (const line of fs.readFileSync(packed, "utf8").split("\n")) {
197
+ const [sha, name] = line.trim().split(" ");
198
+ if (name === ref && sha)
199
+ return sha.slice(0, 40);
200
+ }
201
+ }
202
+ }
203
+ catch {
204
+ /* not a repository, or an unusual layout. Neither is an error. */
205
+ }
206
+ return "";
207
+ }
@@ -0,0 +1,29 @@
1
+ import type { FactSet } from "./facts";
2
+ /** Is this JSON file a translation file?
3
+ *
4
+ * Both conditions, not either: a `data/en.json` of sample records is not a
5
+ * translation file, and a `locales/schema.json` is not one either. Indexing
6
+ * the wrong JSON would fill the map with things nobody asked about. */
7
+ export declare function looksLikeTranslations(rel: string): boolean;
8
+ /** The language a translation file is for, from its own name. */
9
+ export declare function languageOf(rel: string): string;
10
+ interface KeyAt {
11
+ key: string;
12
+ line: number;
13
+ }
14
+ /**
15
+ * Every leaf key in a translation file, with the line it sits on.
16
+ *
17
+ * A single pass over the text, tracking strings, escapes, nesting and lines. It
18
+ * reads the structure rather than the values: what a translation SAYS never
19
+ * leaves the machine, only that the key exists and where.
20
+ */
21
+ export declare function scanKeys(text: string): KeyAt[];
22
+ export declare function readLocales(root: string, rels: string[]): {
23
+ facts: Record<string, FactSet>;
24
+ failures: {
25
+ file: string;
26
+ reason: string;
27
+ }[];
28
+ };
29
+ export {};