@weftlet/toolkit 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.
Files changed (157) hide show
  1. package/LICENSE +176 -0
  2. package/NOTICE +6 -0
  3. package/README.md +162 -2
  4. package/THIRD_PARTY_NOTICES.md +610 -0
  5. package/dist/application/agent-context.d.ts +23 -0
  6. package/dist/application/agent-context.js +94 -0
  7. package/dist/application/application.d.ts +23 -0
  8. package/dist/application/application.js +91 -0
  9. package/dist/application/hydrator.d.ts +49 -0
  10. package/dist/application/hydrator.js +275 -0
  11. package/dist/application/overlay.d.ts +6 -0
  12. package/dist/application/overlay.js +110 -0
  13. package/dist/application/saved-evidence.d.ts +38 -0
  14. package/dist/application/saved-evidence.js +59 -0
  15. package/dist/application/snapshot-checks.d.ts +16 -0
  16. package/dist/application/snapshot-checks.js +51 -0
  17. package/dist/cli.d.ts +3 -0
  18. package/dist/cli.js +4 -0
  19. package/dist/configuration/cache.d.ts +2 -0
  20. package/dist/configuration/cache.js +10 -0
  21. package/dist/configuration/config.d.ts +79 -0
  22. package/dist/configuration/config.js +165 -0
  23. package/dist/core.d.ts +1 -0
  24. package/dist/core.js +2 -0
  25. package/dist/extraction/codex-auth.d.ts +12 -0
  26. package/dist/extraction/codex-auth.js +74 -0
  27. package/dist/extraction/document.d.ts +5 -0
  28. package/dist/extraction/document.js +54 -0
  29. package/dist/extraction/ontology.d.ts +32 -0
  30. package/dist/extraction/ontology.js +164 -0
  31. package/dist/extraction/providers.d.ts +8 -0
  32. package/dist/extraction/providers.js +58 -0
  33. package/dist/extraction/runtime-evidence.d.ts +2 -0
  34. package/dist/extraction/runtime-evidence.js +84 -0
  35. package/dist/extraction/semantic-citations.d.ts +7 -0
  36. package/dist/extraction/semantic-citations.js +27 -0
  37. package/dist/extraction/semantic-process.d.ts +11 -0
  38. package/dist/extraction/semantic-process.js +68 -0
  39. package/dist/extraction/semantic.d.ts +73 -0
  40. package/dist/extraction/semantic.js +414 -0
  41. package/dist/extraction/static.d.ts +52 -0
  42. package/dist/extraction/static.js +322 -0
  43. package/dist/extraction/telemetry.d.ts +19 -0
  44. package/dist/extraction/telemetry.js +259 -0
  45. package/dist/governance/governance.d.ts +60 -0
  46. package/dist/governance/governance.js +209 -0
  47. package/dist/graph/authority.d.ts +2 -0
  48. package/dist/graph/authority.js +13 -0
  49. package/dist/graph/diagnostics.d.ts +5 -0
  50. package/dist/graph/diagnostics.js +61 -0
  51. package/dist/graph/query.d.ts +16 -0
  52. package/dist/graph/query.js +373 -0
  53. package/dist/graph/rdf.d.ts +26 -0
  54. package/dist/graph/rdf.js +144 -0
  55. package/dist/index.d.ts +11 -0
  56. package/dist/index.js +12 -0
  57. package/dist/infrastructure/module-resolution.d.ts +2 -0
  58. package/dist/infrastructure/module-resolution.js +10 -0
  59. package/dist/infrastructure/process.d.ts +5 -0
  60. package/dist/infrastructure/process.js +27 -0
  61. package/dist/infrastructure/util.d.ts +1 -0
  62. package/dist/infrastructure/util.js +2 -0
  63. package/dist/interfaces/cli.d.ts +4 -0
  64. package/dist/interfaces/cli.js +197 -0
  65. package/dist/interfaces/doctor.d.ts +25 -0
  66. package/dist/interfaces/doctor.js +49 -0
  67. package/dist/interfaces/init.d.ts +24 -0
  68. package/dist/interfaces/init.js +154 -0
  69. package/dist/interfaces/mcp-facade.d.ts +3 -0
  70. package/dist/interfaces/mcp-facade.js +4 -0
  71. package/dist/interfaces/mcp.d.ts +2 -0
  72. package/dist/interfaces/mcp.js +81 -0
  73. package/dist/interfaces/request.d.ts +2 -0
  74. package/dist/interfaces/request.js +59 -0
  75. package/dist/mcp.d.ts +3 -0
  76. package/dist/mcp.js +4 -0
  77. package/dist/repository/committed-tree.d.ts +2 -0
  78. package/dist/repository/committed-tree.js +35 -0
  79. package/dist/repository/git.d.ts +26 -0
  80. package/dist/repository/git.js +163 -0
  81. package/dist/repository/glob.d.ts +2 -0
  82. package/dist/repository/glob.js +46 -0
  83. package/dist/repository/working-content.d.ts +3 -0
  84. package/dist/repository/working-content.js +34 -0
  85. package/dist/storage/atomic-rename.d.ts +2 -0
  86. package/dist/storage/atomic-rename.js +22 -0
  87. package/dist/storage/storage.d.ts +15 -0
  88. package/dist/storage/storage.js +115 -0
  89. package/dist/telemetry.d.ts +1 -0
  90. package/dist/telemetry.js +2 -0
  91. package/dist/visualization/viewer-interaction.d.ts +12 -0
  92. package/dist/visualization/viewer-interaction.js +123 -0
  93. package/dist/visualization/visualization.d.ts +26 -0
  94. package/dist/visualization/visualization.js +491 -0
  95. package/docs/ADR-001-standalone-baseline.md +16 -0
  96. package/docs/ADR-002-saved-evidence.md +21 -0
  97. package/docs/ADR-003-draft-overlays.md +23 -0
  98. package/docs/ADR-004-rdf-query-path.md +37 -0
  99. package/docs/ADR-005-governance-and-optional-oml.md +37 -0
  100. package/docs/ADR-006-provider-boundaries.md +15 -0
  101. package/docs/ADR-007-toolkit-modules.md +53 -0
  102. package/docs/ADR-008-weftlet-packages.md +32 -0
  103. package/docs/CLOUD-ENVIRONMENT.md +56 -0
  104. package/docs/ENVIRONMENT-VALIDATION.md +83 -0
  105. package/docs/MIGRATING-TO-WEFTLET.md +54 -0
  106. package/docs/PROVIDERS.md +121 -0
  107. package/docs/RELEASING.md +146 -0
  108. package/docs/ROADMAP.md +55 -0
  109. package/docs/SOURCE-INVENTORY.md +48 -0
  110. package/docs/VALIDATION.md +530 -0
  111. package/docs/VIEWER.md +36 -0
  112. package/docs/architecture-baseline.json +57 -0
  113. package/docs/architecture-policy.md +41 -0
  114. package/docs/extraction-manifest.json +721 -0
  115. package/docs/licenses/REVIEW.md +16 -0
  116. package/docs/licenses/lru-cache-11.5.3-BlueOak.md +55 -0
  117. package/docs/licenses/negotiate-1.0.1-BSD.txt +22 -0
  118. package/docs/licenses/spark-md5-3.0.2-MIT.txt +7 -0
  119. package/examples/generic-typescript/context-graph.config.json +47 -0
  120. package/examples/generic-typescript/context-graph.oml.config.json +49 -0
  121. package/examples/generic-typescript/docs/search-capability.md +14 -0
  122. package/examples/generic-typescript/ontology/build.gradle +38 -0
  123. package/examples/generic-typescript/ontology/catalog.xml +5 -0
  124. package/examples/generic-typescript/ontology/settings.gradle +1 -0
  125. package/examples/generic-typescript/ontology/src/oml/context-graph.dev/core.oml +99 -0
  126. package/examples/generic-typescript/package.json +6 -0
  127. package/examples/generic-typescript/src/search.ts +11 -0
  128. package/examples/generic-typescript/test/search.test.ts +8 -0
  129. package/examples/generic-typescript/tsconfig.json +10 -0
  130. package/examples/pipeline/context-graph.config.json +49 -0
  131. package/examples/pipeline/docs/admission.md +6 -0
  132. package/examples/pipeline/docs/analysis.md +6 -0
  133. package/examples/pipeline/docs/discovery.md +6 -0
  134. package/examples/pipeline/docs/execution.md +6 -0
  135. package/examples/pipeline/docs/outcomes.md +6 -0
  136. package/examples/pipeline/docs/planning.md +6 -0
  137. package/examples/pipeline/package.json +6 -0
  138. package/examples/pipeline/src/admission.ts +9 -0
  139. package/examples/pipeline/src/analysis.ts +9 -0
  140. package/examples/pipeline/src/contracts/admission.ts +1 -0
  141. package/examples/pipeline/src/contracts/analysis.ts +1 -0
  142. package/examples/pipeline/src/contracts/discovery.ts +1 -0
  143. package/examples/pipeline/src/contracts/execution.ts +1 -0
  144. package/examples/pipeline/src/contracts/outcomes.ts +1 -0
  145. package/examples/pipeline/src/contracts/planning.ts +1 -0
  146. package/examples/pipeline/src/discovery.ts +9 -0
  147. package/examples/pipeline/src/execution.ts +9 -0
  148. package/examples/pipeline/src/outcomes.ts +9 -0
  149. package/examples/pipeline/src/planning.ts +9 -0
  150. package/examples/pipeline/src/queue.ts +12 -0
  151. package/examples/pipeline/test/pipeline.test.ts +17 -0
  152. package/examples/pipeline/tsconfig.json +12 -0
  153. package/package.json +116 -3
  154. package/runtime-check.d.mts +2 -0
  155. package/runtime-check.mjs +12 -0
  156. package/templates/context-graph-development/SKILL.md +37 -0
  157. package/third-party-tools.json +34 -0
