@untangle-code/context-artifacts 0.0.0-stage → 0.1.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/LICENSE +661 -0
- package/NOTICE +90 -0
- package/README.md +247 -2
- package/THIRD_PARTY_LICENSES.md +24 -0
- package/dist/adapters/asyncapi.d.ts +20 -0
- package/dist/adapters/asyncapi.d.ts.map +1 -0
- package/dist/adapters/asyncapi.js +95 -0
- package/dist/adapters/asyncapi.js.map +1 -0
- package/dist/adapters/docker.d.ts +43 -0
- package/dist/adapters/docker.d.ts.map +1 -0
- package/dist/adapters/docker.js +97 -0
- package/dist/adapters/docker.js.map +1 -0
- package/dist/adapters/kubernetes.d.ts +22 -0
- package/dist/adapters/kubernetes.d.ts.map +1 -0
- package/dist/adapters/kubernetes.js +56 -0
- package/dist/adapters/kubernetes.js.map +1 -0
- package/dist/adapters/markdown.d.ts +35 -0
- package/dist/adapters/markdown.d.ts.map +1 -0
- package/dist/adapters/markdown.js +36 -0
- package/dist/adapters/markdown.js.map +1 -0
- package/dist/adapters/openapi.d.ts +21 -0
- package/dist/adapters/openapi.d.ts.map +1 -0
- package/dist/adapters/openapi.js +101 -0
- package/dist/adapters/openapi.js.map +1 -0
- package/dist/adapters/protobuf.d.ts +34 -0
- package/dist/adapters/protobuf.d.ts.map +1 -0
- package/dist/adapters/protobuf.js +221 -0
- package/dist/adapters/protobuf.js.map +1 -0
- package/dist/adapters/shared.d.ts +65 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/adapters/shared.js +95 -0
- package/dist/adapters/shared.js.map +1 -0
- package/dist/adapters/sql.d.ts +104 -0
- package/dist/adapters/sql.d.ts.map +1 -0
- package/dist/adapters/sql.js +427 -0
- package/dist/adapters/sql.js.map +1 -0
- package/dist/adapters/terraform.d.ts +40 -0
- package/dist/adapters/terraform.d.ts.map +1 -0
- package/dist/adapters/terraform.js +186 -0
- package/dist/adapters/terraform.js.map +1 -0
- package/dist/adapters/text.d.ts +24 -0
- package/dist/adapters/text.d.ts.map +1 -0
- package/dist/adapters/text.js +31 -0
- package/dist/adapters/text.js.map +1 -0
- package/dist/config-validation.d.ts +18 -0
- package/dist/config-validation.d.ts.map +1 -0
- package/dist/config-validation.js +30 -0
- package/dist/config-validation.js.map +1 -0
- package/dist/coverage.d.ts +114 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/coverage.js +0 -0
- package/dist/coverage.js.map +1 -0
- package/dist/ids.d.ts +12 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +42 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +51 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +86 -0
- package/dist/registry.js.map +1 -0
- package/dist/staleness.d.ts +44 -0
- package/dist/staleness.d.ts.map +1 -0
- package/dist/staleness.js +35 -0
- package/dist/staleness.js.map +1 -0
- package/dist/text/delimiter-balance.d.ts +95 -0
- package/dist/text/delimiter-balance.d.ts.map +1 -0
- package/dist/text/delimiter-balance.js +209 -0
- package/dist/text/delimiter-balance.js.map +1 -0
- package/dist/text/line-index.d.ts +61 -0
- package/dist/text/line-index.d.ts.map +1 -0
- package/dist/text/line-index.js +75 -0
- package/dist/text/line-index.js.map +1 -0
- package/dist/text/yaml-lite.d.ts +73 -0
- package/dist/text/yaml-lite.d.ts.map +1 -0
- package/dist/text/yaml-lite.js +486 -0
- package/dist/text/yaml-lite.js.map +1 -0
- package/package.json +61 -3
- package/src/adapters/__golden__/adr-0001-sections.json +68 -0
- package/src/adapters/__golden__/app-compose-services.json +24 -0
- package/src/adapters/__golden__/app-image-stages.json +24 -0
- package/src/adapters/__golden__/infra-main-sections.json +46 -0
- package/src/adapters/__golden__/orders-events-operations.json +24 -0
- package/src/adapters/__golden__/orders-proto-sections.json +57 -0
- package/src/adapters/__golden__/orders-schema-statements.json +68 -0
- package/src/adapters/__golden__/users-api-operations.json +46 -0
- package/src/adapters/__golden__/web-manifests-resources.json +24 -0
- package/src/adapters/asyncapi.ts +147 -0
- package/src/adapters/docker.ts +170 -0
- package/src/adapters/kubernetes.ts +103 -0
- package/src/adapters/markdown.ts +88 -0
- package/src/adapters/openapi.ts +156 -0
- package/src/adapters/protobuf.ts +286 -0
- package/src/adapters/shared.ts +151 -0
- package/src/adapters/sql.ts +555 -0
- package/src/adapters/terraform.ts +261 -0
- package/src/adapters/text.ts +72 -0
- package/src/config-validation.ts +41 -0
- package/src/coverage.ts +336 -0
- package/src/ids.ts +57 -0
- package/src/index.ts +105 -0
- package/src/registry.ts +155 -0
- package/src/staleness.ts +69 -0
- package/src/text/delimiter-balance.ts +295 -0
- package/src/text/line-index.ts +108 -0
- package/src/text/yaml-lite.ts +565 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kubernetes YAML adapter (architecture.md §20: "je Dokument eine
|
|
3
|
+
* Ressource, apiVersion/kind/name erhalten").
|
|
4
|
+
*
|
|
5
|
+
* Each `---`-separated YAML document in the file becomes exactly one
|
|
6
|
+
* chunk, kind `"kubernetes_resource"`, spanning that document's full line
|
|
7
|
+
* range — Kubernetes manifests are not sectioned further, since a
|
|
8
|
+
* Deployment's `spec.template` is meaningless read apart from its
|
|
9
|
+
* `metadata`. A document whose YAML fails to parse still gets its full raw
|
|
10
|
+
* span turned into a chunk (kind `"kubernetes_resource_parse_error"`, title
|
|
11
|
+
* carries the reason) rather than aborting the whole file: one malformed
|
|
12
|
+
* manifest in a directory of many must not hide the rest (architecture.md
|
|
13
|
+
* §22.2's cross-repo-search principle, applied at document granularity).
|
|
14
|
+
*/
|
|
15
|
+
import type {
|
|
16
|
+
ArtifactAdapter,
|
|
17
|
+
ArtifactChunk,
|
|
18
|
+
ArtifactDocument,
|
|
19
|
+
ArtifactInput,
|
|
20
|
+
ArtifactLink,
|
|
21
|
+
} from "@untangle-code/domain";
|
|
22
|
+
|
|
23
|
+
import { buildLineIndex } from "../text/line-index.js";
|
|
24
|
+
import {
|
|
25
|
+
mapGet,
|
|
26
|
+
parseYamlNode,
|
|
27
|
+
scalarString,
|
|
28
|
+
splitYamlDocuments,
|
|
29
|
+
YamlParseError,
|
|
30
|
+
} from "../text/yaml-lite.js";
|
|
31
|
+
|
|
32
|
+
import type { ChunkDraft, SkipLedgerReporter } from "./shared.js";
|
|
33
|
+
import { buildChunksReportingSkips, buildDocuments } from "./shared.js";
|
|
34
|
+
|
|
35
|
+
const YAML_EXTENSION = /\.ya?ml$/i;
|
|
36
|
+
const COMPOSE_FILENAME = /(^|\/)(docker-)?compose\.ya?ml$/i;
|
|
37
|
+
|
|
38
|
+
function kubernetesDrafts(content: string): ChunkDraft[] {
|
|
39
|
+
return splitYamlDocuments(content).map((span): ChunkDraft => {
|
|
40
|
+
try {
|
|
41
|
+
const root = parseYamlNode(span.text);
|
|
42
|
+
const apiVersion = scalarString(mapGet(root, "apiVersion"));
|
|
43
|
+
const kind = scalarString(mapGet(root, "kind"));
|
|
44
|
+
const name = scalarString(mapGet(mapGet(root, "metadata"), "name"));
|
|
45
|
+
const title =
|
|
46
|
+
[apiVersion, kind].filter((part) => part !== undefined).join("/") +
|
|
47
|
+
(name === undefined ? "" : ` ${name}`);
|
|
48
|
+
return {
|
|
49
|
+
kind: "kubernetes_resource",
|
|
50
|
+
startLine: span.startLine,
|
|
51
|
+
endLine: span.endLine,
|
|
52
|
+
...(title.trim().length === 0 ? {} : { title: title.trim() }),
|
|
53
|
+
};
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const reason = error instanceof YamlParseError ? error.message : "unknown parse error";
|
|
56
|
+
return {
|
|
57
|
+
kind: "kubernetes_resource_parse_error",
|
|
58
|
+
startLine: span.startLine,
|
|
59
|
+
endLine: span.endLine,
|
|
60
|
+
title: `parse error: ${reason}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface KubernetesAdapterOptions {
|
|
67
|
+
/** Overrides how a real skip-ledger finding surfaces from `chunk()` (default: a stderr warning) — see `coverage.ts`'s `SkipLedgerReporter`. */
|
|
68
|
+
onSkipLedgerGap?: SkipLedgerReporter;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createKubernetesAdapter(options: KubernetesAdapterOptions = {}): ArtifactAdapter {
|
|
72
|
+
return {
|
|
73
|
+
supports(type: string, path: string): boolean {
|
|
74
|
+
// "auto" detection only claims .yaml/.yml files that are not a
|
|
75
|
+
// Compose file (see docker.ts) — every other ambiguous case (a plain
|
|
76
|
+
// config .yaml, an OpenAPI/AsyncAPI file without a name hint) should
|
|
77
|
+
// be configured with an explicit `type`, since path alone cannot
|
|
78
|
+
// disambiguate them (spec.md §19.1's `supports` has no content
|
|
79
|
+
// access — see README "Entscheidungen").
|
|
80
|
+
return (
|
|
81
|
+
type === "kubernetes" ||
|
|
82
|
+
(type === "auto" && YAML_EXTENSION.test(path) && !COMPOSE_FILENAME.test(path))
|
|
83
|
+
);
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
async read(input: ArtifactInput): Promise<ArtifactDocument[]> {
|
|
87
|
+
return buildDocuments(input, "kubernetes");
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
chunk(document: ArtifactDocument): ArtifactChunk[] {
|
|
91
|
+
return buildChunksReportingSkips(
|
|
92
|
+
document,
|
|
93
|
+
buildLineIndex(document.content),
|
|
94
|
+
kubernetesDrafts(document.content),
|
|
95
|
+
options.onSkipLedgerGap,
|
|
96
|
+
);
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
links(): ArtifactLink[] {
|
|
100
|
+
return [];
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown / ADR adapter (architecture.md §20, spec.md §19.2 "semantische
|
|
3
|
+
* Abschnitte statt fester Zeilenfenster").
|
|
4
|
+
*
|
|
5
|
+
* ADRs are not a separate `ContextArtifactType` (spec.md §19.1's type union
|
|
6
|
+
* has no "adr" literal — an Architecture Decision Record IS a Markdown
|
|
7
|
+
* document, conventionally with `## Status`/`## Context`/`## Decision`/
|
|
8
|
+
* `## Consequences` headings) — this one adapter's heading-based sectioning
|
|
9
|
+
* handles both.
|
|
10
|
+
*
|
|
11
|
+
* WP-110: the sectioning itself no longer lives here. It moved verbatim to
|
|
12
|
+
* `@untangle-code/domain`'s `outlineMarkdown()` so that the SAME partition
|
|
13
|
+
* also serves ordinary `.md` files through the parser-registry prose adapter
|
|
14
|
+
* and the prose metrics — before that move, a configured artifact got
|
|
15
|
+
* breadcrumbed sections while the identical file outside an artifact
|
|
16
|
+
* configuration got arbitrary line windows. Behaviour here is unchanged; the
|
|
17
|
+
* implementation is simply no longer duplicated.
|
|
18
|
+
*
|
|
19
|
+
* Sectioning (defined on `outlineMarkdown`): each ATX heading (`#` …
|
|
20
|
+
* `######`, ignored inside fenced code blocks) starts a new chunk that runs
|
|
21
|
+
* up to the START of the very next heading of ANY level (or EOF) — a flat
|
|
22
|
+
* partition of the file with no overlap. Each chunk's `title` carries the
|
|
23
|
+
* breadcrumb (ancestor headings joined by " › ") instead, so the hierarchy
|
|
24
|
+
* is still fully recoverable even though `content` stays the exact raw slice
|
|
25
|
+
* — the breadcrumb is never spliced into the preserved raw content
|
|
26
|
+
* (spec.md §19.2).
|
|
27
|
+
*/
|
|
28
|
+
import type {
|
|
29
|
+
ArtifactAdapter,
|
|
30
|
+
ArtifactChunk,
|
|
31
|
+
ArtifactDocument,
|
|
32
|
+
ArtifactInput,
|
|
33
|
+
ArtifactLink,
|
|
34
|
+
} from "@untangle-code/domain";
|
|
35
|
+
import { outlineMarkdown } from "@untangle-code/domain";
|
|
36
|
+
|
|
37
|
+
import { buildLineIndex, splitLines } from "../text/line-index.js";
|
|
38
|
+
|
|
39
|
+
import type { ChunkDraft, SkipLedgerReporter } from "./shared.js";
|
|
40
|
+
import { buildChunksReportingSkips, buildDocuments } from "./shared.js";
|
|
41
|
+
|
|
42
|
+
const MARKDOWN_EXTENSION = /\.(md|markdown|mdx)$/i;
|
|
43
|
+
|
|
44
|
+
function markdownDrafts(lines: string[]): ChunkDraft[] {
|
|
45
|
+
return outlineMarkdown(lines).map((section) => {
|
|
46
|
+
const draft: ChunkDraft = {
|
|
47
|
+
kind: "markdown_section",
|
|
48
|
+
startLine: section.startLine,
|
|
49
|
+
endLine: section.endLine,
|
|
50
|
+
};
|
|
51
|
+
// Der Vorspann vor der ersten Ueberschrift hat keinen Breadcrumb; ein
|
|
52
|
+
// leerer `title` waere eine Behauptung ueber eine Ueberschrift, die es
|
|
53
|
+
// nicht gibt.
|
|
54
|
+
return section.breadcrumb === "" ? draft : { ...draft, title: section.breadcrumb };
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface MarkdownAdapterOptions {
|
|
59
|
+
/** Overrides how a real skip-ledger finding surfaces from `chunk()` (default: a stderr warning) — see `coverage.ts`'s `SkipLedgerReporter`. */
|
|
60
|
+
onSkipLedgerGap?: SkipLedgerReporter;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createMarkdownAdapter(options: MarkdownAdapterOptions = {}): ArtifactAdapter {
|
|
64
|
+
return {
|
|
65
|
+
supports(type: string, path: string): boolean {
|
|
66
|
+
return type === "markdown" || (type === "auto" && MARKDOWN_EXTENSION.test(path));
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
async read(input: ArtifactInput): Promise<ArtifactDocument[]> {
|
|
70
|
+
return buildDocuments(input, "markdown");
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
chunk(document: ArtifactDocument): ArtifactChunk[] {
|
|
74
|
+
const { lines } = splitLines(document.content);
|
|
75
|
+
const index = buildLineIndex(document.content);
|
|
76
|
+
return buildChunksReportingSkips(
|
|
77
|
+
document,
|
|
78
|
+
index,
|
|
79
|
+
markdownDrafts(lines),
|
|
80
|
+
options.onSkipLedgerGap,
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
links(): ArtifactLink[] {
|
|
85
|
+
return [];
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI adapter (architecture.md §20: "je Operation ein Abschnitt,
|
|
3
|
+
* Operation-IDs erhalten").
|
|
4
|
+
*
|
|
5
|
+
* Parses the document with the package's YAML subset parser
|
|
6
|
+
* (`text/yaml-lite.ts`) and emits one chunk per `paths.<path>.<method>`
|
|
7
|
+
* operation, titled with the HTTP method, path and (when present)
|
|
8
|
+
* `operationId` — never dropped even when absent from the title, since
|
|
9
|
+
* `content` is always the exact raw YAML block, `operationId` field
|
|
10
|
+
* included. A malformed document degrades to a single whole-document
|
|
11
|
+
* `"openapi_parse_error"` chunk (architecture.md §22.2) rather than
|
|
12
|
+
* throwing — the raw content is still fully preserved in that chunk.
|
|
13
|
+
*/
|
|
14
|
+
import type {
|
|
15
|
+
ArtifactAdapter,
|
|
16
|
+
ArtifactChunk,
|
|
17
|
+
ArtifactDocument,
|
|
18
|
+
ArtifactInput,
|
|
19
|
+
ArtifactLink,
|
|
20
|
+
} from "@untangle-code/domain";
|
|
21
|
+
|
|
22
|
+
import { buildLineIndex } from "../text/line-index.js";
|
|
23
|
+
import {
|
|
24
|
+
mapGet,
|
|
25
|
+
parseYamlNode,
|
|
26
|
+
scalarString,
|
|
27
|
+
YamlParseError,
|
|
28
|
+
type YamlNode,
|
|
29
|
+
} from "../text/yaml-lite.js";
|
|
30
|
+
|
|
31
|
+
import type { ChunkDraft, SkipLedgerReporter } from "./shared.js";
|
|
32
|
+
import {
|
|
33
|
+
buildChunks,
|
|
34
|
+
buildChunksReportingSkips,
|
|
35
|
+
buildDocuments,
|
|
36
|
+
wholeDocumentDraft,
|
|
37
|
+
} from "./shared.js";
|
|
38
|
+
|
|
39
|
+
const OPENAPI_NAME_HINT = /openapi/i;
|
|
40
|
+
const YAML_OR_JSON_EXTENSION = /\.(ya?ml|json)$/i;
|
|
41
|
+
|
|
42
|
+
const HTTP_METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"] as const;
|
|
43
|
+
|
|
44
|
+
function openApiDrafts(root: YamlNode | null): ChunkDraft[] {
|
|
45
|
+
if (root === null || root.kind !== "map") return [];
|
|
46
|
+
const drafts: ChunkDraft[] = [];
|
|
47
|
+
|
|
48
|
+
const info = mapGet(root, "info");
|
|
49
|
+
if (info !== undefined) {
|
|
50
|
+
const title = scalarString(mapGet(info, "title"));
|
|
51
|
+
drafts.push({
|
|
52
|
+
kind: "openapi_info",
|
|
53
|
+
startLine: info.startLine,
|
|
54
|
+
endLine: info.endLine,
|
|
55
|
+
...(title === undefined ? {} : { title: `info: ${title}` }),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const paths = mapGet(root, "paths");
|
|
60
|
+
if (paths !== undefined && paths.kind === "map") {
|
|
61
|
+
for (const pathEntry of paths.entries) {
|
|
62
|
+
if (pathEntry.value.kind !== "map") continue;
|
|
63
|
+
for (const methodEntry of pathEntry.value.entries) {
|
|
64
|
+
const method = methodEntry.key.toLowerCase();
|
|
65
|
+
if (!(HTTP_METHODS as readonly string[]).includes(method)) continue;
|
|
66
|
+
const operationId = scalarString(mapGet(methodEntry.value, "operationId"));
|
|
67
|
+
const title =
|
|
68
|
+
operationId === undefined
|
|
69
|
+
? `${method.toUpperCase()} ${pathEntry.key}`
|
|
70
|
+
: `${method.toUpperCase()} ${pathEntry.key} (${operationId})`;
|
|
71
|
+
drafts.push({
|
|
72
|
+
kind: "openapi_operation",
|
|
73
|
+
startLine: methodEntry.value.startLine,
|
|
74
|
+
endLine: methodEntry.value.endLine,
|
|
75
|
+
title,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return drafts;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function operationIdOf(chunk: ArtifactChunk): string | undefined {
|
|
84
|
+
if (chunk.kind !== "openapi_operation" || chunk.title === undefined) return undefined;
|
|
85
|
+
const match = /\(([^()]+)\)$/.exec(chunk.title);
|
|
86
|
+
return match?.[1];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function draftsFor(document: ArtifactDocument): ChunkDraft[] {
|
|
90
|
+
try {
|
|
91
|
+
return openApiDrafts(parseYamlNode(document.content));
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const reason = error instanceof YamlParseError ? error.message : "unknown parse error";
|
|
94
|
+
return [
|
|
95
|
+
wholeDocumentDraft(
|
|
96
|
+
"openapi_parse_error",
|
|
97
|
+
buildLineIndex(document.content),
|
|
98
|
+
`parse error: ${reason}`,
|
|
99
|
+
),
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface OpenApiAdapterOptions {
|
|
105
|
+
/** Overrides how a real skip-ledger finding surfaces from `chunk()` (default: a stderr warning) — see `coverage.ts`'s `SkipLedgerReporter`. */
|
|
106
|
+
onSkipLedgerGap?: SkipLedgerReporter;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function createOpenApiAdapter(options: OpenApiAdapterOptions = {}): ArtifactAdapter {
|
|
110
|
+
return {
|
|
111
|
+
supports(type: string, path: string): boolean {
|
|
112
|
+
return (
|
|
113
|
+
type === "openapi" ||
|
|
114
|
+
(type === "auto" && OPENAPI_NAME_HINT.test(path) && YAML_OR_JSON_EXTENSION.test(path))
|
|
115
|
+
);
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
async read(input: ArtifactInput): Promise<ArtifactDocument[]> {
|
|
119
|
+
return buildDocuments(input, "openapi");
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
chunk(document: ArtifactDocument): ArtifactChunk[] {
|
|
123
|
+
return buildChunksReportingSkips(
|
|
124
|
+
document,
|
|
125
|
+
buildLineIndex(document.content),
|
|
126
|
+
draftsFor(document),
|
|
127
|
+
options.onSkipLedgerGap,
|
|
128
|
+
);
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Heuristic (architecture.md §20's canonical example): assumes the
|
|
133
|
+
* common REST-codegen convention that a controller method is named
|
|
134
|
+
* after the operation's `operationId`. No AST/symbol table is
|
|
135
|
+
* consulted here — `targetId` stays unset; a later graph-linking stage
|
|
136
|
+
* resolves `targetName` against real symbols, if any match.
|
|
137
|
+
*/
|
|
138
|
+
links(document: ArtifactDocument): ArtifactLink[] {
|
|
139
|
+
const chunks = buildChunks(document, buildLineIndex(document.content), draftsFor(document));
|
|
140
|
+
const links: ArtifactLink[] = [];
|
|
141
|
+
for (const chunk of chunks) {
|
|
142
|
+
const operationId = operationIdOf(chunk);
|
|
143
|
+
if (operationId === undefined) continue;
|
|
144
|
+
links.push({
|
|
145
|
+
fromChunkId: chunk.chunkId,
|
|
146
|
+
targetKind: "symbol",
|
|
147
|
+
targetName: operationId,
|
|
148
|
+
heuristic: true,
|
|
149
|
+
confidence: 0.5,
|
|
150
|
+
note: `naming-convention guess: OpenAPI operationId "${operationId}" → a controller method of the same name`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return links;
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protobuf adapter (architecture.md §20: "Messages, Services, RPCs").
|
|
3
|
+
*
|
|
4
|
+
* Brace-depth line scanning (no full grammar needed): a top-level
|
|
5
|
+
* (depth-0) `message Foo {` or `service Foo {` opens a block that runs to
|
|
6
|
+
* its matching `}`. Nested `message`s inside another message are part of
|
|
7
|
+
* the parent's content (not chunked separately — a deliberate scope limit,
|
|
8
|
+
* see README "Entscheidungen"; their bytes are still fully present in the
|
|
9
|
+
* parent chunk's content, so nothing is silently dropped). Inside a
|
|
10
|
+
* `service` block, each `rpc Name(...) returns (...);` additionally gets
|
|
11
|
+
* its own finer-grained chunk — intentionally OVERLAPPING the enclosing
|
|
12
|
+
* service chunk's range, so a search hit on one RPC's name returns a tight
|
|
13
|
+
* chunk while a hit on shared service-level comments/options still returns
|
|
14
|
+
* full context.
|
|
15
|
+
*
|
|
16
|
+
* The brace counter is string/comment-aware (WP-81 review finding: a `{` or
|
|
17
|
+
* `}` inside a string default value, e.g. `[default = "{"]`, or inside a
|
|
18
|
+
* `//`/`/* *\/` comment, must never move the depth counter — a naive raw
|
|
19
|
+
* count desyncs PERMANENTLY on such a line, degrading "runs to EOF" and
|
|
20
|
+
* silently swallowing every following top-level block). `scanLine` below
|
|
21
|
+
* carries string/block-comment state across lines through ONE continuous
|
|
22
|
+
* scan (a single shared `ScanState`, not a fresh one per block) — depth is
|
|
23
|
+
* tracked exactly once per line, not once in this function and again in a
|
|
24
|
+
* separate look-ahead, so there is only one source of truth for "are we
|
|
25
|
+
* inside a string/comment right now" across the whole file.
|
|
26
|
+
*/
|
|
27
|
+
import type {
|
|
28
|
+
ArtifactAdapter,
|
|
29
|
+
ArtifactChunk,
|
|
30
|
+
ArtifactDocument,
|
|
31
|
+
ArtifactInput,
|
|
32
|
+
ArtifactLink,
|
|
33
|
+
} from "@untangle-code/domain";
|
|
34
|
+
|
|
35
|
+
import type { DelimiterScanOptions } from "../text/delimiter-balance.js";
|
|
36
|
+
import { describeDelimiterImbalances, findDelimiterImbalances } from "../text/delimiter-balance.js";
|
|
37
|
+
import { buildLineIndex, splitLines } from "../text/line-index.js";
|
|
38
|
+
|
|
39
|
+
import type { ChunkDraft, ChunkPlausibilityCheck, SkipLedgerReporter } from "./shared.js";
|
|
40
|
+
import { buildChunks, buildChunksReportingSkips, buildDocuments } from "./shared.js";
|
|
41
|
+
|
|
42
|
+
const PROTO_EXTENSION = /\.proto$/i;
|
|
43
|
+
const MESSAGE_RE = /^\s*message\s+([\w.]+)\s*\{/;
|
|
44
|
+
const SERVICE_RE = /^\s*service\s+([\w.]+)\s*\{/;
|
|
45
|
+
const RPC_RE = /^\s*rpc\s+(\w+)\s*\(/;
|
|
46
|
+
|
|
47
|
+
/** Cross-line scanner state: which of a string literal / block comment (if any) the scan is currently inside. */
|
|
48
|
+
interface ScanState {
|
|
49
|
+
inSingle: boolean;
|
|
50
|
+
inDouble: boolean;
|
|
51
|
+
inBlockComment: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Net brace delta for one line, skipping `'...'`/`"..."` string literals
|
|
56
|
+
* (with `\`-escapes) and `//`/`/* *\/` comments, so a brace character
|
|
57
|
+
* inside either never affects depth. `state` is mutated in place for
|
|
58
|
+
* anything that spans past this line's end (an unterminated string or an
|
|
59
|
+
* open block comment) — the NEXT call for the following line continues
|
|
60
|
+
* from exactly that state, which is what makes the counter immune to
|
|
61
|
+
* per-line resets.
|
|
62
|
+
*/
|
|
63
|
+
function scanLine(line: string, state: ScanState): number {
|
|
64
|
+
let delta = 0;
|
|
65
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
66
|
+
const c = line[i];
|
|
67
|
+
const next = line[i + 1];
|
|
68
|
+
if (state.inBlockComment) {
|
|
69
|
+
if (c === "*" && next === "/") {
|
|
70
|
+
state.inBlockComment = false;
|
|
71
|
+
i += 1;
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (state.inSingle) {
|
|
76
|
+
if (c === "\\") {
|
|
77
|
+
i += 1;
|
|
78
|
+
} else if (c === "'") {
|
|
79
|
+
state.inSingle = false;
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (state.inDouble) {
|
|
84
|
+
if (c === "\\") {
|
|
85
|
+
i += 1;
|
|
86
|
+
} else if (c === '"') {
|
|
87
|
+
state.inDouble = false;
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (c === "/" && next === "/") {
|
|
92
|
+
break; // line comment: rest of the line is never code, and cannot itself open a block comment
|
|
93
|
+
}
|
|
94
|
+
if (c === "/" && next === "*") {
|
|
95
|
+
state.inBlockComment = true;
|
|
96
|
+
i += 1;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (c === "'") {
|
|
100
|
+
state.inSingle = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (c === '"') {
|
|
104
|
+
state.inDouble = true;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (c === "{") delta += 1;
|
|
108
|
+
else if (c === "}") delta -= 1;
|
|
109
|
+
}
|
|
110
|
+
return delta;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Letzte Zeile einer `rpc`-Deklaration.
|
|
115
|
+
*
|
|
116
|
+
* Zwei Formen: die Kurzform endet an ihrem `;` (das bei mehrzeiliger
|
|
117
|
+
* Signatur erst auf einer späteren Zeile steht), die Langform an der
|
|
118
|
+
* schließenden `}` ihres Optionsrumpfs. WP-118: bisher wurde nur die erste
|
|
119
|
+
* Form erkannt, also endete `rpc A(X) returns (Y) {` schon an der ersten
|
|
120
|
+
* `;` IM RUMPF — mitten im Konstrukt, mit fehlender schließender Klammer,
|
|
121
|
+
* und ohne Ledger-Eintrag, weil die Range des Service-Chunks alles
|
|
122
|
+
* abdeckte. Der Klammerstand wird deshalb mitgezählt.
|
|
123
|
+
*/
|
|
124
|
+
function findRpcEnd(lines: string[], startIndex: number, hardLimit: number): number {
|
|
125
|
+
const state: ScanState = { inSingle: false, inDouble: false, inBlockComment: false };
|
|
126
|
+
let depth = 0;
|
|
127
|
+
for (let i = startIndex; i < hardLimit; i += 1) {
|
|
128
|
+
const line = lines[i] ?? "";
|
|
129
|
+
const depthBefore = depth;
|
|
130
|
+
depth += scanLine(line, state);
|
|
131
|
+
if (depthBefore > 0 && depth === 0) return i + 1; // Optionsrumpf geschlossen
|
|
132
|
+
if (depthBefore === 0 && depth === 0 && (line.includes(";") || line.includes("}")))
|
|
133
|
+
return i + 1;
|
|
134
|
+
}
|
|
135
|
+
return hardLimit;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Pushes `pending`'s finished draft (and, for a service, its nested RPC drafts scanned from its now-final range) into `drafts`. */
|
|
139
|
+
function finalizePendingBlock(
|
|
140
|
+
drafts: ChunkDraft[],
|
|
141
|
+
lines: string[],
|
|
142
|
+
pending: { kind: "protobuf_message" | "protobuf_service"; title: string; startLine: number },
|
|
143
|
+
endLine: number,
|
|
144
|
+
): void {
|
|
145
|
+
drafts.push({ kind: pending.kind, startLine: pending.startLine, endLine, title: pending.title });
|
|
146
|
+
if (pending.kind !== "protobuf_service") return;
|
|
147
|
+
for (let j = pending.startLine - 1; j < endLine; j += 1) {
|
|
148
|
+
const rpc = RPC_RE.exec(lines[j] ?? "");
|
|
149
|
+
if (rpc) {
|
|
150
|
+
const rpcEnd = findRpcEnd(lines, j, endLine);
|
|
151
|
+
drafts.push({
|
|
152
|
+
kind: "protobuf_rpc",
|
|
153
|
+
startLine: j + 1,
|
|
154
|
+
endLine: rpcEnd,
|
|
155
|
+
title: `${pending.title}.${rpc[1]!}`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function protobufDrafts(lines: string[]): ChunkDraft[] {
|
|
162
|
+
const drafts: ChunkDraft[] = [];
|
|
163
|
+
const state: ScanState = { inSingle: false, inDouble: false, inBlockComment: false };
|
|
164
|
+
let depth = 0;
|
|
165
|
+
let pending:
|
|
166
|
+
{ kind: "protobuf_message" | "protobuf_service"; title: string; startLine: number } | undefined;
|
|
167
|
+
|
|
168
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
169
|
+
const line = lines[i] ?? "";
|
|
170
|
+
if (depth === 0 && pending === undefined) {
|
|
171
|
+
const message = MESSAGE_RE.exec(line);
|
|
172
|
+
const service = SERVICE_RE.exec(line);
|
|
173
|
+
if (message) {
|
|
174
|
+
pending = { kind: "protobuf_message", title: message[1]!, startLine: i + 1 };
|
|
175
|
+
} else if (service) {
|
|
176
|
+
pending = { kind: "protobuf_service", title: service[1]!, startLine: i + 1 };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
depth += scanLine(line, state);
|
|
180
|
+
if (pending !== undefined && depth === 0) {
|
|
181
|
+
finalizePendingBlock(drafts, lines, pending, i + 1);
|
|
182
|
+
pending = undefined;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (pending !== undefined) {
|
|
186
|
+
// Unterminated top-level block (malformed source, or a still-open
|
|
187
|
+
// string/comment at EOF): degrade to "runs to EOF" rather than
|
|
188
|
+
// dropping it — never silently, see the module doc.
|
|
189
|
+
finalizePendingBlock(drafts, lines, pending, Math.max(lines.length, pending.startLine));
|
|
190
|
+
}
|
|
191
|
+
return drafts;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Trennzeichen-Konventionen für die Plausibilitätsprüfung (WP-118).
|
|
196
|
+
* Zeichenketten dürfen keine Zeile überschreiten — ein Anführungszeichen,
|
|
197
|
+
* das bis zum Zeilenende offen bleibt, ist in Protobuf bereits ein Befund.
|
|
198
|
+
*/
|
|
199
|
+
const PROTOBUF_SCAN_OPTIONS: DelimiterScanOptions = {
|
|
200
|
+
quotes: [
|
|
201
|
+
{ open: "'", close: "'", backslashEscapes: true, doubledEscapes: false, spansLines: false },
|
|
202
|
+
{ open: '"', close: '"', backslashEscapes: true, doubledEscapes: false, spansLines: false },
|
|
203
|
+
],
|
|
204
|
+
brackets: [
|
|
205
|
+
{ open: "{", close: "}" },
|
|
206
|
+
{ open: "(", close: ")" },
|
|
207
|
+
{ open: "[", close: "]" },
|
|
208
|
+
],
|
|
209
|
+
lineComments: ["//"],
|
|
210
|
+
blockComment: { open: "/*", close: "*/" },
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* WP-118 (dieselbe Blindstelle wie in `sql.ts`): der Blockzähler dieses
|
|
215
|
+
* Adapters degradiert einen unabgeschlossenen Top-Level-Block zu „läuft
|
|
216
|
+
* bis Dateiende". Die Byte-Abdeckung bleibt dabei lückenlos, der Chunk ist
|
|
217
|
+
* weder leer noch überlappend — der Ledger sah also NICHTS, obwohl der
|
|
218
|
+
* Blockzähler nachweislich aus dem Tritt geraten war. Ein Fragment mit
|
|
219
|
+
* offener Klammer, offener Zeichenkette oder offenem Kommentar ist jetzt
|
|
220
|
+
* ein Befund.
|
|
221
|
+
*/
|
|
222
|
+
const checkProtobufPlausibility: ChunkPlausibilityCheck = (chunk) => {
|
|
223
|
+
const imbalances = findDelimiterImbalances(chunk.content, PROTOBUF_SCAN_OPTIONS);
|
|
224
|
+
return imbalances.length === 0 ? undefined : describeDelimiterImbalances(imbalances);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
function messageName(chunk: ArtifactChunk): string | undefined {
|
|
228
|
+
return chunk.kind === "protobuf_service" ? chunk.title : undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface ProtobufAdapterOptions {
|
|
232
|
+
/** Overrides how a real skip-ledger finding surfaces from `chunk()` (default: a stderr warning) — see `coverage.ts`'s `SkipLedgerReporter`. */
|
|
233
|
+
onSkipLedgerGap?: SkipLedgerReporter;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function createProtobufAdapter(options: ProtobufAdapterOptions = {}): ArtifactAdapter {
|
|
237
|
+
return {
|
|
238
|
+
supports(type: string, path: string): boolean {
|
|
239
|
+
return type === "protobuf" || (type === "auto" && PROTO_EXTENSION.test(path));
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
async read(input: ArtifactInput): Promise<ArtifactDocument[]> {
|
|
243
|
+
return buildDocuments(input, "protobuf");
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
chunk(document: ArtifactDocument): ArtifactChunk[] {
|
|
247
|
+
const { lines } = splitLines(document.content);
|
|
248
|
+
const index = buildLineIndex(document.content);
|
|
249
|
+
return buildChunksReportingSkips(
|
|
250
|
+
document,
|
|
251
|
+
index,
|
|
252
|
+
protobufDrafts(lines),
|
|
253
|
+
options.onSkipLedgerGap,
|
|
254
|
+
checkProtobufPlausibility,
|
|
255
|
+
);
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Heuristic (architecture.md §20): a gRPC service `FooService` often
|
|
260
|
+
* backs a generated/handwritten server symbol named `FooServiceServer`
|
|
261
|
+
* or `FooServiceImpl`. Both are offered as separate low-confidence
|
|
262
|
+
* hints — this is a naming guess, not a resolved reference.
|
|
263
|
+
*/
|
|
264
|
+
links(document: ArtifactDocument): ArtifactLink[] {
|
|
265
|
+
const { lines } = splitLines(document.content);
|
|
266
|
+
const index = buildLineIndex(document.content);
|
|
267
|
+
const chunks = buildChunks(document, index, protobufDrafts(lines));
|
|
268
|
+
const links: ArtifactLink[] = [];
|
|
269
|
+
for (const chunk of chunks) {
|
|
270
|
+
const name = messageName(chunk);
|
|
271
|
+
if (name === undefined) continue;
|
|
272
|
+
for (const suffix of ["Server", "Impl"]) {
|
|
273
|
+
links.push({
|
|
274
|
+
fromChunkId: chunk.chunkId,
|
|
275
|
+
targetKind: "symbol",
|
|
276
|
+
targetName: `${name}${suffix}`,
|
|
277
|
+
heuristic: true,
|
|
278
|
+
confidence: 0.3,
|
|
279
|
+
note: `naming-convention guess: gRPC service "${name}" → generated/handwritten server symbol`,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return links;
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|