graphlin 0.1.3 → 0.2.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 (106) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +12 -3
  4. package/docs/decision-service.md +393 -0
  5. package/docs/extension-authoring.md +553 -0
  6. package/docs/model-api.md +293 -0
  7. package/docs/usage.md +472 -0
  8. package/docs/visualizer-views.md +240 -0
  9. package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
  10. package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
  11. package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
  12. package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
  13. package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
  14. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
  15. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
  16. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
  17. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
  18. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
  19. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
  20. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
  21. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
  22. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
  23. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
  24. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
  25. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
  26. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
  27. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
  28. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
  29. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
  30. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
  31. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
  32. package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
  33. package/package.json +78 -9
  34. package/plugin.json +4 -2
  35. package/runtime/architecture/analysis.mjs +344 -0
  36. package/runtime/architecture/controller.mjs +209 -0
  37. package/runtime/architecture/evidence.mjs +108 -0
  38. package/runtime/architecture/profile.mjs +56 -0
  39. package/runtime/core/evidence.mjs +43 -9
  40. package/runtime/core/graph.mjs +11 -6
  41. package/runtime/core/privacy.mjs +1 -0
  42. package/runtime/daemon/auth.mjs +7 -3
  43. package/runtime/daemon/diagnostics.mjs +1 -1
  44. package/runtime/daemon/extension-api.mjs +203 -0
  45. package/runtime/daemon/lineage.mjs +70 -0
  46. package/runtime/daemon/manager.mjs +9 -6
  47. package/runtime/daemon/model-api.mjs +728 -0
  48. package/runtime/daemon/model-persistence.mjs +220 -0
  49. package/runtime/daemon/server.mjs +81 -14
  50. package/runtime/daemon/settings.mjs +11 -3
  51. package/runtime/decisions/broker.mjs +349 -0
  52. package/runtime/decisions/contracts.mjs +179 -0
  53. package/runtime/decisions/evaluation.mjs +305 -0
  54. package/runtime/decisions/faults.mjs +32 -0
  55. package/runtime/decisions/index.mjs +818 -0
  56. package/runtime/decisions/profiles.mjs +93 -0
  57. package/runtime/decisions/questions.mjs +268 -0
  58. package/runtime/discovery/index.mjs +2 -0
  59. package/runtime/discovery/inventory.mjs +160 -0
  60. package/runtime/discovery/parser.mjs +40 -0
  61. package/runtime/discovery/structure.mjs +232 -0
  62. package/runtime/extensions/contracts.mjs +59 -0
  63. package/runtime/extensions/frame.mjs +64 -0
  64. package/runtime/extensions/index.mjs +9 -0
  65. package/runtime/extensions/manifest.mjs +95 -0
  66. package/runtime/extensions/packages.mjs +222 -0
  67. package/runtime/extensions/profiles.mjs +36 -0
  68. package/runtime/extensions/projection.mjs +130 -0
  69. package/runtime/extensions/registry.mjs +285 -0
  70. package/runtime/extensions/scene.mjs +105 -0
  71. package/runtime/extensions/sdk.d.ts +205 -0
  72. package/runtime/extensions/sdk.mjs +88 -0
  73. package/runtime/jev/index.mjs +13 -777
  74. package/runtime/jev/provider.mjs +101 -0
  75. package/runtime/jev/questions.mjs +16 -258
  76. package/runtime/jev/wire.mjs +17 -25
  77. package/runtime/model/changes.mjs +42 -0
  78. package/runtime/model/history.mjs +124 -0
  79. package/runtime/model/index.mjs +2 -0
  80. package/runtime/model/project-model.mjs +1020 -0
  81. package/runtime/model/records.mjs +240 -0
  82. package/runtime/pipeline.mjs +267 -55
  83. package/runtime/platform.mjs +254 -0
  84. package/runtime/visualizers/blocks.mjs +5 -0
  85. package/runtime/visualizers/c4.mjs +154 -0
  86. package/runtime/visualizers/changes.mjs +24 -0
  87. package/runtime/visualizers/code.mjs +5 -0
  88. package/runtime/visualizers/index.mjs +23 -0
  89. package/runtime/visualizers/structure.mjs +120 -0
  90. package/runtime/visualizers/timeline.mjs +66 -0
  91. package/runtime/web/app.js +225 -63
  92. package/runtime/web/extension-frame.js +128 -0
  93. package/runtime/web/index.html +38 -1
  94. package/runtime/web/model-client.js +162 -0
  95. package/runtime/web/platform.js +445 -0
  96. package/runtime/web/scene.js +111 -0
  97. package/runtime/web/style.css +51 -0
  98. package/schemas/graph.schema.json +4 -1
  99. package/scripts/arguments.mjs +5 -1
  100. package/scripts/build-packages.mjs +6 -2
  101. package/scripts/control.mjs +1 -1
  102. package/scripts/daemon.mjs +2 -1
  103. package/scripts/extensions.mjs +44 -0
  104. package/scripts/graphlin.mjs +23 -3
  105. package/scripts/onboarding.mjs +10 -3
  106. package/scripts/validate-packages.mjs +54 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphlin",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "Live architecture and activity diagrams from observable coding-agent work.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -14,6 +14,7 @@