@@ -0,0 +1,322 @@
1
+ // Modified for standalone extraction; see docs/extraction-manifest.json.
2
+ import { randomUUID } from 'node:crypto';
3
+ import { withCommittedTree } from "../repository/committed-tree.js";
4
+ import { copyFile, mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
5
+ import { createRequire } from 'node:module';
6
+ import { dirname, join } from 'node:path';
7
+ import { pathToFileURL } from 'node:url';
8
+ import { matchesRepositoryGlob } from "../repository/glob.js";
9
+ import { runProcess } from "../infrastructure/process.js";
10
+ import { sha256, stableJson } from "../infrastructure/util.js";
11
+ export class ScipTypeScriptIndexer {
12
+ config;
13
+ storageRoot;
14
+ constructor(config, storageRoot) {
15
+ this.config = config;
16
+ this.storageRoot = storageRoot;
17
+ }
18
+ async index(context) {
19
+ const require = createRequire(import.meta.url);
20
+ const destination = join(this.storageRoot, 'scip', `${context.snapshotId}.scip`);
21
+ await mkdir(join(this.storageRoot, 'scip'), { recursive: true });
22
+ const temporary = `${destination}.${randomUUID()}.tmp`;
23
+ const command = this.config.command === 'bundled-scip-typescript' ? process.execPath : this.config.command;
24
+ const args = this.config.command === 'bundled-scip-typescript'
25
+ ? [
26
+ require.resolve('@sourcegraph/scip-typescript'),
27
+ ...this.config.args,
28
+ '--output',
29
+ temporary,
30
+ ]
31
+ : this.config.args;
32
+ await withCommittedTree(context.repositoryRoot, context.commitSha, async (worktreeRoot) => {
33
+ await runProcess(command, args, worktreeRoot, {
34
+ ...process.env,
35
+ NODE_OPTIONS: process.env.NODE_OPTIONS ?? '--max-old-space-size=8192',
36
+ });
37
+ const source = this.config.command === 'bundled-scip-typescript'
38
+ ? temporary
39
+ : join(worktreeRoot, 'index.scip');
40
+ await stat(source);
41
+ if (source !== temporary)
42
+ await copyFile(source, temporary);
43
+ // SCIP embeds this disposable checkout in metadata and inferred-type hover text.
44
+ // Store the normalized artifact itself so its digest still authenticates its bytes.
45
+ await writeFile(temporary, normalizeScipCheckout(await readFile(temporary), worktreeRoot));
46
+ });
47
+ await rename(temporary, destination);
48
+ return {
49
+ path: destination,
50
+ digest: sha256(await readFile(destination)),
51
+ mode: 'full',
52
+ version: this.config.version,
53
+ };
54
+ }
55
+ }
56
+ function loadScipRuntime() {
57
+ const require = createRequire(import.meta.url);
58
+ const main = require.resolve('@sourcegraph/scip-typescript');
59
+ return require(join(dirname(main), 'scip.js'));
60
+ }
61
+ export function normalizeScipCheckout(bytes, checkoutRoot) {
62
+ const runtime = loadScipRuntime();
63
+ const roots = [pathToFileURL(checkoutRoot).href, checkoutRoot.replace(/\\/g, '/'), checkoutRoot];
64
+ const normalize = (value) => {
65
+ for (const root of roots) {
66
+ if (value === root)
67
+ return 'file:///context-graph/repository';
68
+ value = value.split(`${root}/`).join('file:///context-graph/repository/');
69
+ value = value.split(`${root}\\`).join('file:///context-graph/repository/');
70
+ }
71
+ return value;
72
+ };
73
+ const index = runtime.scip.Index.deserializeBinary(bytes);
74
+ if (index.metadata)
75
+ index.metadata.project_root = normalize(index.metadata.project_root);
76
+ for (const symbol of [
77
+ ...index.documents.flatMap((document) => document.symbols),
78
+ ...index.external_symbols,
79
+ ]) {
80
+ symbol.documentation = symbol.documentation.map(normalize);
81
+ }
82
+ return index.serializeBinary();
83
+ }
84
+ export class ScipSymbolExtractor {
85
+ repository;
86
+ index;
87
+ identity = { id: 'scip-symbol-extractor', version: '1.0.0' };
88
+ constructor(repository, index) {
89
+ this.repository = repository;
90
+ this.index = index;
91
+ }
92
+ async extract(context) {
93
+ const parsed = loadScipRuntime().scip.Index.deserializeBinary(await readFile(this.index.path));
94
+ const symbols = new Map();
95
+ for (const document of parsed.documents) {
96
+ const path = document.relative_path.replace(/\\/g, '/');
97
+ const content = await this.repository.readFileAt(context.commitSha, path);
98
+ const digest = sha256(content);
99
+ const component = {
100
+ id: `component:file:${path}`,
101
+ kind: (/\.(spec|test)\.[^.]+$/i.test(path) ? 'Evidence' : 'Component'),
102
+ label: path,
103
+ };
104
+ for (const occurrence of document.occurrences) {
105
+ if (!occurrence.symbol || occurrence.symbol.startsWith('local '))
106
+ continue;
107
+ const definition = (occurrence.symbol_roles & 1) === 1;
108
+ const sites = symbols.get(occurrence.symbol) ?? {
109
+ definitions: new Map(),
110
+ references: new Map(),
111
+ };
112
+ symbols.set(occurrence.symbol, sites);
113
+ const target = definition ? sites.definitions : sites.references;
114
+ if (target.has(path))
115
+ continue;
116
+ target.set(path, {
117
+ path,
118
+ line: (occurrence.range[0] ?? 0) + 1,
119
+ digest,
120
+ component,
121
+ });
122
+ }
123
+ }
124
+ const facts = [];
125
+ for (const [symbolText, sites] of [...symbols.entries()].sort(([left], [right]) => left.localeCompare(right))) {
126
+ const definitionPaths = new Set(sites.definitions.keys());
127
+ const downstream = [...sites.references.values()].filter((site) => !definitionPaths.has(site.path));
128
+ if (sites.definitions.size === 0 || downstream.length === 0)
129
+ continue;
130
+ const symbol = {
131
+ id: `symbol:scip:${sha256(symbolText)}`,
132
+ kind: 'Contract',
133
+ label: symbolText,
134
+ attributes: { scipIndexDigest: this.index.digest },
135
+ };
136
+ const relationships = [
137
+ ...[...sites.definitions.values()].map((site) => ({ site, definition: true })),
138
+ ...downstream.map((site) => ({ site, definition: false })),
139
+ ];
140
+ for (const { site, definition } of relationships) {
141
+ const relation = definition ? 'definition' : 'reference';
142
+ const provenance = {
143
+ path: site.path,
144
+ startLine: site.line,
145
+ endLine: site.line,
146
+ digest: site.digest,
147
+ commitSha: context.commitSha,
148
+ };
149
+ facts.push({
150
+ id: sha256(stableJson({ relation, path: site.path, symbol: symbolText, provenance })),
151
+ subject: site.component,
152
+ predicate: definition ? 'contains' : 'consumes',
153
+ object: symbol,
154
+ provenance,
155
+ extractor: this.identity,
156
+ confidence: 1,
157
+ snapshotId: context.snapshotId,
158
+ });
159
+ }
160
+ }
161
+ return facts;
162
+ }
163
+ }
164
+ export class RepositoryFileExtractor {
165
+ repository;
166
+ roots;
167
+ scipDigest;
168
+ identity = { id: 'repository-file-extractor', version: '1.0.0' };
169
+ constructor(repository, roots, scipDigest) {
170
+ this.repository = repository;
171
+ this.roots = roots;
172
+ this.scipDigest = scipDigest;
173
+ }
174
+ async extract(context) {
175
+ const paths = await this.repository.listFilesAt(context.commitSha, this.roots);
176
+ const facts = [];
177
+ const project = {
178
+ id: `project:${context.repositoryId}`,
179
+ kind: 'Project',
180
+ label: context.repositoryId,
181
+ };
182
+ for (const path of paths) {
183
+ if (!/\.(ts|tsx|js|mjs|cjs|json|prisma|ya?ml)$/i.test(path))
184
+ continue;
185
+ const content = await this.repository.readFileAt(context.commitSha, path);
186
+ const digest = sha256(content);
187
+ const component = {
188
+ id: `component:file:${path}`,
189
+ kind: (/\.(spec|test)\.[^.]+$/i.test(path) ? 'Evidence' : 'Component'),
190
+ label: path,
191
+ attributes: { scipIndexDigest: this.scipDigest },
192
+ };
193
+ const provenance = {
194
+ path,
195
+ startLine: 1,
196
+ endLine: Math.max(1, content.split('\n').length),
197
+ digest,
198
+ commitSha: context.commitSha,
199
+ };
200
+ facts.push({
201
+ id: sha256(stableJson({ project, component, provenance })),
202
+ subject: project,
203
+ predicate: 'contains',
204
+ object: component,
205
+ provenance,
206
+ extractor: this.identity,
207
+ confidence: 1,
208
+ snapshotId: context.snapshotId,
209
+ });
210
+ const lines = content.split('\n');
211
+ for (const [index, line] of lines.entries()) {
212
+ const imported = line.match(/(?:from\s+|import\s*\()['"]([^'"]+)['"]/)?.[1];
213
+ if (!imported)
214
+ continue;
215
+ const contract = {
216
+ id: `contract:module:${imported}`,
217
+ kind: 'Contract',
218
+ label: imported,
219
+ };
220
+ const importProvenance = { ...provenance, startLine: index + 1, endLine: index + 1 };
221
+ facts.push({
222
+ id: sha256(stableJson({ component, imported, importProvenance })),
223
+ subject: component,
224
+ predicate: 'consumes',
225
+ object: contract,
226
+ provenance: importProvenance,
227
+ extractor: this.identity,
228
+ confidence: 1,
229
+ snapshotId: context.snapshotId,
230
+ });
231
+ }
232
+ }
233
+ return facts;
234
+ }
235
+ }
236
+ export class RepositoryContractExtractor {
237
+ repository;
238
+ patterns;
239
+ consumerRoots;
240
+ aliases;
241
+ identity = { id: 'repository-contract-extractor', version: '1.1.0' };
242
+ constructor(repository, patterns, consumerRoots, aliases = {}) {
243
+ this.repository = repository;
244
+ this.patterns = patterns;
245
+ this.consumerRoots = consumerRoots;
246
+ this.aliases = aliases;
247
+ }
248
+ async extract(context) {
249
+ if (this.patterns.length === 0)
250
+ return [];
251
+ const paths = (await this.repository.listAllFilesAt(context.commitSha)).filter((path) => this.patterns.some((pattern) => matchesRepositoryGlob(path, pattern)));
252
+ const packageManifests = (await this.repository.listAllFilesAt(context.commitSha)).filter((path) => path.endsWith('package.json'));
253
+ const facts = [];
254
+ for (const path of paths) {
255
+ const content = await this.repository.readFileAt(context.commitSha, path);
256
+ const provenance = {
257
+ path,
258
+ startLine: 1,
259
+ endLine: Math.max(1, content.split('\n').length),
260
+ digest: sha256(content),
261
+ commitSha: context.commitSha,
262
+ };
263
+ const contract = {
264
+ id: `contract:file:${path}`,
265
+ kind: 'Contract',
266
+ label: path,
267
+ };
268
+ const source = {
269
+ id: `evidence:contract-file:${path}`,
270
+ kind: 'Evidence',
271
+ label: path,
272
+ };
273
+ facts.push({
274
+ id: sha256(stableJson({ source, contract, provenance })),
275
+ subject: source,
276
+ predicate: 'contains',
277
+ object: contract,
278
+ provenance,
279
+ extractor: this.identity,
280
+ confidence: 1,
281
+ snapshotId: context.snapshotId,
282
+ });
283
+ const needles = [path, ...(this.aliases[path] ?? [])];
284
+ const manifest = packageManifests
285
+ .filter((candidate) => path.startsWith(candidate.slice(0, -'package.json'.length)))
286
+ .sort((left, right) => right.length - left.length)[0];
287
+ if (manifest) {
288
+ const parsed = JSON.parse(await this.repository.readFileAt(context.commitSha, manifest));
289
+ if (typeof parsed.name === 'string' && parsed.name.startsWith('@')) {
290
+ needles.push(parsed.name);
291
+ }
292
+ }
293
+ const matches = await this.repository.findTextAt(context.commitSha, needles, this.consumerRoots);
294
+ for (const match of matches.filter((candidate) => candidate.path !== path && candidate.path !== manifest)) {
295
+ const consumerContent = await this.repository.readFileAt(context.commitSha, match.path);
296
+ const consumerProvenance = {
297
+ path: match.path,
298
+ startLine: match.line,
299
+ endLine: match.line,
300
+ digest: sha256(consumerContent),
301
+ commitSha: context.commitSha,
302
+ };
303
+ const consumer = {
304
+ id: `component:file:${match.path}`,
305
+ kind: 'Component',
306
+ label: match.path,
307
+ };
308
+ facts.push({
309
+ id: sha256(stableJson({ consumer, contract, consumerProvenance })),
310
+ subject: consumer,
311
+ predicate: 'consumes',
312
+ object: contract,
313
+ provenance: consumerProvenance,
314
+ extractor: this.identity,
315
+ confidence: 1,
316
+ snapshotId: context.snapshotId,
317
+ });
318
+ }
319
+ }
320
+ return facts;
321
+ }
322
+ }
@@ -0,0 +1,19 @@
1
+ import type { ExtractionContext, RuntimeEvidenceProvider, RuntimeTraceReference } from "@weftlet/core";
2
+ import type { ContextGraphConfig } from "../configuration/config.js";
3
+ export declare class OtlpJsonRuntimeEvidenceProvider implements RuntimeEvidenceProvider {
4
+ private readonly patterns;
5
+ private readonly now;
6
+ private readonly policy;
7
+ constructor(patterns: readonly string[], policy?: ContextGraphConfig['runtimeEvidencePolicy'], now?: () => Date);
8
+ collect(context: ExtractionContext): Promise<readonly RuntimeTraceReference[]>;
9
+ }
10
+ export declare function captureValidationTrace(outputPath: string, options: {
11
+ readonly serviceName: string;
12
+ readonly serviceVersion: string;
13
+ readonly capability: string;
14
+ readonly operation?: string;
15
+ readonly environment: string;
16
+ readonly serviceInstanceId?: string;
17
+ readonly backendUrl?: string;
18
+ readonly run: () => Promise<void>;
19
+ }): Promise<void>;
@@ -0,0 +1,259 @@
1
+ // Modified for standalone extraction; see docs/extraction-manifest.json.
2
+ import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
3
+ import { dirname, join, relative, resolve } from 'node:path';
4
+ import { trace } from '@opentelemetry/api';
5
+ import { resourceFromAttributes } from '@opentelemetry/resources';
6
+ import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor, } from '@opentelemetry/sdk-trace-base';
7
+ import { sha256 } from "../infrastructure/util.js";
8
+ function attribute(attributes, key) {
9
+ return attributes?.find((candidate) => candidate.key === key)?.value.stringValue;
10
+ }
11
+ async function expandPatterns(root, patterns) {
12
+ const files = [];
13
+ for (const pattern of patterns) {
14
+ const normalized = pattern.replace(/\\/g, '/');
15
+ const wildcard = normalized.indexOf('*');
16
+ const directory = resolve(root, wildcard >= 0 ? normalized.slice(0, wildcard) : dirname(normalized));
17
+ const suffix = wildcard >= 0 ? normalized.slice(normalized.lastIndexOf('*') + 1) : normalized;
18
+ try {
19
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
20
+ const path = join(directory, entry.name);
21
+ if (entry.isFile() && path.replace(/\\/g, '/').endsWith(suffix))
22
+ files.push(path);
23
+ }
24
+ }
25
+ catch (error) {
26
+ if (error.code !== 'ENOENT')
27
+ throw error;
28
+ }
29
+ }
30
+ return files.sort();
31
+ }
32
+ export class OtlpJsonRuntimeEvidenceProvider {
33
+ patterns;
34
+ now;
35
+ policy;
36
+ constructor(patterns, policy = undefined, now = () => new Date()) {
37
+ this.patterns = patterns;
38
+ this.now = now;
39
+ this.policy = policy ?? {
40
+ retentionDays: 14,
41
+ maxReferences: 500,
42
+ maxSourceBytes: 5 * 1024 * 1024,
43
+ requireEnvironment: false,
44
+ requireServiceInstance: false,
45
+ capabilityKeys: ['context.graph.capability'],
46
+ operationKeys: ['context.graph.operation'],
47
+ };
48
+ }
49
+ async collect(context) {
50
+ const references = [];
51
+ const paths = await expandPatterns(context.repositoryRoot, this.patterns);
52
+ if (paths.length > this.policy.maxReferences) {
53
+ throw new Error(`Runtime evidence matched ${paths.length} sources, exceeding the ${this.policy.maxReferences}-reference cap`);
54
+ }
55
+ let cumulativeSourceBytes = 0;
56
+ for (const path of paths) {
57
+ const sourcePath = relative(context.repositoryRoot, path).replace(/\\/g, '/');
58
+ const sourceBytes = (await stat(path)).size;
59
+ if (sourceBytes > this.policy.maxSourceBytes) {
60
+ throw new Error(`Runtime evidence source exceeds ${this.policy.maxSourceBytes} bytes: ${sourcePath}`);
61
+ }
62
+ cumulativeSourceBytes += sourceBytes;
63
+ if (cumulativeSourceBytes > this.policy.maxSourceBytes) {
64
+ throw new Error(`Runtime evidence sources exceed the cumulative ${this.policy.maxSourceBytes}-byte cap`);
65
+ }
66
+ }
67
+ let expandedReferenceCount = 0;
68
+ for (const path of paths) {
69
+ const sourcePath = relative(context.repositoryRoot, path).replace(/\\/g, '/');
70
+ const content = await readFile(path, 'utf8');
71
+ const document = JSON.parse(content);
72
+ const sourceDigest = sha256(content);
73
+ for (const resourceSpan of document.resourceSpans ?? []) {
74
+ const resourceAttributes = resourceSpan.resource?.attributes;
75
+ const configuredServiceName = attribute(resourceAttributes, 'service.name');
76
+ const serviceName = configuredServiceName ?? 'unknown';
77
+ const serviceVersion = attribute(resourceAttributes, 'service.version') ?? '';
78
+ const environment = attribute(resourceAttributes, 'deployment.environment.name');
79
+ const serviceInstanceId = attribute(resourceAttributes, 'service.instance.id');
80
+ for (const scope of resourceSpan.scopeSpans ?? []) {
81
+ for (const span of scope.spans ?? []) {
82
+ expandedReferenceCount += 1;
83
+ if (expandedReferenceCount > this.policy.maxReferences) {
84
+ throw new Error(`Runtime evidence expands beyond the ${this.policy.maxReferences}-reference cap`);
85
+ }
86
+ const capability = this.policy.capabilityKeys
87
+ .map((key) => attribute(span.attributes, key))
88
+ .find(Boolean);
89
+ const operation = this.policy.operationKeys
90
+ .map((key) => attribute(span.attributes, key))
91
+ .find(Boolean);
92
+ const observedAt = parseUnixNano(span.endTimeUnixNano ?? span.startTimeUnixNano);
93
+ const ageMs = observedAt ? this.now().getTime() - observedAt.getTime() : undefined;
94
+ const retentionMs = this.policy.retentionDays * 24 * 60 * 60 * 1000;
95
+ const reason = serviceVersion !== context.commitSha
96
+ ? 'service.version does not match snapshot commit SHA'
97
+ : !configuredServiceName
98
+ ? 'service.name is required by runtime evidence policy'
99
+ : this.policy.allowedServiceNames &&
100
+ !this.policy.allowedServiceNames.includes(configuredServiceName)
101
+ ? 'service.name is not allowed by runtime evidence policy'
102
+ : this.policy.requireEnvironment && !environment
103
+ ? 'deployment.environment.name is required by runtime evidence policy'
104
+ : environment &&
105
+ this.policy.allowedEnvironments &&
106
+ !this.policy.allowedEnvironments.includes(environment)
107
+ ? 'deployment.environment.name is not allowed by runtime evidence policy'
108
+ : this.policy.requireServiceInstance && !serviceInstanceId
109
+ ? 'service.instance.id is required by runtime evidence policy'
110
+ : !validOtelId(span.traceId, 32) || !validOtelId(span.spanId, 16)
111
+ ? 'runtime evidence trace/span identity is missing or invalid'
112
+ : !observedAt
113
+ ? 'runtime evidence timestamp is missing or invalid'
114
+ : !capability
115
+ ? 'runtime evidence capability is required'
116
+ : !operation
117
+ ? 'runtime evidence operation is required'
118
+ : ageMs !== undefined && ageMs < 0
119
+ ? 'runtime evidence timestamp is in the future'
120
+ : ageMs !== undefined && ageMs > retentionMs
121
+ ? `runtime evidence is outside the ${this.policy.retentionDays}-day retention window`
122
+ : undefined;
123
+ references.push({
124
+ traceId: span.traceId,
125
+ spanId: span.spanId,
126
+ serviceName,
127
+ serviceVersion,
128
+ capability,
129
+ operation,
130
+ environment,
131
+ serviceInstanceId,
132
+ observedAt: observedAt?.toISOString(),
133
+ sourcePath,
134
+ sourceDigest,
135
+ accepted: reason === undefined,
136
+ reason,
137
+ });
138
+ }
139
+ }
140
+ }
141
+ }
142
+ const unique = new Map();
143
+ for (const reference of references) {
144
+ const key = `${reference.traceId}:${reference.spanId}`;
145
+ const existing = unique.get(key);
146
+ if (!existing || preferRuntimeReference(reference, existing) < 0) {
147
+ unique.set(key, reference);
148
+ }
149
+ }
150
+ return [...unique.values()].sort(preferRuntimeReference).slice(0, this.policy.maxReferences);
151
+ }
152
+ }
153
+ function validOtelId(value, length) {
154
+ return new RegExp(`^[a-f0-9]{${length}}$`, 'i').test(value) && !/^0+$/.test(value);
155
+ }
156
+ function preferRuntimeReference(left, right) {
157
+ return (Number(right.accepted) - Number(left.accepted) ||
158
+ (right.observedAt ?? '').localeCompare(left.observedAt ?? '') ||
159
+ left.sourcePath.localeCompare(right.sourcePath) ||
160
+ left.traceId.localeCompare(right.traceId) ||
161
+ left.spanId.localeCompare(right.spanId));
162
+ }
163
+ function parseUnixNano(value) {
164
+ if (!value || !/^\d+$/.test(value))
165
+ return undefined;
166
+ const milliseconds = Number(BigInt(value) / 1000000n);
167
+ if (!Number.isFinite(milliseconds))
168
+ return undefined;
169
+ const parsed = new Date(milliseconds);
170
+ return Number.isNaN(parsed.getTime()) ? undefined : parsed;
171
+ }
172
+ export async function captureValidationTrace(outputPath, options) {
173
+ const exporter = new InMemorySpanExporter();
174
+ const provider = new BasicTracerProvider({
175
+ resource: resourceFromAttributes({
176
+ 'service.name': options.serviceName,
177
+ 'service.version': options.serviceVersion,
178
+ 'deployment.environment.name': options.environment,
179
+ }),
180
+ spanProcessors: [new SimpleSpanProcessor(exporter)],
181
+ });
182
+ const tracer = provider.getTracer('context-graph-validation', '1.0.0');
183
+ const span = tracer.startSpan(options.operation ?? 'context-graph.validation', {
184
+ attributes: {
185
+ 'context.graph.capability': options.capability,
186
+ 'context.graph.operation': options.operation ?? 'context-graph.validation',
187
+ },
188
+ });
189
+ try {
190
+ await options.run();
191
+ }
192
+ catch (error) {
193
+ span.recordException(error);
194
+ throw error;
195
+ }
196
+ finally {
197
+ span.end();
198
+ await provider.forceFlush();
199
+ }
200
+ const spans = exporter.getFinishedSpans();
201
+ const document = {
202
+ resourceSpans: [
203
+ {
204
+ resource: {
205
+ attributes: [
206
+ { key: 'service.name', value: { stringValue: options.serviceName } },
207
+ { key: 'service.version', value: { stringValue: options.serviceVersion } },
208
+ {
209
+ key: 'service.instance.id',
210
+ value: { stringValue: options.serviceInstanceId ?? 'context-graph-validation' },
211
+ },
212
+ {
213
+ key: 'deployment.environment.name',
214
+ value: { stringValue: options.environment },
215
+ },
216
+ ],
217
+ },
218
+ scopeSpans: [
219
+ {
220
+ spans: spans.map((finished) => ({
221
+ traceId: finished.spanContext().traceId,
222
+ spanId: finished.spanContext().spanId,
223
+ name: finished.name,
224
+ startTimeUnixNano: hrTimeToUnixNano(finished.startTime),
225
+ endTimeUnixNano: hrTimeToUnixNano(finished.endTime),
226
+ attributes: [
227
+ {
228
+ key: 'context.graph.capability',
229
+ value: { stringValue: options.capability },
230
+ },
231
+ {
232
+ key: 'context.graph.operation',
233
+ value: { stringValue: options.operation ?? finished.name },
234
+ },
235
+ ],
236
+ })),
237
+ },
238
+ ],
239
+ },
240
+ ],
241
+ };
242
+ await mkdir(dirname(outputPath), { recursive: true });
243
+ const serialized = `${JSON.stringify(document, null, 2)}\n`;
244
+ await writeFile(outputPath, serialized, 'utf8');
245
+ if (options.backendUrl) {
246
+ const response = await fetch(options.backendUrl, {
247
+ method: 'POST',
248
+ headers: { 'content-type': 'application/json' },
249
+ body: serialized,
250
+ });
251
+ if (!response.ok) {
252
+ throw new Error(`OTLP backend rejected validation trace with HTTP ${response.status}`);
253
+ }
254
+ }
255
+ trace.disable();
256
+ }
257
+ function hrTimeToUnixNano(value) {
258
+ return (BigInt(value[0]) * 1000000000n + BigInt(value[1])).toString();
259
+ }
@@ -0,0 +1,60 @@
1
+ import type { GraphSnapshot } from "@weftlet/core";
2
+ export interface ArchitecturePolicy {
3
+ schemaVersion: 1;
4
+ roots: string[];
5
+ modules: {
6
+ id: string;
7
+ path: string;
8
+ domain: string;
9
+ layer: string;
10
+ adapter?: boolean;
11
+ }[];
12
+ contracts?: {
13
+ path: string;
14
+ owner: string;
15
+ }[];
16
+ rules: {
17
+ id: string;
18
+ kind: 'forbidden-import' | 'no-cycles' | 'contract-owner';
19
+ from?: string;
20
+ to?: string;
21
+ approved: boolean;
22
+ approvalRef?: string;
23
+ transitive?: boolean;
24
+ }[];
25
+ advisory: {
26
+ maxLines: number;
27
+ maxComplexity: number;
28
+ duplicateMinTokens: number;
29
+ };
30
+ baseline?: {
31
+ fingerprint: string;
32
+ approvalRef: string;
33
+ reason: string;
34
+ }[];
35
+ exceptions?: {
36
+ fingerprint: string;
37
+ ruleId: string;
38
+ from: string;
39
+ to: string;
40
+ expires: string;
41
+ approvalRef: string;
42
+ reason: string;
43
+ }[];
44
+ }
45
+ export interface GovernanceFinding {
46
+ category: 'rule-violation' | 'extraction-gap' | 'infrastructure-failure' | 'advisory';
47
+ ruleId: string;
48
+ from: string;
49
+ to: string;
50
+ message: string;
51
+ fingerprint: string;
52
+ disposition: 'new' | 'baseline' | 'exception';
53
+ }
54
+ export declare function analyzeArchitecture(root: string, policy: ArchitecturePolicy, policyPath?: string): Promise<{
55
+ policyPath: string;
56
+ inputDigest: string;
57
+ snapshot: GraphSnapshot;
58
+ findings: GovernanceFinding[];
59
+ passed: boolean;
60
+ }>;