projectkeeper 0.0.0-stage → 1.0.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/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/README.md +283 -2
- package/README.zh-CN.md +285 -0
- package/THIRD_PARTY_NOTICES.md +53 -0
- package/bin/pk.mjs +6 -0
- package/bin/postinstall.mjs +5 -0
- package/bin/strip-types.mjs +28 -0
- package/package.json +68 -4
- package/patches/README.md +111 -0
- package/patches/pi-ai-0.85.0.ts +122 -0
- package/patches/pi-ai-0.87.1.ts +122 -0
- package/scripts/apply-pi-streaming-patch.ts +86 -0
- package/skills/clerk/coverage/SKILL.md +35 -0
- package/skills/clerk/cross-check/SKILL.md +118 -0
- package/skills/clerk/deepen-dispatch/SKILL.md +69 -0
- package/skills/clerk/lane-follow-up/SKILL.md +94 -0
- package/skills/clerk/lane-plan/SKILL.md +107 -0
- package/skills/clerk/lane-slot/SKILL.md +113 -0
- package/skills/clerk/lane-topic/SKILL.md +116 -0
- package/skills/clerk/orientation/SKILL.md +160 -0
- package/skills/clerk/reconcile/SKILL.md +77 -0
- package/skills/clerk/skeleton-dispatch/SKILL.md +43 -0
- package/skills/clerk/spot-check/SKILL.md +52 -0
- package/skills/clerk/synthesis/SKILL.md +65 -0
- package/skills/table-follow-up/SKILL.md +42 -0
- package/src/cli.ts +319 -0
- package/src/codemap/README.md +13 -0
- package/src/codemap/engines.ts +102 -0
- package/src/codemap/facts.ts +504 -0
- package/src/context/agent-entry.ts +232 -0
- package/src/context/ask.ts +97 -0
- package/src/context/assemble.ts +1058 -0
- package/src/context/code-source.ts +103 -0
- package/src/context/k-briefs.ts +114 -0
- package/src/context/lineage.ts +100 -0
- package/src/context/object-brief.ts +300 -0
- package/src/context/owner-words.ts +191 -0
- package/src/context/pack-fixture.ts +342 -0
- package/src/context/project-rules.ts +160 -0
- package/src/intake/intake.ts +392 -0
- package/src/intake/material-rules.ts +166 -0
- package/src/intake/removal.ts +316 -0
- package/src/intake/skipped.ts +72 -0
- package/src/keeper/adjustment.ts +565 -0
- package/src/keeper/bounds/boundary.ts +509 -0
- package/src/keeper/bounds/command.ts +1665 -0
- package/src/keeper/bounds/paths.ts +164 -0
- package/src/keeper/bounds/reads.ts +718 -0
- package/src/keeper/bounds/shell-write-guard.ts +521 -0
- package/src/keeper/capabilities.ts +136 -0
- package/src/keeper/clerk-steps.ts +151 -0
- package/src/keeper/clerk-tools.ts +1424 -0
- package/src/keeper/conversation.ts +253 -0
- package/src/keeper/evidence.ts +662 -0
- package/src/keeper/fake-provider.ts +132 -0
- package/src/keeper/held.ts +15 -0
- package/src/keeper/history-tools.ts +163 -0
- package/src/keeper/jobs/answering.ts +53 -0
- package/src/keeper/key-check.ts +105 -0
- package/src/keeper/ledger-tools.ts +295 -0
- package/src/keeper/link-check.ts +185 -0
- package/src/keeper/merge.ts +260 -0
- package/src/keeper/note-origin.ts +56 -0
- package/src/keeper/numbers-check.ts +102 -0
- package/src/keeper/open-in-pi.ts +57 -0
- package/src/keeper/organize/absence.ts +266 -0
- package/src/keeper/organize/carried-on.ts +214 -0
- package/src/keeper/organize/clerk-coverage.ts +293 -0
- package/src/keeper/organize/clerk-prompts.ts +132 -0
- package/src/keeper/organize/clerk.ts +1395 -0
- package/src/keeper/organize/coverage-gate.ts +19 -0
- package/src/keeper/organize/coverage-tools.ts +765 -0
- package/src/keeper/organize/entries.ts +153 -0
- package/src/keeper/organize/entry-gate.ts +289 -0
- package/src/keeper/organize/fill-tools.ts +718 -0
- package/src/keeper/organize/follow-up.ts +280 -0
- package/src/keeper/organize/generation-check.ts +504 -0
- package/src/keeper/organize/graph.ts +230 -0
- package/src/keeper/organize/lane-tools.ts +289 -0
- package/src/keeper/organize/looked-tools.ts +54 -0
- package/src/keeper/organize/materials.ts +243 -0
- package/src/keeper/organize/owner-lines.ts +493 -0
- package/src/keeper/organize/place-tools.ts +216 -0
- package/src/keeper/organize/placement-inference.ts +1045 -0
- package/src/keeper/organize/placement-time.ts +320 -0
- package/src/keeper/organize/placing.ts +821 -0
- package/src/keeper/organize/positioning.ts +33 -0
- package/src/keeper/organize/readiness.ts +47 -0
- package/src/keeper/organize/reading.ts +401 -0
- package/src/keeper/organize/round-blocks.ts +65 -0
- package/src/keeper/organize/round-news.ts +164 -0
- package/src/keeper/organize/round-open.ts +586 -0
- package/src/keeper/organize/schedule-state.ts +39 -0
- package/src/keeper/organize/schedule.ts +137 -0
- package/src/keeper/organize/service.ts +409 -0
- package/src/keeper/organize/skills.ts +116 -0
- package/src/keeper/organize/slots.ts +85 -0
- package/src/keeper/organize/stage-gate.ts +175 -0
- package/src/keeper/organize/stage-tools.ts +471 -0
- package/src/keeper/organize/standing-notes.ts +141 -0
- package/src/keeper/organize/table-follow-up.ts +330 -0
- package/src/keeper/organize/takeover-clerk.ts +832 -0
- package/src/keeper/organize/takeover-state.ts +103 -0
- package/src/keeper/organize/takeover.ts +216 -0
- package/src/keeper/organize/update-pending.ts +535 -0
- package/src/keeper/organize/workbench-placement.ts +92 -0
- package/src/keeper/own-keys.ts +114 -0
- package/src/keeper/owner-text.ts +165 -0
- package/src/keeper/project-folder.ts +334 -0
- package/src/keeper/prompts.ts +101 -0
- package/src/keeper/references.ts +177 -0
- package/src/keeper/relook/compose.ts +120 -0
- package/src/keeper/relook/prompts.ts +36 -0
- package/src/keeper/roles.ts +259 -0
- package/src/keeper/round-tools.ts +227 -0
- package/src/keeper/route.ts +148 -0
- package/src/keeper/rules.ts +97 -0
- package/src/keeper/runtime.ts +1858 -0
- package/src/keeper/scope-tools.ts +194 -0
- package/src/keeper/sendback-text.ts +83 -0
- package/src/keeper/step-timing.ts +207 -0
- package/src/keeper/testing/clerk-script.ts +212 -0
- package/src/keeper/testing/streaming-arguments.ts +57 -0
- package/src/keeper/tools.ts +2340 -0
- package/src/keeper/utterance-tools.ts +64 -0
- package/src/keeper/verbatim.ts +397 -0
- package/src/ledger/adapters.ts +186 -0
- package/src/ledger/arrangements.ts +257 -0
- package/src/ledger/blame.ts +58 -0
- package/src/ledger/code-engine-child.ts +69 -0
- package/src/ledger/code-engine.ts +548 -0
- package/src/ledger/code.ts +607 -0
- package/src/ledger/docs.ts +294 -0
- package/src/ledger/git-read.ts +321 -0
- package/src/ledger/index.ts +1850 -0
- package/src/ledger/item-range.ts +16 -0
- package/src/ledger/lines.ts +328 -0
- package/src/ledger/numbering.ts +269 -0
- package/src/ledger/ranges.ts +74 -0
- package/src/ledger/rebuild-worker.ts +15 -0
- package/src/ledger/rebuild.ts +387 -0
- package/src/ledger/repo-scan.ts +181 -0
- package/src/ledger/schema.ts +450 -0
- package/src/ledger/sessions.ts +134 -0
- package/src/ledger/text-scan.ts +466 -0
- package/src/ledger/time.ts +185 -0
- package/src/ledger/ts-symbols.ts +180 -0
- package/src/ledger/views.ts +377 -0
- package/src/mcp/server.ts +127 -0
- package/src/mcp/tools.ts +187 -0
- package/src/model/ids.ts +23 -0
- package/src/model/k-types.ts +912 -0
- package/src/model/time.ts +85 -0
- package/src/model/types.ts +1382 -0
- package/src/model/views-k.ts +707 -0
- package/src/model/vocab.ts +269 -0
- package/src/process/analysis.ts +79 -0
- package/src/process/breakpoint-candidates.ts +383 -0
- package/src/process/breakpoints.ts +746 -0
- package/src/process/claim-check.ts +295 -0
- package/src/process/delivery-links.ts +52 -0
- package/src/process/facts.ts +487 -0
- package/src/process/generations.ts +138 -0
- package/src/process/index.ts +243 -0
- package/src/process/integration.ts +128 -0
- package/src/process/placement.ts +107 -0
- package/src/process/plan.ts +276 -0
- package/src/process/sendbacks.ts +244 -0
- package/src/process/text.ts +185 -0
- package/src/process/units.ts +543 -0
- package/src/process/unnumbered.ts +431 -0
- package/src/process/work.ts +884 -0
- package/src/scope/candidates.ts +86 -0
- package/src/scope/decisions.ts +229 -0
- package/src/scope/discover.ts +741 -0
- package/src/scope/ignore.ts +126 -0
- package/src/scope/ignored-place.ts +93 -0
- package/src/scope/skip.ts +161 -0
- package/src/scope/toolchain.ts +180 -0
- package/src/scope/worktree.ts +187 -0
- package/src/server/api.ts +549 -0
- package/src/server/app.ts +595 -0
- package/src/server/drafts-api.ts +170 -0
- package/src/server/folders-api.ts +243 -0
- package/src/server/graph-view.ts +639 -0
- package/src/server/http.ts +224 -0
- package/src/server/k-agent-api.ts +56 -0
- package/src/server/k-rounds-d99.ts +91 -0
- package/src/server/k-views.ts +405 -0
- package/src/server/keeper-page.ts +446 -0
- package/src/server/keys-api.ts +239 -0
- package/src/server/project-folder-api.ts +104 -0
- package/src/server/vendor.ts +14 -0
- package/src/server/workbench-content.ts +561 -0
- package/src/sources/anchor.ts +129 -0
- package/src/sources/files.ts +267 -0
- package/src/sources/gitobs.ts +41 -0
- package/src/sources/history.ts +231 -0
- package/src/sources/scan-worker.ts +10 -0
- package/src/sources/sessions/locate.ts +292 -0
- package/src/sources/sessions/read.ts +346 -0
- package/src/sources/sessions/scope.ts +42 -0
- package/src/sources/sessions/utterances.ts +223 -0
- package/src/sources/watch.ts +215 -0
- package/src/store/json-file.ts +50 -0
- package/src/store/paths.ts +39 -0
- package/src/store/project-store.ts +295 -0
- package/src/store/versions.ts +172 -0
- package/src/store/workspace.ts +216 -0
- package/src/table/check.ts +333 -0
- package/src/table/follow-up-tools.ts +30 -0
- package/src/table/follow-up.ts +344 -0
- package/src/table/import.ts +610 -0
- package/src/table/project-home.ts +61 -0
- package/src/table/sessions.ts +168 -0
- package/src/table/takeover.ts +483 -0
- package/src/util/git-check.ts +29 -0
- package/src/util/git.ts +411 -0
- package/src/util/own-ts.ts +15 -0
- package/src/util/paths.ts +225 -0
- package/template/FILLING-IN.md +148 -0
- package/template/examples/papertrail.json +747 -0
- package/template/projectkeeper.schema.json +1001 -0
- package/ui/app.js +1135 -0
- package/ui/failures.js +82 -0
- package/ui/flyout.js +120 -0
- package/ui/folder-chooser.js +264 -0
- package/ui/glossary.js +156 -0
- package/ui/graph-fit.js +157 -0
- package/ui/graph-material.js +61 -0
- package/ui/graph-palette.js +109 -0
- package/ui/graph-tools.js +62 -0
- package/ui/graph.js +1712 -0
- package/ui/index.html +31 -0
- package/ui/k/code-view.js +230 -0
- package/ui/k/draft-view.js +93 -0
- package/ui/k/k-views.css +224 -0
- package/ui/k/round-tree.js +450 -0
- package/ui/k/scope-k.js +159 -0
- package/ui/k-data.js +118 -0
- package/ui/k-fold.js +106 -0
- package/ui/k-process.css +302 -0
- package/ui/k-process.js +934 -0
- package/ui/keeper-activity.js +175 -0
- package/ui/keeper-view.js +801 -0
- package/ui/keeper.css +101 -0
- package/ui/key-order.js +62 -0
- package/ui/list-fold.js +67 -0
- package/ui/markdown.d.ts +21 -0
- package/ui/markdown.js +472 -0
- package/ui/placement.d.ts +92 -0
- package/ui/placement.js +445 -0
- package/ui/popover-place.d.ts +16 -0
- package/ui/popover-place.js +89 -0
- package/ui/popover.js +198 -0
- package/ui/project-folder.js +37 -0
- package/ui/shell-state.js +40 -0
- package/ui/strip-rows.js +171 -0
- package/ui/styles.css +492 -0
- package/ui/theme-pick.js +36 -0
- package/ui/theme.js +74 -0
- package/ui/themes/LICENSE.md +13 -0
- package/ui/themes/README.md +44 -0
- package/ui/themes/TOKENS.md +291 -0
- package/ui/themes/a1/board.css +1557 -0
- package/ui/themes/a1/pk.css +392 -0
- package/ui/themes/b1/board.css +1163 -0
- package/ui/themes/b1/pk.css +218 -0
- package/ui/themes/d2/board.css +518 -0
- package/ui/themes/d2/pk.css +243 -0
- package/ui/themes/guard.css +73 -0
- package/ui/themes/i1/board.css +776 -0
- package/ui/themes/i1/pk.css +223 -0
- package/ui/themes/manifest.json +13 -0
- package/ui/views.js +2796 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code structure of the version the checkout has (Spec §1.16 row 7, §1.19; CKC-22 AC-8; D98, R-61): the file tree with
|
|
3
|
+
* sizes, what is generated (and how that was recognised), what the scope classifies as third-party or other non-product
|
|
4
|
+
* material, the dependencies between files, test files and their test cases, the last change of every file and directory,
|
|
5
|
+
* and which directories each merge changed.
|
|
6
|
+
*
|
|
7
|
+
* Who reads the references between files (`code_files.reader`; the coverage gives what each reached, measured):
|
|
8
|
+
* - TypeScript / JavaScript: the TypeScript compiler. File level through its own pre-parser (`preProcessFile`: imports,
|
|
9
|
+
* `import()`, triple-slash references), relative specifiers resolved to the repository's files (`.js` → `.ts` as
|
|
10
|
+
* TypeScript's NodeNext resolution does, `/index`); symbol level on demand (ts-symbols.ts). Exact there, so it stays.
|
|
11
|
+
* - Every other language the code engine reads (code-engine.ts): on a snapshot of this version, the references it resolved
|
|
12
|
+
* — through an import, a path, a qualified name, a function reference or an instance method, never by a name alone — are
|
|
13
|
+
* the file dependencies, with their kind (import, calls, references …); its unresolved imports are the external ones;
|
|
14
|
+
* symbols on demand. No code here for any particular language. A file the engine only tracks (no symbol, no import:
|
|
15
|
+
* YAML, a manifest) has no reader.
|
|
16
|
+
* - Anything else: the file tree and sizes only.
|
|
17
|
+
* Who names a code file no counted reference reaches (`named_by`, `NamingKind`), read from every tracked text file: a
|
|
18
|
+
* build or manifest file (a C++ source its CMakeLists.txt compiles, the activity AndroidManifest.xml launches) makes it a
|
|
19
|
+
* named entry point; code makes it a reference no reader resolved (an include directive the engine does not follow, a
|
|
20
|
+
* worker started by its URL). Either way that nothing references it is no computed fact (Ledger.referenceReach). Any other
|
|
21
|
+
* text — a document, data, a test's fixture — only mentions it.
|
|
22
|
+
* Generated: file names (`*.g.dart`, `*.freezed.dart`, `*.pb.*`, lock files, minified files, source maps), build and
|
|
23
|
+
* output directories, and a generated-code header in the first 2 KB. Third-party: the scope's classification of the
|
|
24
|
+
* location, else a directory named `third_party`, `vendor` or the like.
|
|
25
|
+
*/
|
|
26
|
+
import { readdirSync, readFileSync, statSync, type Dirent } from 'node:fs';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
import { nameForm } from '../util/paths.ts';
|
|
29
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
30
|
+
import { preProcessFile } from 'typescript';
|
|
31
|
+
import { isSkippedName } from '../scope/skip.ts';
|
|
32
|
+
import { catBlobs, isText, revList, treeFiles } from './git-read.ts';
|
|
33
|
+
import { getState, setState, tx } from './schema.ts';
|
|
34
|
+
import type { RepoHandle } from './repo-scan.ts';
|
|
35
|
+
import {
|
|
36
|
+
ENGINE_SIGNATURE, contentKey, dropEngineDir, engineDirOf, engineIndexExists, engineLanguage, engineUnavailable, indexSnapshot, mirrorSnapshot,
|
|
37
|
+
readEngineIndex, snapshotSkips, type EngineLanguageReach, type EngineRun, type MirrorStats, type SnapshotFile,
|
|
38
|
+
} from './code-engine.ts';
|
|
39
|
+
|
|
40
|
+
export type Lang = 'typescript' | 'javascript' | 'dart' | 'python' | 'java' | 'kotlin' | 'swift' | 'go' | 'rust' | 'csharp' | 'c' | 'cpp' | 'css' | 'html' | 'json' | 'yaml' | 'shell' | 'sql' | 'xml' | 'markdown' | 'text' | 'other';
|
|
41
|
+
|
|
42
|
+
export const LANG_BY_EXT: Record<string, Lang> = {
|
|
43
|
+
'.ts': 'typescript', '.tsx': 'typescript', '.mts': 'typescript', '.cts': 'typescript',
|
|
44
|
+
'.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
|
|
45
|
+
'.dart': 'dart', '.py': 'python', '.java': 'java', '.kt': 'kotlin', '.kts': 'kotlin', '.swift': 'swift', '.go': 'go',
|
|
46
|
+
'.rs': 'rust', '.cs': 'csharp', '.c': 'c', '.h': 'c', '.cc': 'cpp', '.cpp': 'cpp', '.hpp': 'cpp', '.m': 'c', '.mm': 'cpp',
|
|
47
|
+
'.css': 'css', '.scss': 'css', '.html': 'html', '.htm': 'html', '.json': 'json', '.yaml': 'yaml', '.yml': 'yaml',
|
|
48
|
+
'.sh': 'shell', '.ps1': 'shell', '.bat': 'shell', '.cmd': 'shell', '.sql': 'sql', '.xml': 'xml', '.gradle': 'other',
|
|
49
|
+
'.md': 'markdown', '.markdown': 'markdown', '.txt': 'text',
|
|
50
|
+
};
|
|
51
|
+
export const langOf = (path: string): Lang | null => {
|
|
52
|
+
const m = /\.[^./\\]+$/.exec(path);
|
|
53
|
+
return m ? LANG_BY_EXT[m[0].toLowerCase()] ?? 'other' : null;
|
|
54
|
+
};
|
|
55
|
+
/** A file whose references the TypeScript compiler reads: TypeScript and JavaScript, by extension. */
|
|
56
|
+
export const compilerReads = (path: string): boolean => { const l = langOf(path); return l === 'typescript' || l === 'javascript'; };
|
|
57
|
+
/** Prose and data: they reference nothing a residual judgement rests on (the ledger's own kinds, by extension). */
|
|
58
|
+
export const NOT_CODE: ReadonlySet<string> = new Set<Lang>(['markdown', 'text', 'json', 'yaml', 'xml', 'other']);
|
|
59
|
+
|
|
60
|
+
const BINARY_EXT = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.icns', '.bmp', '.svgz', '.woff', '.woff2', '.ttf', '.otf', '.eot', '.zip', '.gz', '.tgz', '.tar', '.7z', '.rar', '.jar', '.aar', '.apk', '.aab', '.so', '.dll', '.exe', '.dylib', '.bin', '.dat', '.db', '.sqlite', '.mp4', '.mov', '.mp3', '.wav', '.ogg', '.pdf', '.class', '.keystore', '.jks', '.pyc', '.wasm']);
|
|
61
|
+
const extOf = (p: string) => /\.[^./\\]+$/.exec(p)?.[0]?.toLowerCase() ?? '';
|
|
62
|
+
|
|
63
|
+
const GENERATED_NAME: readonly { re: RegExp; how: string }[] = [
|
|
64
|
+
{ re: /\.(g|freezed|gr|mocks|config|pb|pbenum|pbjson|pbserver|grpc|generated|gen)\.(dart|go|ts|js|py|java|kt|swift|cs)$/i, how: 'file name (*.g.dart, *.freezed.dart, *.pb.*, *.generated.* …)' },
|
|
65
|
+
{ re: /(^|\/)(package-lock\.json|npm-shrinkwrap\.json|pubspec\.lock|yarn\.lock|pnpm-lock\.yaml|Gemfile\.lock|Podfile\.lock|composer\.lock|Cargo\.lock|poetry\.lock|go\.sum)$/i, how: 'lock file' },
|
|
66
|
+
{ re: /\.min\.(js|css)$|\.map$/i, how: 'minified file or source map' },
|
|
67
|
+
{ re: /\.designer\.(cs|vb)$|(^|\/)GeneratedPluginRegistrant\.\w+$|(^|\/)generated_plugin_registrant\.\w+$|(^|\/)generated_plugins\.cmake$/i, how: 'tool-written file' },
|
|
68
|
+
];
|
|
69
|
+
const GENERATED_DIR = /(^|\/)(build|dist|out|coverage|\.dart_tool|\.pub-cache|\.next|\.nuxt|__generated__|generated)(\/)/i;
|
|
70
|
+
const GENERATED_HEAD = /(GENERATED CODE|code generated by|auto-?generated|automatically generated|do not (?:edit|modify)(?: by hand)?|generated by (?:the )?[\w -]+ tool)/i;
|
|
71
|
+
const THIRD_PARTY_DIR = /(^|\/)(third[_-]party|thirdparty|vendor|vendored|external|extern)(\/)/i;
|
|
72
|
+
|
|
73
|
+
export interface GeneratedInfo { readonly generated: boolean; readonly how: string | null }
|
|
74
|
+
export function generatedOf(path: string, head: string | null): GeneratedInfo {
|
|
75
|
+
for (const { re, how } of GENERATED_NAME) if (re.test(path)) return { generated: true, how: `name: ${how}` };
|
|
76
|
+
const dir = GENERATED_DIR.exec(path);
|
|
77
|
+
if (dir) return { generated: true, how: `directory: ${dir[2]}/` };
|
|
78
|
+
const h = head !== null ? GENERATED_HEAD.exec(head) : null;
|
|
79
|
+
if (h) return { generated: true, how: `header: "${h[0]}"` };
|
|
80
|
+
return { generated: false, how: null };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const TEST_PATH = /(^|\/)(tests?|__tests__|specs?|integration_test|test_driver|testing)(\/)|\.(test|spec)\.[a-z]+$|_test\.(dart|go|py)$|(^|\/)test_[^/]+\.py$|Tests?\.(java|kt|swift|cs)$/i;
|
|
84
|
+
export const isTestPath = (path: string): boolean => TEST_PATH.test(path);
|
|
85
|
+
|
|
86
|
+
/** Test cases a test file declares: `test(`, `it(`, `testWidgets(` …, `def test_`, `func Test`, `@Test`. */
|
|
87
|
+
export function testCases(lang: Lang | null, text: string): number {
|
|
88
|
+
const count = (re: RegExp) => (text.match(re) ?? []).length;
|
|
89
|
+
if (lang === 'typescript' || lang === 'javascript') return count(/(?<![\w.])(?:test|it)(?:\.(?:only|skip|todo|concurrent))?\s*\(\s*['"`]/g);
|
|
90
|
+
if (lang === 'dart') return count(/(?<![\w.])(?:test|testWidgets|testGoldens|patrolTest|blocTest)\s*(?:<[^>]*>)?\s*\(\s*['"r]/g);
|
|
91
|
+
if (lang === 'python') return count(/^\s*(?:async\s+)?def\s+test_\w+/gm);
|
|
92
|
+
if (lang === 'go') return count(/^func\s+Test\w+\s*\(/gm);
|
|
93
|
+
if (lang === 'java' || lang === 'kotlin' || lang === 'csharp') return count(/@Test\b|\[(?:Test|Fact|Theory)\]/g);
|
|
94
|
+
if (lang === 'swift') return count(/func\s+test\w+\s*\(/g);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ───────────────────────── dependencies ─────────────────────────
|
|
99
|
+
|
|
100
|
+
export interface Dep { readonly dst: string; readonly kind: string; readonly external: boolean }
|
|
101
|
+
|
|
102
|
+
const posixDir = (p: string): string => (p.includes('/') ? p.slice(0, p.lastIndexOf('/')) : '');
|
|
103
|
+
export const dirOf = posixDir;
|
|
104
|
+
const posixJoin = (dir: string, rel: string): string => {
|
|
105
|
+
const out: string[] = [];
|
|
106
|
+
for (const p of [...(dir ? dir.split('/') : []), ...rel.split('/')]) {
|
|
107
|
+
if (p === '' || p === '.') continue;
|
|
108
|
+
if (p === '..') out.pop();
|
|
109
|
+
else out.push(p);
|
|
110
|
+
}
|
|
111
|
+
return out.join('/');
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** TypeScript / JavaScript imports and triple-slash references, through the compiler's own pre-parser. */
|
|
115
|
+
export function tsDeps(path: string, content: string, tracked: ReadonlySet<string>): Dep[] {
|
|
116
|
+
const pre = preProcessFile(content, true, true);
|
|
117
|
+
const specs = [...pre.importedFiles.map((m) => ({ s: m.fileName, kind: 'import' })), ...pre.referencedFiles.map((r) => ({ s: r.fileName, kind: 'reference' }))];
|
|
118
|
+
const out: Dep[] = [];
|
|
119
|
+
const seen = new Set<string>();
|
|
120
|
+
const dir = posixDir(path);
|
|
121
|
+
for (const { s, kind } of specs) {
|
|
122
|
+
const key = `${kind}:${s}`;
|
|
123
|
+
if (seen.has(key)) continue;
|
|
124
|
+
seen.add(key);
|
|
125
|
+
if (!s.startsWith('.') && kind === 'import') { out.push({ dst: s, kind, external: true }); continue; }
|
|
126
|
+
const base = posixJoin(dir, s);
|
|
127
|
+
const swapped = base.replace(/\.(js|jsx|mjs|cjs)$/, (m) => ({ '.js': '.ts', '.jsx': '.tsx', '.mjs': '.mts', '.cjs': '.cts' })[m] ?? m);
|
|
128
|
+
const hit = [base, swapped, `${base}.ts`, `${base}.tsx`, `${base}.d.ts`, `${base}.js`, `${base}.jsx`, `${base}.mjs`, `${base}/index.ts`, `${base}/index.tsx`, `${base}/index.js`].find((c) => tracked.has(c));
|
|
129
|
+
out.push(hit ? { dst: hit, kind, external: false } : { dst: base, kind, external: true });
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Dependencies of one file as the TypeScript path reads them; every other language is read by the code engine. */
|
|
135
|
+
export function depsOf(lang: Lang | null, path: string, content: string, tracked: ReadonlySet<string>): Dep[] {
|
|
136
|
+
return lang === 'typescript' || lang === 'javascript' ? tsDeps(path, content, tracked) : [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ───────────────────────── who reads what ─────────────────────────
|
|
140
|
+
|
|
141
|
+
export type Reader = 'compiler' | 'engine';
|
|
142
|
+
|
|
143
|
+
/** What one language's references reached in one repository, measured when the code was read (the coverage shows it). */
|
|
144
|
+
export interface LanguageReach {
|
|
145
|
+
readonly language: string;
|
|
146
|
+
readonly reader: Reader;
|
|
147
|
+
/** Files whose references it read. */
|
|
148
|
+
readonly files: number;
|
|
149
|
+
/** Import specifiers resolved to a file of the repository, and not (packages, SDKs — and, for the engine, any it missed). */
|
|
150
|
+
readonly imports: { readonly resolved: number; readonly unresolved: number; readonly unresolvedNamingRepoFiles?: number; readonly sample?: readonly string[] };
|
|
151
|
+
/** The engine: references between files by resolution method, counted and matched by a name only (not counted). */
|
|
152
|
+
readonly counted?: Readonly<Record<string, number>>;
|
|
153
|
+
readonly nameOnly?: Readonly<Record<string, number>>;
|
|
154
|
+
/** The engine: files it only tracks (no symbol, no import), and files it could not parse. */
|
|
155
|
+
readonly trackedOnly?: number;
|
|
156
|
+
readonly parseErrors?: number;
|
|
157
|
+
/** Product files no counted reference reaches that code names — where references went unresolved (`NamingKind` reference). */
|
|
158
|
+
readonly namedButUnreferenced: number;
|
|
159
|
+
readonly namedSample: readonly string[];
|
|
160
|
+
/**
|
|
161
|
+
* Product files no counted reference reaches that a build or manifest file names: named entry points (`NamingKind`
|
|
162
|
+
* entry). They say where the code is entered, not how deep it is read. Absent from a reach recorded before build and
|
|
163
|
+
* manifest files were read.
|
|
164
|
+
*/
|
|
165
|
+
readonly namedEntryPoints?: number;
|
|
166
|
+
readonly entrySample?: readonly string[];
|
|
167
|
+
}
|
|
168
|
+
export interface CodeReach {
|
|
169
|
+
readonly engine: { readonly signature: string; readonly unavailable: string | null };
|
|
170
|
+
/** The engine's last run on this repository, and its snapshot; null when it had no file to read. */
|
|
171
|
+
readonly run: EngineRun | null;
|
|
172
|
+
readonly snapshot: MirrorStats | null;
|
|
173
|
+
/** Why the engine's references are missing, when they are. */
|
|
174
|
+
readonly error: string | null;
|
|
175
|
+
readonly languages: readonly LanguageReach[];
|
|
176
|
+
}
|
|
177
|
+
/** The reach the last read of the code recorded for a repository (or a directory outside version control). */
|
|
178
|
+
export function reachOf(db: DatabaseSync, id: string): CodeReach | null {
|
|
179
|
+
try { const v = getState(db, `codeReach:${id}`); return v ? JSON.parse(v) as CodeReach : null; } catch { return null; }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ───────────────────────── who names a file no counted reference reaches ─────────────────────────
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* How another file names a product code file no counted reference reaches (`code_files.named_by`):
|
|
186
|
+
* - `entry`: a build or manifest file names it — a named entry point, compiled, launched, loaded or run by what reads that
|
|
187
|
+
* file (a C++ source its CMakeLists.txt compiles, the activity AndroidManifest.xml launches);
|
|
188
|
+
* - `reference`: code names it — a reference no reader resolved (an include directive the engine does not follow, a
|
|
189
|
+
* worker started by its URL, a script nothing reads that runs it);
|
|
190
|
+
* - `mention`: any other text names it — a document, data, a test's fixture: it is said there, and nothing is referenced.
|
|
191
|
+
* The first two make "nothing references it" no computed fact (Ledger.referenceReach); a mention does not.
|
|
192
|
+
*/
|
|
193
|
+
export type NamingKind = 'entry' | 'reference' | 'mention';
|
|
194
|
+
export interface Naming { readonly path: string; readonly kind: NamingKind }
|
|
195
|
+
const NAMING_KINDS: readonly NamingKind[] = ['entry', 'reference', 'mention'];
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Build and manifest files, by name: what a build, a package manager, a platform or a runner reads to know what to compile,
|
|
199
|
+
* launch, load or run. The same table for every project; nothing in it parses a language.
|
|
200
|
+
*/
|
|
201
|
+
const BUILD_FILES: readonly RegExp[] = [
|
|
202
|
+
// Android and the JVM: the manifest, Gradle, Maven, Ant.
|
|
203
|
+
/(^|\/)AndroidManifest\.xml$/, /(^|\/)(build|settings)\.gradle(\.kts)?$/, /(^|\/)(pom|build|ivy)\.xml$/,
|
|
204
|
+
// C and C++: CMake, Make, Meson, Bazel, GN, GYP, SCons, autotools; Windows resource scripts and application manifests.
|
|
205
|
+
/(^|\/)CMakeLists\.txt$/, /\.cmake$/i, /(^|\/)(GNUmakefile|[Mm]akefile)$/, /\.(mk|mak)$/i, /(^|\/)meson\.build$/,
|
|
206
|
+
/(^|\/)(BUILD|WORKSPACE|MODULE)(\.bazel)?$/, /\.(bzl|gn|gni|gyp|gypi)$/i, /(^|\/)(SConstruct|SConscript|configure\.ac|Makefile\.am)$/, /\.(rc|manifest)$/i,
|
|
207
|
+
// Apple: Xcode projects, schemes and build settings, property lists, entitlements, storyboards, CocoaPods.
|
|
208
|
+
/\.(pbxproj|xcscheme|xcconfig|plist|entitlements|storyboard|xib)$/i, /(^|\/)Podfile$/, /\.podspec$/i,
|
|
209
|
+
// .NET and MSBuild.
|
|
210
|
+
/\.(csproj|vbproj|fsproj|vcxproj|sln|props|targets)$/i,
|
|
211
|
+
// Package manifests: JavaScript, Dart and Flutter, Python, Rust, Go, Ruby, PHP.
|
|
212
|
+
/(^|\/)(package\.json|deno\.jsonc?|pubspec\.yaml|pyproject\.toml|setup\.cfg|Pipfile|Cargo\.toml|go\.mod|Gemfile|Rakefile|composer\.json)$/, /\.gemspec$/i,
|
|
213
|
+
// What runs it: containers, process files, task runners, continuous integration.
|
|
214
|
+
/(^|\/)(Dockerfile|Containerfile|Procfile|Jenkinsfile|Vagrantfile|Taskfile\.ya?ml|[Jj]ustfile)$/, /\.dockerfile$/i, /(^|\/)(docker-)?compose(\.[\w-]+)?\.ya?ml$/,
|
|
215
|
+
/(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/, /(^|\/)\.circleci\/config\.ya?ml$/, /(^|\/)(\.gitlab-ci|azure-pipelines|\.travis|appveyor|bitbucket-pipelines)\.ya?ml$/,
|
|
216
|
+
// Web and platform manifests.
|
|
217
|
+
/(^|\/)(manifest\.json|firebase\.json|vercel\.json|netlify\.toml|app\.json|web\.config)$/, /\.webmanifest$/i,
|
|
218
|
+
];
|
|
219
|
+
export const isBuildFile = (path: string): boolean => BUILD_FILES.some((re) => re.test(path));
|
|
220
|
+
|
|
221
|
+
/** A file larger than this is not read for the names in it, unless a reader reads its references. */
|
|
222
|
+
export const NAMING_MAX_BYTES = 1_000_000;
|
|
223
|
+
/** How the code was read for who names what: a change reads every repository's code again at the next rebuild. */
|
|
224
|
+
export const NAMING_RULE = 'named-2';
|
|
225
|
+
|
|
226
|
+
/** A file's `named_by` as recorded: the files naming it and how, entry points first. A ledger read before build and
|
|
227
|
+
* manifest files were read recorded code files only, by path. */
|
|
228
|
+
export function namingOf(raw: string | null | undefined): Naming[] {
|
|
229
|
+
if (!raw) return [];
|
|
230
|
+
let v: unknown;
|
|
231
|
+
try { v = JSON.parse(raw); } catch { return []; }
|
|
232
|
+
if (!Array.isArray(v)) return [];
|
|
233
|
+
return v.flatMap((x): Naming[] => {
|
|
234
|
+
if (typeof x === 'string') return [{ path: x, kind: 'reference' }];
|
|
235
|
+
const n = x as { path?: unknown; kind?: unknown } | null;
|
|
236
|
+
return n && typeof n.path === 'string' && NAMING_KINDS.includes(n.kind as NamingKind) ? [{ path: n.path, kind: n.kind as NamingKind }] : [];
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
/** What makes it no computed fact that nothing references a file: a build or manifest file, or code, names it. */
|
|
240
|
+
export const namingThatCounts = (naming: readonly Naming[]): Naming[] => naming.filter((n) => n.kind !== 'mention');
|
|
241
|
+
/** A file name as said for the file naming another: `AndroidManifest.xml`. */
|
|
242
|
+
export const fileNameOf = (path: string): string => path.slice(path.lastIndexOf('/') + 1);
|
|
243
|
+
|
|
244
|
+
// ───────────────────────── the scan ─────────────────────────
|
|
245
|
+
|
|
246
|
+
export interface CodeScanStats {
|
|
247
|
+
readonly head: string | null;
|
|
248
|
+
readonly skipped: boolean;
|
|
249
|
+
readonly files: number;
|
|
250
|
+
readonly lines: number;
|
|
251
|
+
readonly productFiles: number;
|
|
252
|
+
readonly productLines: number;
|
|
253
|
+
readonly generatedFiles: number;
|
|
254
|
+
readonly otherFiles: number;
|
|
255
|
+
readonly testFiles: number;
|
|
256
|
+
readonly testCases: number;
|
|
257
|
+
readonly deps: number;
|
|
258
|
+
readonly depsExternal: number;
|
|
259
|
+
/** The code engine's run at this scan (its time, peak memory and index size), when it had files to read. */
|
|
260
|
+
readonly engine: (EngineRun & { readonly snapshot: MirrorStats }) | null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export interface CodeScanInput {
|
|
264
|
+
/** The scope's classification of the location holding a file (innermost wins), or null. */
|
|
265
|
+
readonly classify?: (absPath: string) => string | null;
|
|
266
|
+
/**
|
|
267
|
+
* Where the code engine keeps this project's snapshots and indexes, a folder ProjectKeeper owns next to the ledger
|
|
268
|
+
* (`<project dir>/code-index`). Absent: the engine is not run and only TypeScript and JavaScript references are read.
|
|
269
|
+
*/
|
|
270
|
+
readonly engineRoot?: string | null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const countLines = (buf: Buffer): number => {
|
|
274
|
+
let n = 0;
|
|
275
|
+
for (let i = 0; i < buf.length; i++) if (buf[i] === 10) n++;
|
|
276
|
+
return buf.length && buf[buf.length - 1] !== 10 ? n + 1 : n;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
interface FileFacts {
|
|
280
|
+
path: string; bytes: number; lines: number | null; lang: Lang | null; generated: GeneratedInfo; classification: string | null; test: boolean; cases: number; blob: string | null;
|
|
281
|
+
deps: Dep[]; reader: Reader | null; refLang: string | null; namedBy: Naming[] | null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* When the files were read (Spec §2.11; CKC-22 AC-10): `now`, the rebuild reading them, is the first reading of a file the
|
|
286
|
+
* ledger had not recorded (one it had keeps its first reading); `fileTimes`, outside version control, each file's own time
|
|
287
|
+
* as read (`File time`).
|
|
288
|
+
*/
|
|
289
|
+
interface ReadTimes { readonly now: string; readonly fileTimes?: ReadonlyMap<string, string> }
|
|
290
|
+
|
|
291
|
+
function writeFiles(db: DatabaseSync, repoId: string, files: readonly FileFacts[], last: ReadonlyMap<string, { hash: string; at: string; ms: number }>, read: ReadTimes, reach: CodeReach): void {
|
|
292
|
+
tx(db, () => {
|
|
293
|
+
const firstSeen = new Map((db.prepare('SELECT path, first_seen FROM code_files WHERE repo = ?').all(repoId) as { path: string; first_seen: string | null }[]).map((r) => [r.path, r.first_seen]));
|
|
294
|
+
db.prepare('DELETE FROM code_files WHERE repo = ?').run(repoId);
|
|
295
|
+
db.prepare('DELETE FROM code_deps WHERE repo = ?').run(repoId);
|
|
296
|
+
db.prepare('DELETE FROM dir_stats WHERE repo = ?').run(repoId);
|
|
297
|
+
const insFile = db.prepare('INSERT INTO code_files (repo, path, bytes, lines, lang, generated, generated_how, blob, classification, test, test_cases, last_commit, last_at, file_at, first_seen, reader, ref_lang, named_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
|
298
|
+
const insDep = db.prepare('INSERT OR IGNORE INTO code_deps (repo, src, dst, kind, external) VALUES (?, ?, ?, ?, ?)');
|
|
299
|
+
for (const f of files) {
|
|
300
|
+
const l = last.get(f.path);
|
|
301
|
+
insFile.run(repoId, f.path, f.bytes, f.lines, f.lang, f.generated.generated ? 1 : 0, f.generated.how, f.blob, f.classification, f.test ? 1 : 0, f.cases, l?.hash ?? null, l?.at ?? null,
|
|
302
|
+
read.fileTimes?.get(f.path) ?? null, firstSeen.get(f.path) ?? read.now, f.reader, f.refLang, f.namedBy ? JSON.stringify(f.namedBy) : null);
|
|
303
|
+
for (const d of f.deps) insDep.run(repoId, f.path, d.dst, d.kind, d.external ? 1 : 0);
|
|
304
|
+
}
|
|
305
|
+
// Every directory: its whole subtree, split into product code, generated code and other classified material.
|
|
306
|
+
const agg = new Map<string, { files: number; lines: number; pf: number; pl: number; gf: number; gl: number; of: number; ol: number; tests: number; cases: number; last: { hash: string; at: string; ms: number } | null }>();
|
|
307
|
+
for (const f of files) {
|
|
308
|
+
const segs = f.path.split('/');
|
|
309
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
310
|
+
const dir = segs.slice(0, i + 1).join('/');
|
|
311
|
+
const a = agg.get(dir) ?? { files: 0, lines: 0, pf: 0, pl: 0, gf: 0, gl: 0, of: 0, ol: 0, tests: 0, cases: 0, last: null };
|
|
312
|
+
const lines = f.lines ?? 0;
|
|
313
|
+
a.files += 1; a.lines += lines;
|
|
314
|
+
if (f.classification) { a.of += 1; a.ol += lines; } else if (f.generated.generated) { a.gf += 1; a.gl += lines; } else { a.pf += 1; a.pl += lines; }
|
|
315
|
+
if (f.test) { a.tests += 1; a.cases += f.cases; }
|
|
316
|
+
const l = last.get(f.path);
|
|
317
|
+
if (l && (!a.last || l.ms > a.last.ms)) a.last = l;
|
|
318
|
+
agg.set(dir, a);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const insDir = db.prepare('INSERT INTO dir_stats (repo, dir, files, lines, product_files, product_lines, generated_files, generated_lines, other_files, other_lines, tests, test_cases, last_at, last_commit) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
|
322
|
+
for (const [dir, a] of agg) insDir.run(repoId, dir, a.files, a.lines, a.pf, a.pl, a.gf, a.gl, a.of, a.ol, a.tests, a.cases, a.last?.at ?? null, a.last?.hash ?? null);
|
|
323
|
+
setState(db, `codeReach:${repoId}`, JSON.stringify(reach));
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function facts(path: string, bytes: number, buf: Buffer | null, blob: string | null, classification: string | null): FileFacts {
|
|
328
|
+
const lang = langOf(path);
|
|
329
|
+
const text = buf !== null && isText(buf) ? buf : null;
|
|
330
|
+
const head = text ? text.subarray(0, Math.min(text.length, 2048)).toString('utf8') : null;
|
|
331
|
+
const generated = generatedOf(path, head);
|
|
332
|
+
const test = isTestPath(path);
|
|
333
|
+
const content = text ? text.toString('utf8') : null;
|
|
334
|
+
return {
|
|
335
|
+
path, bytes, lines: text ? countLines(text) : null, lang, generated,
|
|
336
|
+
classification: classification ?? (THIRD_PARTY_DIR.test(path) ? 'third-party (directory name)' : null),
|
|
337
|
+
test, cases: test && content ? testCases(lang, content) : 0, blob, deps: [], reader: null, refLang: null, namedBy: null,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const baseName = fileNameOf;
|
|
342
|
+
const bump = (m: Map<string, number>, k: string) => m.set(k, (m.get(k) ?? 0) + 1);
|
|
343
|
+
const push = <K, V>(m: Map<K, V[]>, k: K, v: V) => { const list = m.get(k); if (list) list.push(v); else m.set(k, [v]); };
|
|
344
|
+
const stemOf = (name: string): string | null => (name.lastIndexOf('.') > 0 ? name.slice(0, name.lastIndexOf('.')) : null);
|
|
345
|
+
|
|
346
|
+
/** A file name as text says it: `utils.cpp`, `MainActivity.kt` (the same token for every language). */
|
|
347
|
+
const FILE_NAME = /[\w.-]+\.[A-Za-z0-9]+/g;
|
|
348
|
+
/** A dotted name, read as a path: `.MainActivity` (relative), `com.example.app.MainActivity`, `pkg.module`. */
|
|
349
|
+
const DOTTED_NAME = /(?<![\w$.])(?:[A-Za-z_$][\w$]*)?(?:\.[A-Za-z_$][\w$]*)+/g;
|
|
350
|
+
|
|
351
|
+
/** How a file names others: a build or manifest file, code (read by a reader or not), or any other text. */
|
|
352
|
+
function namingKindOf(f: FileFacts): NamingKind {
|
|
353
|
+
if (isBuildFile(f.path)) return 'entry';
|
|
354
|
+
return f.reader || (f.lang !== null && !NOT_CODE.has(f.lang)) ? 'reference' : 'mention';
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Who names each product code file no counted reference reaches (`NamingKind`), from every tracked text file: a file name
|
|
359
|
+
* anywhere (`"utils.cpp"` in a CMakeLists.txt, `part 'x.dart'`); in a build or manifest file also a dotted name read as a
|
|
360
|
+
* path, its last part the file's stem and its other parts, when there are any, the directories just above it
|
|
361
|
+
* (`android:name=".MainActivity"`, `com.example.app.MainActivity`, `pkg.module:main`). A file over `NAMING_MAX_BYTES` is
|
|
362
|
+
* read for names only when a reader reads its references. At most five files of each kind are kept, entry points first.
|
|
363
|
+
*/
|
|
364
|
+
function readNames(files: FileFacts[], texts: ReadonlyMap<string, Buffer>): void {
|
|
365
|
+
const incoming = new Set<string>();
|
|
366
|
+
for (const f of files) for (const d of f.deps) if (!d.external && d.dst !== f.path) incoming.add(d.dst);
|
|
367
|
+
const byName = new Map<string, FileFacts[]>();
|
|
368
|
+
const byStem = new Map<string, FileFacts[]>();
|
|
369
|
+
for (const f of files) {
|
|
370
|
+
if (f.generated.generated || f.classification || f.test || incoming.has(f.path)) continue;
|
|
371
|
+
// Code only: what a reader reads, or a language the ledger knows is code though nothing reads it (as the code map counts).
|
|
372
|
+
if (!f.reader && (f.lang === null || NOT_CODE.has(f.lang))) continue;
|
|
373
|
+
push(byName, baseName(f.path), f);
|
|
374
|
+
const stem = stemOf(baseName(f.path));
|
|
375
|
+
if (stem) push(byStem, stem, f);
|
|
376
|
+
}
|
|
377
|
+
if (!byName.size) return;
|
|
378
|
+
const found = new Map<FileFacts, Map<NamingKind, Set<string>>>();
|
|
379
|
+
const note = (c: FileFacts, kind: NamingKind, by: string) => {
|
|
380
|
+
if (c.path === by) return;
|
|
381
|
+
const kinds = found.get(c) ?? found.set(c, new Map()).get(c)!;
|
|
382
|
+
(kinds.get(kind) ?? kinds.set(kind, new Set()).get(kind)!).add(by);
|
|
383
|
+
};
|
|
384
|
+
for (const f of files) {
|
|
385
|
+
const buf = texts.get(f.path);
|
|
386
|
+
if (!buf || (!f.reader && buf.length > NAMING_MAX_BYTES)) continue;
|
|
387
|
+
const kind = namingKindOf(f);
|
|
388
|
+
const text = buf.toString('utf8');
|
|
389
|
+
for (const m of text.matchAll(FILE_NAME)) for (const c of byName.get(m[0]) ?? []) note(c, kind, f.path);
|
|
390
|
+
if (kind !== 'entry') continue;
|
|
391
|
+
for (const m of text.matchAll(DOTTED_NAME)) {
|
|
392
|
+
const parts = m[0].split('.');
|
|
393
|
+
const within = parts.slice(0, -1).filter(Boolean);
|
|
394
|
+
for (const c of byStem.get(parts[parts.length - 1]!) ?? []) {
|
|
395
|
+
const dirs = c.path.split('/').slice(0, -1);
|
|
396
|
+
if (within.length <= dirs.length && within.every((p, i) => dirs[dirs.length - within.length + i] === p)) note(c, kind, f.path);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
for (const [c, kinds] of found) c.namedBy = NAMING_KINDS.flatMap((kind) => [...(kinds.get(kind) ?? [])].sort().slice(0, 5).map((path) => ({ path, kind })));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* The references of every file of one version (see the module comment): the compiler for TypeScript and JavaScript, the
|
|
405
|
+
* code engine on the snapshot for the rest, and what each reached. `texts` are the files read as text, `keys` what
|
|
406
|
+
* identifies each content (a git blob; outside version control, a content hash).
|
|
407
|
+
*/
|
|
408
|
+
function readReferences(files: FileFacts[], texts: ReadonlyMap<string, Buffer>, keys: ReadonlyMap<string, string>, engineDir: string | null): CodeReach {
|
|
409
|
+
const tracked = new Set(files.map((f) => f.path));
|
|
410
|
+
for (const f of files) {
|
|
411
|
+
const buf = texts.get(f.path);
|
|
412
|
+
if (!buf || !compilerReads(f.path)) continue;
|
|
413
|
+
f.reader = 'compiler';
|
|
414
|
+
f.refLang = f.lang;
|
|
415
|
+
if (!f.generated.generated) f.deps = tsDeps(f.path, buf.toString('utf8'), tracked);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Every other language: the code engine, on a snapshot it indexes (nothing is written into the project).
|
|
419
|
+
const unavailable = engineUnavailable();
|
|
420
|
+
let run: EngineRun | null = null;
|
|
421
|
+
let snapshot: MirrorStats | null = null;
|
|
422
|
+
let error: string | null = null;
|
|
423
|
+
let engineLanguages: ReturnType<typeof engineReachOf> = [];
|
|
424
|
+
if (engineDir && !unavailable) {
|
|
425
|
+
const wanted: SnapshotFile[] = [];
|
|
426
|
+
for (const f of files) {
|
|
427
|
+
const buf = texts.get(f.path);
|
|
428
|
+
if (!buf || compilerReads(f.path) || snapshotSkips(f.path) || engineLanguage(f.path) === null) continue;
|
|
429
|
+
wanted.push({ path: f.path, key: keys.get(f.path) ?? contentKey(buf), content: buf });
|
|
430
|
+
}
|
|
431
|
+
if (wanted.length === 0) dropEngineDir(engineDir);
|
|
432
|
+
else {
|
|
433
|
+
snapshot = mirrorSnapshot(engineDir, wanted);
|
|
434
|
+
run = indexSnapshot(engineDir, snapshot.written + snapshot.removed > 0);
|
|
435
|
+
const read = run.ok ? readEngineIndex(engineDir) : null;
|
|
436
|
+
if (!run.ok) error = `${run.error ?? 'the engine did not finish'}`;
|
|
437
|
+
else if (typeof read === 'string') error = read;
|
|
438
|
+
else if (read) {
|
|
439
|
+
const bySrc = new Map<string, Dep[]>();
|
|
440
|
+
for (const d of read.deps) {
|
|
441
|
+
const list = bySrc.get(d.src);
|
|
442
|
+
const dep = { dst: d.dst, kind: d.kind, external: d.external };
|
|
443
|
+
if (list) list.push(dep); else bySrc.set(d.src, [dep]);
|
|
444
|
+
}
|
|
445
|
+
for (const f of files) {
|
|
446
|
+
const e = read.files.get(f.path);
|
|
447
|
+
if (!e || f.reader) continue;
|
|
448
|
+
f.refLang = e.language;
|
|
449
|
+
f.reader = e.code ? 'engine' : null;
|
|
450
|
+
if (f.reader && !f.generated.generated) f.deps = bySrc.get(f.path) ?? [];
|
|
451
|
+
}
|
|
452
|
+
engineLanguages = engineReachOf(read.languages);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
} else if (!engineDir) error = 'the code engine was not given a place for its snapshot';
|
|
456
|
+
else error = unavailable;
|
|
457
|
+
|
|
458
|
+
// Unreferenced but named: no counted reference reaches the file, yet another file names it (NamingKind).
|
|
459
|
+
readNames(files, texts);
|
|
460
|
+
|
|
461
|
+
// Per language: the files code names — references its reader did not resolve, how far the reading went — and, apart,
|
|
462
|
+
// the named entry points, which say where the language's code is entered, not how deep it is read.
|
|
463
|
+
const named = new Map<string, number>();
|
|
464
|
+
const namedSample = new Map<string, string[]>();
|
|
465
|
+
const entries = new Map<string, number>();
|
|
466
|
+
const entrySample = new Map<string, string[]>();
|
|
467
|
+
const sample = (m: Map<string, string[]>, lang: string, line: string) => { const s = m.get(lang) ?? []; if (s.length < 5) m.set(lang, [...s, line]); };
|
|
468
|
+
for (const f of files) {
|
|
469
|
+
if (!f.refLang) continue;
|
|
470
|
+
const code = f.namedBy?.find((n) => n.kind === 'reference');
|
|
471
|
+
const entry = f.namedBy?.find((n) => n.kind === 'entry');
|
|
472
|
+
if (code) { bump(named, f.refLang); sample(namedSample, f.refLang, `${f.path} (named in ${code.path})`); }
|
|
473
|
+
if (entry) { bump(entries, f.refLang); sample(entrySample, f.refLang, `${f.path} (named in ${entry.path})`); }
|
|
474
|
+
}
|
|
475
|
+
const namedOf = (language: string) => ({
|
|
476
|
+
namedButUnreferenced: named.get(language) ?? 0, namedSample: namedSample.get(language) ?? [],
|
|
477
|
+
namedEntryPoints: entries.get(language) ?? 0, entrySample: entrySample.get(language) ?? [],
|
|
478
|
+
});
|
|
479
|
+
const languages: LanguageReach[] = [];
|
|
480
|
+
const compilerLangs = new Map<string, FileFacts[]>();
|
|
481
|
+
for (const f of files) if (f.reader === 'compiler') compilerLangs.set(f.refLang!, [...(compilerLangs.get(f.refLang!) ?? []), f]);
|
|
482
|
+
for (const [language, fs] of compilerLangs) {
|
|
483
|
+
const imports = fs.flatMap((f) => f.deps.filter((d) => d.kind === 'import'));
|
|
484
|
+
languages.push({ language, reader: 'compiler', files: fs.length, imports: { resolved: imports.filter((d) => !d.external).length, unresolved: imports.filter((d) => d.external).length }, ...namedOf(language) });
|
|
485
|
+
}
|
|
486
|
+
for (const l of engineLanguages) languages.push({ ...l, ...namedOf(l.language) });
|
|
487
|
+
return { engine: { signature: ENGINE_SIGNATURE, unavailable }, run, snapshot, error, languages: languages.sort((a, b) => b.files - a.files || a.language.localeCompare(b.language)) };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function engineReachOf(langs: readonly EngineLanguageReach[]): Omit<LanguageReach, 'namedButUnreferenced' | 'namedSample' | 'namedEntryPoints' | 'entrySample'>[] {
|
|
491
|
+
return langs.map((l) => ({
|
|
492
|
+
language: l.language, reader: 'engine' as const, files: l.files, trackedOnly: l.trackedOnly, parseErrors: l.parseErrors,
|
|
493
|
+
imports: l.imports, counted: l.counted, nameOnly: l.nameOnly,
|
|
494
|
+
}));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** A recorded engine run whose index is gone (the folder removed): the code is read again even if nothing else moved. */
|
|
498
|
+
function engineGone(db: DatabaseSync, id: string, engineDir: string | null): boolean {
|
|
499
|
+
const reach = reachOf(db, id);
|
|
500
|
+
return Boolean(engineDir && reach?.snapshot && reach.snapshot.files > 0 && !engineIndexExists(engineDir));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* One repository's code structure at the checkout's HEAD; skipped when HEAD did not move (and nothing is forced). `now` is
|
|
505
|
+
* the rebuild reading it: the first reading of a file the ledger had not recorded.
|
|
506
|
+
*/
|
|
507
|
+
export function scanCode(db: DatabaseSync, repo: RepoHandle, input: CodeScanInput = {}, opts: { force?: boolean; now?: string } = {}): CodeScanStats {
|
|
508
|
+
const head = (db.prepare('SELECT head FROM repos WHERE id = ?').get(repo.id) as { head: string | null } | undefined)?.head ?? null;
|
|
509
|
+
const stateKey = `code:${repo.id}`;
|
|
510
|
+
const signature = `${head}|${ENGINE_SIGNATURE}|${NAMING_RULE}`;
|
|
511
|
+
const engineDir = input.engineRoot ? engineDirOf(input.engineRoot, repo.id) : null;
|
|
512
|
+
if (!opts.force && head !== null && getState(db, stateKey) === signature && !engineGone(db, repo.id, engineDir)) return { ...codeTotals(db, repo.id), head, skipped: true, engine: null };
|
|
513
|
+
const tree = head ? treeFiles(repo.path, head) : [];
|
|
514
|
+
const readable = tree.filter((f) => !f.submodule && !BINARY_EXT.has(extOf(f.path)) && f.size !== null && f.size <= 4_000_000);
|
|
515
|
+
const contents = catBlobs(repo.path, readable.map((f) => f.blob));
|
|
516
|
+
const texts = new Map<string, Buffer>();
|
|
517
|
+
const keys = new Map<string, string>();
|
|
518
|
+
const files: FileFacts[] = tree.map((f) => {
|
|
519
|
+
const buf = contents.get(f.blob) ?? null;
|
|
520
|
+
const ff = facts(f.path, f.size ?? 0, buf, f.blob, input.classify?.(join(repo.path, ...f.path.split('/'))) ?? null);
|
|
521
|
+
if (buf && ff.lines !== null) { texts.set(f.path, buf); keys.set(f.path, f.blob); }
|
|
522
|
+
return ff;
|
|
523
|
+
});
|
|
524
|
+
const reach = readReferences(files, texts, keys, engineDir);
|
|
525
|
+
// The last change of each file: the newest commit in HEAD's history whose first-parent change touched it.
|
|
526
|
+
const ancestry = new Set(head ? revList(repo.path, [head]) ?? [] : []);
|
|
527
|
+
const last = new Map<string, { hash: string; at: string; ms: number }>();
|
|
528
|
+
const tracked = new Set(tree.map((f) => f.path));
|
|
529
|
+
for (const r of db.prepare('SELECT cf.path, cf.hash, c.author_at, c.author_ms FROM commit_files cf JOIN commits c ON c.repo = cf.repo AND c.hash = cf.hash WHERE cf.repo = ?').all(repo.id) as { path: string; hash: string; author_at: string; author_ms: number }[]) {
|
|
530
|
+
if (!tracked.has(r.path) || !ancestry.has(r.hash)) continue;
|
|
531
|
+
const cur = last.get(r.path);
|
|
532
|
+
if (!cur || r.author_ms > cur.ms) last.set(r.path, { hash: r.hash, at: r.author_at, ms: r.author_ms });
|
|
533
|
+
}
|
|
534
|
+
writeFiles(db, repo.id, files, last, { now: opts.now ?? new Date().toISOString() }, reach);
|
|
535
|
+
// Merges: which directories each merge changed, against its first parent.
|
|
536
|
+
tx(db, () => {
|
|
537
|
+
db.prepare('DELETE FROM merge_dirs WHERE repo = ?').run(repo.id);
|
|
538
|
+
const ins = db.prepare('INSERT OR REPLACE INTO merge_dirs (repo, commit_hash, at, on_trunk, dirs) VALUES (?, ?, ?, ?, ?)');
|
|
539
|
+
const byMerge = new Map<string, Map<string, number>>();
|
|
540
|
+
for (const r of db.prepare('SELECT cf.hash, cf.path FROM commit_files cf JOIN commits c ON c.repo = cf.repo AND c.hash = cf.hash WHERE cf.repo = ? AND c.merge = 1').all(repo.id) as { hash: string; path: string }[]) {
|
|
541
|
+
const m = byMerge.get(r.hash) ?? new Map<string, number>();
|
|
542
|
+
const d = posixDir(r.path) || '.';
|
|
543
|
+
m.set(d, (m.get(d) ?? 0) + 1);
|
|
544
|
+
byMerge.set(r.hash, m);
|
|
545
|
+
}
|
|
546
|
+
for (const m of db.prepare('SELECT hash, author_at, on_trunk FROM commits WHERE repo = ? AND merge = 1').all(repo.id) as { hash: string; author_at: string; on_trunk: number }[]) {
|
|
547
|
+
ins.run(repo.id, m.hash, m.author_at, m.on_trunk, JSON.stringify([...(byMerge.get(m.hash) ?? new Map()).entries()].sort((a, b) => a[0].localeCompare(b[0]))));
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
if (head) setState(db, stateKey, signature);
|
|
551
|
+
return { ...codeTotals(db, repo.id), head, skipped: false, engine: reach.run && reach.snapshot ? { ...reach.run, snapshot: reach.snapshot } : null };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function codeTotals(db: DatabaseSync, repo: string): Omit<CodeScanStats, 'head' | 'skipped' | 'engine'> {
|
|
555
|
+
const t = db.prepare(`SELECT count(*) f, ifnull(sum(lines), 0) l,
|
|
556
|
+
ifnull(sum(CASE WHEN classification IS NULL AND generated = 0 THEN 1 ELSE 0 END), 0) pf,
|
|
557
|
+
ifnull(sum(CASE WHEN classification IS NULL AND generated = 0 THEN ifnull(lines, 0) ELSE 0 END), 0) pl,
|
|
558
|
+
ifnull(sum(CASE WHEN classification IS NULL AND generated = 1 THEN 1 ELSE 0 END), 0) gf,
|
|
559
|
+
ifnull(sum(CASE WHEN classification IS NOT NULL THEN 1 ELSE 0 END), 0) otf,
|
|
560
|
+
ifnull(sum(test), 0) t, ifnull(sum(test_cases), 0) tc FROM code_files WHERE repo = ?`).get(repo) as Record<string, number>;
|
|
561
|
+
const d = db.prepare('SELECT ifnull(sum(external = 0), 0) i, ifnull(sum(external = 1), 0) e FROM code_deps WHERE repo = ?').get(repo) as { i: number; e: number };
|
|
562
|
+
return { files: t.f!, lines: t.l!, productFiles: t.pf!, productLines: t.pl!, generatedFiles: t.gf!, otherFiles: t.otf!, testFiles: t.t!, testCases: t.tc!, deps: d.i, depsExternal: d.e };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* A directory outside version control: its files on disk (the scope's skip names apply; other scope items are left to
|
|
567
|
+
* themselves), each with its own time as read (Spec §2.11: `File time`) and the rebuild that first read it (`now`). Its
|
|
568
|
+
* code gets the same snapshot for the engine as a repository's, keyed by content.
|
|
569
|
+
*/
|
|
570
|
+
export function scanLooseCode(db: DatabaseSync, itemId: string, root: string, skip: (abs: string) => boolean, input: CodeScanInput = {}, force = false, now: string = new Date().toISOString()): CodeScanStats {
|
|
571
|
+
const found: { abs: string; rel: string; size: number; mtimeMs: number }[] = [];
|
|
572
|
+
const walk = (dir: string, depth: number) => {
|
|
573
|
+
if (depth > 20) return;
|
|
574
|
+
let entries: Dirent[];
|
|
575
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
576
|
+
for (const e of entries) {
|
|
577
|
+
if (e.isSymbolicLink()) continue;
|
|
578
|
+
const name = nameForm(e.name);
|
|
579
|
+
const full = join(dir, name);
|
|
580
|
+
if (e.isDirectory()) { if (!isSkippedName(name) && name !== '.git' && !skip(full)) walk(full, depth + 1); continue; }
|
|
581
|
+
if (!e.isFile()) continue;
|
|
582
|
+
try { const st = statSync(full); found.push({ abs: full, rel: full.slice(root.length + 1).split('\\').join('/'), size: st.size, mtimeMs: st.mtimeMs }); } catch { continue; }
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
walk(root, 0);
|
|
586
|
+
const engineDir = input.engineRoot ? engineDirOf(input.engineRoot, itemId) : null;
|
|
587
|
+
// Nothing on disk moved since the last read: the recorded structure stands.
|
|
588
|
+
const signature = `${found.length}:${found.reduce((n, f) => n + f.size, 0)}:${Math.round(found.reduce((m, f) => Math.max(m, f.mtimeMs), 0))}|${ENGINE_SIGNATURE}|${NAMING_RULE}`;
|
|
589
|
+
if (!force && getState(db, `loosecode:${itemId}`) === signature && !engineGone(db, itemId, engineDir)) return { ...codeTotals(db, itemId), head: null, skipped: true, engine: null };
|
|
590
|
+
const bufs = new Map<string, Buffer | null>();
|
|
591
|
+
for (const f of found) {
|
|
592
|
+
if (BINARY_EXT.has(extOf(f.rel)) || f.size > 4_000_000) { bufs.set(f.rel, null); continue; }
|
|
593
|
+
try { bufs.set(f.rel, readFileSync(f.abs)); } catch { bufs.set(f.rel, null); }
|
|
594
|
+
}
|
|
595
|
+
const texts = new Map<string, Buffer>();
|
|
596
|
+
const files = found.map((f) => {
|
|
597
|
+
const buf = bufs.get(f.rel) ?? null;
|
|
598
|
+
const ff = facts(f.rel, f.size, buf, null, input.classify?.(f.abs) ?? null);
|
|
599
|
+
if (buf && ff.lines !== null) texts.set(f.rel, buf);
|
|
600
|
+
return ff;
|
|
601
|
+
});
|
|
602
|
+
const reach = readReferences(files, texts, new Map(), engineDir);
|
|
603
|
+
// A file time the system cannot give (none, or the epoch) is no time: such a file is dated by its first reading.
|
|
604
|
+
writeFiles(db, itemId, files, new Map(), { now, fileTimes: new Map(found.filter((f) => Number.isFinite(f.mtimeMs) && f.mtimeMs > 0).map((f) => [f.rel, new Date(f.mtimeMs).toISOString()])) }, reach);
|
|
605
|
+
setState(db, `loosecode:${itemId}`, signature);
|
|
606
|
+
return { ...codeTotals(db, itemId), head: null, skipped: false, engine: reach.run && reach.snapshot ? { ...reach.run, snapshot: reach.snapshot } : null };
|
|
607
|
+
}
|