@c4a/extract 0.5.35-beta.1 → 0.5.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +147 -103
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,61 +1,162 @@
|
|
|
1
1
|
# @c4a/extract
|
|
2
2
|
|
|
3
|
-
Code extraction framework
|
|
3
|
+
Code extraction framework for C4A. It owns the language-plugin protocol, repository runner, raw code snapshot contract, digest generation, and shared Tree-sitter parsing utilities.
|
|
4
4
|
|
|
5
|
-
## Role in the
|
|
5
|
+
## Role in the Monorepo
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- Plugin registry for language-specific extraction plugins
|
|
9
|
-
- ExtractionResult v2 schema (SymbolInfo + RelationInfo)
|
|
10
|
-
- Digest generation (exported/internal symbol partitioning) and symbol diff
|
|
11
|
-
- Shared Tree-sitter parsing and file scanning utilities
|
|
7
|
+
`@c4a/extract` is the protocol and runner layer under `context capture --code`.
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
- Language plugins implement `ExtractionPlugin` and return `ExtractionResult` v2.
|
|
10
|
+
- The runner loads one or more plugins, scans repository modules, emits progress/module-error/summary events, and can build a raw code snapshot payload.
|
|
11
|
+
- `@c4a/context-cli` writes the snapshot under `.context/raw/aspect/code/<source-slug>/<snapshot-id>/`.
|
|
12
|
+
- `context compile --code <source-slug>` reads that snapshot and materializes package/category/symbol knowledge Nodes.
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
**Depends on:** `@c4a/core`, `web-tree-sitter`, `zod`
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
**Depended on by:** `@c4a/extract-ts`, `@c4a/context-cli`, `@c4a/daemon`, `@c4a/e2e`
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
- `ExtractionPluginRegistry` — register/resolve plugins by source manifest
|
|
22
|
-
- `FileSystem` — abstraction for file access (plugins don't touch disk directly)
|
|
23
|
-
- `SourceInfo` / `ManifestInfo` — source metadata passed to `canHandle`/`detectEntries`
|
|
24
|
-
- `EntryDetectionResult` — package info + entry files (supports monorepo recursion via `subPackages`)
|
|
25
|
-
- `PatternDetectionResult` — framework pattern detection (endpoints, implicit deps)
|
|
18
|
+
## Protocol Layers
|
|
26
19
|
|
|
27
|
-
###
|
|
20
|
+
### 1. Language Plugin Protocol
|
|
28
21
|
|
|
29
|
-
|
|
30
|
-
- `SymbolInfo` — extracted symbol: name, kind, visibility, file, line, members, params, returnType, etc.
|
|
31
|
-
- `RelationInfo` — extracted relation: type, from, to, grounding, confidence, source
|
|
22
|
+
Language packages implement `ExtractionPlugin` from `protocol.ts`.
|
|
32
23
|
|
|
33
|
-
|
|
24
|
+
```ts
|
|
25
|
+
interface ExtractionPlugin {
|
|
26
|
+
id: string;
|
|
27
|
+
languages: string[];
|
|
28
|
+
packageManagers: string[];
|
|
29
|
+
canHandle(source: SourceInfo): boolean;
|
|
30
|
+
detectEntries(manifest: ManifestInfo, fs: FileSystem): Promise<EntryDetectionResult>;
|
|
31
|
+
extractSymbols(entries: EntryFile[], fs: FileSystem): Promise<ExtractionResult>;
|
|
32
|
+
detectPatterns?(fs: FileSystem): Promise<PatternDetectionResult>;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Important constraints:
|
|
37
|
+
|
|
38
|
+
- Plugins read through `FileSystem`; they should not access `node:fs` directly.
|
|
39
|
+
- `detectEntries()` must return package identity, package kind, language, optional version, and entry files.
|
|
40
|
+
- `extractSymbols()` must return `ExtractionResult` v2 with stable symbols and relations.
|
|
41
|
+
- `detectEntries()` is called before `extractSymbols()`; plugins may keep per-detection package context between those calls.
|
|
42
|
+
|
|
43
|
+
### 2. ExtractionResult v2
|
|
44
|
+
|
|
45
|
+
Every plugin returns:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
{
|
|
49
|
+
version: "2",
|
|
50
|
+
meta: { extractedAt, pluginId, commitHash, language },
|
|
51
|
+
package: { name, kind, language, version? },
|
|
52
|
+
files: [{ path, language, lines }],
|
|
53
|
+
symbols: SymbolInfo[],
|
|
54
|
+
relations: RelationInfo[],
|
|
55
|
+
stats: { files, lines, exportedSymbols, internalSymbols, relations }
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`SymbolInfo` supports:
|
|
60
|
+
|
|
61
|
+
- identity: `name`, `kind`, `visibility`, `file`, `line`, `endLine`
|
|
62
|
+
- structure: nested `members`
|
|
63
|
+
- type surfaces: `params`, `returnType`, `typeAnnotation`, `extends`, `implements`, `propsType`, `unionValues`
|
|
64
|
+
- source documentation: `doc`
|
|
65
|
+
|
|
66
|
+
`RelationInfo` supports code edges such as `imports`, `imports_type`, `calls`, `extends`, `implements`, `param_type`, `return_type`, `of_type`, `depends_on`, and `contains`.
|
|
67
|
+
|
|
68
|
+
### 3. Repository Runner Protocol
|
|
69
|
+
|
|
70
|
+
The package exposes `c4a-extract-code`, a NDJSON runner used by `context capture --code`.
|
|
71
|
+
|
|
72
|
+
Input is JSON on stdin:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"repoPath": "/path/to/repo",
|
|
77
|
+
"modules": ["packages/example"],
|
|
78
|
+
"commitHash": "abc123",
|
|
79
|
+
"pathFilter": {},
|
|
80
|
+
"plugins": [{ "package": "@c4a/extract-ts", "exportName": "TypeScriptPlugin" }],
|
|
81
|
+
"snapshot": {
|
|
82
|
+
"sourceId": "aspect:code:example",
|
|
83
|
+
"sourceSlug": "example",
|
|
84
|
+
"snapshotId": "code-abc123-deadbeef",
|
|
85
|
+
"codeSnapshotContractVersion": "<contract-version>",
|
|
86
|
+
"scriptHash": "sha256:...",
|
|
87
|
+
"toolchain": {
|
|
88
|
+
"manager_package": "@c4a/context-cli",
|
|
89
|
+
"manager_version": "<manager-version>",
|
|
90
|
+
"runner_package": "@c4a/extract",
|
|
91
|
+
"runner_package_version": "<runner-version>",
|
|
92
|
+
"runner_bin": "c4a-extract-code",
|
|
93
|
+
"plugin_package": "@c4a/extract-ts",
|
|
94
|
+
"plugin_package_version": "<plugin-version>",
|
|
95
|
+
"plugin_export": "TypeScriptPlugin"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Output is one JSON object per line:
|
|
102
|
+
|
|
103
|
+
- `{ "type": "progress", "phase": "scanning|parsing|uploading", ... }`
|
|
104
|
+
- `{ "type": "module_error", "module_name": "...", "module_path": "...", "error": "..." }`
|
|
105
|
+
- `{ "type": "summary", "extraction": ..., "snapshot": ... }`
|
|
106
|
+
- `{ "type": "error", "code": "runner-failed", "message": "..." }`
|
|
107
|
+
|
|
108
|
+
The runner does not write `.context` directly. It returns snapshot files in the summary; `@c4a/context-cli` validates and writes them atomically.
|
|
109
|
+
|
|
110
|
+
### 4. Raw Code Snapshot Contract
|
|
34
111
|
|
|
35
|
-
|
|
36
|
-
- `generateSymbolDiff(current, previous)` — compare two digests, output `{ added, removed }`
|
|
37
|
-
- `DigestData` / `SymbolDiff` types
|
|
112
|
+
When `snapshot` input is provided, the runner builds these files:
|
|
38
113
|
|
|
39
|
-
|
|
114
|
+
| File | Purpose |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `source.yaml` | Source manifest for the code aspect source |
|
|
117
|
+
| `manifest.json` | Snapshot manifest: contract version, toolchain, counts, hash, dirty state |
|
|
118
|
+
| `_meta.yaml` | Backward-compatible snapshot metadata and input summary |
|
|
119
|
+
| `digests.jsonl` | Per-module digest rows with version, hash, dirty state, and digest payload |
|
|
120
|
+
| `source-files.jsonl` | Source-to-module/digest mapping |
|
|
121
|
+
| `packages.jsonl` | Package rows: name, kind, language, module path, optional version/description |
|
|
122
|
+
| `symbols.jsonl` | Flat symbol rows; nested members are flattened and retain package/module fields |
|
|
123
|
+
| `edges.jsonl` | Code relation rows with package/module/version/hash fields |
|
|
40
124
|
|
|
41
|
-
-
|
|
42
|
-
- `scanSourceFiles()` / `detectModules()` / `detectModuleAt()` — file system scanning and module detection
|
|
43
|
-
- `getGitCommitHash()` — reads the current git commit hash
|
|
44
|
-
- `detectTechStack()` — identifies languages and frameworks in a repository
|
|
125
|
+
`@c4a/context-cli` validates this contract before projection. Required fields include package/module identity, version labels on digest/source-file/edge rows, symbol identity fields, and matching edge/digest versions.
|
|
45
126
|
|
|
46
|
-
|
|
127
|
+
During projection, code-owned Sections receive code `source_ref` values derived from these rows:
|
|
47
128
|
|
|
48
|
-
|
|
129
|
+
- package rows: `src-N#package:<package>@<hash>`
|
|
130
|
+
- symbol rows: `src-N#symbol:<locator>:<kind>@<hash>`
|
|
49
131
|
|
|
50
|
-
|
|
132
|
+
These refs are verified against the raw code snapshot JSONL indexes. They are separate from prose evidence refs, because code snapshots use `evidence.mode: none` and do not create raw block manifests.
|
|
133
|
+
|
|
134
|
+
## Writing a New Language Plugin
|
|
135
|
+
|
|
136
|
+
Create a package such as `@c4a/extract-python` and export an `ExtractionPlugin`.
|
|
137
|
+
|
|
138
|
+
Minimum requirements:
|
|
139
|
+
|
|
140
|
+
1. Detect the language manifest in `canHandle()` and `detectEntries()`.
|
|
141
|
+
2. Return stable package identity: package name, kind, language, and version when available.
|
|
142
|
+
3. Return `subPackages` when one manifest represents a nested package layout.
|
|
143
|
+
4. Resolve public entry files so exported symbols can be distinguished from internal symbols.
|
|
144
|
+
5. Emit `SymbolInfo[]` with stable `name`, `kind`, `visibility`, `file`, `line`, and `endLine`.
|
|
145
|
+
6. Emit `RelationInfo[]` for imports and important type/inheritance/use edges.
|
|
146
|
+
7. Keep paths module-relative inside the plugin; the repository runner prefixes them to repo-relative paths.
|
|
147
|
+
8. Register the plugin in the runner input used by `context capture --code`.
|
|
148
|
+
|
|
149
|
+
Example skeleton:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
51
152
|
import type {
|
|
153
|
+
EntryDetectionResult,
|
|
154
|
+
EntryFile,
|
|
52
155
|
ExtractionPlugin,
|
|
53
156
|
ExtractionResult,
|
|
54
|
-
EntryDetectionResult,
|
|
55
157
|
FileSystem,
|
|
56
158
|
ManifestInfo,
|
|
57
159
|
SourceInfo,
|
|
58
|
-
EntryFile,
|
|
59
160
|
} from "@c4a/extract";
|
|
60
161
|
|
|
61
162
|
export class PythonPlugin implements ExtractionPlugin {
|
|
@@ -64,85 +165,28 @@ export class PythonPlugin implements ExtractionPlugin {
|
|
|
64
165
|
readonly packageManagers = ["pip"];
|
|
65
166
|
|
|
66
167
|
canHandle(source: SourceInfo): boolean {
|
|
67
|
-
|
|
68
|
-
return source.manifests.some(
|
|
69
|
-
(m) => m.type === "pyproject.toml",
|
|
70
|
-
);
|
|
168
|
+
return source.manifests.some((manifest) => manifest.type === "pyproject.toml");
|
|
71
169
|
}
|
|
72
170
|
|
|
73
|
-
async detectEntries(
|
|
74
|
-
|
|
75
|
-
fs: FileSystem,
|
|
76
|
-
): Promise<EntryDetectionResult> {
|
|
77
|
-
// Parse pyproject.toml to find package name, kind, entry points
|
|
78
|
-
// Return: { package: { name, kind, language }, entries: [...] }
|
|
171
|
+
async detectEntries(manifest: ManifestInfo, fs: FileSystem): Promise<EntryDetectionResult> {
|
|
172
|
+
// Parse pyproject.toml/setup metadata and return package + entry files.
|
|
79
173
|
}
|
|
80
174
|
|
|
81
|
-
async extractSymbols(
|
|
82
|
-
|
|
83
|
-
fs: FileSystem,
|
|
84
|
-
): Promise<ExtractionResult> {
|
|
85
|
-
// Use Tree-sitter with Python grammar to parse AST
|
|
86
|
-
// Extract SymbolInfo[] (functions, classes, etc.)
|
|
87
|
-
// Extract RelationInfo[] (imports, inheritance, etc.)
|
|
88
|
-
// Return ExtractionResult v2
|
|
175
|
+
async extractSymbols(entries: EntryFile[], fs: FileSystem): Promise<ExtractionResult> {
|
|
176
|
+
// Parse entry graph, classify exported/internal symbols, emit relations.
|
|
89
177
|
}
|
|
90
178
|
}
|
|
91
179
|
```
|
|
92
180
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
| Method | When called | Returns |
|
|
96
|
-
|--------|------------|---------|
|
|
97
|
-
| `canHandle(source)` | Registry resolves which plugin handles a source | `boolean` |
|
|
98
|
-
| `detectEntries(manifest, fs)` | Layer 0: identify package info + entry files | `EntryDetectionResult` |
|
|
99
|
-
| `extractSymbols(entries, fs)` | Layer 1: AST parsing + symbol extraction | `ExtractionResult` |
|
|
100
|
-
| `detectPatterns?(fs)` | Layer 2 (optional): framework-specific patterns | `PatternDetectionResult` |
|
|
101
|
-
|
|
102
|
-
### Registration
|
|
103
|
-
|
|
104
|
-
Plugins are currently registered statically in the daemon:
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
import { ExtractionPluginRegistry } from "@c4a/extract";
|
|
108
|
-
import { TypeScriptPlugin } from "@c4a/extract-ts";
|
|
109
|
-
|
|
110
|
-
const registry = new ExtractionPluginRegistry();
|
|
111
|
-
registry.register(new TypeScriptPlugin());
|
|
112
|
-
|
|
113
|
-
const plugin = registry.resolve(sourceInfo);
|
|
114
|
-
if (plugin) {
|
|
115
|
-
const entries = await plugin.detectEntries(manifest, fs);
|
|
116
|
-
const result = await plugin.extractSymbols(entries.entries, fs);
|
|
117
|
-
}
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### FileSystem abstraction
|
|
121
|
-
|
|
122
|
-
Plugins access files through the `FileSystem` interface, not directly via `node:fs`. This enables future API-based file loading without changing plugin code.
|
|
181
|
+
## Relationship to Code Compile and Obsidian
|
|
123
182
|
|
|
124
|
-
|
|
125
|
-
interface FileSystem {
|
|
126
|
-
readFile(path: string): Promise<string>;
|
|
127
|
-
readdir(path: string): Promise<string[]>;
|
|
128
|
-
exists(path: string): Promise<boolean>;
|
|
129
|
-
readJson<T = unknown>(path: string): Promise<T>;
|
|
130
|
-
}
|
|
131
|
-
```
|
|
183
|
+
`@c4a/extract` is upstream of code compile; it does not render knowledge itself.
|
|
132
184
|
|
|
133
|
-
|
|
185
|
+
- `context capture --code` uses the runner to produce raw code snapshots.
|
|
186
|
+
- `context compile --code` consumes `packages.jsonl`, `symbols.jsonl`, `edges.jsonl`, and `digests.jsonl` to build package/category/symbol Nodes such as `pkg`, `pkg/components`, and `pkg/symbol/button`.
|
|
187
|
+
- Obsidian Render reads the compiled Markdown, `_edges.yaml`, and `_external.yaml`. It does not read raw runner snapshots directly.
|
|
134
188
|
|
|
135
|
-
|
|
136
|
-
{
|
|
137
|
-
version: "2",
|
|
138
|
-
meta: { extractedAt, pluginId, commitHash, language },
|
|
139
|
-
package: { name, kind, language },
|
|
140
|
-
files: [{ path, language, lines }],
|
|
141
|
-
symbols: SymbolInfo[], // exported + internal
|
|
142
|
-
relations: RelationInfo[], // imports, calls, type refs, etc.
|
|
143
|
-
stats: { files, lines, exportedSymbols, internalSymbols, relations },
|
|
144
|
-
}
|
|
145
|
-
```
|
|
189
|
+
That means language plugins affect Obsidian only through the compiled knowledge graph: better symbols/relations produce better symbol Nodes, graph edges, source refs, and source-status chips.
|
|
146
190
|
|
|
147
191
|
## Development
|
|
148
192
|
|