14
14
  "start": "node scripts/graphlin.mjs start",
15
15
  "demo": "node scripts/graphlin.mjs demo",
16
16
  "test": "node --test --test-timeout=30000",
17
+ "test:stress": "node --test --test-timeout=90000 tests/stress/*.mjs",
17
18
  "build": "node scripts/build-packages.mjs",
18
19
  "doctor": "node scripts/graphlin.mjs doctor",
19
20
  "eval:jev": "node scripts/evaluate-jev.mjs",
@@ -51,20 +52,28 @@
51
52
  "registry": "https://registry.npmjs.org/"
52
53
  },
53
54
  "files": [
54
- "./LICENSE",
55
- "./README.md",
56
- "./plugin.json",
57
- "./mcp.json",
58
- "./.mcp.json",
59
55
  "./.claude-plugin/plugin.json",
60
56
  "./.codex-plugin/plugin.json",
57
+ "./.mcp.json",
58
+ "./LICENSE",
59
+ "./README.md",
61
60
  "./adapters/README.md",
62
61
  "./adapters/claude/hooks.json",
63
62
  "./adapters/claude/profile.json",
64
63
  "./adapters/codex/hooks.json",
65
64
  "./adapters/codex/profile.json",
66
65
  "./adapters/kiro/profile.json",
67
- "./skills/graphlin/SKILL.md",
66
+ "./docs/decision-service.md",
67
+ "./docs/extension-authoring.md",
68
+ "./docs/model-api.md",
69
+ "./docs/usage.md",
70
+ "./docs/visualizer-views.md",
71
+ "./mcp.json",
72
+ "./plugin.json",
73
+ "./runtime/architecture/analysis.mjs",
74
+ "./runtime/architecture/controller.mjs",
75
+ "./runtime/architecture/evidence.mjs",
76
+ "./runtime/architecture/profile.mjs",
68
77
  "./runtime/collector/index.mjs",
69
78
  "./runtime/core/candidates.mjs",
70
79
  "./runtime/core/common.mjs",
@@ -80,21 +89,65 @@
80
89
  "./runtime/daemon/demo.mjs",
81
90
  "./runtime/daemon/diagnostics.mjs",
82
91
  "./runtime/daemon/export.mjs",
92
+ "./runtime/daemon/extension-api.mjs",
83
93
  "./runtime/daemon/ipc.mjs",
94
+ "./runtime/daemon/lineage.mjs",
84
95
  "./runtime/daemon/lock.mjs",
85
96
  "./runtime/daemon/manager.mjs",
97
+ "./runtime/daemon/model-api.mjs",
98
+ "./runtime/daemon/model-persistence.mjs",
86
99
  "./runtime/daemon/paths.mjs",
87
100
  "./runtime/daemon/persistence.mjs",
88
101
  "./runtime/daemon/server.mjs",
89
102
  "./runtime/daemon/settings.mjs",
103
+ "./runtime/decisions/broker.mjs",
104
+ "./runtime/decisions/contracts.mjs",
105
+ "./runtime/decisions/evaluation.mjs",
106
+ "./runtime/decisions/faults.mjs",
107
+ "./runtime/decisions/index.mjs",
108
+ "./runtime/decisions/profiles.mjs",
109
+ "./runtime/decisions/questions.mjs",
110
+ "./runtime/discovery/index.mjs",
111
+ "./runtime/discovery/inventory.mjs",
112
+ "./runtime/discovery/parser.mjs",
113
+ "./runtime/discovery/structure.mjs",
114
+ "./runtime/extensions/contracts.mjs",
115
+ "./runtime/extensions/frame.mjs",
116
+ "./runtime/extensions/index.mjs",
117
+ "./runtime/extensions/manifest.mjs",
118
+ "./runtime/extensions/packages.mjs",
119
+ "./runtime/extensions/profiles.mjs",
120
+ "./runtime/extensions/projection.mjs",
121
+ "./runtime/extensions/registry.mjs",
122
+ "./runtime/extensions/scene.mjs",
123
+ "./runtime/extensions/sdk.d.ts",
124
+ "./runtime/extensions/sdk.mjs",
90
125
  "./runtime/jev/fixture.mjs",
91
126
  "./runtime/jev/index.mjs",
127
+ "./runtime/jev/provider.mjs",
92
128
  "./runtime/jev/questions.mjs",
93
129
  "./runtime/jev/wire.mjs",
130
+ "./runtime/model/changes.mjs",
131
+ "./runtime/model/history.mjs",
132
+ "./runtime/model/index.mjs",
133
+ "./runtime/model/project-model.mjs",
134
+ "./runtime/model/records.mjs",
94
135
  "./runtime/pipeline.mjs",
136
+ "./runtime/platform.mjs",
137
+ "./runtime/visualizers/blocks.mjs",
138
+ "./runtime/visualizers/c4.mjs",
139
+ "./runtime/visualizers/changes.mjs",
140
+ "./runtime/visualizers/code.mjs",
141
+ "./runtime/visualizers/index.mjs",
142
+ "./runtime/visualizers/structure.mjs",
143
+ "./runtime/visualizers/timeline.mjs",
95
144
  "./runtime/web/app.js",
145
+ "./runtime/web/extension-frame.js",
96
146
  "./runtime/web/index.html",
97
147
  "./runtime/web/layout.js",
148
+ "./runtime/web/model-client.js",
149
+ "./runtime/web/platform.js",
150
+ "./runtime/web/scene.js",
98
151
  "./runtime/web/sidebar.js",
99
152
  "./runtime/web/sketch.js",
100
153
  "./runtime/web/style.css",
@@ -108,8 +161,24 @@
108
161
  "./scripts/collector.mjs",
109
162
  "./scripts/control.mjs",
110
163
  "./scripts/daemon.mjs",
164
+ "./scripts/extensions.mjs",
111
165
  "./scripts/graphlin.mjs",
112
166
  "./scripts/onboarding.mjs",
113
- "./scripts/validate-packages.mjs"
114
- ]
167
+ "./scripts/validate-packages.mjs",
168
+ "./skills/graphlin/SKILL.md"
169
+ ],
170
+ "dependencies": {
171
+ "@vscode/tree-sitter-wasm": "0.3.1"
172
+ },
173
+ "bundledDependencies": [
174
+ "@vscode/tree-sitter-wasm"
175
+ ],
176
+ "exports": {
177
+ "./extensions/sdk": {
178
+ "types": "./runtime/extensions/sdk.d.ts",
179
+ "import": "./runtime/extensions/sdk.mjs",
180
+ "default": "./runtime/extensions/sdk.mjs"
181
+ },
182
+ "./extensions/scene": "./runtime/extensions/scene.mjs"
183
+ }
115
184
  }
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "graphlin",
4
- "version": "0.1.3",
4
+ "version": "0.2.1",
5
5
  "description": "Local architecture and activity diagrams from observable coding-agent work.",
