@c4a/extract 0.5.41-beta.7 → 0.6.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,15 +1,16 @@
1
1
  # @c4a/extract
2
2
 
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.
3
+ Code extraction framework for Context. It owns the language-plugin protocol, repository runner, raw code snapshot contract, digest generation, and shared Tree-sitter parsing utilities.
4
4
 
5
5
  ## Role in the Monorepo
6
6
 
7
- `@c4a/extract` is the protocol and runner layer under `context capture --code`.
7
+ `@c4a/extract` is the protocol and runner layer used by declared Context
8
+ project extraction phases such as `extractTs({ source, collection: "wikis" })`.
8
9
 
9
10
  - 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.
11
+ - The runner loads one or more plugins, scans repository modules, emits progress/module-error/summary events, and can build an optional low-level code snapshot payload for tooling/tests.
12
+ - `@c4a/context-cli` invokes the runner from `context run extract:<source>:wikis` after the workspace declares an `extractTs` phase.
13
+ - The project flow stages review candidates under `unapproved/entities.jsonl` and keeps runtime candidate artifacts under `.tmp/context-runtime/extract/`; approved Markdown is written only by review/apply.
13
14
 
14
15
  **Depends on:** `@c4a/core`, `web-tree-sitter`, `zod`
15
16
 
@@ -67,7 +68,10 @@ Every plugin returns:
67
68
 
68
69
  ### 3. Repository Runner Protocol
69
70
 
70
- The package exposes `c4a-extract-code`, a NDJSON runner used by `context capture --code`.
71
+ The package exposes `c4a-extract-code`, a low-level NDJSON runner. The current
72
+ Context project flow normally reaches it through a declared `extractTs` phase
73
+ and `context run extract:<source>:wikis`; agents should not hand-build runner
74
+ input for normal workspace operation.
71
75
 
72
76
  Input is JSON on stdin:
73
77
 
@@ -129,7 +133,11 @@ During projection, code-owned Sections receive code `source_ref` values derived
129
133
  - package rows: `src-N#package:<package>@<hash>`
130
134
  - symbol rows: `src-N#symbol:<locator>:<kind>@<hash>`
131
135
 
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.
136
+ These refs are verified against the raw code snapshot JSONL indexes. They are
137
+ separate from prose evidence refs, because code snapshots use
138
+ `evidence.mode: none` and do not create raw block manifests. Prose/raw evidence
139
+ uses `src-N#block:<locator> L<start>-<end>@<block-hash>` so it can keep line
140
+ ranges for review, diffing, and re-anchoring.
133
141
 
134
142
  ## Writing a New Language Plugin
135
143
 
@@ -144,7 +152,9 @@ Minimum requirements:
144
152
  5. Emit `SymbolInfo[]` with stable `name`, `kind`, `visibility`, `file`, `line`, and `endLine`.
145
153
  6. Emit `RelationInfo[]` for imports and important type/inheritance/use edges.
146
154
  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`.
155
+ 8. Register the plugin in the runner input used by the project extraction
156
+ phase. User-facing workspaces should declare the matching phase in
157
+ `src/index.ts` and execute it through `context run <phase-id>`.
148
158
 
149
159
  Example skeleton:
150
160
 
@@ -178,15 +188,29 @@ export class PythonPlugin implements ExtractionPlugin {
178
188
  }
179
189
  ```
180
190
 
181
- ## Relationship to Code Compile and Obsidian
182
-
183
- `@c4a/extract` is upstream of code compile; it does not render knowledge itself.
184
-
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.
188
-
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.
191
+ ## Relationship to Context Project Extraction
192
+
193
+ `@c4a/extract` is upstream of the Context project extraction phase; it does not
194
+ render approved knowledge itself.
195
+
196
+ - `context source add repo <name> --local <repo-or-subdir>` registers the code
197
+ source boundary.
198
+ - `src/index.ts` declares `extractTs({ source: source("<name>"), collection: "wikis" })`.
199
+ - `context run extract:<name>:wikis --dry-run` previews resolved modules,
200
+ candidate estimates, knowledge path examples, and module warnings.
201
+ - `context run extract:<name>:wikis` writes draft candidates for review. It
202
+ uses `.tmp/context-runtime/extract/` for candidate snapshots, source
203
+ fingerprints, and symbol indexes; that scratch directory is not committed
204
+ state.
205
+ - `context review html` and `context review apply` own human approval and
206
+ approved Markdown materialization.
207
+ - `context close`, `context verify`, and `context build` own final package
208
+ readiness.
209
+
210
+ That means language plugins affect published knowledge only through the current
211
+ project flow: better symbols/relations produce better draft candidates, stable
212
+ `repo:<source>#symbol:...` source refs, review evidence, approved Markdown, and
213
+ package output.
190
214
 
191
215
  ## Development
192
216
 
@@ -14111,6 +14111,29 @@ var readModuleName = async (dirPath, manifestFile) => {
14111
14111
  }
14112
14112
  return null;
14113
14113
  };
14114
+ var readModuleVersion = async (dirPath, manifestFile) => {
14115
+ try {
14116
+ const content = await readFile(join(dirPath, manifestFile), "utf-8");
14117
+ if (manifestFile === "package.json") {
14118
+ const parsed = JSON.parse(content);
14119
+ return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : null;
14120
+ }
14121
+ if (manifestFile === "pyproject.toml" || manifestFile === "Cargo.toml") {
14122
+ const match = content.match(/^\s*version\s*=\s*["']([^"']+)["']/m);
14123
+ return match?.[1] ?? null;
14124
+ }
14125
+ if (manifestFile === "setup.py") {
14126
+ const match = content.match(/version\s*=\s*["']([^"']+)["']/);
14127
+ return match?.[1] ?? null;
14128
+ }
14129
+ if (manifestFile === "pom.xml") {
14130
+ return content.match(/<version>\s*([^<]+?)\s*<\/version>/i)?.[1] ?? null;
14131
+ }
14132
+ } catch {
14133
+ return null;
14134
+ }
14135
+ return null;
14136
+ };
14114
14137
  var MANIFEST_NAMES = new Set(MANIFEST_FILES.map((f) => f.toLowerCase()));
