@red-hat-developer-hub/backstage-plugin-ai-skills-common 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +118 -2
- package/dist/catalog-helpers.cjs.js +198 -0
- package/dist/catalog-helpers.cjs.js.map +1 -0
- package/dist/catalog-helpers.esm.js +190 -0
- package/dist/catalog-helpers.esm.js.map +1 -0
- package/dist/fixtures.cjs.js +269 -0
- package/dist/fixtures.cjs.js.map +1 -0
- package/dist/fixtures.esm.js +239 -0
- package/dist/fixtures.esm.js.map +1 -0
- package/dist/index.cjs.js +62 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +764 -0
- package/dist/index.esm.js +6 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/rest-contract.cjs.js +8 -0
- package/dist/rest-contract.cjs.js.map +1 -0
- package/dist/rest-contract.esm.js +5 -0
- package/dist/rest-contract.esm.js.map +1 -0
- package/dist/snapshot.cjs.js +115 -0
- package/dist/snapshot.cjs.js.map +1 -0
- package/dist/snapshot.esm.js +110 -0
- package/dist/snapshot.esm.js.map +1 -0
- package/dist/validation.cjs.js +442 -0
- package/dist/validation.cjs.js.map +1 -0
- package/dist/validation.esm.js +432 -0
- package/dist/validation.esm.js.map +1 -0
- package/package.json +47 -3
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { MAX_SNAPSHOT_BYTES, MAX_SNAPSHOT_RECORDS, SUPPORTED_SCHEMA_VERSION, isNpxSkillRecord, isOciSkillRecord, isValidDigest, isValidUtcTimestamp, validateSnapshot, validateSnapshotSize } from './validation.esm.js';
|
|
2
|
+
export { buildNpxRef, buildOciRef, computeCatalogName, normalizeTags, parseNpxRef, parseOciRef, resolveVersion } from './catalog-helpers.esm.js';
|
|
3
|
+
export { boundSnapshot, createFailedSnapshot, createLoadingSnapshot, sortRecordsByKey } from './snapshot.esm.js';
|
|
4
|
+
export { MAX_RESPONSE_BYTES, SKILLS_ENDPOINT_BASE } from './rest-contract.esm.js';
|
|
5
|
+
export { invalidFailedWithSkills, invalidLoadingWithObservedAt, invalidLoadingWithSkills, invalidNpxRecordWithOciExtension, invalidOciRecordUnknownExtKey, invalidOciRecordWithNpxExtension, invalidRecordBadDigest, invalidRecordEmptyKey, invalidRecordShortDigest, invalidSnapshotBadVersion, invalidSnapshotDuplicateKeys, invalidSnapshotNonUtcTimestamp, invalidSnapshotOverlappingKeys, invalidSnapshotReadyWithFailedKeys, npxSource, ociSource, validFailedSnapshot, validLoadingSnapshot, validNpxRecordFull, validNpxRecordMinimal, validNpxRecordWithQuery, validNpxSnapshotReady, validOciRecordEmptyExtensions, validOciRecordFull, validOciRecordMinimal, validOciSnapshotReady, validPartialEmptySnapshot, validPartialSnapshot, validReadyEmptySnapshot } from './fixtures.esm.js';
|
|
6
|
+
//# sourceMappingURL=index.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rest-contract.cjs.js","sources":["../src/rest-contract.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Shared REST contract constants for skill connector endpoints.\n *\n * Each connector exposes `GET /skills/:sourceId` relative to its Backstage\n * plugin base URL. The common catalog provider resolves the connector\n * through Backstage discovery and authenticates with a service token whose\n * `targetPluginId` matches the connector's registered plugin ID.\n *\n * ## Endpoint: GET /skills/:sourceId\n *\n * ### Path parameters\n * - `sourceId` — the configured source identifier, encoded as one path\n * segment.\n *\n * ### Authentication\n * Uses Backstage's default backend service-to-service authentication.\n * The endpoint is not public merely because upstream registries may be\n * public.\n *\n * ### Responses\n * - **200** — returns a `SkillSnapshot` v1 JSON body.\n * - `Content-Type: application/json`\n * - The response contains one bounded snapshot with at most 1,000\n * records and 5 MiB of serialized JSON.\n * - **404** — the `sourceId` is not configured on this connector.\n *\n * ### Consumer responsibilities\n * The consumer bounds its response read and rejects an oversized,\n * malformed, unsupported-version, or source-mismatched snapshot without\n * changing that source's catalog state.\n *\n * @packageDocumentation\n * @public\n */\n\n/**\n * Base path for the skills endpoint on each connector.\n *\n * @public\n */\nexport const SKILLS_ENDPOINT_BASE = '/skills';\n\n/**\n * Maximum response read size in bytes that consumers should accept.\n * Matches the 5 MiB snapshot serialized-size bound.\n *\n * @public\n */\nexport const MAX_RESPONSE_BYTES = 5 * 1024 * 1024;\n"],"names":[],"mappings":";;AAwDO,MAAM,oBAAA,GAAuB;AAQ7B,MAAM,kBAAA,GAAqB,IAAI,IAAA,GAAO;;;;;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rest-contract.esm.js","sources":["../src/rest-contract.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Shared REST contract constants for skill connector endpoints.\n *\n * Each connector exposes `GET /skills/:sourceId` relative to its Backstage\n * plugin base URL. The common catalog provider resolves the connector\n * through Backstage discovery and authenticates with a service token whose\n * `targetPluginId` matches the connector's registered plugin ID.\n *\n * ## Endpoint: GET /skills/:sourceId\n *\n * ### Path parameters\n * - `sourceId` — the configured source identifier, encoded as one path\n * segment.\n *\n * ### Authentication\n * Uses Backstage's default backend service-to-service authentication.\n * The endpoint is not public merely because upstream registries may be\n * public.\n *\n * ### Responses\n * - **200** — returns a `SkillSnapshot` v1 JSON body.\n * - `Content-Type: application/json`\n * - The response contains one bounded snapshot with at most 1,000\n * records and 5 MiB of serialized JSON.\n * - **404** — the `sourceId` is not configured on this connector.\n *\n * ### Consumer responsibilities\n * The consumer bounds its response read and rejects an oversized,\n * malformed, unsupported-version, or source-mismatched snapshot without\n * changing that source's catalog state.\n *\n * @packageDocumentation\n * @public\n */\n\n/**\n * Base path for the skills endpoint on each connector.\n *\n * @public\n */\nexport const SKILLS_ENDPOINT_BASE = '/skills';\n\n/**\n * Maximum response read size in bytes that consumers should accept.\n * Matches the 5 MiB snapshot serialized-size bound.\n *\n * @public\n */\nexport const MAX_RESPONSE_BYTES = 5 * 1024 * 1024;\n"],"names":[],"mappings":"AAwDO,MAAM,oBAAA,GAAuB;AAQ7B,MAAM,kBAAA,GAAqB,IAAI,IAAA,GAAO;;;;"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var validation = require('./validation.cjs.js');
|
|
4
|
+
|
|
5
|
+
function sortRecordsByKey(records) {
|
|
6
|
+
return [...records].sort((a, b) => {
|
|
7
|
+
if (a.key < b.key) return -1;
|
|
8
|
+
if (a.key > b.key) return 1;
|
|
9
|
+
return 0;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
function boundSnapshot(options) {
|
|
13
|
+
const { source, records, failedSkillKeys, discoveryComplete, observedAt } = options;
|
|
14
|
+
const sortedRecords = sortRecordsByKey(records);
|
|
15
|
+
const sortedFailedKeys = [...failedSkillKeys].sort((a, b) => {
|
|
16
|
+
if (a < b) return -1;
|
|
17
|
+
if (a > b) return 1;
|
|
18
|
+
return 0;
|
|
19
|
+
});
|
|
20
|
+
let bounded = false;
|
|
21
|
+
let boundedRecords = sortedRecords;
|
|
22
|
+
if (boundedRecords.length > validation.MAX_SNAPSHOT_RECORDS) {
|
|
23
|
+
boundedRecords = boundedRecords.slice(0, validation.MAX_SNAPSHOT_RECORDS);
|
|
24
|
+
bounded = true;
|
|
25
|
+
}
|
|
26
|
+
boundedRecords = applyByteLimit(
|
|
27
|
+
boundedRecords,
|
|
28
|
+
source,
|
|
29
|
+
sortedFailedKeys,
|
|
30
|
+
observedAt
|
|
31
|
+
);
|
|
32
|
+
if (boundedRecords.length < sortedRecords.length) {
|
|
33
|
+
bounded = true;
|
|
34
|
+
}
|
|
35
|
+
let status;
|
|
36
|
+
if (bounded || !discoveryComplete) {
|
|
37
|
+
status = "partial";
|
|
38
|
+
} else if (sortedFailedKeys.length > 0) {
|
|
39
|
+
status = "partial";
|
|
40
|
+
} else {
|
|
41
|
+
status = "ready";
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
schemaVersion: "1",
|
|
45
|
+
source,
|
|
46
|
+
status,
|
|
47
|
+
observedAt,
|
|
48
|
+
skills: boundedRecords,
|
|
49
|
+
failedSkillKeys: sortedFailedKeys
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function applyByteLimit(sortedRecords, source, failedSkillKeys, observedAt) {
|
|
53
|
+
const fullSnapshot = {
|
|
54
|
+
schemaVersion: "1",
|
|
55
|
+
source,
|
|
56
|
+
status: "partial",
|
|
57
|
+
// worst-case placeholder — 'partial' is the longest status value
|
|
58
|
+
observedAt,
|
|
59
|
+
skills: sortedRecords,
|
|
60
|
+
failedSkillKeys
|
|
61
|
+
};
|
|
62
|
+
const fullSize = byteLength(JSON.stringify(fullSnapshot));
|
|
63
|
+
if (fullSize <= validation.MAX_SNAPSHOT_BYTES) {
|
|
64
|
+
return sortedRecords;
|
|
65
|
+
}
|
|
66
|
+
let lo = 0;
|
|
67
|
+
let hi = sortedRecords.length;
|
|
68
|
+
while (lo < hi) {
|
|
69
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
70
|
+
const candidate = {
|
|
71
|
+
schemaVersion: "1",
|
|
72
|
+
source,
|
|
73
|
+
status: "partial",
|
|
74
|
+
observedAt,
|
|
75
|
+
skills: sortedRecords.slice(0, mid),
|
|
76
|
+
failedSkillKeys
|
|
77
|
+
};
|
|
78
|
+
const size = byteLength(JSON.stringify(candidate));
|
|
79
|
+
if (size <= validation.MAX_SNAPSHOT_BYTES) {
|
|
80
|
+
lo = mid;
|
|
81
|
+
} else {
|
|
82
|
+
hi = mid - 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return sortedRecords.slice(0, lo);
|
|
86
|
+
}
|
|
87
|
+
function byteLength(str) {
|
|
88
|
+
return Buffer.byteLength(str, "utf8");
|
|
89
|
+
}
|
|
90
|
+
function createLoadingSnapshot(source) {
|
|
91
|
+
return {
|
|
92
|
+
schemaVersion: "1",
|
|
93
|
+
source,
|
|
94
|
+
status: "loading",
|
|
95
|
+
observedAt: null,
|
|
96
|
+
skills: [],
|
|
97
|
+
failedSkillKeys: []
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function createFailedSnapshot(source, observedAt) {
|
|
101
|
+
return {
|
|
102
|
+
schemaVersion: "1",
|
|
103
|
+
source,
|
|
104
|
+
status: "failed",
|
|
105
|
+
observedAt,
|
|
106
|
+
skills: [],
|
|
107
|
+
failedSkillKeys: []
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
exports.boundSnapshot = boundSnapshot;
|
|
112
|
+
exports.createFailedSnapshot = createFailedSnapshot;
|
|
113
|
+
exports.createLoadingSnapshot = createLoadingSnapshot;
|
|
114
|
+
exports.sortRecordsByKey = sortRecordsByKey;
|
|
115
|
+
//# sourceMappingURL=snapshot.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.cjs.js","sources":["../src/snapshot.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n NpxSkillRecord,\n OciSkillRecord,\n SkillSnapshot,\n SnapshotSource,\n SnapshotStatus,\n} from './types';\nimport { MAX_SNAPSHOT_BYTES, MAX_SNAPSHOT_RECORDS } from './validation';\n\n/**\n * Sorts skill records by stable `key` in ascending Unicode code-point order.\n *\n * This is the deterministic ordering required by design D2: before applying\n * count or serialized-size limits, connectors sort records by stable `key`\n * and include the longest prefix that fits.\n *\n * @public\n */\nexport function sortRecordsByKey<T extends OciSkillRecord | NpxSkillRecord>(\n records: T[],\n): T[] {\n return [...records].sort((a, b) => {\n if (a.key < b.key) return -1;\n if (a.key > b.key) return 1;\n return 0;\n });\n}\n\n/**\n * Options for creating a bounded snapshot.\n *\n * @public\n */\nexport interface BoundSnapshotOptions {\n /** Source identity. */\n source: SnapshotSource;\n /** All successfully normalized records (will be sorted and bounded). */\n records: Array<OciSkillRecord | NpxSkillRecord>;\n /** Stable keys of skills that failed processing. */\n failedSkillKeys: string[];\n /**\n * Whether discovery completed without failures or limits.\n * When `true` and no failed keys, status is `ready`.\n * When `false`, status is `partial`.\n */\n discoveryComplete: boolean;\n /** UTC observation time. */\n observedAt: string;\n}\n\n/**\n * Creates a bounded v1 skill snapshot with deterministic ordering.\n *\n * Sorts records by stable key in ascending Unicode code-point order,\n * then selects the longest prefix that fits within:\n * - {@link MAX_SNAPSHOT_RECORDS} (1,000) records\n * - {@link MAX_SNAPSHOT_BYTES} (5 MiB) serialized JSON\n *\n * If bounding removes records, the status becomes `partial` — a\n * truncated `ready` response is never produced per design D2.\n *\n * Failed keys are sorted in the same ascending order.\n *\n * @public\n */\nexport function boundSnapshot(options: BoundSnapshotOptions): SkillSnapshot {\n const { source, records, failedSkillKeys, discoveryComplete, observedAt } =\n options;\n\n // Sort records and failed keys by stable key\n const sortedRecords = sortRecordsByKey(records);\n const sortedFailedKeys = [...failedSkillKeys].sort((a, b) => {\n if (a < b) return -1;\n if (a > b) return 1;\n return 0;\n });\n\n // Determine initial status\n let bounded = false;\n\n // Apply count bound\n let boundedRecords = sortedRecords;\n if (boundedRecords.length > MAX_SNAPSHOT_RECORDS) {\n boundedRecords = boundedRecords.slice(0, MAX_SNAPSHOT_RECORDS);\n bounded = true;\n }\n\n // Apply size bound by finding the longest prefix that fits\n boundedRecords = applyByteLimit(\n boundedRecords,\n source,\n sortedFailedKeys,\n observedAt,\n );\n if (boundedRecords.length < sortedRecords.length) {\n bounded = true;\n }\n\n // Determine final status per design D6\n let status: SnapshotStatus;\n if (bounded || !discoveryComplete) {\n status = 'partial';\n } else if (sortedFailedKeys.length > 0) {\n status = 'partial';\n } else {\n status = 'ready';\n }\n\n return {\n schemaVersion: '1',\n source,\n status,\n observedAt,\n skills: boundedRecords,\n failedSkillKeys: sortedFailedKeys,\n };\n}\n\n/**\n * Finds the longest prefix of sorted records whose snapshot serialization\n * fits within MAX_SNAPSHOT_BYTES.\n */\nfunction applyByteLimit(\n sortedRecords: Array<OciSkillRecord | NpxSkillRecord>,\n source: SnapshotSource,\n failedSkillKeys: string[],\n observedAt: string,\n): Array<OciSkillRecord | NpxSkillRecord> {\n // First check if all records fit\n const fullSnapshot: SkillSnapshot = {\n schemaVersion: '1',\n source,\n status: 'partial', // worst-case placeholder — 'partial' is the longest status value\n observedAt,\n skills: sortedRecords,\n failedSkillKeys,\n };\n const fullSize = byteLength(JSON.stringify(fullSnapshot));\n if (fullSize <= MAX_SNAPSHOT_BYTES) {\n return sortedRecords;\n }\n\n // Binary search for the longest prefix that fits\n let lo = 0;\n let hi = sortedRecords.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n const candidate: SkillSnapshot = {\n schemaVersion: '1',\n source,\n status: 'partial',\n observedAt,\n skills: sortedRecords.slice(0, mid),\n failedSkillKeys,\n };\n const size = byteLength(JSON.stringify(candidate));\n if (size <= MAX_SNAPSHOT_BYTES) {\n lo = mid;\n } else {\n hi = mid - 1;\n }\n }\n\n return sortedRecords.slice(0, lo);\n}\n\n/**\n * Returns the UTF-8 byte length of a string.\n *\n * This private helper is only called by `boundSnapshot` which is a\n * backend producer function, so `Buffer` is always available.\n */\nfunction byteLength(str: string): number {\n return Buffer.byteLength(str, 'utf8');\n}\n\n/**\n * Creates a `loading` snapshot for a source that has not yet completed\n * its first acquisition.\n *\n * @public\n */\nexport function createLoadingSnapshot(source: SnapshotSource): SkillSnapshot {\n return {\n schemaVersion: '1',\n source,\n status: 'loading',\n observedAt: null,\n skills: [],\n failedSkillKeys: [],\n };\n}\n\n/**\n * Creates a `failed` snapshot when no usable discovery result could\n * be obtained.\n *\n * @public\n */\nexport function createFailedSnapshot(\n source: SnapshotSource,\n observedAt: string,\n): SkillSnapshot {\n return {\n schemaVersion: '1',\n source,\n status: 'failed',\n observedAt,\n skills: [],\n failedSkillKeys: [],\n };\n}\n"],"names":["MAX_SNAPSHOT_RECORDS","MAX_SNAPSHOT_BYTES"],"mappings":"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"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { MAX_SNAPSHOT_RECORDS, MAX_SNAPSHOT_BYTES } from './validation.esm.js';
|
|
2
|
+
|
|
3
|
+
function sortRecordsByKey(records) {
|
|
4
|
+
return [...records].sort((a, b) => {
|
|
5
|
+
if (a.key < b.key) return -1;
|
|
6
|
+
if (a.key > b.key) return 1;
|
|
7
|
+
return 0;
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
function boundSnapshot(options) {
|
|
11
|
+
const { source, records, failedSkillKeys, discoveryComplete, observedAt } = options;
|
|
12
|
+
const sortedRecords = sortRecordsByKey(records);
|
|
13
|
+
const sortedFailedKeys = [...failedSkillKeys].sort((a, b) => {
|
|
14
|
+
if (a < b) return -1;
|
|
15
|
+
if (a > b) return 1;
|
|
16
|
+
return 0;
|
|
17
|
+
});
|
|
18
|
+
let bounded = false;
|
|
19
|
+
let boundedRecords = sortedRecords;
|
|
20
|
+
if (boundedRecords.length > MAX_SNAPSHOT_RECORDS) {
|
|
21
|
+
boundedRecords = boundedRecords.slice(0, MAX_SNAPSHOT_RECORDS);
|
|
22
|
+
bounded = true;
|
|
23
|
+
}
|
|
24
|
+
boundedRecords = applyByteLimit(
|
|
25
|
+
boundedRecords,
|
|
26
|
+
source,
|
|
27
|
+
sortedFailedKeys,
|
|
28
|
+
observedAt
|
|
29
|
+
);
|
|
30
|
+
if (boundedRecords.length < sortedRecords.length) {
|
|
31
|
+
bounded = true;
|
|
32
|
+
}
|
|
33
|
+
let status;
|
|
34
|
+
if (bounded || !discoveryComplete) {
|
|
35
|
+
status = "partial";
|
|
36
|
+
} else if (sortedFailedKeys.length > 0) {
|
|
37
|
+
status = "partial";
|
|
38
|
+
} else {
|
|
39
|
+
status = "ready";
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
schemaVersion: "1",
|
|
43
|
+
source,
|
|
44
|
+
status,
|
|
45
|
+
observedAt,
|
|
46
|
+
skills: boundedRecords,
|
|
47
|
+
failedSkillKeys: sortedFailedKeys
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function applyByteLimit(sortedRecords, source, failedSkillKeys, observedAt) {
|
|
51
|
+
const fullSnapshot = {
|
|
52
|
+
schemaVersion: "1",
|
|
53
|
+
source,
|
|
54
|
+
status: "partial",
|
|
55
|
+
// worst-case placeholder — 'partial' is the longest status value
|
|
56
|
+
observedAt,
|
|
57
|
+
skills: sortedRecords,
|
|
58
|
+
failedSkillKeys
|
|
59
|
+
};
|
|
60
|
+
const fullSize = byteLength(JSON.stringify(fullSnapshot));
|
|
61
|
+
if (fullSize <= MAX_SNAPSHOT_BYTES) {
|
|
62
|
+
return sortedRecords;
|
|
63
|
+
}
|
|
64
|
+
let lo = 0;
|
|
65
|
+
let hi = sortedRecords.length;
|
|
66
|
+
while (lo < hi) {
|
|
67
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
68
|
+
const candidate = {
|
|
69
|
+
schemaVersion: "1",
|
|
70
|
+
source,
|
|
71
|
+
status: "partial",
|
|
72
|
+
observedAt,
|
|
73
|
+
skills: sortedRecords.slice(0, mid),
|
|
74
|
+
failedSkillKeys
|
|
75
|
+
};
|
|
76
|
+
const size = byteLength(JSON.stringify(candidate));
|
|
77
|
+
if (size <= MAX_SNAPSHOT_BYTES) {
|
|
78
|
+
lo = mid;
|
|
79
|
+
} else {
|
|
80
|
+
hi = mid - 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return sortedRecords.slice(0, lo);
|
|
84
|
+
}
|
|
85
|
+
function byteLength(str) {
|
|
86
|
+
return Buffer.byteLength(str, "utf8");
|
|
87
|
+
}
|
|
88
|
+
function createLoadingSnapshot(source) {
|
|
89
|
+
return {
|
|
90
|
+
schemaVersion: "1",
|
|
91
|
+
source,
|
|
92
|
+
status: "loading",
|
|
93
|
+
observedAt: null,
|
|
94
|
+
skills: [],
|
|
95
|
+
failedSkillKeys: []
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function createFailedSnapshot(source, observedAt) {
|
|
99
|
+
return {
|
|
100
|
+
schemaVersion: "1",
|
|
101
|
+
source,
|
|
102
|
+
status: "failed",
|
|
103
|
+
observedAt,
|
|
104
|
+
skills: [],
|
|
105
|
+
failedSkillKeys: []
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { boundSnapshot, createFailedSnapshot, createLoadingSnapshot, sortRecordsByKey };
|
|
110
|
+
//# sourceMappingURL=snapshot.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.esm.js","sources":["../src/snapshot.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n NpxSkillRecord,\n OciSkillRecord,\n SkillSnapshot,\n SnapshotSource,\n SnapshotStatus,\n} from './types';\nimport { MAX_SNAPSHOT_BYTES, MAX_SNAPSHOT_RECORDS } from './validation';\n\n/**\n * Sorts skill records by stable `key` in ascending Unicode code-point order.\n *\n * This is the deterministic ordering required by design D2: before applying\n * count or serialized-size limits, connectors sort records by stable `key`\n * and include the longest prefix that fits.\n *\n * @public\n */\nexport function sortRecordsByKey<T extends OciSkillRecord | NpxSkillRecord>(\n records: T[],\n): T[] {\n return [...records].sort((a, b) => {\n if (a.key < b.key) return -1;\n if (a.key > b.key) return 1;\n return 0;\n });\n}\n\n/**\n * Options for creating a bounded snapshot.\n *\n * @public\n */\nexport interface BoundSnapshotOptions {\n /** Source identity. */\n source: SnapshotSource;\n /** All successfully normalized records (will be sorted and bounded). */\n records: Array<OciSkillRecord | NpxSkillRecord>;\n /** Stable keys of skills that failed processing. */\n failedSkillKeys: string[];\n /**\n * Whether discovery completed without failures or limits.\n * When `true` and no failed keys, status is `ready`.\n * When `false`, status is `partial`.\n */\n discoveryComplete: boolean;\n /** UTC observation time. */\n observedAt: string;\n}\n\n/**\n * Creates a bounded v1 skill snapshot with deterministic ordering.\n *\n * Sorts records by stable key in ascending Unicode code-point order,\n * then selects the longest prefix that fits within:\n * - {@link MAX_SNAPSHOT_RECORDS} (1,000) records\n * - {@link MAX_SNAPSHOT_BYTES} (5 MiB) serialized JSON\n *\n * If bounding removes records, the status becomes `partial` — a\n * truncated `ready` response is never produced per design D2.\n *\n * Failed keys are sorted in the same ascending order.\n *\n * @public\n */\nexport function boundSnapshot(options: BoundSnapshotOptions): SkillSnapshot {\n const { source, records, failedSkillKeys, discoveryComplete, observedAt } =\n options;\n\n // Sort records and failed keys by stable key\n const sortedRecords = sortRecordsByKey(records);\n const sortedFailedKeys = [...failedSkillKeys].sort((a, b) => {\n if (a < b) return -1;\n if (a > b) return 1;\n return 0;\n });\n\n // Determine initial status\n let bounded = false;\n\n // Apply count bound\n let boundedRecords = sortedRecords;\n if (boundedRecords.length > MAX_SNAPSHOT_RECORDS) {\n boundedRecords = boundedRecords.slice(0, MAX_SNAPSHOT_RECORDS);\n bounded = true;\n }\n\n // Apply size bound by finding the longest prefix that fits\n boundedRecords = applyByteLimit(\n boundedRecords,\n source,\n sortedFailedKeys,\n observedAt,\n );\n if (boundedRecords.length < sortedRecords.length) {\n bounded = true;\n }\n\n // Determine final status per design D6\n let status: SnapshotStatus;\n if (bounded || !discoveryComplete) {\n status = 'partial';\n } else if (sortedFailedKeys.length > 0) {\n status = 'partial';\n } else {\n status = 'ready';\n }\n\n return {\n schemaVersion: '1',\n source,\n status,\n observedAt,\n skills: boundedRecords,\n failedSkillKeys: sortedFailedKeys,\n };\n}\n\n/**\n * Finds the longest prefix of sorted records whose snapshot serialization\n * fits within MAX_SNAPSHOT_BYTES.\n */\nfunction applyByteLimit(\n sortedRecords: Array<OciSkillRecord | NpxSkillRecord>,\n source: SnapshotSource,\n failedSkillKeys: string[],\n observedAt: string,\n): Array<OciSkillRecord | NpxSkillRecord> {\n // First check if all records fit\n const fullSnapshot: SkillSnapshot = {\n schemaVersion: '1',\n source,\n status: 'partial', // worst-case placeholder — 'partial' is the longest status value\n observedAt,\n skills: sortedRecords,\n failedSkillKeys,\n };\n const fullSize = byteLength(JSON.stringify(fullSnapshot));\n if (fullSize <= MAX_SNAPSHOT_BYTES) {\n return sortedRecords;\n }\n\n // Binary search for the longest prefix that fits\n let lo = 0;\n let hi = sortedRecords.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n const candidate: SkillSnapshot = {\n schemaVersion: '1',\n source,\n status: 'partial',\n observedAt,\n skills: sortedRecords.slice(0, mid),\n failedSkillKeys,\n };\n const size = byteLength(JSON.stringify(candidate));\n if (size <= MAX_SNAPSHOT_BYTES) {\n lo = mid;\n } else {\n hi = mid - 1;\n }\n }\n\n return sortedRecords.slice(0, lo);\n}\n\n/**\n * Returns the UTF-8 byte length of a string.\n *\n * This private helper is only called by `boundSnapshot` which is a\n * backend producer function, so `Buffer` is always available.\n */\nfunction byteLength(str: string): number {\n return Buffer.byteLength(str, 'utf8');\n}\n\n/**\n * Creates a `loading` snapshot for a source that has not yet completed\n * its first acquisition.\n *\n * @public\n */\nexport function createLoadingSnapshot(source: SnapshotSource): SkillSnapshot {\n return {\n schemaVersion: '1',\n source,\n status: 'loading',\n observedAt: null,\n skills: [],\n failedSkillKeys: [],\n };\n}\n\n/**\n * Creates a `failed` snapshot when no usable discovery result could\n * be obtained.\n *\n * @public\n */\nexport function createFailedSnapshot(\n source: SnapshotSource,\n observedAt: string,\n): SkillSnapshot {\n return {\n schemaVersion: '1',\n source,\n status: 'failed',\n observedAt,\n skills: [],\n failedSkillKeys: [],\n };\n}\n"],"names":[],"mappings":"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"}
|