6
6
  "extensions": {
7
7
  "com.openai": {
@@ -11,7 +11,9 @@
11
11
  "longDescription": "A local viewer with passive event collection, explicit source-sharing controls, and an offline fixture demo.",
12
12
  "developerName": "Graphlin contributors",
13
13
  "category": "Productivity",
14
- "defaultPrompt": ["Start Graphlin for this project in metadata-only mode."],
14
+ "defaultPrompt": [
15
+ "Start Graphlin for this project in metadata-only mode."
16
+ ],
15
17
  "brandColor": "#20BFA9"
16
18
  },
17
19
  "hooks": "./adapters/codex/hooks.json"
@@ -0,0 +1,344 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+ import { freeze, hash, integer } from '../core/common.mjs';
3
+ import { createPolicy } from '../core/privacy.mjs';
4
+ import { validBundle } from '../core/candidates.mjs';
5
+ import { buildProfileQuestions } from '../decisions/profiles.mjs';
6
+ import { validateResult, isProbability } from '../decisions/contracts.mjs';
7
+ import { buildEvaluation } from '../decisions/evaluation.mjs';
8
+ import { withAbort } from '../decisions/faults.mjs';
9
+ import { DEFAULT_ADMISSION_POLICY as thresholds } from '../decisions/index.mjs';
10
+ import {
11
+ ARCHITECTURE_NAMESPACE, ARCHITECTURE_VERSION, ARCHITECTURE_PROFILES, ROLE_PROFILE_ID, MEMBERSHIP_PROFILE_ID,
12
+ } from './profile.mjs';
13
+ import {
14
+ ARCHITECTURE_LIMITS as L, requireValue, indexModel, moduleForArtifact, requestedArtifacts,
15
+ candidatesForCapture, unionRefs, analysisEvent, unchanged, lineageOf, recordId,
16
+ } from './evidence.mjs';
17
+
18
+ export { ARCHITECTURE_PROFILES, ARCHITECTURE_NAMESPACE, ARCHITECTURE_VERSION } from './profile.mjs';
19
+ export { ARCHITECTURE_LIMITS } from './evidence.mjs';
20
+ const capabilities = { boolean: { probability: true }, choice: { probabilities: true, confidence: true } };
21
+ const knownKind = value => ['application', 'component'].includes(value);
22
+ const currentPolicy = policy => createPolicy(typeof policy === 'function' ? policy() : policy);
23
+ const versionRef = artifact => ({ artifactId: artifact.id, hash: artifact.hash, generation: artifact.generation });
24
+ const supported = answer => isProbability(answer?.probability) && answer.probability >= thresholds.nodeSupportMin;
25
+ const sufficient = answer => isProbability(answer?.probability) && answer.probability <= thresholds.missingContextMax;
26
+
27
+ function roleAnswer(result, selection, event, policy) {
28
+ if (!validBundle(result?.bundle, policy) || result.bundle.candidates.some(candidate =>
29
+ !selection.candidates.some(input => isDeepStrictEqual(input, candidate)))) return undefined;
30
+ if (!result.bundle.candidates.length && result.diagnostics?.code === 'no_approved_candidates') return null;
31
+ const analysis = result.analysis;
32
+ if (analysis?.profileId !== ROLE_PROFILE_ID || analysis.profileVersion !== ARCHITECTURE_VERSION
33
+ || analysis.status !== 'answered' || !result.bundle.candidates.length) return undefined;
34
+ const { request, subjects } = buildProfileQuestions(ARCHITECTURE_PROFILES[0], event, result.bundle);
35
+ if (!isDeepStrictEqual(analysis.subjects, subjects)) return undefined;
36
+ const answers = validateResult({ answers: analysis.answers }, request, capabilities).answers;
37
+ const role = answers.kind;
38
+ return knownKind(role.choice) && isProbability(role.probabilities?.[role.choice])
39
+ && role.probabilities[role.choice] >= thresholds.roleProbabilityMin
40
+ && isProbability(role.confidence) && role.confidence >= thresholds.roleConfidenceMin
41
+ && supported(answers.supported) && sufficient(answers.missing_context) ? role.choice : null;
42
+ }
43
+
44
+ function boundary(projectId, anchor, kind, ref) {
45
+ return {
46
+ id: recordId(projectId, 'boundary', anchor.id), namespace: ARCHITECTURE_NAMESPACE,
47
+ kind, label: anchor.label, entityIds: [anchor.id], sourceRefs: [ref],
48
+ basis: 'decision', validity: 'current', support: 'supported', classification: 'accepted',
49
+ version: ARCHITECTURE_VERSION,
50
+ };
51
+ }
52
+
53
+ function priorRoles(model, index) {
54
+ const roles = new Map(), conflicting = new Set();
55
+ for (const record of model.interpretations) {
56
+ if (record.namespace !== ARCHITECTURE_NAMESPACE || record.version !== ARCHITECTURE_VERSION
57
+ || !knownKind(record.kind) || record.validity !== 'current'
58
+ || record.support !== 'supported' || record.classification !== 'accepted'
59
+ || record.entityIds?.length !== 1 || !index.currentRefs(record.sourceRefs)) continue;
60
+ const entity = index.entities.get(record.entityIds[0]);
61
+ const anchor = moduleForArtifact(index, entity?.artifactId);
62
+ if (!anchor || entity.id !== anchor.id
63
+ || record.sourceRefs.some(ref => ref.artifactId !== anchor.artifactId)) continue;
64
+ if (roles.has(anchor.id)) conflicting.add(anchor.id);
65
+ roles.set(anchor.id, { anchor, kind: record.kind, ref: versionRef(index.artifacts.get(anchor.artifactId)) });
66
+ }
67
+ for (const id of conflicting) roles.delete(id);
68
+ return roles;
69
+ }
70
+
71
+ function membershipPairs(index, roles, processed) {
72
+ const pairs = new Map();
73
+ for (const relation of index.relations) {
74
+ if (relation.kind === 'contains') continue;
75
+ const parent = moduleForArtifact(index, index.entities.get(relation.source).artifactId);
76
+ const child = moduleForArtifact(index, index.entities.get(relation.target).artifactId);
77
+ if (!parent || !child || parent.id === child.id || roles.get(parent.id)?.kind !== 'application'
78
+ || roles.get(child.id)?.kind !== 'component'
79
+ || ![parent.artifactId, child.artifactId].some(id => processed.has(id))
80
+ || relation.sourceRefs.some(ref => ![parent.artifactId, child.artifactId].includes(ref.artifactId))
81
+ || ![parent.artifactId, child.artifactId].every(id => relation.sourceRefs.some(ref => ref.artifactId === id))) continue;
82
+ const key = `${parent.id}:${child.id}`;
83
+ // Both canonical parsed anchors belong to this single-project index, and
84
+ // the resolved parsed relation carries current versions of both artifacts.
85
+ if (!pairs.has(key)) pairs.set(key, {
86
+ parent: roles.get(parent.id), child: roles.get(child.id), kinds: new Set(),
87
+ sameProject: index.entities.has(parent.id) && index.entities.has(child.id),
88
+ resolvedLocalDependency: [parent.artifactId, child.artifactId].every(id => index.artifacts.has(id)),
89
+ currentSourceVersions: Boolean(index.currentRefs(relation.sourceRefs)),
90
+ });
91
+ pairs.get(key).kinds.add(relation.kind);
92
+ }
93
+ return [...pairs].sort(([a], [b]) => a.localeCompare(b)).map(([, value]) => value);
94
+ }
95
+
96
+ function priorMembership(model, index, pair) {
97
+ return model.interpretations.find(value => value.namespace === ARCHITECTURE_NAMESPACE
98
+ && value.version === ARCHITECTURE_VERSION && value.kind === 'architecture_membership'
99
+ && value.validity === 'current'
100
+ && (value.support === 'supported' && value.classification === 'accepted'
101
+ || value.support === 'unknown' && value.classification === 'unknown')
102
+ && isDeepStrictEqual(value.entityIds, [pair.parent.anchor.id, pair.child.anchor.id])
103
+ && index.currentRefs(value.sourceRefs)
104
+ && value.sourceRefs.length === 2 && value.sourceRefs.every(ref =>
105
+ [pair.parent.ref, pair.child.ref].some(expected => isDeepStrictEqual(ref, expected))));
106
+ }
107
+
108
+ function membership(projectId, pair, accepted) {
109
+ return {
110
+ id: recordId(projectId, 'membership', pair.parent.anchor.id, pair.child.anchor.id),
111
+ namespace: ARCHITECTURE_NAMESPACE, kind: 'architecture_membership',
112
+ label: accepted ? 'Application component' : 'Membership unknown',
113
+ entityIds: [pair.parent.anchor.id, pair.child.anchor.id], sourceRefs: [pair.parent.ref, pair.child.ref],
114
+ basis: 'decision', validity: 'current',
115
+ support: accepted ? 'supported' : 'unknown', classification: accepted ? 'accepted' : 'unknown',
116
+ version: ARCHITECTURE_VERSION,
117
+ };
118
+ }
119
+
120
+ function membershipEvaluation(pairs, basis) {
121
+ const roles = new Map();
122
+ for (const pair of pairs) for (const role of [pair.parent, pair.child]) {
123
+ roles.set(role.anchor.id, {
124
+ id: role.anchor.id, kind: role.kind, label: role.anchor.label, support: 'supported', classification: 'accepted',
125
+ basis: 'decision', anchorBasis: role.anchor.basis, validity: role.anchor.validity,
126
+ });
127
+ }
128
+ const state = {
129
+ context: {
130
+ basis: 'current_source_backed_roles_and_parsed_dependencies',
131
+ membership: 'A source composition link means an accepted application source module directly declares a parsed '
132
+ + 'import, call or depends_on dependency on an accepted component source module. This records a static source '
133
+ + 'dependency between the roles, not exclusive ownership, active usage, deployment or runtime hosting.',
134
+ instruction: 'Roles were accepted from current source through privacy intake. Dependencies are parsed facts. '
135
+ + 'sameProject, resolvedLocalDependency and currentSourceVersions are core-verified from canonical parsed '
136
+ + 'anchors and exact current references for both endpoints, not inferred from their labels. '
137
+ + 'Assess only the proposed finite pairs. Do not infer membership from folders or names, invent entities, '
138
+ + 'or assert exclusive ownership, runtime deployment or successful execution.',
139
+ },
140
+ boundaries: [...roles.values()],
141
+ proposals: pairs.map(pair => ({
142
+ parentId: pair.parent.anchor.id, childId: pair.child.anchor.id, relationKinds: [...pair.kinds].sort(),
143
+ sameProject: pair.sameProject, resolvedLocalDependency: pair.resolvedLocalDependency,
144
+ currentSourceVersions: pair.currentSourceVersions,
145
+ })),
146
+ };
147
+ const questions = pairs.flatMap((_pair, i) => [
148
+ {
149
+ id: `member_${i}`, kind: 'boolean', requiredMetrics: ['probability'],
150
+ question: `Do the accepted roles in \`boundaries\` and directed parsed dependency in \`proposals[${i}]\` `
151
+ + 'establish this exact source composition link as defined by `context.membership`?',
152
+ focus: 'Both endpoints must have accepted, supported application/component roles. A direct parsed import, call '
153
+ + 'or depends_on relation between these endpoints suffices for the declared source dependency. A folder/name '
154
+ + 'match, co-occurrence, unknown role or unrelated dependency does not. Imported implementation, proof of active '
155
+ + 'use, exclusive ownership, upstream callers and runtime hosting are outside this static claim.',
156
+ },
157
+ {
158
+ id: `missing_${i}`, kind: 'boolean', requiredMetrics: ['probability'],
159
+ question: `Is an accepted endpoint role or the exact directed parsed dependency missing from \`proposals[${i}]\` `
160
+ + 'and `boundaries`, preventing determination of the source composition link defined by `context.membership`?',
161
+ focus: 'The role analysis already inspected approved source. Both accepted roles and an exact parsed import, call '
162
+ + 'or depends_on relation are sufficient here. Do not require source bodies again, proof the import is actively '
163
+ + 'used, imported dependency internals, callers, runtime observations, deployment facts or optional documents.',
164
+ },
165
+ ]);
166
+ return {
167
+ state, questions, profile: { id: MEMBERSHIP_PROFILE_ID, version: ARCHITECTURE_VERSION },
168
+ cacheContext: {
169
+ projectId: basis.projectId, worktreeId: basis.projectId, lineage: basis.lineageId,
170
+ policyVersion: basis.policyVersion,
171
+ evidenceVersion: hash([basis.revision, pairs.map(pair => [pair.parent.ref, pair.child.ref])]),
172
+ taskScope: ARCHITECTURE_NAMESPACE,
173
+ },
174
+ };
175
+ }
176
+
177
+ function membershipAnswers(result, evaluation) {
178
+ requireValue(result?.status === 'accepted' && Array.isArray(result.answers)
179
+ && result.answers.length === evaluation.questions.length);
180
+ const answers = {};
181
+ for (const answer of result.answers) {
182
+ requireValue(answer.kind === 'boolean' && evaluation.questions.some(q => q.id === answer.id)
183
+ && !Object.hasOwn(answers, answer.id));
184
+ answers[answer.id] = { type: 'boolean', value: answer.value, probability: answer.probability };
185
+ }
186
+ const { request } = buildEvaluation(evaluation, { maxQuestionsPerStage: 40, maxRequestBytes: 64 * 1024 });
187
+ return validateResult({ answers }, request, capabilities).answers;
188
+ }
189
+
190
+ /**
191
+ * One bounded, stateless analysis job over authorized captures and a model
192
+ * snapshot. The controller owns capture, scheduling, freshness revalidation and
193
+ * replaceInterpretations(namespace, interpretations, { affectedEntityIds, sourceRefs }).
194
+ * Explicit empty scope means no replacement. Missing captures also return
195
+ * coverage.withdrawnEntityIds for a separate, controller-version-guarded clear;
196
+ * present-source guards cannot authorize absence. No source text is returned.
197
+ */
198
+ export async function analyzeArchitecture({ model, artifacts = [], service, policy, signal, affectedArtifactIds } = {}) {
199
+ const coverage = {
200
+ requestedArtifactIds: [], analyzedArtifactIds: [], deferredArtifactIds: [], unavailableArtifactIds: [],
201
+ missingArtifactIds: [], withdrawnEntityIds: [], deferredMembershipArtifactIds: [],
202
+ unknownArtifactIds: [], omittedCandidates: 0, membershipProposals: 0, membershipChecks: 0,
203
+ deferredMemberships: 0, supportedBoundaries: 0, supportedMemberships: 0, unknownMemberships: 0, complete: false,
204
+ };
205
+ const diagnostics = { code: 'architecture_unavailable', profileCalls: 0, evaluationCalls: 0, providerRequests: 0 };
206
+ const empty = status => ({ status, interpretations: [], sourceRefs: [], affectedEntityIds: [], coverage, diagnostics });
207
+ try {
208
+ const consent = currentPolicy(policy);
209
+ requireValue(consent.transmitSource && typeof service?.analyze === 'function'
210
+ && Array.isArray(artifacts) && artifacts.length <= 64);
211
+ const index = indexModel(model, consent);
212
+ const basis = { projectId: model.projectId, revision: model.revision,
213
+ lineageId: lineageOf(model), policyVersion: consent.version };
214
+ Object.assign(diagnostics, basis);
215
+ const check = () => requireValue(unchanged(model, basis, currentPolicy(policy), signal));
216
+ const wait = operation => signal ? withAbort(operation, signal) : operation();
217
+ const countRequests = result => {
218
+ if (integer(result?.diagnostics?.calls, 0, 2)) diagnostics.providerRequests += result.diagnostics.calls;
219
+ };
220
+ check();
221
+ const roles = priorRoles(model, index);
222
+ const priorArtifacts = new Set([...roles.values()].map(role => role.anchor.artifactId));
223
+ const seeds = new Set(affectedArtifactIds ?? []);
224
+ const requested = requestedArtifacts(model, index, artifacts, affectedArtifactIds)
225
+ .filter(id => affectedArtifactIds === undefined || seeds.has(id) || !priorArtifacts.has(id));
226
+ coverage.requestedArtifactIds = requested;
227
+ const captures = new Map(artifacts.map(capture => [capture.id, capture]));
228
+ const interpretations = [], affected = [], guards = new Map(), processed = new Set(), missing = new Map();
229
+ let sourceBytes = 0, partial = false;
230
+ const event = analysisEvent(model);
231
+ for (const artifactId of requested) {
232
+ check();
233
+ const capture = captures.get(artifactId);
234
+ if (!capture || coverage.analyzedArtifactIds.length + coverage.unavailableArtifactIds.length
235
+ + coverage.missingArtifactIds.length >= L.artifacts) {
236
+ coverage.deferredArtifactIds.push(artifactId); partial = true; continue;
237
+ }
238
+ const observation = index.artifacts.get(artifactId);
239
+ if (capture.status === 'missing' && capture.exists === false && capture.complete === true
240
+ && capture.hash === null && integer(capture.generation, 1)
241
+ && observation?.status === 'missing' && observation.complete === true && observation.hash === null
242
+ && observation.generation === capture.generation) {
243
+ const ids = new Set(model.interpretations.filter(value => value.namespace === ARCHITECTURE_NAMESPACE)
244
+ .flatMap(value => Array.isArray(value.entityIds) && value.entityIds.length <= 2 ? value.entityIds : [])
245
+ .filter(id => index.entities.get(id)?.artifactId === artifactId));
246
+ affected.push(...ids); coverage.withdrawnEntityIds.push(...ids);
247
+ coverage.missingArtifactIds.push(artifactId); missing.set(artifactId, capture.generation);
248
+ for (const id of ids) roles.delete(id);
249
+ continue;
250
+ }
251
+ const size = typeof capture.text === 'string' ? Buffer.byteLength(capture.text) : 0;
252
+ const selected = candidatesForCapture(capture, index, event, consent);
253
+ if (!selected) {
254
+ coverage.unavailableArtifactIds.push(artifactId); partial = true; continue;
255
+ }
256
+ if (sourceBytes + size > L.sourceBytes) {
257
+ coverage.deferredArtifactIds.push(artifactId); partial = true; continue;
258
+ }
259
+ sourceBytes += size;
260
+ coverage.omittedCandidates += selected.omitted;
261
+ if (selected.omitted) partial = true;
262
+ let kind = null;
263
+ if (selected.candidates.length) {
264
+ try {
265
+ diagnostics.profileCalls++;
266
+ const result = await wait(() => service.analyze({
267
+ event, candidates: selected.candidates, profileId: ROLE_PROFILE_ID, policy: consent,
268
+ ...(signal ? { signal } : {}),
269
+ }));
270
+ check(); countRequests(result);
271
+ kind = roleAnswer(result, selected, event, consent);
272
+ } catch {
273
+ check(); kind = undefined;
274
+ }
275
+ }
276
+ if (kind === undefined) {
277
+ coverage.unavailableArtifactIds.push(artifactId); partial = true; continue;
278
+ }
279
+ coverage.analyzedArtifactIds.push(artifactId);
280
+ processed.add(artifactId); affected.push(selected.anchor.id); guards.set(artifactId, selected.sourceRef);
281
+ roles.delete(selected.anchor.id);
282
+ if (kind) {
283
+ roles.set(selected.anchor.id, { anchor: selected.anchor, kind, ref: selected.sourceRef });
284
+ interpretations.push(boundary(model.projectId, selected.anchor, kind, selected.sourceRef));
285
+ } else coverage.unknownArtifactIds.push(artifactId);
286
+ }
287
+ coverage.supportedBoundaries = interpretations.length;
288
+ const proposed = membershipPairs(index, roles, processed);
289
+ coverage.membershipProposals = proposed.length;
290
+ const pending = [];
291
+ for (const pair of proposed) {
292
+ const previous = priorMembership(model, index, pair);
293
+ if (!previous) { pending.push(pair); continue; }
294
+ interpretations.push(structuredClone(previous));
295
+ for (const role of [pair.parent, pair.child]) guards.set(role.ref.artifactId, role.ref);
296
+ coverage[previous.support === 'supported' ? 'supportedMemberships' : 'unknownMemberships']++;
297
+ }
298
+ const pairs = pending.slice(0, L.membershipChecks);
299
+ coverage.deferredMemberships = pending.length - pairs.length;
300
+ coverage.deferredMembershipArtifactIds = [...new Set(pending.slice(L.membershipChecks).map(pair => pair.child.ref.artifactId))];
301
+ if (coverage.deferredMemberships) partial = true;
302
+ if (pairs.length) {
303
+ for (const pair of pairs) for (const role of [pair.parent, pair.child]) guards.set(role.ref.artifactId, role.ref);
304
+ let answers;
305
+ try {
306
+ check(); requireValue(typeof service.evaluate === 'function');
307
+ const evaluation = membershipEvaluation(pairs, basis);
308
+ diagnostics.evaluationCalls++;
309
+ const result = await wait(() => service.evaluate({ ...evaluation, ...(signal ? { signal } : {}) }));
310
+ check(); countRequests(result);
311
+ answers = membershipAnswers(result, evaluation);
312
+ coverage.membershipChecks = pairs.length;
313
+ } catch { check(); partial = true; }
314
+ // Only validated answers establish a versioned observation. A transient
315
+ // failure leaves no pair record, so a later Discover can retry it.
316
+ if (answers) pairs.forEach((pair, i) => {
317
+ const accepted = supported(answers[`member_${i}`]) && sufficient(answers[`missing_${i}`]);
318
+ interpretations.push(membership(model.projectId, pair, accepted));
319
+ coverage[accepted ? 'supportedMemberships' : 'unknownMemberships']++;
320
+ });
321
+ }
322
+ check();
323
+ const sourceRefs = unionRefs([...guards.values()]);
324
+ const fresh = indexModel(model, consent);
325
+ requireValue(sourceRefs && (!sourceRefs.length || fresh.currentRefs(sourceRefs, L.guardRefs))
326
+ && [...missing].every(([id, generation]) => fresh.artifacts.get(id)?.status === 'missing'
327
+ && fresh.artifacts.get(id)?.complete === true
328
+ && fresh.artifacts.get(id)?.generation === generation && fresh.artifacts.get(id)?.hash === null)
329
+ && affected.every(id => fresh.entities.get(id)?.validity === 'current' || coverage.withdrawnEntityIds.includes(id))
330
+ && interpretations.every(value => value.entityIds.every(id => fresh.entities.get(id)?.validity === 'current')));
331
+ coverage.complete = !partial;
332
+ diagnostics.code = partial ? 'architecture_partial'
333
+ : interpretations.length ? 'architecture_complete' : 'architecture_unknown';
334
+ return freeze({
335
+ status: partial ? (affected.length ? 'partial' : 'unavailable') : 'complete',
336
+ interpretations, sourceRefs, affectedEntityIds: affected, coverage, diagnostics,
337
+ });
338
+ } catch {
339
+ diagnostics.code = signal?.aborted ? 'architecture_cancelled' : 'architecture_unavailable';
340
+ coverage.deferredArtifactIds = [];
341
+ coverage.deferredMembershipArtifactIds = [];
342
+ return freeze(empty(signal?.aborted ? 'cancelled' : 'unavailable'));
343
+ }
344
+ }