14115
14138
  var findSubModuleDirs = async (rootDir, excludeDirs, gitFiles, pathFilter2) => {
14116
14139
  const root = resolve(rootDir);
@@ -14228,6 +14251,35 @@ var detectModules = async (repoPath, ref, pathFilter2) => {
14228
14251
  }
14229
14252
  return modules.sort((a, b) => a.name.localeCompare(b.name));
14230
14253
  };
14254
+ var detectModuleBoundaries = async (repoPath, ref, pathFilter2) => {
14255
+ const repoRoot = resolve(repoPath);
14256
+ const gitFiles = await tryGitListFiles(repoRoot, ref);
14257
+ const subModuleDirs = await findSubModuleDirs(repoRoot, undefined, gitFiles, pathFilter2);
14258
+ const boundaries = [];
14259
+ const rootManifest = await findManifestFile(repoRoot);
14260
+ if (rootManifest) {
14261
+ const version2 = await readModuleVersion(repoRoot, rootManifest);
14262
+ boundaries.push({
14263
+ name: await readModuleName(repoRoot, rootManifest) ?? basename(repoRoot),
14264
+ path: ".",
14265
+ manifest: rootManifest,
14266
+ ...version2 !== null ? { version: version2 } : {}
14267
+ });
14268
+ }
14269
+ for (const subDir of subModuleDirs) {
14270
+ const manifest = await findManifestFile(subDir);
14271
+ if (!manifest)
14272
+ continue;
14273
+ const version2 = await readModuleVersion(subDir, manifest);
14274
+ boundaries.push({
14275
+ name: await readModuleName(subDir, manifest) ?? basename(subDir),
14276
+ path: toPosixPath(relative(repoRoot, subDir)) || ".",
14277
+ manifest,
14278
+ ...version2 !== null ? { version: version2 } : {}
14279
+ });
14280
+ }
14281
+ return boundaries.sort((left, right) => left.path.localeCompare(right.path));
14282
+ };
14231
14283
  var detectModuleAt = async (repoPath, modulePath, ref, pathFilter2) => {
14232
14284
  const repoRoot = resolve(repoPath);
14233
14285
  const moduleDir = modulePath === "." ? repoRoot : resolve(repoRoot, modulePath);
package/index.js CHANGED
@@ -8660,7 +8660,7 @@ var require_picomatch2 = __commonJS((exports, module2) => {
8660
8660
 
8661
8661
  // ../../node_modules/.bun/web-tree-sitter@0.20.8/node_modules/web-tree-sitter/tree-sitter.js
8662
8662
  var require_tree_sitter = __commonJS((exports, module2) => {
8663
- var __dirname = "/Users/bytedance/github/c4a/node_modules/.bun/web-tree-sitter@0.20.8/node_modules/web-tree-sitter";
8663
+ var __dirname = "/Users/bytedance/github/c4a/context/node_modules/.bun/web-tree-sitter@0.20.8/node_modules/web-tree-sitter";
8664
8664
  var Module = Module !== undefined ? Module : {};
8665
8665
  var TreeSitter = function() {
8666
8666
  var initPromise, document = typeof window == "object" ? { currentScript: window.document.currentScript } : null;
@@ -15263,7 +15263,9 @@ var symbolInfoSchema = exports_external.lazy(() => exports_external.object({
15263
15263
  implements: exports_external.array(exports_external.string().min(1)).optional(),
15264
15264
  doc: exports_external.string().nullable().optional(),
15265
15265
  propsType: exports_external.string().min(1).nullable().optional(),
15266
- unionValues: exports_external.array(exports_external.string()).optional()
15266
+ unionValues: exports_external.array(exports_external.string()).optional(),
15267
+ initializer: exports_external.string().min(1).nullable().optional(),
15268
+ signature: exports_external.string().min(1).nullable().optional()
15267
15269
  }));
15268
15270
  var relationInfoSchema = exports_external.object({
15269
15271
  type: relationTypeSchema,
@@ -15463,6 +15465,530 @@ var generateSymbolDiff = (current, previous) => {
15463
15465
  removed: previousNames.filter((name) => !currentSet.has(name))
15464
15466
  };
15465
15467
  };
15468
+ // src/documentEvidence.ts
15469
+ import { Buffer as Buffer2 } from "node:buffer";
15470
+ import { createHash as createHash2 } from "node:crypto";
15471
+ var DOCUMENT_EVIDENCE_NORMALIZER_VERSION = "document-evidence-normalizer.v1";
15472
+ var DOCUMENT_SNAPSHOT_MANIFEST_SCHEMA_VERSION = "document.snapshot.v2";
15473
+ var DEFAULT_SOURCE_SPAN_HASH_LENGTH = 12;
15474
+ var BOM = "\uFEFF";
15475
+ var HASH_ID_RE = /^(?:sha256:)?[a-f0-9]{64}$/u;
15476
+ var SOURCE_SPAN_HASH_RE = /^[a-f0-9]{8,64}$/u;
15477
+ var DOCUMENT_SOURCE_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/u;
15478
+ function isRecord(value) {
15479
+ return value !== null && typeof value === "object" && !Array.isArray(value);
15480
+ }
15481
+ function bytesOf(value) {
15482
+ return typeof value === "string" ? Buffer2.from(value, "utf8") : Buffer2.from(value);
15483
+ }
15484
+ function sha256Hex(value) {
15485
+ return createHash2("sha256").update(bytesOf(value)).digest("hex");
15486
+ }
15487
+ function normalizeHashId(value) {
15488
+ const trimmed = value.trim().toLowerCase();
15489
+ const hex = trimmed.startsWith("sha256:") ? trimmed.slice("sha256:".length) : trimmed;
15490
+ if (!/^[a-f0-9]{64}$/u.test(hex)) {
15491
+ throw new TypeError(`invalid sha256 hash id: ${value}`);
15492
+ }
15493
+ return `sha256:${hex}`;
15494
+ }
15495
+ function assertDocumentSourceType(value) {
15496
+ if (value !== "file" && value !== "lark") {
15497
+ throw new TypeError(`document source_type must be file or lark: ${value}`);
15498
+ }
15499
+ }
15500
+ function normalizeDocumentSourceName(name) {
15501
+ const value = name.trim();
15502
+ if (!DOCUMENT_SOURCE_NAME_RE.test(value)) {
15503
+ throw new TypeError(`document source name must be a lowercase path-safe slug: ${name}`);
15504
+ }
15505
+ return value;
15506
+ }
15507
+ function normalizeSnapshotRelativePath(path2) {
15508
+ const value = path2.trim();
15509
+ if (value.length === 0 || value.includes("\x00") || value.startsWith("/") || /^[a-zA-Z]:[\\/]/u.test(value)) {
15510
+ throw new TypeError(`snapshot path must be a POSIX relative path: ${path2}`);
15511
+ }
15512
+ if (value.includes("\\")) {
15513
+ throw new TypeError(`snapshot path must use POSIX separators: ${path2}`);
15514
+ }
15515
+ const segments = value.split("/");
15516
+ if (segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
15517
+ throw new TypeError(`snapshot path must not contain empty, dot, or traversal segments: ${path2}`);
15518
+ }
15519
+ return value;
15520
+ }
15521
+ function encodeSnapshotLocatorPath(path2) {
15522
+ return normalizeSnapshotRelativePath(path2).split("/").map((segment) => encodeURIComponent(segment)).join("/");
15523
+ }
15524
+ function decodeSnapshotLocatorPath(path2) {
15525
+ try {
15526
+ return normalizeSnapshotRelativePath(path2.split("/").map((segment) => decodeURIComponent(segment)).join("/"));
15527
+ } catch (error) {
15528
+ const message = error instanceof Error ? error.message : String(error);
15529
+ throw new TypeError(`invalid encoded snapshot locator path: ${path2}: ${message}`);
15530
+ }
15531
+ }
15532
+ function parseDocumentSourceLocator(source2) {
15533
+ const match = /^(file|lark):([^/]+)\/(.+)$/u.exec(source2);
15534
+ if (match?.[1] === undefined || match[2] === undefined || match[3] === undefined)
15535
+ return null;
15536
+ try {
15537
+ return {
15538
+ sourceType: match[1],
15539
+ sourceName: normalizeDocumentSourceName(match[2]),
15540
+ documentPath: decodeSnapshotLocatorPath(match[3])
15541
+ };
15542
+ } catch {
15543
+ return null;
15544
+ }
15545
+ }
15546
+ function normalizeMarkdownDocument(input) {
15547
+ let text = input.normalize("NFC");
15548
+ if (text.startsWith(BOM)) {
15549
+ text = text.slice(BOM.length);
15550
+ }
15551
+ text = text.replace(/\r\n?/g, `
15552
+ `);
15553
+ text = text.split(`
15554
+ `).map((line) => line.replace(/[ \t]+$/u, "")).join(`
15555
+ `);
15556
+ text = text.replace(/\n{3,}/g, `
15557
+
15558
+ `);
15559
+ text = text.replace(/^(\s*)[*+]([ \t])/gmu, "$1-$2");
15560
+ const fenceCount = (text.match(/^```/gmu) ?? []).length;
15561
+ if (fenceCount % 2 === 1) {
15562
+ if (!text.endsWith(`
15563
+ `))
15564
+ text += `
15565
+ `;
15566
+ text += "```\n";
15567
+ }
15568
+ return text;
15569
+ }
15570
+ function computeDocumentContentHash(bytes) {
15571
+ return `sha256:${sha256Hex(bytes)}`;
15572
+ }
15573
+ function countMarkdownLines(bytes) {
15574
+ const text = bytesOf(bytes).toString("utf8");
15575
+ if (text.length === 0)
15576
+ return 0;
15577
+ const normalized = text.replace(/\r\n?/g, `
15578
+ `);
15579
+ return normalized.endsWith(`
15580
+ `) ? normalized.split(`
15581
+ `).length - 1 : normalized.split(`
15582
+ `).length;
15583
+ }
15584
+ function computeLogicalRawHash(files) {
15585
+ const hash2 = createHash2("sha256");
15586
+ const sorted = [...files].map((file) => ({
15587
+ path: normalizeSnapshotRelativePath(file.path),
15588
+ bytes: bytesOf(file.bytes)
15589
+ })).sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
15590
+ for (const file of sorted) {
15591
+ hash2.update(Buffer2.from(file.path, "utf8"));
15592
+ hash2.update("\x00");
15593
+ hash2.update(Buffer2.from(String(file.bytes.byteLength), "utf8"));
15594
+ hash2.update("\x00");
15595
+ hash2.update(file.bytes);
15596
+ hash2.update(`
15597
+ `);
15598
+ }
15599
+ return `sha256:${hash2.digest("hex")}`;
15600
+ }
15601
+ function slugSegment(value) {
15602
+ return value.trim().toLowerCase().normalize("NFC").replace(/[`*_~[\]()]/gu, "").replace(/[^\p{Letter}\p{Number}]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 64) || "document";
15603
+ }
15604
+ function headingFromLine(line) {
15605
+ const match = /^(#{1,6})\s+(.+?)\s*#*\s*$/u.exec(line);
15606
+ if (!match)
15607
+ return null;
15608
+ const title = (match[2] ?? "").trim();
15609
+ if (title.length === 0)
15610
+ return null;
15611
+ return { level: match[1].length, title };
15612
+ }
15613
+ function fenceMarker(line) {
15614
+ const match = /^\s{0,3}(`{3,}|~{3,})/u.exec(line);
15615
+ if (!match?.[1])
15616
+ return null;
15617
+ return { marker: match[1][0], length: match[1].length };
15618
+ }
15619
+ function truncateSourceSpanHash(hash2, length = DEFAULT_SOURCE_SPAN_HASH_LENGTH) {
15620
+ if (!/^[a-f0-9]{64}$/u.test(hash2)) {
15621
+ throw new TypeError(`source span hash must be a 64-character lowercase sha256 hex digest: ${hash2}`);
15622
+ }
15623
+ if (!Number.isInteger(length) || length < 8 || length > 64) {
15624
+ throw new TypeError(`source span hash length must be an integer between 8 and 64: ${length}`);
15625
+ }
15626
+ return hash2.slice(0, length);
15627
+ }
15628
+ function snapshotTextLines(markdown) {
15629
+ if (markdown.length === 0)
15630
+ return [];
15631
+ const lines = markdown.split(`
15632
+ `);
15633
+ if (markdown.endsWith(`
15634
+ `))
15635
+ lines.pop();
15636
+ return lines;
15637
+ }
15638
+ function headingContextForLine(markdown, lineStart) {
15639
+ const lines = snapshotTextLines(markdown);
15640
+ const headings = [];
15641
+ let fence = null;
15642
+ const maxIndex = Math.min(Math.max(lineStart, 1), lines.length);
15643
+ for (let index = 0;index < maxIndex; index += 1) {
15644
+ const line = lines[index] ?? "";
15645
+ const marker = fenceMarker(line);
15646
+ if (fence !== null) {
15647
+ if (marker !== null && marker.marker === fence.marker && marker.length >= fence.length) {
15648
+ fence = null;
15649
+ }
15650
+ continue;
15651
+ }
15652
+ if (marker !== null) {
15653
+ fence = marker;
15654
+ continue;
15655
+ }
15656
+ const heading = headingFromLine(line);
15657
+ if (heading !== null) {
15658
+ headings.splice(heading.level - 1);
15659
+ headings[heading.level - 1] = {
15660
+ ...heading,
15661
+ headingSlug: slugSegment(heading.title)
15662
+ };
15663
+ }
15664
+ }
15665
+ const headingPath = headings.flatMap((heading) => heading === undefined ? [] : [heading.title]);
15666
+ const headingHint = [...headings].reverse().find((heading) => heading !== undefined)?.headingSlug ?? "document";
15667
+ return {
15668
+ heading_hint: headingHint,
15669
+ heading_path: headingPath
15670
+ };
15671
+ }
15672
+ function createDocumentSourceSpan(markdown, input) {
15673
+ const lines = snapshotTextLines(markdown);
15674
+ const lineStart = input.lineStart;
15675
+ const lineEnd = input.lineEnd;
15676
+ if (!Number.isInteger(lineStart) || !Number.isInteger(lineEnd) || lineStart < 1 || lineEnd < lineStart) {
15677
+ throw new TypeError(`source span line range must be positive and ordered: L${lineStart}-${lineEnd}`);
15678
+ }
15679
+ if (lineEnd > lines.length) {
15680
+ throw new TypeError(`source span line range exceeds document line count: L${lineStart}-${lineEnd}`);
15681
+ }
15682
+ const text = lines.slice(lineStart - 1, lineEnd).join(`
15683
+ `);
15684
+ const fullSpanHash = sha256Hex(text);
15685
+ const heading = headingContextForLine(markdown, lineStart);
15686
+ return {
15687
+ heading_hint: heading.heading_hint,
15688
+ heading_path: heading.heading_path,
15689
+ line_start: lineStart,
15690
+ line_end: lineEnd,
15691
+ line_range: `L${lineStart}-${lineEnd}`,
15692
+ span_hash: truncateSourceSpanHash(fullSpanHash, input.hashLength ?? DEFAULT_SOURCE_SPAN_HASH_LENGTH),
15693
+ full_span_hash: fullSpanHash,
15694
+ text,
15695
+ text_preview: text.replace(/\s+/gu, " ").slice(0, 160)
15696
+ };
15697
+ }
15698
+ function formatSpanSourceRef(span, options = {}) {
15699
+ const hash2 = options.hashLength === undefined ? span.span_hash : truncateSourceSpanHash(span.full_span_hash, options.hashLength);
15700
+ const body2 = `#span:${span.heading_hint} L${span.line_start}-${span.line_end}@${hash2}`;
15701
+ return options.locator !== undefined ? `${options.locator}${body2}` : body2;
15702
+ }
15703
+ function formatCanonicalProseSourceRef(input) {
15704
+ const sourceName = normalizeDocumentSourceName(input.sourceName);
15705
+ const documentPath = normalizeSnapshotRelativePath(input.documentPath);
15706
+ return formatSpanSourceRef(input.span, {
15707
+ locator: `${input.sourceType}:${sourceName}/${encodeSnapshotLocatorPath(documentPath)}`,
15708
+ ...input.hashLength !== undefined ? { hashLength: input.hashLength } : {}
15709
+ });
15710
+ }
15711
+ function parseSpanSourceRef(value) {
15712
+ const normalized = value.trim();
15713
+ const match = /^(?:(?<locator>[^#\s]+))?#span:(?<heading>[^\s#]+)\s+L(?<start>\d+)-(?<end>\d+)@(?<hash>[a-f0-9]{8,64})$/u.exec(normalized);
15714
+ if (!match?.groups)
15715
+ return null;
15716
+ const lineStart = Number(match.groups.start);
15717
+ const lineEnd = Number(match.groups.end);
15718
+ if (!Number.isInteger(lineStart) || !Number.isInteger(lineEnd) || lineStart < 1 || lineEnd < lineStart) {
15719
+ return null;
15720
+ }
15721
+ return {
15722
+ ...match.groups.locator !== undefined ? { locator: match.groups.locator } : {},
15723
+ heading_hint: match.groups.heading,
15724
+ line_start: lineStart,
15725
+ line_end: lineEnd,
15726
+ span_hash: match.groups.hash
15727
+ };
15728
+ }
15729
+ function sourceSpanHashMatches(refHash, fullSpanHash) {
15730
+ return SOURCE_SPAN_HASH_RE.test(refHash) && /^[a-f0-9]{64}$/u.test(fullSpanHash) && fullSpanHash.startsWith(refHash);
15731
+ }
15732
+ function createDocumentSnapshotFileEntry(input) {
15733
+ const path2 = normalizeSnapshotRelativePath(input.path);
15734
+ if ((input.title?.trim().length ?? 0) === 0 && (input.locator?.trim().length ?? 0) === 0) {
15735
+ throw new TypeError(`snapshot file "${path2}" must include title or locator`);
15736
+ }
15737
+ return {
15738
+ path: path2,
15739
+ content_hash: computeDocumentContentHash(input.bytes),
15740
+ line_count: countMarkdownLines(input.bytes),
15741
+ ...input.title !== undefined ? { title: input.title.trim() } : {},
15742
+ ...input.locator !== undefined ? { locator: input.locator.trim() } : {}
15743
+ };
15744
+ }
15745
+ function createDocumentSnapshotManifest(input) {
15746
+ const files = input.files.map(createDocumentSnapshotFileEntry);
15747
+ const sourceName = normalizeDocumentSourceName(input.sourceName);
15748
+ return parseDocumentSnapshotManifest({
15749
+ schema_version: DOCUMENT_SNAPSHOT_MANIFEST_SCHEMA_VERSION,
15750
+ source_type: input.sourceType,
15751
+ source_name: sourceName,
15752
+ captured_at: input.capturedAt,
15753
+ snapshot_hash: computeLogicalRawHash(input.files),
15754
+ normalizer_version: input.normalizerVersion ?? DOCUMENT_EVIDENCE_NORMALIZER_VERSION,
15755
+ files,
15756
+ ...input.assets !== undefined ? { assets: input.assets } : {},
15757
+ ...input.metadata !== undefined ? { metadata: input.metadata } : {}
15758
+ });
15759
+ }
15760
+ function parseFileEntry(value, index) {
15761
+ if (!isRecord(value)) {
15762
+ throw new TypeError(`snapshot manifest files[${index}] must be an object`);
15763
+ }
15764
+ if (typeof value.path !== "string") {
15765
+ throw new TypeError(`snapshot manifest files[${index}].path must be a string`);
15766
+ }
15767
+ if (typeof value.content_hash !== "string" || !HASH_ID_RE.test(value.content_hash)) {
15768
+ throw new TypeError(`snapshot manifest files[${index}].content_hash must be sha256:<64 hex>`);
15769
+ }
15770
+ if (!Number.isInteger(value.line_count) || value.line_count < 0) {
15771
+ throw new TypeError(`snapshot manifest files[${index}].line_count must be a non-negative integer`);
15772
+ }
15773
+ const title = typeof value.title === "string" && value.title.trim().length > 0 ? value.title.trim() : undefined;
15774
+ const locator = typeof value.locator === "string" && value.locator.trim().length > 0 ? value.locator.trim() : undefined;
15775
+ if (title === undefined && locator === undefined) {
15776
+ throw new TypeError(`snapshot manifest files[${index}] must include title or locator`);
15777
+ }
15778
+ return {
15779
+ path: normalizeSnapshotRelativePath(value.path),
15780
+ content_hash: normalizeHashId(value.content_hash),
15781
+ line_count: value.line_count,
15782
+ ...title !== undefined ? { title } : {},
15783
+ ...locator !== undefined ? { locator } : {}
15784
+ };
15785
+ }
15786
+ function parseAssetEntry(value, index) {
15787
+ if (!isRecord(value)) {
15788
+ throw new TypeError(`snapshot manifest assets[${index}] must be an object`);
15789
+ }
15790
+ if (typeof value.path !== "string") {
15791
+ throw new TypeError(`snapshot manifest assets[${index}].path must be a string`);
15792
+ }
15793
+ const contentHash2 = typeof value.content_hash === "string" && HASH_ID_RE.test(value.content_hash) ? normalizeHashId(value.content_hash) : undefined;
15794
+ const mediaType = typeof value.media_type === "string" && value.media_type.trim().length > 0 ? value.media_type.trim() : undefined;
15795
+ const source2 = parseStringRecord(value.source, `snapshot manifest assets[${index}].source`);
15796
+ return {
15797
+ path: normalizeSnapshotRelativePath(value.path),
15798
+ ...contentHash2 !== undefined ? { content_hash: contentHash2 } : {},
15799
+ ...mediaType !== undefined ? { media_type: mediaType } : {},
15800
+ ...source2 !== undefined ? { source: source2 } : {}
15801
+ };
15802
+ }
15803
+ function parseStringRecord(value, field) {
15804
+ if (value === undefined)
15805
+ return;
15806
+ if (!isRecord(value)) {
15807
+ throw new TypeError(`${field} must be an object`);
15808
+ }
15809
+ const result = {};
15810
+ for (const [key, item] of Object.entries(value)) {
15811
+ if (typeof item !== "string") {
15812
+ throw new TypeError(`${field}.${key} must be a string`);
15813
+ }
15814
+ const trimmed = item.trim();
15815
+ if (trimmed.length > 0)
15816
+ result[key] = trimmed;
15817
+ }
15818
+ return Object.keys(result).length > 0 ? result : undefined;
15819
+ }
15820
+ function optionalMetadataString(value, field) {
15821
+ if (value === undefined)
15822
+ return;
15823
+ if (typeof value !== "string") {
15824
+ throw new TypeError(`${field} must be a string`);
15825
+ }
15826
+ const trimmed = value.trim();
15827
+ return trimmed.length > 0 ? trimmed : undefined;
15828
+ }
15829
+ function optionalMetadataStringArray(value, field) {
15830
+ if (value === undefined)
15831
+ return;
15832
+ if (!Array.isArray(value)) {
15833
+ throw new TypeError(`${field} must be an array`);
15834
+ }
15835
+ return value.map((item, index) => {
15836
+ if (typeof item !== "string") {
15837
+ throw new TypeError(`${field}[${index}] must be a string`);
15838
+ }
15839
+ const trimmed = item.trim();
15840
+ if (trimmed.length === 0) {
15841
+ throw new TypeError(`${field}[${index}] must be a non-empty string`);
15842
+ }
15843
+ return trimmed;
15844
+ });
15845
+ }
15846
+ function optionalRouteFiles(value, field) {
15847
+ if (value === undefined)
15848
+ return;
15849
+ if (!Array.isArray(value)) {
15850
+ throw new TypeError(`${field} must be an array`);
15851
+ }
15852
+ const result = value.map((item, index) => {
15853
+ if (!isRecord(item)) {
15854
+ throw new TypeError(`${field}[${index}] must be an object`);
15855
+ }
15856
+ const path2 = optionalMetadataString(item.path, `${field}[${index}].path`);
15857
+ if (path2 === undefined) {
15858
+ throw new TypeError(`${field}[${index}].path must be a non-empty string`);
15859
+ }
15860
+ const routes = optionalMetadataStringArray(item.routes, `${field}[${index}].routes`) ?? [];
15861
+ return {
15862
+ path: normalizeSnapshotRelativePath(path2),
15863
+ routes
15864
+ };
15865
+ }).filter((item) => item.routes.length > 0);
15866
+ return result.length > 0 ? result : undefined;
15867
+ }
15868
+ function optionalRouteHints(value, field) {
15869
+ if (value === undefined)
15870
+ return;
15871
+ if (!Array.isArray(value)) {
15872
+ throw new TypeError(`${field} must be an array`);
15873
+ }
15874
+ const result = value.map((item, index) => {
15875
+ if (!isRecord(item)) {
15876
+ throw new TypeError(`${field}[${index}] must be an object`);
15877
+ }
15878
+ const documentPath = optionalMetadataString(item.documentPath, `${field}[${index}].documentPath`);
15879
+ const route = optionalMetadataString(item.route, `${field}[${index}].route`);
15880
+ const metadataPath = optionalMetadataString(item.metadataPath, `${field}[${index}].metadataPath`);
15881
+ if (documentPath === undefined || route === undefined || metadataPath === undefined) {
15882
+ throw new TypeError(`${field}[${index}] must include documentPath, route, and metadataPath`);
15883
+ }
15884
+ return {
15885
+ documentPath: normalizeSnapshotRelativePath(documentPath),
15886
+ route,
15887
+ metadataPath: normalizeSnapshotRelativePath(metadataPath)
15888
+ };
15889
+ });
15890
+ return result.length > 0 ? result : undefined;
15891
+ }
15892
+ function parseManifestMetadata(value) {
15893
+ if (value === undefined)
15894
+ return;
15895
+ if (!isRecord(value)) {
15896
+ throw new TypeError("snapshot manifest metadata must be an object");
15897
+ }
15898
+ let source2;
15899
+ if (value.source !== undefined) {
15900
+ if (!isRecord(value.source)) {
15901
+ throw new TypeError("snapshot manifest metadata.source must be an object");
15902
+ }
15903
+ const url = optionalMetadataString(value.source.url, "snapshot manifest metadata.source.url");
15904
+ const docToken = optionalMetadataString(value.source.docToken, "snapshot manifest metadata.source.docToken");
15905
+ const wikiToken = optionalMetadataString(value.source.wikiToken, "snapshot manifest metadata.source.wikiToken");
15906
+ const title = optionalMetadataString(value.source.title, "snapshot manifest metadata.source.title");
15907
+ const revisionId = optionalMetadataString(value.source.revisionId, "snapshot manifest metadata.source.revisionId");
15908
+ source2 = {
15909
+ ...url !== undefined ? { url } : {},
15910
+ ...docToken !== undefined ? { docToken } : {},
15911
+ ...wikiToken !== undefined ? { wikiToken } : {},
15912
+ ...title !== undefined ? { title } : {},
15913
+ ...revisionId !== undefined ? { revisionId } : {}
15914
+ };
15915
+ if (Object.keys(source2).length === 0)
15916
+ source2 = undefined;
15917
+ }
15918
+ let capture;
15919
+ if (value.capture !== undefined) {
15920
+ if (!isRecord(value.capture)) {
15921
+ throw new TypeError("snapshot manifest metadata.capture must be an object");
15922
+ }
15923
+ const include = optionalMetadataStringArray(value.capture.include, "snapshot manifest metadata.capture.include");
15924
+ const documentExtensions = optionalMetadataStringArray(value.capture.documentExtensions, "snapshot manifest metadata.capture.documentExtensions");
15925
+ const routeFiles = optionalRouteFiles(value.capture.routeFiles, "snapshot manifest metadata.capture.routeFiles");
15926
+ const routeHints = optionalRouteHints(value.capture.routeHints, "snapshot manifest metadata.capture.routeHints");
15927
+ capture = {
15928
+ ...include !== undefined ? { include } : {},
15929
+ ...documentExtensions !== undefined ? { documentExtensions } : {},
15930
+ ...routeFiles !== undefined ? { routeFiles } : {},
15931
+ ...routeHints !== undefined ? { routeHints } : {}
15932
+ };
15933
+ if (Object.keys(capture).length === 0)
15934
+ capture = undefined;
15935
+ }
15936
+ return source2 !== undefined || capture !== undefined ? {
15937
+ ...source2 !== undefined ? { source: source2 } : {},
15938
+ ...capture !== undefined ? { capture } : {}
15939
+ } : {};
15940
+ }
15941
+ function parseDocumentSnapshotManifest(value) {
15942
+ if (!isRecord(value)) {
15943
+ throw new TypeError("snapshot manifest must be an object");
15944
+ }
15945
+ if (value.schema_version !== DOCUMENT_SNAPSHOT_MANIFEST_SCHEMA_VERSION) {
15946
+ throw new TypeError(`snapshot manifest schema_version must be ${DOCUMENT_SNAPSHOT_MANIFEST_SCHEMA_VERSION}`);
15947
+ }
15948
+ if (typeof value.source_type !== "string") {
15949
+ throw new TypeError("snapshot manifest source_type must be a string");
15950
+ }
15951
+ assertDocumentSourceType(value.source_type);
15952
+ if (typeof value.source_name !== "string" || value.source_name.trim().length === 0) {
15953
+ throw new TypeError("snapshot manifest source_name must be a non-empty string");
15954
+ }
15955
+ const sourceName = normalizeDocumentSourceName(value.source_name);
15956
+ if (typeof value.captured_at !== "string" || value.captured_at.trim().length === 0) {
15957
+ throw new TypeError("snapshot manifest captured_at must be a non-empty string");
15958
+ }
15959
+ if (typeof value.snapshot_hash !== "string" || !HASH_ID_RE.test(value.snapshot_hash)) {
15960
+ throw new TypeError("snapshot manifest snapshot_hash must be sha256:<64 hex>");
15961
+ }
15962
+ if (typeof value.normalizer_version !== "string" || value.normalizer_version.trim().length === 0) {
15963
+ throw new TypeError("snapshot manifest normalizer_version must be a non-empty string");
15964
+ }
15965
+ if (!Array.isArray(value.files)) {
15966
+ throw new TypeError("snapshot manifest files must be an array");
15967
+ }
15968
+ const assets = value.assets === undefined ? undefined : Array.isArray(value.assets) ? value.assets.map(parseAssetEntry) : (() => {
15969
+ throw new TypeError("snapshot manifest assets must be an array");
15970
+ })();
15971
+ const metadata2 = parseManifestMetadata(value.metadata);
15972
+ const files = value.files.map(parseFileEntry);
15973
+ const seenPaths = new Set;
15974
+ for (const file of files) {
15975
+ if (seenPaths.has(file.path)) {
15976
+ throw new TypeError(`snapshot manifest files contains duplicate path: ${file.path}`);
15977
+ }
15978
+ seenPaths.add(file.path);
15979
+ }
15980
+ return {
15981
+ schema_version: DOCUMENT_SNAPSHOT_MANIFEST_SCHEMA_VERSION,
15982
+ source_type: value.source_type,
15983
+ source_name: sourceName,
15984
+ captured_at: value.captured_at,
15985
+ snapshot_hash: normalizeHashId(value.snapshot_hash),
15986
+ normalizer_version: value.normalizer_version,
15987
+ files,
15988
+ ...assets !== undefined ? { assets } : {},
15989
+ ...metadata2 !== undefined ? { metadata: metadata2 } : {}
15990
+ };
15991
+ }
15466
15992
  // src/repository.ts
15467
15993
  import { readFile as readFile2, readdir as readdir2, stat } from "node:fs/promises";
15468
15994
  import path2 from "node:path";
@@ -15688,6 +16214,29 @@ var readModuleName = async (dirPath, manifestFile) => {
15688
16214
  }
15689
16215
  return null;
15690
16216
  };
16217
+ var readModuleVersion = async (dirPath, manifestFile) => {
16218
+ try {
16219
+ const content = await readFile(join(dirPath, manifestFile), "utf-8");
16220
+ if (manifestFile === "package.json") {
16221
+ const parsed = JSON.parse(content);
16222
+ return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : null;
16223
+ }
16224
+ if (manifestFile === "pyproject.toml" || manifestFile === "Cargo.toml") {
16225
+ const match = content.match(/^\s*version\s*=\s*["']([^"']+)["']/m);
16226
+ return match?.[1] ?? null;
16227
+ }
16228
+ if (manifestFile === "setup.py") {
16229
+ const match = content.match(/version\s*=\s*["']([^"']+)["']/);
16230
+ return match?.[1] ?? null;
16231
+ }
16232
+ if (manifestFile === "pom.xml") {
16233
+ return content.match(/<version>\s*([^<]+?)\s*<\/version>/i)?.[1] ?? null;
16234
+ }
16235
+ } catch {
16236
+ return null;
16237
+ }
16238
+ return null;
16239
+ };
15691
16240
  var MANIFEST_NAMES = new Set(MANIFEST_FILES.map((f) => f.toLowerCase()));
15692
16241
  var findSubModuleDirs = async (rootDir, excludeDirs, gitFiles, pathFilter2) => {
15693
16242
  const root = resolve(rootDir);
@@ -15805,6 +16354,35 @@ var detectModules = async (repoPath, ref, pathFilter2) => {
15805
16354
  }
15806
16355
  return modules.sort((a, b) => a.name.localeCompare(b.name));
15807
16356
  };
16357
+ var detectModuleBoundaries = async (repoPath, ref, pathFilter2) => {
16358
+ const repoRoot = resolve(repoPath);
16359
+ const gitFiles = await tryGitListFiles(repoRoot, ref);
16360
+ const subModuleDirs = await findSubModuleDirs(repoRoot, undefined, gitFiles, pathFilter2);
16361
+ const boundaries = [];
16362
+ const rootManifest = await findManifestFile(repoRoot);
16363
+ if (rootManifest) {
16364
+ const version2 = await readModuleVersion(repoRoot, rootManifest);
16365
+ boundaries.push({
16366
+ name: await readModuleName(repoRoot, rootManifest) ?? basename(repoRoot),
16367
+ path: ".",
16368
+ manifest: rootManifest,
16369
+ ...version2 !== null ? { version: version2 } : {}
16370
+ });
16371
+ }
16372
+ for (const subDir of subModuleDirs) {
16373
+ const manifest = await findManifestFile(subDir);
16374
+ if (!manifest)
16375
+ continue;
16376
+ const version2 = await readModuleVersion(subDir, manifest);
16377
+ boundaries.push({
16378
+ name: await readModuleName(subDir, manifest) ?? basename(subDir),
16379
+ path: toPosixPath(relative(repoRoot, subDir)) || ".",
16380
+ manifest,
16381
+ ...version2 !== null ? { version: version2 } : {}
16382
+ });
16383
+ }
16384
+ return boundaries.sort((left, right) => left.path.localeCompare(right.path));
16385
+ };
15808
16386
  var detectModuleAt = async (repoPath, modulePath, ref, pathFilter2) => {
15809
16387
  const repoRoot = resolve(repoPath);
15810
16388
  const moduleDir = modulePath === "." ? repoRoot : resolve(repoRoot, modulePath);
@@ -16578,10 +17156,12 @@ var detectTechStack = async (repoPath) => {
16578
17156
  return labels;
16579
17157
  };
16580
17158
  export {
17159
+ truncateSourceSpanHash,
16581
17160
  symbolParamSchema,
16582
17161
  symbolInfoSchema,
16583
17162
  symbolDiffSchema,
16584
17163
  stableStringify,
17164
+ sourceSpanHashMatches,
16585
17165
  sourceInfoSchema,
16586
17166
  scanSourceFiles,
16587
17167
  runRepositoryExtraction,
@@ -16596,9 +17176,15 @@ export {
16596
17176
  prefixExtractionPaths,
16597
17177
  prefixEntryDetectionPaths,
16598
17178
  patternDetectionResultSchema,
17179
+ parseSpanSourceRef,
16599
17180
  parseFile,
17181
+ parseDocumentSourceLocator,
17182
+ parseDocumentSnapshotManifest,
17183
+ normalizeSnapshotRelativePath,
16600
17184
  normalizeRelativePath,
17185
+ normalizeMarkdownDocument,
16601
17186
  normalizeExtractionPaths,
17187
+ normalizeDocumentSourceName,
16602
17188
  metaText,
16603
17189
  manifestTypeSchema,
16604
17190
  manifestText,
@@ -16612,6 +17198,8 @@ export {
16612
17198
  getGitCommitHash,
16613
17199
  generateSymbolDiff,
16614
17200
  generateDigest,
17201
+ formatSpanSourceRef,
17202
+ formatCanonicalProseSourceRef,
16615
17203
  flattenSymbols,
16616
17204
  fileInfoSchema,
16617
17205
  extractionStatsSchema,
@@ -16620,12 +17208,21 @@ export {
16620
17208
  extractionMetaSchema,
16621
17209
  entryFileSchema,
16622
17210
  entryDetectionResultSchema,
17211
+ encodeSnapshotLocatorPath,
16623
17212
  digestStatsSchema,
16624
17213
  digestDataSchema,
16625
17214
  detectTechStack,
16626
17215
  detectModules,
17216
+ detectModuleBoundaries,
16627
17217
  detectModuleAt,
17218
+ decodeSnapshotLocatorPath,
16628
17219
  createModuleFileSystem,
17220
+ createDocumentSourceSpan,
17221
+ createDocumentSnapshotManifest,
17222
+ createDocumentSnapshotFileEntry,
17223
+ countMarkdownLines,
17224
+ computeLogicalRawHash,
17225
+ computeDocumentContentHash,
16629
17226
  codeExtractRunnerInputSchema,
16630
17227
  canonicalizeCodeVersionLabel,
16631
17228
  buildSourceFileHashId,
@@ -16637,5 +17234,8 @@ export {
16637
17234
  SCAN_EXCLUDED_DIRS,
16638
17235
  PACKAGE_JSON,
16639
17236
  ExtractionPluginRegistry,
17237
+ DOCUMENT_SNAPSHOT_MANIFEST_SCHEMA_VERSION,
17238
+ DOCUMENT_EVIDENCE_NORMALIZER_VERSION,
17239
+ DEFAULT_SOURCE_SPAN_HASH_LENGTH,
16640
17240
  CODE_SNAPSHOT_META_SCHEMA_VERSION
16641
17241
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c4a/extract",
3
- "version": "0.5.41-beta.7",
3
+ "version": "0.6.0-alpha.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "c4a-extract-code": "./bin/c4a-extract-code.js"
Binary file