@untangle-code/context-artifacts 0.0.0-stage → 0.1.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/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,24 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"content": " build: .\n ports:\n - \"3000:3000\"\n",
|
|
4
|
+
"kind": "compose_service",
|
|
5
|
+
"range": {
|
|
6
|
+
"endByte": 61,
|
|
7
|
+
"endLine": 5,
|
|
8
|
+
"startByte": 17,
|
|
9
|
+
"startLine": 3
|
|
10
|
+
},
|
|
11
|
+
"title": "api"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"content": " image: postgres:16\n environment:\n POSTGRES_PASSWORD: secret\n",
|
|
15
|
+
"kind": "compose_service",
|
|
16
|
+
"range": {
|
|
17
|
+
"endByte": 139,
|
|
18
|
+
"endLine": 9,
|
|
19
|
+
"startByte": 67,
|
|
20
|
+
"startLine": 7
|
|
21
|
+
},
|
|
22
|
+
"title": "db"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"content": "FROM node:22-slim AS build\nWORKDIR /app\nCOPY . .\nRUN npm ci && npm run build\n\n",
|
|
4
|
+
"kind": "docker_stage",
|
|
5
|
+
"range": {
|
|
6
|
+
"endByte": 78,
|
|
7
|
+
"endLine": 5,
|
|
8
|
+
"startByte": 0,
|
|
9
|
+
"startLine": 1
|
|
10
|
+
},
|
|
11
|
+
"title": "build"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"content": "FROM node:22-slim\nWORKDIR /app\nCOPY --from=build /app/dist ./dist\nCMD [\"node\", \"dist/index.js\"]\n",
|
|
15
|
+
"kind": "docker_stage",
|
|
16
|
+
"range": {
|
|
17
|
+
"endByte": 174,
|
|
18
|
+
"endLine": 10,
|
|
19
|
+
"startByte": 78,
|
|
20
|
+
"startLine": 6
|
|
21
|
+
},
|
|
22
|
+
"title": "stage 1 (node:22-slim)"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"content": "variable \"region\" {\n type = string\n default = \"eu-central-1\"\n}\n",
|
|
4
|
+
"kind": "terraform_variable",
|
|
5
|
+
"range": {
|
|
6
|
+
"endByte": 123,
|
|
7
|
+
"endLine": 5,
|
|
8
|
+
"startByte": 55,
|
|
9
|
+
"startLine": 2
|
|
10
|
+
},
|
|
11
|
+
"title": "region"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"content": "resource \"aws_s3_bucket\" \"data\" {\n bucket = \"my-data-bucket\"\n tags = {\n Owner = \"platform\"\n }\n}\n",
|
|
15
|
+
"kind": "terraform_resource",
|
|
16
|
+
"range": {
|
|
17
|
+
"endByte": 226,
|
|
18
|
+
"endLine": 12,
|
|
19
|
+
"startByte": 124,
|
|
20
|
+
"startLine": 7
|
|
21
|
+
},
|
|
22
|
+
"title": "aws_s3_bucket.data"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"content": "module \"network\" {\n source = \"./modules/network\"\n cidr = \"10.0.0.0/16\"\n}\n",
|
|
26
|
+
"kind": "terraform_module",
|
|
27
|
+
"range": {
|
|
28
|
+
"endByte": 304,
|
|
29
|
+
"endLine": 17,
|
|
30
|
+
"startByte": 227,
|
|
31
|
+
"startLine": 14
|
|
32
|
+
},
|
|
33
|
+
"title": "network"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"content": "output \"bucket_arn\" {\n value = aws_s3_bucket.data.arn\n}\n",
|
|
37
|
+
"kind": "terraform_output",
|
|
38
|
+
"range": {
|
|
39
|
+
"endByte": 362,
|
|
40
|
+
"endLine": 21,
|
|
41
|
+
"startByte": 305,
|
|
42
|
+
"startLine": 19
|
|
43
|
+
},
|
|
44
|
+
"title": "bucket_arn"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"content": " operationId: onOrderCreated\n summary: Emitted when a new order is placed\n",
|
|
4
|
+
"kind": "asyncapi_operation",
|
|
5
|
+
"range": {
|
|
6
|
+
"endByte": 190,
|
|
7
|
+
"endLine": 9,
|
|
8
|
+
"startByte": 106,
|
|
9
|
+
"startLine": 8
|
|
10
|
+
},
|
|
11
|
+
"title": "order.created subscribe (onOrderCreated)"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"content": " operationId: publishOrderCancelled\n",
|
|
15
|
+
"kind": "asyncapi_operation",
|
|
16
|
+
"range": {
|
|
17
|
+
"endByte": 263,
|
|
18
|
+
"endLine": 12,
|
|
19
|
+
"startByte": 222,
|
|
20
|
+
"startLine": 12
|
|
21
|
+
},
|
|
22
|
+
"title": "order.cancelled publish (publishOrderCancelled)"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"content": "message Order {\n string id = 1;\n string customer_id = 2;\n int64 total_cents = 3;\n}\n",
|
|
4
|
+
"kind": "protobuf_message",
|
|
5
|
+
"range": {
|
|
6
|
+
"endByte": 126,
|
|
7
|
+
"endLine": 9,
|
|
8
|
+
"startByte": 40,
|
|
9
|
+
"startLine": 5
|
|
10
|
+
},
|
|
11
|
+
"title": "Order"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"content": "message OrderItem {\n string sku = 1;\n int32 qty = 2;\n}\n",
|
|
15
|
+
"kind": "protobuf_message",
|
|
16
|
+
"range": {
|
|
17
|
+
"endByte": 184,
|
|
18
|
+
"endLine": 14,
|
|
19
|
+
"startByte": 127,
|
|
20
|
+
"startLine": 11
|
|
21
|
+
},
|
|
22
|
+
"title": "OrderItem"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"content": "service OrderService {\n rpc GetOrder(GetOrderRequest) returns (Order);\n rpc CreateOrder(CreateOrderRequest)\n returns (Order);\n}\n",
|
|
26
|
+
"kind": "protobuf_service",
|
|
27
|
+
"range": {
|
|
28
|
+
"endByte": 320,
|
|
29
|
+
"endLine": 20,
|
|
30
|
+
"startByte": 185,
|
|
31
|
+
"startLine": 16
|
|
32
|
+
},
|
|
33
|
+
"title": "OrderService"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"content": " rpc GetOrder(GetOrderRequest) returns (Order);\n",
|
|
37
|
+
"kind": "protobuf_rpc",
|
|
38
|
+
"range": {
|
|
39
|
+
"endByte": 257,
|
|
40
|
+
"endLine": 17,
|
|
41
|
+
"startByte": 208,
|
|
42
|
+
"startLine": 17
|
|
43
|
+
},
|
|
44
|
+
"title": "OrderService.GetOrder"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"content": " rpc CreateOrder(CreateOrderRequest)\n returns (Order);\n",
|
|
48
|
+
"kind": "protobuf_rpc",
|
|
49
|
+
"range": {
|
|
50
|
+
"endByte": 318,
|
|
51
|
+
"endLine": 19,
|
|
52
|
+
"startByte": 257,
|
|
53
|
+
"startLine": 18
|
|
54
|
+
},
|
|
55
|
+
"title": "OrderService.CreateOrder"
|
|
56
|
+
}
|
|
57
|
+
]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"content": "-- schema for the orders service\nCREATE TABLE IF NOT EXISTS orders (\n id UUID PRIMARY KEY,\n customer_id UUID NOT NULL,\n total_cents INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending' -- semicolons in comments; must not split statements\n);\n",
|
|
4
|
+
"kind": "sql_table",
|
|
5
|
+
"range": {
|
|
6
|
+
"endByte": 250,
|
|
7
|
+
"endLine": 7,
|
|
8
|
+
"startByte": 0,
|
|
9
|
+
"startLine": 1
|
|
10
|
+
},
|
|
11
|
+
"title": "orders"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"content": "\nCREATE TABLE order_items (\n order_id UUID NOT NULL REFERENCES orders(id),\n sku TEXT NOT NULL,\n qty INTEGER NOT NULL\n);\n",
|
|
15
|
+
"kind": "sql_table",
|
|
16
|
+
"range": {
|
|
17
|
+
"endByte": 373,
|
|
18
|
+
"endLine": 13,
|
|
19
|
+
"startByte": 250,
|
|
20
|
+
"startLine": 8
|
|
21
|
+
},
|
|
22
|
+
"title": "order_items"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"content": "\nCREATE UNIQUE INDEX idx_orders_customer ON orders (customer_id);\n",
|
|
26
|
+
"kind": "sql_index",
|
|
27
|
+
"range": {
|
|
28
|
+
"endByte": 439,
|
|
29
|
+
"endLine": 15,
|
|
30
|
+
"startByte": 373,
|
|
31
|
+
"startLine": 14
|
|
32
|
+
},
|
|
33
|
+
"title": "idx_orders_customer ON orders"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"content": "\nCREATE VIEW open_orders AS\n SELECT * FROM orders WHERE status <> 'closed';\n",
|
|
37
|
+
"kind": "sql_view",
|
|
38
|
+
"range": {
|
|
39
|
+
"endByte": 516,
|
|
40
|
+
"endLine": 18,
|
|
41
|
+
"startByte": 439,
|
|
42
|
+
"startLine": 16
|
|
43
|
+
},
|
|
44
|
+
"title": "open_orders"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"content": "\nALTER TABLE order_items ADD CONSTRAINT fk_order_items_order FOREIGN KEY (order_id) REFERENCES orders(id);\n",
|
|
48
|
+
"kind": "sql_constraint",
|
|
49
|
+
"range": {
|
|
50
|
+
"endByte": 623,
|
|
51
|
+
"endLine": 20,
|
|
52
|
+
"startByte": 516,
|
|
53
|
+
"startLine": 19
|
|
54
|
+
},
|
|
55
|
+
"title": "fk_order_items_order ON order_items"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"content": "\nINSERT INTO orders (id, customer_id, total_cents, status) VALUES ('seed', 'seed', 0, 'pending');\n",
|
|
59
|
+
"kind": "sql_statement",
|
|
60
|
+
"range": {
|
|
61
|
+
"endByte": 721,
|
|
62
|
+
"endLine": 22,
|
|
63
|
+
"startByte": 623,
|
|
64
|
+
"startLine": 21
|
|
65
|
+
},
|
|
66
|
+
"title": "INSERT"
|
|
67
|
+
}
|
|
68
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"content": " title: Users API\n version: \"1.0.0\"\n",
|
|
4
|
+
"kind": "openapi_info",
|
|
5
|
+
"range": {
|
|
6
|
+
"endByte": 59,
|
|
7
|
+
"endLine": 4,
|
|
8
|
+
"startByte": 21,
|
|
9
|
+
"startLine": 3
|
|
10
|
+
},
|
|
11
|
+
"title": "info: Users API"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"content": " operationId: listUsers\n summary: List users\n responses:\n \"200\":\n description: OK\n",
|
|
15
|
+
"kind": "openapi_operation",
|
|
16
|
+
"range": {
|
|
17
|
+
"endByte": 198,
|
|
18
|
+
"endLine": 12,
|
|
19
|
+
"startByte": 85,
|
|
20
|
+
"startLine": 8
|
|
21
|
+
},
|
|
22
|
+
"title": "GET /users (listUsers)"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"content": " operationId: createUser\n summary: Create a user\n responses:\n \"201\":\n description: Created\n",
|
|
26
|
+
"kind": "openapi_operation",
|
|
27
|
+
"range": {
|
|
28
|
+
"endByte": 330,
|
|
29
|
+
"endLine": 18,
|
|
30
|
+
"startByte": 208,
|
|
31
|
+
"startLine": 14
|
|
32
|
+
},
|
|
33
|
+
"title": "POST /users (createUser)"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"content": " operationId: getUserById\n summary: Get a single user\n responses:\n \"200\":\n description: OK\n",
|
|
37
|
+
"kind": "openapi_operation",
|
|
38
|
+
"range": {
|
|
39
|
+
"endByte": 476,
|
|
40
|
+
"endLine": 25,
|
|
41
|
+
"startByte": 354,
|
|
42
|
+
"startLine": 21
|
|
43
|
+
},
|
|
44
|
+
"title": "GET /users/{id} (getUserById)"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: web\nspec:\n replicas: 2\n template:\n spec:\n containers:\n - name: web\n image: web:latest\n",
|
|
4
|
+
"kind": "kubernetes_resource",
|
|
5
|
+
"range": {
|
|
6
|
+
"endByte": 167,
|
|
7
|
+
"endLine": 11,
|
|
8
|
+
"startByte": 0,
|
|
9
|
+
"startLine": 1
|
|
10
|
+
},
|
|
11
|
+
"title": "apps/v1/Deployment web"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"content": "apiVersion: v1\nkind: Service\nmetadata:\n name: web\nspec:\n selector:\n app: web\n ports:\n - port: 80\n",
|
|
15
|
+
"kind": "kubernetes_resource",
|
|
16
|
+
"range": {
|
|
17
|
+
"endByte": 277,
|
|
18
|
+
"endLine": 22,
|
|
19
|
+
"startByte": 171,
|
|
20
|
+
"startLine": 13
|
|
21
|
+
},
|
|
22
|
+
"title": "v1/Service web"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AsyncAPI adapter (architecture.md §20: "je Channel ein Abschnitt,
|
|
3
|
+
* Operation-IDs erhalten").
|
|
4
|
+
*
|
|
5
|
+
* AsyncAPI v2's shape (`channels.<channel>.subscribe`/`.publish`, each
|
|
6
|
+
* optionally carrying `operationId`) is what this adapter targets — one
|
|
7
|
+
* chunk per channel operation, kind `"asyncapi_operation"`, titled
|
|
8
|
+
* `"<channel> <subscribe|publish> (<operationId>)"`. A channel with
|
|
9
|
+
* neither `subscribe` nor `publish` (rare, but not invalid AsyncAPI) still
|
|
10
|
+
* gets a whole-channel `"asyncapi_channel"` chunk rather than being
|
|
11
|
+
* dropped. Malformed documents degrade the same way `openapi.ts` does.
|
|
12
|
+
*/
|
|
13
|
+
import type {
|
|
14
|
+
ArtifactAdapter,
|
|
15
|
+
ArtifactChunk,
|
|
16
|
+
ArtifactDocument,
|
|
17
|
+
ArtifactInput,
|
|
18
|
+
ArtifactLink,
|
|
19
|
+
} from "@untangle-code/domain";
|
|
20
|
+
|
|
21
|
+
import { buildLineIndex } from "../text/line-index.js";
|
|
22
|
+
import {
|
|
23
|
+
mapGet,
|
|
24
|
+
parseYamlNode,
|
|
25
|
+
scalarString,
|
|
26
|
+
YamlParseError,
|
|
27
|
+
type YamlNode,
|
|
28
|
+
} from "../text/yaml-lite.js";
|
|
29
|
+
|
|
30
|
+
import type { ChunkDraft, SkipLedgerReporter } from "./shared.js";
|
|
31
|
+
import {
|
|
32
|
+
buildChunks,
|
|
33
|
+
buildChunksReportingSkips,
|
|
34
|
+
buildDocuments,
|
|
35
|
+
wholeDocumentDraft,
|
|
36
|
+
} from "./shared.js";
|
|
37
|
+
|
|
38
|
+
const ASYNCAPI_NAME_HINT = /asyncapi/i;
|
|
39
|
+
const YAML_OR_JSON_EXTENSION = /\.(ya?ml|json)$/i;
|
|
40
|
+
|
|
41
|
+
const OPERATION_KEYS = ["subscribe", "publish"] as const;
|
|
42
|
+
|
|
43
|
+
function asyncApiDrafts(root: YamlNode | null): ChunkDraft[] {
|
|
44
|
+
if (root === null || root.kind !== "map") return [];
|
|
45
|
+
const drafts: ChunkDraft[] = [];
|
|
46
|
+
const channels = mapGet(root, "channels");
|
|
47
|
+
if (channels === undefined || channels.kind !== "map") return drafts;
|
|
48
|
+
|
|
49
|
+
for (const channelEntry of channels.entries) {
|
|
50
|
+
if (channelEntry.value.kind !== "map") continue;
|
|
51
|
+
let foundOperation = false;
|
|
52
|
+
for (const operationKey of OPERATION_KEYS) {
|
|
53
|
+
const operation = mapGet(channelEntry.value, operationKey);
|
|
54
|
+
if (operation === undefined) continue;
|
|
55
|
+
foundOperation = true;
|
|
56
|
+
const operationId = scalarString(mapGet(operation, "operationId"));
|
|
57
|
+
const title =
|
|
58
|
+
operationId === undefined
|
|
59
|
+
? `${channelEntry.key} ${operationKey}`
|
|
60
|
+
: `${channelEntry.key} ${operationKey} (${operationId})`;
|
|
61
|
+
drafts.push({
|
|
62
|
+
kind: "asyncapi_operation",
|
|
63
|
+
startLine: operation.startLine,
|
|
64
|
+
endLine: operation.endLine,
|
|
65
|
+
title,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (!foundOperation) {
|
|
69
|
+
drafts.push({
|
|
70
|
+
kind: "asyncapi_channel",
|
|
71
|
+
startLine: channelEntry.value.startLine,
|
|
72
|
+
endLine: channelEntry.value.endLine,
|
|
73
|
+
title: channelEntry.key,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return drafts;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function operationIdOf(chunk: ArtifactChunk): string | undefined {
|
|
81
|
+
if (chunk.kind !== "asyncapi_operation" || chunk.title === undefined) return undefined;
|
|
82
|
+
const match = /\(([^()]+)\)$/.exec(chunk.title);
|
|
83
|
+
return match?.[1];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function draftsFor(document: ArtifactDocument): ChunkDraft[] {
|
|
87
|
+
try {
|
|
88
|
+
return asyncApiDrafts(parseYamlNode(document.content));
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const reason = error instanceof YamlParseError ? error.message : "unknown parse error";
|
|
91
|
+
return [
|
|
92
|
+
wholeDocumentDraft(
|
|
93
|
+
"asyncapi_parse_error",
|
|
94
|
+
buildLineIndex(document.content),
|
|
95
|
+
`parse error: ${reason}`,
|
|
96
|
+
),
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface AsyncApiAdapterOptions {
|
|
102
|
+
/** Overrides how a real skip-ledger finding surfaces from `chunk()` (default: a stderr warning) — see `coverage.ts`'s `SkipLedgerReporter`. */
|
|
103
|
+
onSkipLedgerGap?: SkipLedgerReporter;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function createAsyncApiAdapter(options: AsyncApiAdapterOptions = {}): ArtifactAdapter {
|
|
107
|
+
return {
|
|
108
|
+
supports(type: string, path: string): boolean {
|
|
109
|
+
return (
|
|
110
|
+
type === "asyncapi" ||
|
|
111
|
+
(type === "auto" && ASYNCAPI_NAME_HINT.test(path) && YAML_OR_JSON_EXTENSION.test(path))
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
async read(input: ArtifactInput): Promise<ArtifactDocument[]> {
|
|
116
|
+
return buildDocuments(input, "asyncapi");
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
chunk(document: ArtifactDocument): ArtifactChunk[] {
|
|
120
|
+
return buildChunksReportingSkips(
|
|
121
|
+
document,
|
|
122
|
+
buildLineIndex(document.content),
|
|
123
|
+
draftsFor(document),
|
|
124
|
+
options.onSkipLedgerGap,
|
|
125
|
+
);
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
/** Heuristic, same convention as the OpenAPI adapter's `operationId` link. */
|
|
129
|
+
links(document: ArtifactDocument): ArtifactLink[] {
|
|
130
|
+
const chunks = buildChunks(document, buildLineIndex(document.content), draftsFor(document));
|
|
131
|
+
const links: ArtifactLink[] = [];
|
|
132
|
+
for (const chunk of chunks) {
|
|
133
|
+
const operationId = operationIdOf(chunk);
|
|
134
|
+
if (operationId === undefined) continue;
|
|
135
|
+
links.push({
|
|
136
|
+
fromChunkId: chunk.chunkId,
|
|
137
|
+
targetKind: "symbol",
|
|
138
|
+
targetName: operationId,
|
|
139
|
+
heuristic: true,
|
|
140
|
+
confidence: 0.5,
|
|
141
|
+
note: `naming-convention guess: AsyncAPI operationId "${operationId}" → a handler method of the same name`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return links;
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker / Compose adapter (architecture.md §20: "Services, Stages").
|
|
3
|
+
*
|
|
4
|
+
* spec.md §19.1's `ContextArtifactType` union has no `"docker"` (or
|
|
5
|
+
* `"compose"`) literal — see README "Entscheidungen" and this WP's
|
|
6
|
+
* openIssues entry. `ArtifactDocument.type` is therefore `"text"` for
|
|
7
|
+
* documents this adapter produces (the closest fit in the frozen contract).
|
|
8
|
+
* `supports()` matches on filename for BOTH `type === "auto"` and
|
|
9
|
+
* `type === "text"` — `"text"` is the only literal an explicit config
|
|
10
|
+
* could even use for a Dockerfile/Compose artifact, so treating it the
|
|
11
|
+
* same as `"auto"` here is what lets an explicit `type: "text"` still get
|
|
12
|
+
* real stage/service chunking instead of being indistinguishable from a
|
|
13
|
+
* plain text file; the registry's `resolve()` tries this adapter before
|
|
14
|
+
* the text fallback either way, so a genuine plain-text file is never
|
|
15
|
+
* misrouted here. `ArtifactDocument.language` carries which of the two a
|
|
16
|
+
* given document is (`"dockerfile"` / `"docker-compose"`), so this
|
|
17
|
+
* adapter's own `chunk()` (and the registry's document→adapter dispatch)
|
|
18
|
+
* can tell a Dockerfile from a Compose file from a genuinely generic text
|
|
19
|
+
* document — `language` is a general-purpose optional field on the frozen
|
|
20
|
+
* contract, so this uses it without needing any contract change.
|
|
21
|
+
*
|
|
22
|
+
* Dockerfile: each `FROM ... [AS name]` starts a build stage that runs to
|
|
23
|
+
* the line before the next `FROM` (or EOF) — kind `"docker_stage"`.
|
|
24
|
+
* Compose: each entry under top-level `services:` becomes one chunk — kind
|
|
25
|
+
* `"compose_service"`.
|
|
26
|
+
*
|
|
27
|
+
* `FROM` detection skips heredoc bodies (WP-81 review's defect class,
|
|
28
|
+
* applied here: a separator-looking token inside a "quoted" region must
|
|
29
|
+
* never be mistaken for a real one). A modern Dockerfile can write
|
|
30
|
+
* `RUN <<EOF` / `COPY <<-EOF /dst` and have arbitrary script text up to a
|
|
31
|
+
* closing `EOF` line — that text is user content, not Dockerfile
|
|
32
|
+
* instructions, so a line reading e.g. `FROM scratch` inside such a heredoc
|
|
33
|
+
* (a Python/shell snippet building its OWN Dockerfile-ish text, or just a
|
|
34
|
+
* coincidence) must never be read as a new build stage.
|
|
35
|
+
*/
|
|
36
|
+
import type {
|
|
37
|
+
ArtifactAdapter,
|
|
38
|
+
ArtifactChunk,
|
|
39
|
+
ArtifactDocument,
|
|
40
|
+
ArtifactInput,
|
|
41
|
+
ArtifactLink,
|
|
42
|
+
} from "@untangle-code/domain";
|
|
43
|
+
|
|
44
|
+
import { buildLineIndex, splitLines } from "../text/line-index.js";
|
|
45
|
+
import { mapGet, parseYamlNode, YamlParseError } from "../text/yaml-lite.js";
|
|
46
|
+
|
|
47
|
+
import type { ChunkDraft, SkipLedgerReporter } from "./shared.js";
|
|
48
|
+
import { buildChunksReportingSkips, buildDocuments, wholeDocumentDraft } from "./shared.js";
|
|
49
|
+
|
|
50
|
+
const DOCKERFILE_NAME = /(^|\/)Dockerfile(\.[\w-]+)?$/;
|
|
51
|
+
const COMPOSE_NAME = /(^|\/)(docker-)?compose\.ya?ml$/i;
|
|
52
|
+
|
|
53
|
+
const FROM_RE = /^\s*FROM\s+(\S+)(?:\s+AS\s+(\S+))?\s*$/i;
|
|
54
|
+
|
|
55
|
+
/** `RUN <<EOF`, `COPY <<-'EOF' /dst`, … — an optional `-` (strip leading tabs on the closing line) and an optional quoted/bare delimiter word. */
|
|
56
|
+
const HEREDOC_OPEN_RE = /<<(-)?\s*(['"]?)([A-Za-z_][\w]*)\2/;
|
|
57
|
+
|
|
58
|
+
function dockerfileDrafts(lines: string[]): ChunkDraft[] {
|
|
59
|
+
const stageStarts: { line: number; base: string; name?: string }[] = [];
|
|
60
|
+
let heredocDelimiter: string | null = null;
|
|
61
|
+
let heredocStripLeadingTabs = false;
|
|
62
|
+
for (const [index, line] of lines.entries()) {
|
|
63
|
+
if (heredocDelimiter !== null) {
|
|
64
|
+
const closingCandidate = heredocStripLeadingTabs ? line.replace(/^\t+/, "") : line;
|
|
65
|
+
if (closingCandidate === heredocDelimiter) {
|
|
66
|
+
heredocDelimiter = null;
|
|
67
|
+
}
|
|
68
|
+
continue; // heredoc body: never Dockerfile instructions, whatever they contain
|
|
69
|
+
}
|
|
70
|
+
const heredocOpen = HEREDOC_OPEN_RE.exec(line);
|
|
71
|
+
if (heredocOpen) {
|
|
72
|
+
heredocDelimiter = heredocOpen[3]!;
|
|
73
|
+
heredocStripLeadingTabs = heredocOpen[1] === "-";
|
|
74
|
+
continue; // the opener line itself (e.g. "RUN <<EOF") is never a FROM either
|
|
75
|
+
}
|
|
76
|
+
const match = FROM_RE.exec(line);
|
|
77
|
+
if (match) {
|
|
78
|
+
const entry: { line: number; base: string; name?: string } = {
|
|
79
|
+
line: index + 1,
|
|
80
|
+
base: match[1]!,
|
|
81
|
+
};
|
|
82
|
+
stageStarts.push(match[2] === undefined ? entry : { ...entry, name: match[2] });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const totalLines = Math.max(lines.length, 1);
|
|
86
|
+
return stageStarts.map((stage, position) => {
|
|
87
|
+
const endLine =
|
|
88
|
+
position + 1 < stageStarts.length ? stageStarts[position + 1]!.line - 1 : totalLines;
|
|
89
|
+
return {
|
|
90
|
+
kind: "docker_stage",
|
|
91
|
+
startLine: stage.line,
|
|
92
|
+
endLine,
|
|
93
|
+
title: stage.name === undefined ? `stage ${position} (${stage.base})` : stage.name,
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function composeDrafts(document: ArtifactDocument): ChunkDraft[] {
|
|
99
|
+
try {
|
|
100
|
+
const root = parseYamlNode(document.content);
|
|
101
|
+
const services = mapGet(root, "services");
|
|
102
|
+
if (services === undefined || services.kind !== "map") return [];
|
|
103
|
+
return services.entries.map((entry): ChunkDraft => ({
|
|
104
|
+
kind: "compose_service",
|
|
105
|
+
startLine: entry.value.startLine,
|
|
106
|
+
endLine: entry.value.endLine,
|
|
107
|
+
title: entry.key,
|
|
108
|
+
}));
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const reason = error instanceof YamlParseError ? error.message : "unknown parse error";
|
|
111
|
+
return [
|
|
112
|
+
wholeDocumentDraft(
|
|
113
|
+
"compose_parse_error",
|
|
114
|
+
buildLineIndex(document.content),
|
|
115
|
+
`parse error: ${reason}`,
|
|
116
|
+
),
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface DockerAdapterOptions {
|
|
122
|
+
/** Overrides how a real skip-ledger finding surfaces from `chunk()` (default: a stderr warning) — see `coverage.ts`'s `SkipLedgerReporter`. */
|
|
123
|
+
onSkipLedgerGap?: SkipLedgerReporter;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function createDockerAdapter(options: DockerAdapterOptions = {}): ArtifactAdapter {
|
|
127
|
+
return {
|
|
128
|
+
supports(type: string, path: string): boolean {
|
|
129
|
+
return (
|
|
130
|
+
(type === "auto" || type === "text") &&
|
|
131
|
+
(DOCKERFILE_NAME.test(path) || COMPOSE_NAME.test(path))
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
async read(input: ArtifactInput): Promise<ArtifactDocument[]> {
|
|
136
|
+
const dockerfiles = input.files.filter((file) => DOCKERFILE_NAME.test(file.path));
|
|
137
|
+
const composeFiles = input.files.filter((file) => COMPOSE_NAME.test(file.path));
|
|
138
|
+
return [
|
|
139
|
+
...buildDocuments({ config: input.config, files: dockerfiles }, "text", "dockerfile"),
|
|
140
|
+
...buildDocuments({ config: input.config, files: composeFiles }, "text", "docker-compose"),
|
|
141
|
+
];
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
chunk(document: ArtifactDocument): ArtifactChunk[] {
|
|
145
|
+
const index = buildLineIndex(document.content);
|
|
146
|
+
if (document.language === "dockerfile") {
|
|
147
|
+
const { lines } = splitLines(document.content);
|
|
148
|
+
return buildChunksReportingSkips(
|
|
149
|
+
document,
|
|
150
|
+
index,
|
|
151
|
+
dockerfileDrafts(lines),
|
|
152
|
+
options.onSkipLedgerGap,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
// Anything else (including "docker-compose" and any unrecognized
|
|
156
|
+
// label an external caller might set) is treated as Compose YAML —
|
|
157
|
+
// the only other shape this adapter's own `read()` ever produces.
|
|
158
|
+
return buildChunksReportingSkips(
|
|
159
|
+
document,
|
|
160
|
+
index,
|
|
161
|
+
composeDrafts(document),
|
|
162
|
+
options.onSkipLedgerGap,
|
|
163
|
+
);
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
links(): ArtifactLink[] {
|
|
167
|
+
return [];
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|