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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +12 -3
- package/docs/decision-service.md +393 -0
- package/docs/extension-authoring.md +553 -0
- package/docs/model-api.md +293 -0
- package/docs/usage.md +472 -0
- package/docs/visualizer-views.md +240 -0
- package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
- package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
- package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
- package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
- package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
- package/package.json +78 -9
- package/plugin.json +4 -2
- package/runtime/architecture/analysis.mjs +344 -0
- package/runtime/architecture/controller.mjs +209 -0
- package/runtime/architecture/evidence.mjs +108 -0
- package/runtime/architecture/profile.mjs +56 -0
- package/runtime/core/evidence.mjs +43 -9
- package/runtime/core/graph.mjs +11 -6
- package/runtime/core/privacy.mjs +1 -0
- package/runtime/daemon/auth.mjs +7 -3
- package/runtime/daemon/diagnostics.mjs +1 -1
- package/runtime/daemon/extension-api.mjs +203 -0
- package/runtime/daemon/lineage.mjs +70 -0
- package/runtime/daemon/manager.mjs +9 -6
- package/runtime/daemon/model-api.mjs +728 -0
- package/runtime/daemon/model-persistence.mjs +220 -0
- package/runtime/daemon/server.mjs +81 -14
- package/runtime/daemon/settings.mjs +11 -3
- package/runtime/decisions/broker.mjs +349 -0
- package/runtime/decisions/contracts.mjs +179 -0
- package/runtime/decisions/evaluation.mjs +305 -0
- package/runtime/decisions/faults.mjs +32 -0
- package/runtime/decisions/index.mjs +818 -0
- package/runtime/decisions/profiles.mjs +93 -0
- package/runtime/decisions/questions.mjs +268 -0
- package/runtime/discovery/index.mjs +2 -0
- package/runtime/discovery/inventory.mjs +160 -0
- package/runtime/discovery/parser.mjs +40 -0
- package/runtime/discovery/structure.mjs +232 -0
- package/runtime/extensions/contracts.mjs +59 -0
- package/runtime/extensions/frame.mjs +64 -0
- package/runtime/extensions/index.mjs +9 -0
- package/runtime/extensions/manifest.mjs +95 -0
- package/runtime/extensions/packages.mjs +222 -0
- package/runtime/extensions/profiles.mjs +36 -0
- package/runtime/extensions/projection.mjs +130 -0
- package/runtime/extensions/registry.mjs +285 -0
- package/runtime/extensions/scene.mjs +105 -0
- package/runtime/extensions/sdk.d.ts +205 -0
- package/runtime/extensions/sdk.mjs +88 -0
- package/runtime/jev/index.mjs +13 -777
- package/runtime/jev/provider.mjs +101 -0
- package/runtime/jev/questions.mjs +16 -258
- package/runtime/jev/wire.mjs +17 -25
- package/runtime/model/changes.mjs +42 -0
- package/runtime/model/history.mjs +124 -0
- package/runtime/model/index.mjs +2 -0
- package/runtime/model/project-model.mjs +1020 -0
- package/runtime/model/records.mjs +240 -0
- package/runtime/pipeline.mjs +267 -55
- package/runtime/platform.mjs +254 -0
- package/runtime/visualizers/blocks.mjs +5 -0
- package/runtime/visualizers/c4.mjs +154 -0
- package/runtime/visualizers/changes.mjs +24 -0
- package/runtime/visualizers/code.mjs +5 -0
- package/runtime/visualizers/index.mjs +23 -0
- package/runtime/visualizers/structure.mjs +120 -0
- package/runtime/visualizers/timeline.mjs +66 -0
- package/runtime/web/app.js +225 -63
- package/runtime/web/extension-frame.js +128 -0
- package/runtime/web/index.html +38 -1
- package/runtime/web/model-client.js +162 -0
- package/runtime/web/platform.js +445 -0
- package/runtime/web/scene.js +111 -0
- package/runtime/web/style.css +51 -0
- package/schemas/graph.schema.json +4 -1
- package/scripts/arguments.mjs +5 -1
- package/scripts/build-packages.mjs +6 -2
- package/scripts/control.mjs +1 -1
- package/scripts/daemon.mjs +2 -1
- package/scripts/extensions.mjs +44 -0
- package/scripts/graphlin.mjs +23 -3
- package/scripts/onboarding.mjs +10 -3
- package/scripts/validate-packages.mjs +54 -8
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
const MAX_PENDING = 10_000;
|
|
2
|
+
const BATCH_SIZE = 6;
|
|
3
|
+
const STATES = new Set(['waiting', 'queued', 'running', 'complete', 'partial', 'unavailable']);
|
|
4
|
+
const version = value => `${value.generation}:${value.hash}:${value.status}`;
|
|
5
|
+
const metadata = value => ({
|
|
6
|
+
artifactId: value.id, generation: value.generation, hash: value.hash, status: value.status,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
/** Coalesces observations. Captured source lives only for the active analysis job. */
|
|
10
|
+
export function createArchitectureController({
|
|
11
|
+
snapshot, capture, commit, analyze, available = () => null, ready = () => true,
|
|
12
|
+
onChange = () => {}, onDiagnostic = () => {}, now = Date.now, settleMs = 300,
|
|
13
|
+
}) {
|
|
14
|
+
const known = new Map(), pending = new Map(), completed = new Map();
|
|
15
|
+
const retries = new Map();
|
|
16
|
+
let state = 'waiting', reason = 'no_source', timer, active, abort, activeVersions;
|
|
17
|
+
let closed = false, epoch = 0, manual = false, omitted = 0, failures = 0, lastRunAt = null;
|
|
18
|
+
|
|
19
|
+
function status() {
|
|
20
|
+
const model = snapshot();
|
|
21
|
+
const supported = (model.interpretations ?? []).filter(value =>
|
|
22
|
+
value.namespace === 'graphlin.architecture' && value.validity === 'current' &&
|
|
23
|
+
value.support === 'supported' && value.classification === 'accepted');
|
|
24
|
+
const blocked = available();
|
|
25
|
+
return {
|
|
26
|
+
status: blocked ? 'unavailable' : state,
|
|
27
|
+
...(blocked || reason ? { reason: blocked || reason } : {}),
|
|
28
|
+
applications: supported.filter(value => value.kind === 'application').length,
|
|
29
|
+
components: supported.filter(value => value.kind === 'component').length,
|
|
30
|
+
pending: pending.size, inspected: completed.size, total: known.size,
|
|
31
|
+
omitted, failures, ...(lastRunAt === null ? {} : { lastRunAt }),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function publish(next, why = null) {
|
|
35
|
+
state = STATES.has(next) ? next : 'unavailable';
|
|
36
|
+
reason = why;
|
|
37
|
+
try { onChange(); } catch { /* Observation must remain fail-open. */ }
|
|
38
|
+
}
|
|
39
|
+
function diagnostic(result) {
|
|
40
|
+
try {
|
|
41
|
+
onDiagnostic({
|
|
42
|
+
status: result.status, code: result.diagnostics?.code ?? 'architecture_unavailable',
|
|
43
|
+
analyzed: result.coverage?.analyzedArtifactIds?.length ?? 0,
|
|
44
|
+
deferred: result.coverage?.deferredArtifactIds?.length ?? 0,
|
|
45
|
+
providerRequests: result.diagnostics?.providerRequests ?? 0,
|
|
46
|
+
});
|
|
47
|
+
} catch { /* Diagnostics cannot prevent admission or shutdown. */ }
|
|
48
|
+
}
|
|
49
|
+
function enqueue(id, { force = false } = {}) {
|
|
50
|
+
const item = known.get(id);
|
|
51
|
+
if (!item || (!force && completed.get(id) === version(item))) return;
|
|
52
|
+
if (!pending.has(id) && pending.size >= MAX_PENDING) { omitted++; return; }
|
|
53
|
+
pending.set(id, item);
|
|
54
|
+
}
|
|
55
|
+
function wake() {
|
|
56
|
+
if (closed) return;
|
|
57
|
+
const blocked = available();
|
|
58
|
+
if (blocked) {
|
|
59
|
+
clearTimeout(timer); timer = null;
|
|
60
|
+
abort?.abort();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (active || timer || (!pending.size && !manual)) return;
|
|
64
|
+
timer = setTimeout(() => {
|
|
65
|
+
timer = null;
|
|
66
|
+
if (!ready()) { wake(); return; }
|
|
67
|
+
void run();
|
|
68
|
+
}, settleMs);
|
|
69
|
+
timer.unref?.();
|
|
70
|
+
}
|
|
71
|
+
function observe(artifacts) {
|
|
72
|
+
if (closed) return;
|
|
73
|
+
for (const artifact of artifacts) {
|
|
74
|
+
const item = metadata(artifact), old = known.get(artifact.id);
|
|
75
|
+
if (old && version(old) === version(item)) continue;
|
|
76
|
+
if (!old && known.size >= MAX_PENDING) { omitted++; continue; }
|
|
77
|
+
known.set(artifact.id, item);
|
|
78
|
+
completed.delete(artifact.id);
|
|
79
|
+
retries.delete(artifact.id);
|
|
80
|
+
enqueue(artifact.id);
|
|
81
|
+
if (activeVersions?.has(artifact.id) && activeVersions.get(artifact.id) !== version(item)) abort?.abort();
|
|
82
|
+
}
|
|
83
|
+
if (!active && pending.size) { state = 'queued'; reason = null; }
|
|
84
|
+
wake();
|
|
85
|
+
}
|
|
86
|
+
async function run() {
|
|
87
|
+
if (active || closed || available()) return active;
|
|
88
|
+
clearTimeout(timer); timer = null;
|
|
89
|
+
if (manual) {
|
|
90
|
+
retries.clear();
|
|
91
|
+
completed.clear();
|
|
92
|
+
omitted = 0;
|
|
93
|
+
for (const id of known.keys()) enqueue(id, { force: true });
|
|
94
|
+
manual = false;
|
|
95
|
+
}
|
|
96
|
+
if (!pending.size) { publish('waiting', 'no_source'); return; }
|
|
97
|
+
const ids = [...pending.keys()].slice(0, BATCH_SIZE), generation = epoch;
|
|
98
|
+
abort = new AbortController();
|
|
99
|
+
const signal = abort.signal;
|
|
100
|
+
publish('running');
|
|
101
|
+
active = (async () => {
|
|
102
|
+
const artifacts = await capture(ids);
|
|
103
|
+
if (closed || signal.aborted || generation !== epoch) return;
|
|
104
|
+
const captured = new Map(artifacts.map(value => [value.id, metadata(value)]));
|
|
105
|
+
activeVersions = new Map(ids.flatMap(id => known.has(id)
|
|
106
|
+
? [[id, version(captured.get(id) ?? known.get(id))]] : []));
|
|
107
|
+
// Parsing can settle after a newer capture was observed. Never mark that
|
|
108
|
+
// newer version inspected using an older capture.
|
|
109
|
+
if (ids.some(id => activeVersions.get(id) !== version(known.get(id) ?? {}))) return;
|
|
110
|
+
const result = await analyze({ model: snapshot(), artifacts, affectedArtifactIds: ids, signal });
|
|
111
|
+
if (closed || signal.aborted || generation !== epoch) return;
|
|
112
|
+
diagnostic(result);
|
|
113
|
+
const applied = result.affectedEntityIds?.length || result.coverage?.missingArtifactIds?.length
|
|
114
|
+
? await commit(result, { artifacts: artifacts.map(metadata), signal, epoch: generation })
|
|
115
|
+
: true;
|
|
116
|
+
if (closed || signal.aborted || generation !== epoch) return;
|
|
117
|
+
if (!applied || applied.accepted === false) {
|
|
118
|
+
// A source race may be retried, but a persistently rejected admission
|
|
119
|
+
// must not become an endless background classification loop.
|
|
120
|
+
for (const id of ids) {
|
|
121
|
+
if (activeVersions.get(id) !== version(known.get(id) ?? {})) continue;
|
|
122
|
+
const count = (retries.get(id) ?? 0) + 1;
|
|
123
|
+
retries.set(id, count);
|
|
124
|
+
if (count >= 2) { pending.delete(id); completed.set(id, activeVersions.get(id)); failures++; }
|
|
125
|
+
}
|
|
126
|
+
publish(pending.size ? 'queued' : 'partial', 'source_changed');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (Number.isSafeInteger(applied.omitted) && applied.omitted > 0) omitted += applied.omitted;
|
|
130
|
+
const deferred = new Set(result.coverage?.deferredArtifactIds ?? []);
|
|
131
|
+
const progressed = ids.some(id => !deferred.has(id));
|
|
132
|
+
for (const id of ids) {
|
|
133
|
+
if (activeVersions.get(id) !== version(known.get(id) ?? {})) continue;
|
|
134
|
+
pending.delete(id);
|
|
135
|
+
// Move budget-deferred captures to the back, so the next batch can
|
|
136
|
+
// spend its source budget on them. Stop if a whole batch made no
|
|
137
|
+
// progress (for example, no capture is available).
|
|
138
|
+
if (deferred.has(id) && progressed) continue;
|
|
139
|
+
completed.set(id, activeVersions.get(id));
|
|
140
|
+
if (deferred.has(id)) failures++;
|
|
141
|
+
}
|
|
142
|
+
for (const id of deferred) enqueue(id);
|
|
143
|
+
for (const id of result.coverage?.deferredMembershipArtifactIds ?? []) {
|
|
144
|
+
const count = retries.get(id) ?? 0;
|
|
145
|
+
if (count < 2) { retries.set(id, count + 1); enqueue(id, { force: true }); }
|
|
146
|
+
else if (count === 2) { retries.set(id, 3); omitted++; }
|
|
147
|
+
}
|
|
148
|
+
lastRunAt = now();
|
|
149
|
+
if (result.status === 'unavailable') { failures++; publish('unavailable', 'analysis_failed'); }
|
|
150
|
+
else if (result.status === 'partial' || omitted) publish('partial', 'partial_coverage');
|
|
151
|
+
else {
|
|
152
|
+
const totals = status();
|
|
153
|
+
publish('complete', totals.applications + totals.components ? null : 'none_supported');
|
|
154
|
+
}
|
|
155
|
+
})().catch(() => {
|
|
156
|
+
if (!closed && !signal.aborted) {
|
|
157
|
+
failures++;
|
|
158
|
+
for (const id of ids) {
|
|
159
|
+
if (activeVersions && activeVersions.get(id) !== version(known.get(id) ?? {})) continue;
|
|
160
|
+
pending.delete(id);
|
|
161
|
+
if (known.has(id)) completed.set(id, version(known.get(id)));
|
|
162
|
+
}
|
|
163
|
+
publish('unavailable', 'analysis_failed');
|
|
164
|
+
}
|
|
165
|
+
}).finally(() => {
|
|
166
|
+
active = null; abort = null; activeVersions = null;
|
|
167
|
+
if (!closed) {
|
|
168
|
+
if (pending.size || manual) { state = 'queued'; wake(); }
|
|
169
|
+
else if (state === 'running') publish(available() ? 'unavailable' : 'partial', available() || 'source_changed');
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
return active;
|
|
173
|
+
}
|
|
174
|
+
function request() {
|
|
175
|
+
if (!closed) {
|
|
176
|
+
manual = true;
|
|
177
|
+
publish('queued', available());
|
|
178
|
+
wake();
|
|
179
|
+
}
|
|
180
|
+
return status();
|
|
181
|
+
}
|
|
182
|
+
function invalidate() {
|
|
183
|
+
epoch++;
|
|
184
|
+
abort?.abort();
|
|
185
|
+
completed.clear();
|
|
186
|
+
retries.clear();
|
|
187
|
+
for (const id of known.keys()) enqueue(id, { force: true });
|
|
188
|
+
wake();
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
observe, wake, request, status, invalidate,
|
|
192
|
+
async whenIdle() {
|
|
193
|
+
clearTimeout(timer); timer = null;
|
|
194
|
+
while (!closed && !available() && (active || pending.size || manual)) {
|
|
195
|
+
if (!ready()) break;
|
|
196
|
+
await (active ?? run());
|
|
197
|
+
}
|
|
198
|
+
clearTimeout(timer); timer = null;
|
|
199
|
+
},
|
|
200
|
+
async close() {
|
|
201
|
+
closed = true;
|
|
202
|
+
clearTimeout(timer); timer = null;
|
|
203
|
+
abort?.abort();
|
|
204
|
+
pending.clear();
|
|
205
|
+
await active;
|
|
206
|
+
known.clear(); completed.clear(); retries.clear();
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
import { LIMITS, hash, isHash, isId, integer, opaque } from '../core/common.mjs';
|
|
3
|
+
import { createPolicy, metadataEvent, safeLabel, safeText } from '../core/privacy.mjs';
|
|
4
|
+
import { buildCandidates } from '../core/candidates.mjs';
|
|
5
|
+
import { id, references, relativePath } from '../model/records.mjs';
|
|
6
|
+
import { ARCHITECTURE_NAMESPACE } from './profile.mjs';
|
|
7
|
+
|
|
8
|
+
export const ARCHITECTURE_LIMITS = Object.freeze({
|
|
9
|
+
artifacts: 6, candidatesPerArtifact: 8, membershipChecks: 6,
|
|
10
|
+
sourceRefs: 16, guardRefs: 128, sourceBytes: 512 * 1024,
|
|
11
|
+
});
|
|
12
|
+
export const requireValue = value => { if (!value) throw new Error('architecture_input_invalid'); };
|
|
13
|
+
export const lineageOf = model => model.coverage?.lineage?.id ?? model.projectId;
|
|
14
|
+
export const recordId = (projectId, ...parts) => opaque('architecture', projectId, ...parts);
|
|
15
|
+
|
|
16
|
+
export function indexModel(model, policy) {
|
|
17
|
+
requireValue(model?.schemaVersion === 2 && id(model.projectId) && integer(model.revision)
|
|
18
|
+
&& !model.replay && model.checkpointId === undefined && id(lineageOf(model))
|
|
19
|
+
&& Array.isArray(model.entities) && model.entities.length <= 20_000
|
|
20
|
+
&& Array.isArray(model.relations) && model.relations.length <= 40_000
|
|
21
|
+
&& Array.isArray(model.interpretations) && model.interpretations.length <= 512
|
|
22
|
+
&& Array.isArray(model.coverage?.artifacts) && model.coverage.artifacts.length <= 10_000);
|
|
23
|
+
const entities = new Map(), artifacts = new Map(), byArtifact = new Map();
|
|
24
|
+
for (const artifact of model.coverage.artifacts) {
|
|
25
|
+
requireValue(id(artifact?.id) && !artifacts.has(artifact.id));
|
|
26
|
+
artifacts.set(artifact.id, artifact);
|
|
27
|
+
}
|
|
28
|
+
function currentRefs(value, max = ARCHITECTURE_LIMITS.sourceRefs) {
|
|
29
|
+
const refs = references(value, max);
|
|
30
|
+
if (!refs?.length || !isDeepStrictEqual(refs, value)) return null;
|
|
31
|
+
return refs.every(ref => {
|
|
32
|
+
const artifact = artifacts.get(ref.artifactId);
|
|
33
|
+
return ref.sourceClass !== 'public_intent' && artifact?.status === 'present'
|
|
34
|
+
&& artifact.fresh === true && artifact.hash === ref.hash && artifact.generation === ref.generation
|
|
35
|
+
&& relativePath(artifact.relativePath, policy);
|
|
36
|
+
}) ? refs : null;
|
|
37
|
+
}
|
|
38
|
+
for (const entity of model.entities) {
|
|
39
|
+
requireValue(id(entity?.id) && !entities.has(entity.id));
|
|
40
|
+
entities.set(entity.id, entity);
|
|
41
|
+
if (!byArtifact.has(entity.artifactId)) byArtifact.set(entity.artifactId, []);
|
|
42
|
+
if (entity.basis === 'parsed' && entity.validity === 'current' && entity.classification === 'accepted'
|
|
43
|
+
&& safeLabel(entity.label) && currentRefs(entity.sourceRefs)) byArtifact.get(entity.artifactId).push(entity);
|
|
44
|
+
}
|
|
45
|
+
const relations = model.relations.filter(value => value?.validity === 'current'
|
|
46
|
+
&& value.basis === 'parsed' && entities.has(value.source) && entities.has(value.target)
|
|
47
|
+
&& ['contains', 'imports', 'calls', 'depends_on'].includes(value.kind) && currentRefs(value.sourceRefs));
|
|
48
|
+
return { entities, artifacts, byArtifact, relations, currentRefs };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function moduleForArtifact(index, artifactId) {
|
|
52
|
+
const modules = (index.byArtifact.get(artifactId) ?? []).filter(entity => ['module', 'file'].includes(entity.kind));
|
|
53
|
+
return modules.length === 1 ? modules[0] : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function requestedArtifacts(model, index, captures, affectedArtifactIds) {
|
|
57
|
+
const supplied = captures.map(value => value?.id);
|
|
58
|
+
requireValue(supplied.every(isId) && new Set(supplied).size === supplied.length);
|
|
59
|
+
if (affectedArtifactIds === undefined) return supplied;
|
|
60
|
+
requireValue(Array.isArray(affectedArtifactIds) && affectedArtifactIds.length <= 128
|
|
61
|
+
&& affectedArtifactIds.every(isId) && new Set(affectedArtifactIds).size === affectedArtifactIds.length);
|
|
62
|
+
const seeds = new Set(affectedArtifactIds), requested = new Set(seeds);
|
|
63
|
+
// One-hop dependencies nominate source for reconsideration, never membership.
|
|
64
|
+
for (const relation of index.relations) {
|
|
65
|
+
const source = index.entities.get(relation.source).artifactId;
|
|
66
|
+
const target = index.entities.get(relation.target).artifactId;
|
|
67
|
+
if (seeds.has(source) && isId(target)) requested.add(target);
|
|
68
|
+
if (seeds.has(target) && isId(source)) requested.add(source);
|
|
69
|
+
}
|
|
70
|
+
// Existing groups can depend on a changed file even if an import disappeared.
|
|
71
|
+
for (const value of model.interpretations) {
|
|
72
|
+
if (value.namespace !== ARCHITECTURE_NAMESPACE || !Array.isArray(value.sourceRefs)
|
|
73
|
+
|| value.sourceRefs.length > ARCHITECTURE_LIMITS.sourceRefs
|
|
74
|
+
|| !value.sourceRefs.some(ref => seeds.has(ref.artifactId))) continue;
|
|
75
|
+
for (const ref of value.sourceRefs) if (isId(ref.artifactId)) requested.add(ref.artifactId);
|
|
76
|
+
}
|
|
77
|
+
return [...seeds, ...[...requested].filter(value => !seeds.has(value)).sort()];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function candidatesForCapture(capture, index, event, policy) {
|
|
81
|
+
const current = index.artifacts.get(capture.id);
|
|
82
|
+
const anchor = moduleForArtifact(index, capture.id);
|
|
83
|
+
if (!anchor || !current || current.status !== 'present' || current.fresh !== true
|
|
84
|
+
|| capture.status !== 'present' || capture.exists !== true || capture.complete !== true
|
|
85
|
+
|| !isHash(capture.hash) || !integer(capture.generation, 1)
|
|
86
|
+
|| current.hash !== capture.hash || current.generation !== capture.generation
|
|
87
|
+
|| !relativePath(capture.relativePath, policy) || current.relativePath !== capture.relativePath
|
|
88
|
+
|| typeof capture.text !== 'string' || Buffer.byteLength(capture.text) > LIMITS.fileBytes
|
|
89
|
+
|| !safeText(capture.text, LIMITS.fileBytes) || hash(capture.text) !== capture.hash) return null;
|
|
90
|
+
const candidates = buildCandidates({ event, artifacts: [capture], policy });
|
|
91
|
+
return { anchor, candidates: candidates.slice(0, ARCHITECTURE_LIMITS.candidatesPerArtifact),
|
|
92
|
+
omitted: Math.max(0, candidates.length - ARCHITECTURE_LIMITS.candidatesPerArtifact),
|
|
93
|
+
sourceRef: { artifactId: capture.id, hash: capture.hash, generation: capture.generation } };
|
|
94
|
+
}
|
|
95
|
+
export function unionRefs(...groups) {
|
|
96
|
+
const refs = new Map(groups.flat().map(ref => [JSON.stringify(ref), structuredClone(ref)]));
|
|
97
|
+
return refs.size <= ARCHITECTURE_LIMITS.guardRefs ? [...refs.values()] : null;
|
|
98
|
+
}
|
|
99
|
+
export function analysisEvent(model) {
|
|
100
|
+
return metadataEvent({
|
|
101
|
+
projectId: model.projectId, id: opaque('event', ARCHITECTURE_NAMESPACE, model.projectId, model.revision, lineageOf(model)),
|
|
102
|
+
kind: 'artifact.changed', toolCategory: 'read', outcome: 'observed', incomplete: false,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
export function unchanged(model, basis, policy, signal) {
|
|
106
|
+
return !signal?.aborted && model.revision === basis.revision && model.projectId === basis.projectId
|
|
107
|
+
&& lineageOf(model) === basis.lineageId && createPolicy(policy).version === basis.policyVersion;
|
|
108
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { freeze } from '../core/common.mjs';
|
|
2
|
+
|
|
3
|
+
export const ARCHITECTURE_NAMESPACE = 'graphlin.architecture';
|
|
4
|
+
export const ARCHITECTURE_VERSION = 'source-boundaries-v4';
|
|
5
|
+
export const ROLE_PROFILE_ID = 'graphlin.architecture.roles';
|
|
6
|
+
export const MEMBERSHIP_PROFILE_ID = 'graphlin.architecture.membership';
|
|
7
|
+
|
|
8
|
+
const boolean = (question, focus, yes, no) => ({
|
|
9
|
+
type: 'boolean', instructions: { question, focus },
|
|
10
|
+
criteria: { true: yes, false: no }, requiredMetrics: ['probability'],
|
|
11
|
+
});
|
|
12
|
+
const rules = 'Use only visible source. Names, directories, documentation, imports alone, and co-occurrence '
|
|
13
|
+
+ 'do not establish an architectural boundary. An application has visible executable bootstrap or '
|
|
14
|
+
+ 'composition of a user-facing program, server or worker. A component has a coherent responsibility '
|
|
15
|
+
+ 'and an implemented interface; an incidental helper, constant, type or external dependency is not '
|
|
16
|
+
+ 'automatically a component. Classify what this code implements when invoked, never claim it ran.';
|
|
17
|
+
|
|
18
|
+
/** Register this source profile with the existing decision service; it cannot bypass A. */
|
|
19
|
+
export const ARCHITECTURE_PROFILES = freeze([
|
|
20
|
+
{
|
|
21
|
+
id: ROLE_PROFILE_ID, version: ARCHITECTURE_VERSION, scope: 'bundle',
|
|
22
|
+
questions: {
|
|
23
|
+
kind: {
|
|
24
|
+
type: 'choice', requiredMetrics: ['probabilities', 'confidence'],
|
|
25
|
+
instructions: {
|
|
26
|
+
question: 'What does this source file locally implement in `evidence`?', focus: rules,
|
|
27
|
+
},
|
|
28
|
+
criteria: {
|
|
29
|
+
application: 'Visible executable bootstrap or composition that starts an application, server or worker when invoked.',
|
|
30
|
+
component: 'A cohesive implemented API with visible operation bodies, beyond an incidental helper.',
|
|
31
|
+
unknown: 'Only imports, declarations, stubs, constants or incidental helpers; no such local responsibility is established.',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
supported: boolean(
|
|
35
|
+
'Does `evidence` directly show implemented behavior for this source file?',
|
|
36
|
+
'Judge implementation presence, independently of the architectural role. Actual startup statements or '
|
|
37
|
+
+ 'operation bodies show implementation. Imports, signatures, empty bodies, placeholder throws, comments '
|
|
38
|
+
+ 'and a name alone do not. External callers, deployment manifests, imported collaborator internals '
|
|
39
|
+
+ 'and runtime execution are not prerequisites for recognizing visible local implementation.',
|
|
40
|
+
'Executable statements directly implement startup or operations in this file.',
|
|
41
|
+
'Only imports, declarations, scalar constants, empty bodies or placeholders are visible.',
|
|
42
|
+
),
|
|
43
|
+
missing_context: boolean(
|
|
44
|
+
'Is a local statement or definition absent from `evidence` that prevents identifying what this file implements?',
|
|
45
|
+
'This is only about classifying the visible file as application, component or unknown. '
|
|
46
|
+
+ 'Missing context requires an omitted/truncated body or unresolved local operation that prevents that decision. '
|
|
47
|
+
+ 'A visible server construction and listen call suffice for application startup. Visible class or function '
|
|
48
|
+
+ 'operation bodies can establish a component. Standard library internals, implementation of imported collaborators, '
|
|
49
|
+
+ 'upstream callers, deployment configuration and runtime execution are not required to identify the file’s own role. '
|
|
50
|
+
+ 'A fully visible constant or helper can conclusively be unknown without missing context.',
|
|
51
|
+
'A necessary local body or statement is absent, so the file’s implemented responsibility cannot be identified.',
|
|
52
|
+
'Visible code suffices to support or reject an application/component role for this file.',
|
|
53
|
+
),
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
]);
|
|
@@ -9,13 +9,16 @@ const fingerprint = stat => [stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.c
|
|
|
9
9
|
|
|
10
10
|
export class EvidenceStore {
|
|
11
11
|
#root; #inputRoot; #policy; #records = new Map(); #byId = new Map();
|
|
12
|
-
|
|
12
|
+
#maxTrackedPaths; #reconcileCursor = 0; #lineage = null; #generationFloor;
|
|
13
|
+
constructor({ projectRoot, policy, maxTrackedPaths = LIMITS.trackedPaths, generationFloor = 0 } = {}) {
|
|
13
14
|
try {
|
|
14
15
|
this.#inputRoot = path.resolve(projectRoot);
|
|
15
16
|
this.#root = realpathSync(projectRoot);
|
|
16
17
|
if (!statSync(this.#root).isDirectory()) fail();
|
|
17
18
|
} catch { fail('INVALID_PROJECT_ROOT'); }
|
|
18
19
|
this.#policy = createPolicy(policy);
|
|
20
|
+
this.#maxTrackedPaths = integer(maxTrackedPaths, 1, 20000) ? maxTrackedPaths : LIMITS.trackedPaths;
|
|
21
|
+
this.#generationFloor = integer(generationFloor, 0, Number.MAX_SAFE_INTEGER - 1) ? generationFloor : 0;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
#locator(input) {
|
|
@@ -43,17 +46,26 @@ export class EvidenceStore {
|
|
|
43
46
|
}
|
|
44
47
|
const parts = locator.relative.split('/');
|
|
45
48
|
let current = this.#root;
|
|
49
|
+
let finalStat;
|
|
46
50
|
try {
|
|
47
51
|
if (await realpath(this.#root) !== this.#root) return this.#unavailable('root_changed');
|
|
48
52
|
for (const part of parts) {
|
|
49
53
|
current = path.join(current, part);
|
|
50
54
|
const stat = await lstat(current, { bigint: true });
|
|
51
55
|
if (stat.isSymbolicLink()) return this.#unavailable(`symlink:${fingerprint(stat)}`);
|
|
56
|
+
finalStat = stat;
|
|
52
57
|
}
|
|
53
58
|
} catch (error) {
|
|
54
59
|
return absent(error) ? { status: 'missing', exists: false, complete: true, hash: null, text: null, stamp: 'missing' }
|
|
55
60
|
: this.#unavailable('unreadable');
|
|
56
61
|
}
|
|
62
|
+
// Metadata consent permits names/stat observations, never content reads or
|
|
63
|
+
// hashes. A local-source grant is distinct from permission to transmit it.
|
|
64
|
+
if (!this.#policy.readSource) {
|
|
65
|
+
if (!finalStat?.isFile()) return this.#unavailable('not_file');
|
|
66
|
+
return { status: 'present', exists: true, complete: true,
|
|
67
|
+
hash: null, text: null, stamp: fingerprint(finalStat) };
|
|
68
|
+
}
|
|
57
69
|
let handle;
|
|
58
70
|
try {
|
|
59
71
|
// NOFOLLOW closes the final-component race. Revalidate the complete path
|
|
@@ -83,7 +95,7 @@ export class EvidenceStore {
|
|
|
83
95
|
if (text.includes('\0')) return { status: 'partial', exists: true, complete: false, hash: hash(bytes), text: null, stamp };
|
|
84
96
|
return {
|
|
85
97
|
status: 'present', exists: true, complete: true, hash: hash(bytes),
|
|
86
|
-
text:
|
|
98
|
+
text: !privateText(text) ? text : null, stamp,
|
|
87
99
|
};
|
|
88
100
|
} catch {
|
|
89
101
|
// A disappearing/racing file during open/read is uncertainty. A subsequent
|
|
@@ -106,9 +118,9 @@ export class EvidenceStore {
|
|
|
106
118
|
async #captureOne(locator) {
|
|
107
119
|
const observed = await this.#inspect(locator);
|
|
108
120
|
const previous = this.#records.get(locator.relative);
|
|
109
|
-
const version = hash([observed.status, observed.hash, observed.stamp]);
|
|
121
|
+
const version = hash([this.#lineage, observed.status, observed.hash, observed.stamp]);
|
|
110
122
|
const id = opaque('artifact', this.#root, locator.relative);
|
|
111
|
-
const generation = previous ? previous.generation + (previous.version !== version ? 1 : 0) : 1;
|
|
123
|
+
const generation = previous ? previous.generation + (previous.version !== version ? 1 : 0) : this.#generationFloor + 1;
|
|
112
124
|
const artifact = {
|
|
113
125
|
id, path: locator.absolute, relativePath: locator.relative,
|
|
114
126
|
hash: observed.hash, generation, exists: observed.exists, status: observed.status,
|
|
@@ -116,7 +128,7 @@ export class EvidenceStore {
|
|
|
116
128
|
};
|
|
117
129
|
// Registry retains no source bytes; returned captures are immutable private
|
|
118
130
|
// snapshots. All path/cache cardinalities are bounded.
|
|
119
|
-
const record = { ...locator, id, generation, hash: observed.hash, status: observed.status, version };
|
|
131
|
+
const record = { ...locator, id, generation, hash: observed.hash, status: observed.status, version, lineage: this.#lineage };
|
|
120
132
|
this.#records.set(locator.relative, record);
|
|
121
133
|
this.#byId.set(id, record);
|
|
122
134
|
return freeze(artifact);
|
|
@@ -129,14 +141,35 @@ export class EvidenceStore {
|
|
|
129
141
|
const locator = this.#locator(input);
|
|
130
142
|
if (!locator || seen.has(locator.relative)) continue;
|
|
131
143
|
seen.add(locator.relative);
|
|
132
|
-
if (!this.#records.has(locator.relative) && this.#records.size >=
|
|
144
|
+
if (!this.#records.has(locator.relative) && this.#records.size >= this.#maxTrackedPaths) continue;
|
|
133
145
|
results.push(await this.#captureOne(locator));
|
|
134
146
|
}
|
|
135
147
|
return results;
|
|
136
148
|
}
|
|
137
|
-
|
|
149
|
+
setLineage(id) {
|
|
150
|
+
if (typeof id !== 'string' || !/^[a-f0-9]{64}$/.test(id)) fail('INVALID_LINEAGE');
|
|
151
|
+
if (id === this.#lineage) return [];
|
|
152
|
+
this.#lineage = id;
|
|
153
|
+
// A checkout change invalidates in-flight references immediately. The next
|
|
154
|
+
// real capture advances each generation, even if its bytes are identical.
|
|
155
|
+
return [...this.#records.values()].map(record => ({
|
|
156
|
+
id: record.id, path: record.absolute, relativePath: record.relative,
|
|
157
|
+
generation: record.generation, hash: record.hash, status: 'unavailable',
|
|
158
|
+
complete: false, exists: null, text: null,
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
async reconcile({ refs, limit } = {}) {
|
|
138
162
|
const results = [];
|
|
139
|
-
|
|
163
|
+
let records;
|
|
164
|
+
if (Array.isArray(refs)) {
|
|
165
|
+
records = [...new Set(refs.slice(0, LIMITS.trackedPaths).map(ref => this.#byId.get(ref.artifactId)).filter(Boolean))];
|
|
166
|
+
} else if (integer(limit, 1, this.#maxTrackedPaths)) {
|
|
167
|
+
const all = [...this.#records.values()];
|
|
168
|
+
const count = Math.min(limit, all.length);
|
|
169
|
+
records = Array.from({ length: count }, (_, index) => all[(this.#reconcileCursor + index) % all.length]);
|
|
170
|
+
this.#reconcileCursor = all.length ? (this.#reconcileCursor + count) % all.length : 0;
|
|
171
|
+
} else records = [...this.#records.values()];
|
|
172
|
+
for (const record of records) results.push(await this.#captureOne(record));
|
|
140
173
|
return results;
|
|
141
174
|
}
|
|
142
175
|
isCurrent(refs) {
|
|
@@ -144,7 +177,8 @@ export class EvidenceStore {
|
|
|
144
177
|
return refs.every(ref => {
|
|
145
178
|
if (!plain(ref) || !isHash(ref.hash) || !integer(ref.generation, 1)) return false;
|
|
146
179
|
const current = this.#byId.get(ref.artifactId);
|
|
147
|
-
return current?.status === 'present' && current.
|
|
180
|
+
return current?.status === 'present' && current.lineage === this.#lineage &&
|
|
181
|
+
current.hash === ref.hash && current.generation === ref.generation;
|
|
148
182
|
});
|
|
149
183
|
}
|
|
150
184
|
}
|
package/runtime/core/graph.mjs
CHANGED
|
@@ -31,7 +31,8 @@ function validReference(ref) {
|
|
|
31
31
|
if (!exactKeys(ref, REF, ['excerpt', 'sourceRef']) || !isId(ref.artifactId) || !isHash(ref.hash) ||
|
|
32
32
|
!integer(ref.generation, 1) || !isId(ref.eventId) || !integer(ref.startLine, 1, 10000000) ||
|
|
33
33
|
!integer(ref.endLine, ref.startLine, 10000000) || ref.endLine - ref.startLine >= LIMITS.snippetLines ||
|
|
34
|
-
!['source', 'public_intent'].includes(ref.sourceClass) ||
|
|
34
|
+
!['source', 'public_intent'].includes(ref.sourceClass) ||
|
|
35
|
+
!['jev_interpretation', 'decision_interpretation'].includes(ref.basis) ||
|
|
35
36
|
(Object.hasOwn(ref, 'excerpt') && !safeText(ref.excerpt, LIMITS.excerptChars))) return false;
|
|
36
37
|
if (ref.sourceClass === 'public_intent' && !ref.sourceRef) return false;
|
|
37
38
|
if (ref.sourceRef) {
|
|
@@ -131,11 +132,11 @@ function makePatch(graph, operations, causedBy = []) {
|
|
|
131
132
|
baseRevision: graph.revision, revision: graph.revision + 1, causedBy, operations,
|
|
132
133
|
});
|
|
133
134
|
}
|
|
134
|
-
function sourceReference(candidate, event, policy) {
|
|
135
|
+
function sourceReference(candidate, event, policy, basis = 'jev_interpretation') {
|
|
135
136
|
const ref = {
|
|
136
137
|
artifactId: candidate.artifactId, hash: candidate.hash, generation: candidate.generation, eventId: event.id,
|
|
137
138
|
startLine: candidate.startLine, endLine: candidate.endLine, sourceClass: candidate.sourceClass,
|
|
138
|
-
basis
|
|
139
|
+
basis, sourceRef: clone(candidate.sourceRef),
|
|
139
140
|
};
|
|
140
141
|
if (policy.displayEvidence || policy.persistEvidence) {
|
|
141
142
|
const excerpt = candidate.text.slice(0, LIMITS.excerptChars);
|
|
@@ -251,6 +252,10 @@ function compileAuditedDecision(graph, { event, decision, policy }, audit) {
|
|
|
251
252
|
new Set(decision.edges.map(e => e?.proposalId)).size !== decision.edges.length) return audit.reject(decision, 'duplicate_judgments');
|
|
252
253
|
if (!decision.nodes.every(n => validNodeJudgment(n, candidates)) ||
|
|
253
254
|
!decision.edges.every(e => validEdgeJudgment(e, bundle, candidates))) return audit.reject(decision, 'invalid_judgments');
|
|
255
|
+
// The service supplies validated provider provenance. Older manual decisions
|
|
256
|
+
// omit it and keep the legacy basis; answers/candidates cannot select a basis.
|
|
257
|
+
const providerId = decision.provider?.id;
|
|
258
|
+
const basis = providerId && providerId !== 'jev' ? 'decision_interpretation' : 'jev_interpretation';
|
|
254
259
|
const existingNodes = new Map(graph.nodes.map(n => [n.id, n]));
|
|
255
260
|
const existingEdges = new Map(graph.edges.map(e => [e.id, e]));
|
|
256
261
|
const admitted = new Map(), operations = [];
|
|
@@ -278,7 +283,7 @@ function compileAuditedDecision(graph, { event, decision, policy }, audit) {
|
|
|
278
283
|
const classification = judgment.classification === 'accepted' && candidate.complete && !event.incomplete &&
|
|
279
284
|
judgment.supportProbability >= 0.85 && judgment.roleProbability >= 0.8 && judgment.roleConfidence >= 0.6 ? 'accepted' : 'tentative';
|
|
280
285
|
const index = projected.nodes.length;
|
|
281
|
-
const refs = mergeReferences(old?.sourceRefs ?? [], [sourceReference(candidate, event, policy)]);
|
|
286
|
+
const refs = mergeReferences(old?.sourceRefs ?? [], [sourceReference(candidate, event, policy, basis)]);
|
|
282
287
|
const node = {
|
|
283
288
|
id, label: candidate.label, kind: judgment.role, shape: ROLE_SHAPES[judgment.role],
|
|
284
289
|
x: old?.x ?? 80 + (index % 6) * 220, y: old?.y ?? 80 + Math.floor(index / 6) * 140,
|
|
@@ -301,10 +306,10 @@ function compileAuditedDecision(graph, { event, decision, policy }, audit) {
|
|
|
301
306
|
(ref.generation > c.generation || (old.validity !== 'current' && ref.generation === c.generation))))) {
|
|
302
307
|
report('skipped', 'stale_generation'); continue;
|
|
303
308
|
}
|
|
304
|
-
const refs = mergeReferences([], evidence.map(c => sourceReference(c, event, policy)));
|
|
309
|
+
const refs = mergeReferences([], evidence.map(c => sourceReference(c, event, policy, basis)));
|
|
305
310
|
// Every relation retains all dependencies; never silently drop evidence
|
|
306
311
|
// when the reference budget is exceeded.
|
|
307
|
-
if (refs.length !== new Set(evidence.map(c => refKey(sourceReference(c, event, policy)))).size) {
|
|
312
|
+
if (refs.length !== new Set(evidence.map(c => refKey(sourceReference(c, event, policy, basis)))).size) {
|
|
308
313
|
report('skipped', 'reference_limit'); continue;
|
|
309
314
|
}
|
|
310
315
|
const classification = judgment.classification === 'accepted' && source.classification === 'accepted' &&
|
package/runtime/core/privacy.mjs
CHANGED
|
@@ -19,6 +19,7 @@ export function createPolicy(options = {}) {
|
|
|
19
19
|
.filter(p => !DEFAULT_EXCLUDES.includes(p)).slice(0, 64) : []
|
|
20
20
|
)])].sort();
|
|
21
21
|
const fields = {
|
|
22
|
+
readSource: options.readSource === true || options.transmitSource === true,
|
|
22
23
|
transmitSource: options.transmitSource === true,
|
|
23
24
|
displayEvidence: options.displayEvidence !== false,
|
|
24
25
|
persistEvidence: options.persistEvidence === true,
|
package/runtime/daemon/auth.mjs
CHANGED
|
@@ -10,12 +10,16 @@ export function createAuth({ origin, instanceId, now = Date.now }) {
|
|
|
10
10
|
for (const [key, expires] of map) if (expires <= now()) map.delete(key);
|
|
11
11
|
while (map.size >= 16) map.delete(map.keys().next().value);
|
|
12
12
|
}
|
|
13
|
+
function validTransport(req) {
|
|
14
|
+
const hosts = req.rawHeaders.filter((value, index) => index % 2 === 0 && value.toLowerCase() === 'host');
|
|
15
|
+
return hosts.length === 1 && req.headers.host === new URL(origin).host &&
|
|
16
|
+
['127.0.0.1', '::ffff:127.0.0.1'].includes(req.socket.remoteAddress);
|
|
17
|
+
}
|
|
13
18
|
return {
|
|
14
19
|
cookieName,
|
|
20
|
+
validTransport,
|
|
15
21
|
validRequest(req, { mutation = false } = {}) {
|
|
16
|
-
|
|
17
|
-
if (hosts.length !== 1 || req.headers.host !== new URL(origin).host) return false;
|
|
18
|
-
if (!['127.0.0.1', '::ffff:127.0.0.1'].includes(req.socket.remoteAddress)) return false;
|
|
22
|
+
if (!validTransport(req)) return false;
|
|
19
23
|
if (req.headers['sec-fetch-site'] && !['same-origin', 'none'].includes(req.headers['sec-fetch-site'])) return false;
|
|
20
24
|
if (mutation) return req.headers.origin === origin;
|
|
21
25
|
return req.headers.origin === undefined || req.headers.origin === origin;
|
|
@@ -4,7 +4,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { CATEGORIES, KINDS, ROLES, RELATIONS, isId, opaque, plain } from '../core/common.mjs';
|
|
6
6
|
import { createPolicy, excluded, privateText, safeLabel } from '../core/privacy.mjs';
|
|
7
|
-
import { ACTIVITIES } from '../
|
|
7
|
+
import { ACTIVITIES } from '../decisions/questions.mjs';
|
|
8
8
|
import { runtimeError, uid } from './paths.mjs';
|
|
9
9
|
|
|
10
10
|
export const DIAGNOSTIC_LIMITS = Object.freeze({
|