sneakoscope 9.0.2 → 9.0.5

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/README.md CHANGED
@@ -22,7 +22,7 @@ Proof-first orchestration for Codex CLI, ChatGPT Desktop, AI coding agents, mult
22
22
  Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.
23
23
  <!-- END SKS SEARCH VISIBILITY MARKETING -->
24
24
 
25
- This README documents package **SKS 9.0.2** — its own identity, read from `package.json` and subject to release-gate verification, not advice about what to install.
25
+ This README documents package **SKS 9.0.5** — its own identity, read from `package.json` and subject to release-gate verification, not advice about what to install.
26
26
 
27
27
  Use the official latest stable SKS and Codex CLI releases. The Codex compatibility SSOT is always the **current latest stable** host; capability probes measure what that host can actually do. Product docs do not crown a fixed `0.x.y` string as SSOT (release pins and schema directories are measured artifacts for the current package, not a permanent product version claim). Menu Bar / Center induce updates to the latest stable build. Run `sks update-check` for what is installed and read the capability report for what is supported. Install SSOT is npm `sneakoscope@latest`; PATH `sks` and Menu Bar stamped generation must match that version or gates fail. It resolves managed SKS skills from the authoritative global install, preserves a runnable Naruto child slot when `max_threads=2`, and keeps Menu Bar repair transactional so stamped generations remain verifiable. Naruto uses stable opt-in multi-agent V2 when the host exposes it (Codex official multi-agent wrap-only; SKS does not reimplement a parallel runtime). Local code search is mode-separated (`sks search files|text|structure|symbol|context`); `context` is answered by the compiled TriWiki Context Graph (`context-graph.json` is exhaustive authority; `context-pack.json` and managed `AGENTS.md` are bounded projections) — see [docs/architecture/context-graph.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/docs/architecture/context-graph.md) and [docs/PRODUCT-CONTRACT.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/docs/PRODUCT-CONTRACT.md). See [CHANGELOG.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/CHANGELOG.md).
28
28
 
@@ -259,7 +259,7 @@ dependencies = [
259
259
 
260
260
  [[package]]
261
261
  name = "sks-core"
262
- version = "9.0.2"
262
+ version = "9.0.5"
263
263
  dependencies = [
264
264
  "globset",
265
265
  "grep-matcher",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "9.0.2"
3
+ version = "9.0.5"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "sks.skills-manifest.v1",
3
- "package_version": "9.0.2",
3
+ "package_version": "9.0.5",
4
4
  "skills": [
5
5
  {
6
6
  "canonical_name": "sks",
@@ -144,8 +144,13 @@ function writeHttpBridgeError(res, error, req) {
144
144
  });
145
145
  res.end(JSON.stringify({ error: { type: 'sks_bridge_error', code, message: code } }));
146
146
  }
147
+ function safeUpstreamErrorId(value) {
148
+ const text = String(value ?? '').trim();
149
+ return /^[A-Za-z0-9_.:-]{1,64}$/.test(text) ? text : null;
150
+ }
147
151
  async function readRedactedUpstreamError(response) {
148
152
  let total = 0;
153
+ const chunks = [];
149
154
  for await (const raw of response) {
150
155
  const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
151
156
  total += chunk.length;
@@ -153,14 +158,30 @@ async function readRedactedUpstreamError(response) {
153
158
  response.destroy();
154
159
  break;
155
160
  }
161
+ chunks.push(chunk);
156
162
  }
157
- return Buffer.from(JSON.stringify({
158
- error: {
159
- type: 'upstream_error',
160
- code: 'bridge_upstream_request_failed',
161
- message: 'Upstream request failed'
162
- }
163
- }));
163
+ let upstreamCode = null;
164
+ let upstreamType = null;
165
+ try {
166
+ const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));
167
+ upstreamCode = safeUpstreamErrorId(parsed?.error?.code) ?? safeUpstreamErrorId(parsed?.detail);
168
+ upstreamType = safeUpstreamErrorId(parsed?.error?.type);
169
+ }
170
+ catch {
171
+ }
172
+ return {
173
+ upstreamCode,
174
+ upstreamType,
175
+ body: Buffer.from(JSON.stringify({
176
+ error: {
177
+ type: 'upstream_error',
178
+ code: 'bridge_upstream_request_failed',
179
+ message: 'Upstream request failed',
180
+ ...(upstreamType ? { upstream_type: upstreamType } : {}),
181
+ ...(upstreamCode ? { upstream_code: upstreamCode } : {})
182
+ }
183
+ }))
184
+ };
164
185
  }
165
186
  const upstreamAgents = new Map();
166
187
  function upstreamAgent(secure, key, idleTimeoutMs) {
@@ -252,21 +273,27 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
252
273
  upstream.once('response', (response) => {
253
274
  const statusCode = response.statusCode || 502;
254
275
  if (statusCode >= 400) {
255
- logHttpRejection({
256
- code: `bridge_upstream_status_${statusCode}`,
257
- transport: 'http',
258
- ...(req.method === undefined ? {} : { method: req.method }),
259
- ...(req.url === undefined ? {} : { url: req.url }),
260
- status: statusCode,
261
- provider_id: provider.provider_id,
262
- public_model: request.route.public_model,
263
- });
264
- void readRedactedUpstreamError(response).then((body) => {
276
+ void readRedactedUpstreamError(response).then(({ body, upstreamCode, upstreamType }) => {
277
+ const transientMislabel = statusCode === 404 && upstreamType === 'upstream_error' && !upstreamCode;
278
+ const clientStatus = transientMislabel ? 503 : statusCode;
279
+ logHttpRejection({
280
+ code: transientMislabel
281
+ ? `bridge_upstream_status_${statusCode}_translated_503`
282
+ : upstreamCode ? `bridge_upstream_status_${statusCode}:${upstreamCode}` : `bridge_upstream_status_${statusCode}`,
283
+ transport: 'http',
284
+ ...(req.method === undefined ? {} : { method: req.method }),
285
+ ...(req.url === undefined ? {} : { url: req.url }),
286
+ status: clientStatus,
287
+ provider_id: provider.provider_id,
288
+ public_model: request.route.public_model,
289
+ });
265
290
  responseStarted = true;
266
291
  const responseHeaders = rewriteResponseHeaders(response.headers, provider.base_url, authenticatedLocalBaseUrl);
267
292
  responseHeaders['content-length'] = String(body.length);
268
293
  delete responseHeaders['transfer-encoding'];
269
- res.writeHead(statusCode, responseHeaders);
294
+ if (transientMislabel)
295
+ responseHeaders['retry-after'] = '10';
296
+ res.writeHead(clientStatus, responseHeaders);
270
297
  res.end(body, () => finish());
271
298
  }).catch(finish);
272
299
  return;
@@ -33,17 +33,21 @@ function sortedHashes(hashes) {
33
33
  }
34
34
  export class CodeGraphExtractor {
35
35
  preparedInventory;
36
+ sourceInventory;
36
37
  id = CODE_GRAPH_EXTRACTOR_ID;
37
38
  revision = CODE_GRAPH_EXTRACTOR_REVISION;
38
- constructor(preparedInventory = null) {
39
+ constructor(preparedInventory = null, sourceInventory = null) {
39
40
  this.preparedInventory = preparedInventory;
41
+ this.sourceInventory = sourceInventory;
40
42
  }
41
43
  async extract(input) {
42
44
  const startedAt = Date.now();
43
45
  const deadline = startedAt + Math.max(1, input.limits.timeoutMs);
44
46
  const root = realRoot(input.root);
45
47
  const sink = new CodeGraphSink(input.limits, input.observedAt);
46
- const inventory = this.preparedInventory ?? walkCodeInventory(root, input.limits);
48
+ const inventory = this.preparedInventory
49
+ ?? this.sourceInventory?.inventory(root, input.limits)
50
+ ?? walkCodeInventory(root, input.limits);
47
51
  for (const skip of inventory.skipped)
48
52
  sink.addSkip(skip);
49
53
  const hashes = new Map();
@@ -273,6 +277,6 @@ function collectNodeRels(factsByRel, parsedByRel, inventory, selectedRels) {
273
277
  return [...rels].sort();
274
278
  }
275
279
  export function createCodeGraphExtractor(options = {}) {
276
- return new CodeGraphExtractor(options.preparedInventory ?? null);
280
+ return new CodeGraphExtractor(options.preparedInventory ?? null, options.sourceInventory ?? null);
277
281
  }
278
282
  export { CODE_GRAPH_EXTRACTOR_ID, CODE_GRAPH_EXTRACTOR_REVISION } from './types.js';
@@ -2,15 +2,25 @@ import { lintWarning } from '../../contracts.js';
2
2
  import { extractContextPackEvidence } from './claims.js';
3
3
  import { extractProofEvidence } from './proofs.js';
4
4
  import { sanitizeEvidenceFragment } from './redaction.js';
5
+ import { createSharedSourceInventory } from '../source-inventory.js';
5
6
  import { CONTEXT_PACK_REL, EVIDENCE_EXTRACTOR_ID, EVIDENCE_EXTRACTOR_REVISION, EvidenceFragmentBuilder, PROOF_BANK_REL, RiskDomainRegistry, evidenceContext, finalizeEvidenceFragment } from './shared.js';
6
7
  export { CONTEXT_PACK_REL, EVIDENCE_EXTRACTOR_ID, EVIDENCE_EXTRACTOR_REVISION, PROOF_BANK_REL, PROOF_INDEX_REL } from './shared.js';
7
8
  export { TRIWIKI_PROOF_INDEX_SCHEMA } from './proof-index.js';
8
9
  const MIN_FAN_IN_FOR_VERIFICATION_WARNING = 3;
9
10
  export class EvidenceGraphExtractor {
11
+ sourceInventory;
10
12
  id = EVIDENCE_EXTRACTOR_ID;
11
13
  revision = EVIDENCE_EXTRACTOR_REVISION;
14
+ constructor(sourceInventory = createSharedSourceInventory()) {
15
+ this.sourceInventory = sourceInventory;
16
+ }
12
17
  async extract(input) {
13
- const ctx = evidenceContext({ root: input.root, observedAt: input.observedAt, limits: input.limits });
18
+ const ctx = evidenceContext({
19
+ root: input.root,
20
+ observedAt: input.observedAt,
21
+ limits: input.limits,
22
+ sourcePaths: this.sourceInventory.sourcePaths(input.root, input.limits)
23
+ });
14
24
  const builder = new EvidenceFragmentBuilder(input.limits, input.observedAt);
15
25
  const risks = new RiskDomainRegistry();
16
26
  const pack = extractContextPackEvidence(builder, ctx, risks);
@@ -26,8 +36,8 @@ export class EvidenceGraphExtractor {
26
36
  return finalizeEvidenceFragment(sanitizeEvidenceFragment(builder.fragment));
27
37
  }
28
38
  }
29
- export function createEvidenceGraphExtractor() {
30
- return new EvidenceGraphExtractor();
39
+ export function createEvidenceGraphExtractor(options = {}) {
40
+ return new EvidenceGraphExtractor(options.sourceInventory ?? createSharedSourceInventory());
31
41
  }
32
42
  function noteUnverifiedFanIn(builder) {
33
43
  const incoming = new Map();
@@ -3,7 +3,7 @@ import { sha256 } from '../../../../fsx.js';
3
3
  import { resolveInsideWorkspace, tryNormalizeGraphPath } from '../../paths.js';
4
4
  import { PROOF_BANK_REL, PROOF_INDEX_REL, asArray, asRecord, asString, asStringList, readWorkspaceFile, statWorkspaceEntry } from './shared.js';
5
5
  export const TRIWIKI_PROOF_INDEX_SCHEMA = 'sks.triwiki-proof-index.v1';
6
- const MAX_PROOF_RECORDS = 512;
6
+ const MAX_PROOF_RECORDS = 4096;
7
7
  const MAX_SCAN_DEPTH = 4;
8
8
  const SCAN_EXCLUDED_DIRS = new Set(['.locks', 'node_modules', '.git']);
9
9
  export function discoverProofRecords(ctx) {
@@ -7,10 +7,6 @@ import { EvidenceFragmentBuilder, PROOF_BANK_REL, RiskDomainRegistry, asRecord,
7
7
  import { discoverProofRecords } from './proof-index.js';
8
8
  const MAX_DERIVED_INPUTS = 12;
9
9
  const PLACEHOLDER_HASHES = new Set(['', 'unknown', 'legacy-missing', 'none', 'null']);
10
- const HASH_PINNED_INPUTS = [
11
- { field: 'package_lock_hash', rel: 'package-lock.json' },
12
- { field: 'release_gates_hash', rel: 'release-gates.v2.json' }
13
- ];
14
10
  export function extractProofEvidence(builder, ctx, risks) {
15
11
  const discovery = discoverProofRecords(ctx);
16
12
  for (const skip of discovery.skipped)
@@ -220,15 +216,7 @@ function linkSubject(builder, record, health, proofNodeId, mode) {
220
216
  }
221
217
  function linkDerivedInputs(builder, ctx, record, proofNodeId) {
222
218
  const raw = record.card;
223
- const candidates = [];
224
- if (raw) {
225
- for (const pinned of HASH_PINNED_INPUTS) {
226
- const hash = asString(raw[pinned.field]);
227
- if (hash && !PLACEHOLDER_HASHES.has(hash))
228
- candidates.push(pinned.rel);
229
- }
230
- candidates.push(...asStringList(raw.input_paths), ...asStringList(raw.source_paths));
231
- }
219
+ const candidates = raw ? [...asStringList(raw.input_paths), ...asStringList(raw.source_paths)] : [];
232
220
  const seen = new Set();
233
221
  for (const candidate of candidates) {
234
222
  if (seen.size >= MAX_DERIVED_INPUTS)
@@ -236,6 +224,8 @@ function linkDerivedInputs(builder, ctx, record, proofNodeId) {
236
224
  const rel = tryNormalizeGraphPath(ctx.root, candidate);
237
225
  if (!rel || seen.has(rel))
238
226
  continue;
227
+ if (!ctx.sourcePaths.has(rel))
228
+ continue;
239
229
  const stat = statWorkspaceEntry(ctx.root, rel);
240
230
  if (!stat || !stat.isFile())
241
231
  continue;
@@ -1,5 +1,6 @@
1
1
  import { REDACTION_MARKER, containsPlaintextSecret, redactString } from '../../../../secret-redaction.js';
2
2
  import { lintError } from '../../contracts.js';
3
+ import { isStructuralValue } from '../../lint/rules.js';
3
4
  import { isWorkspaceRelativePosixPath } from '../../paths.js';
4
5
  export const EVIDENCE_REDACTED_PATH = '[redacted-path]';
5
6
  export const EVIDENCE_MAX_META_STRING = 160;
@@ -134,8 +135,11 @@ function sanitizeMetadataValue(value) {
134
135
  }
135
136
  return { value, changed: false };
136
137
  }
138
+ function identityLooksSecret(value) {
139
+ return !isStructuralValue(value) && containsPlaintextSecret(value);
140
+ }
137
141
  function guardNode(node, extractor) {
138
- if (containsPlaintextSecret(node.id) || containsPlaintextSecret(node.label)) {
142
+ if (identityLooksSecret(node.id) || identityLooksSecret(node.label)) {
139
143
  return {
140
144
  node: null,
141
145
  issue: lintError('secret_like_value', 'evidence node identity looks secret-like and was refused', {
@@ -144,7 +148,7 @@ function guardNode(node, extractor) {
144
148
  })
145
149
  };
146
150
  }
147
- if (node.path !== undefined && containsPlaintextSecret(node.path)) {
151
+ if (node.path !== undefined && identityLooksSecret(node.path)) {
148
152
  return {
149
153
  node: null,
150
154
  issue: lintError('secret_like_value', 'evidence node path looks secret-like and was refused', {
@@ -95,8 +95,9 @@ export class EvidenceSourceGraph {
95
95
  tokenCost: tokenEstimate(rel.length, 64),
96
96
  metadata
97
97
  }, CONTEXT_PACK_REL);
98
- if (!isDirectory && (diskHash || manifestHash))
98
+ if (!isDirectory && (diskHash || manifestHash) && this.ctx.sourcePaths.has(rel)) {
99
99
  this.linkBackingFile(id, rel, stat, diskHash, manifestHash);
100
+ }
100
101
  const state = { id, freshness };
101
102
  this.states.set(rel, state);
102
103
  return state;
@@ -1,14 +1,25 @@
1
1
  import { createCodeGraphExtractor } from './code/index.js';
2
2
  import { createEvidenceGraphExtractor } from './evidence/index.js';
3
+ import { createSharedSourceInventory } from './source-inventory.js';
3
4
  import { createTopologyGraphExtractor } from './topology/index.js';
4
5
  export function contextGraphExtractors() {
5
- return [createCodeGraphExtractor(), createTopologyGraphExtractor(), createEvidenceGraphExtractor()];
6
+ const sourceInventory = createSharedSourceInventory();
7
+ return [
8
+ createCodeGraphExtractor({ sourceInventory }),
9
+ createTopologyGraphExtractor({ sourceInventory }),
10
+ createEvidenceGraphExtractor({ sourceInventory })
11
+ ];
6
12
  }
7
13
  export function codeNavigationGraphExtractors(options = {}) {
8
14
  return [createCodeGraphExtractor(options)];
9
15
  }
10
16
  export function architectureMapGraphExtractors(options = {}) {
11
- return [createCodeGraphExtractor(options), createTopologyGraphExtractor(), createEvidenceGraphExtractor()];
17
+ const sourceInventory = createSharedSourceInventory(options.preparedInventory ?? null);
18
+ return [
19
+ createCodeGraphExtractor({ sourceInventory }),
20
+ createTopologyGraphExtractor({ sourceInventory }),
21
+ createEvidenceGraphExtractor({ sourceInventory })
22
+ ];
12
23
  }
13
24
  export const ARCHITECTURE_MAP_EXTRACTOR_IDS = Object.freeze(['code', 'topology', 'triwiki-evidence']);
14
25
  export { createCodeGraphExtractor, createEvidenceGraphExtractor, createTopologyGraphExtractor };
@@ -0,0 +1,35 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { walkCodeInventory } from './code/inventory.js';
4
+ function canonicalRoot(root) {
5
+ const absolute = path.resolve(root);
6
+ try {
7
+ return fs.realpathSync(absolute);
8
+ }
9
+ catch {
10
+ return absolute;
11
+ }
12
+ }
13
+ export function createSharedSourceInventory(prepared = null) {
14
+ let cachedRoot = null;
15
+ let cachedInventory = prepared;
16
+ let cachedSet = null;
17
+ const inventory = (root, limits) => {
18
+ if (prepared)
19
+ return prepared;
20
+ const canonical = canonicalRoot(root);
21
+ if (!cachedInventory || cachedRoot !== canonical) {
22
+ cachedInventory = walkCodeInventory(canonical, limits);
23
+ cachedRoot = canonical;
24
+ }
25
+ return cachedInventory;
26
+ };
27
+ const sourcePaths = (root, limits) => {
28
+ const current = inventory(root, limits);
29
+ if (!cachedSet || cachedSet.inventory !== current) {
30
+ cachedSet = { inventory: current, set: new Set(current.files.map((file) => file.rel)) };
31
+ }
32
+ return cachedSet.set;
33
+ };
34
+ return { inventory, sourcePaths };
35
+ }
@@ -50,22 +50,26 @@ export function buildGateVerificationEdges(ctx, gates) {
50
50
  const verified = new Set();
51
51
  for (const gate of gates) {
52
52
  let emitted = 0;
53
+ let capped = false;
53
54
  for (const candidate of gateCheckCandidates(gate.command)) {
54
- if (emitted >= TOPOLOGY_GATE_VERIFIED_CAP)
55
- break;
56
55
  const expansion = expandGlob(ctx.files, candidate, TOPOLOGY_GLOB_MATCH_CAP);
57
- if (expansion.capped) {
58
- recordSkip(ctx, gate.manifestPath, 'cap_reached', `check pattern ${candidate} matched ${expansion.total} files`);
56
+ if (!expansion.total) {
57
+ recordSkip(ctx, gate.manifestPath, 'excluded', `check implementation ${candidate} is not in this workspace`);
59
58
  continue;
60
59
  }
61
- if (!expansion.matches.length) {
62
- recordSkip(ctx, gate.manifestPath, 'excluded', `check implementation ${candidate} is not in this workspace`);
60
+ verified.add(gate.id);
61
+ if (expansion.capped) {
62
+ recordSkip(ctx, gate.manifestPath, 'excluded', `check pattern ${candidate} matched ${expansion.total} files; kept whole as checkScripts metadata on the gate node`);
63
63
  continue;
64
64
  }
65
65
  const confidence = isGlobPattern(candidate) ? 'derived' : 'manifest';
66
66
  for (const match of expansion.matches) {
67
- if (emitted >= TOPOLOGY_GATE_VERIFIED_CAP)
67
+ if (!ctx.sourcePaths.has(match))
68
+ continue;
69
+ if (emitted >= TOPOLOGY_GATE_VERIFIED_CAP) {
70
+ capped = true;
68
71
  break;
72
+ }
69
73
  const fileId = ensureFileNode(ctx, match, 'gate_check', gate.manifestPath);
70
74
  if (!fileId)
71
75
  continue;
@@ -78,11 +82,14 @@ export function buildGateVerificationEdges(ctx, gates) {
78
82
  hash: gate.manifestHash,
79
83
  line: gate.line
80
84
  });
81
- if (added) {
85
+ if (added)
82
86
  emitted += 1;
83
- verified.add(gate.id);
84
- }
85
87
  }
88
+ if (capped)
89
+ break;
90
+ }
91
+ if (capped) {
92
+ recordSkip(ctx, gate.manifestPath, 'cap_reached', `gate ${gate.id} hit the verified_by edge cap (${TOPOLOGY_GATE_VERIFIED_CAP}); check relations are missing`);
86
93
  }
87
94
  }
88
95
  return verified;
@@ -91,21 +98,26 @@ export function buildGateAffectedEdges(ctx, gates) {
91
98
  const affected = new Set();
92
99
  for (const gate of gates) {
93
100
  let emitted = 0;
101
+ let capped = false;
94
102
  const seen = new Set();
95
103
  for (const input of gate.cacheInputs) {
96
- if (emitted >= TOPOLOGY_GATE_AFFECTED_CAP)
97
- break;
98
104
  const expansion = expandGlob(ctx.files, input, TOPOLOGY_GLOB_MATCH_CAP);
105
+ if (expansion.total > 0)
106
+ affected.add(gate.id);
99
107
  if (expansion.capped) {
100
- recordSkip(ctx, gate.manifestPath, 'cap_reached', `cache input ${input} matched ${expansion.total} files`);
108
+ recordSkip(ctx, gate.manifestPath, 'excluded', `cache input ${input} matched ${expansion.total} files; kept whole as cacheInputs metadata on the gate node`);
101
109
  continue;
102
110
  }
103
111
  for (const match of expansion.matches) {
104
- if (emitted >= TOPOLOGY_GATE_AFFECTED_CAP)
105
- break;
106
112
  if (seen.has(match))
107
113
  continue;
108
114
  seen.add(match);
115
+ if (!ctx.sourcePaths.has(match))
116
+ continue;
117
+ if (emitted >= TOPOLOGY_GATE_AFFECTED_CAP) {
118
+ capped = true;
119
+ break;
120
+ }
109
121
  const fileId = ensureFileNode(ctx, match, 'gate_cache_input', gate.manifestPath);
110
122
  if (!fileId)
111
123
  continue;
@@ -118,11 +130,14 @@ export function buildGateAffectedEdges(ctx, gates) {
118
130
  hash: gate.manifestHash,
119
131
  line: gate.line
120
132
  });
121
- if (added) {
133
+ if (added)
122
134
  emitted += 1;
123
- affected.add(gate.id);
124
- }
125
135
  }
136
+ if (capped)
137
+ break;
138
+ }
139
+ if (capped) {
140
+ recordSkip(ctx, gate.manifestPath, 'cap_reached', `gate ${gate.id} hit the affected_by edge cap (${TOPOLOGY_GATE_AFFECTED_CAP}); cache-input relations are missing`);
126
141
  }
127
142
  }
128
143
  return affected;
@@ -4,6 +4,7 @@ import { buildCommandGraph, collectRuntimeManifest } from './commands.js';
4
4
  import { buildGateAffectedEdges, buildGateDependencyEdges, buildGatePresetPipelines, buildGateVerificationEdges, reportUnbackedProtectedGates } from './gate-edges.js';
5
5
  import { buildGateNodes, collectTopologyGates } from './gates.js';
6
6
  import { buildFileInventory } from './globs.js';
7
+ import { createSharedSourceInventory } from '../source-inventory.js';
7
8
  import { buildRouteGraph } from './routes.js';
8
9
  import { TOPOLOGY_EXTRACTOR_ID, TOPOLOGY_EXTRACTOR_REVISION, createTopologyContext, recordSkip, topologyExpired } from './shared.js';
9
10
  const INVENTORY_SKIP_PATH = 'release-gates.v2.json';
@@ -45,8 +46,12 @@ function finalize(ctx) {
45
46
  return fragment;
46
47
  }
47
48
  export class TopologyGraphExtractor {
49
+ sourceInventory;
48
50
  id = TOPOLOGY_EXTRACTOR_ID;
49
51
  revision = TOPOLOGY_EXTRACTOR_REVISION;
52
+ constructor(sourceInventory = createSharedSourceInventory()) {
53
+ this.sourceInventory = sourceInventory;
54
+ }
50
55
  async extract(input) {
51
56
  const startedAt = Date.now();
52
57
  const files = buildFileInventory(input.root, input.limits.maxFiles);
@@ -55,6 +60,7 @@ export class TopologyGraphExtractor {
55
60
  observedAt: input.observedAt,
56
61
  limits: input.limits,
57
62
  files,
63
+ sourcePaths: this.sourceInventory.sourcePaths(input.root, input.limits),
58
64
  startedAt
59
65
  });
60
66
  if (files.truncated) {
@@ -83,8 +89,8 @@ export class TopologyGraphExtractor {
83
89
  return finalize(ctx);
84
90
  }
85
91
  }
86
- export function createTopologyGraphExtractor() {
87
- return new TopologyGraphExtractor();
92
+ export function createTopologyGraphExtractor(options = {}) {
93
+ return new TopologyGraphExtractor(options.sourceInventory ?? createSharedSourceInventory());
88
94
  }
89
95
  export { TOPOLOGY_EXTRACTOR_ID, TOPOLOGY_EXTRACTOR_REVISION } from './shared.js';
90
96
  export { TOPOLOGY_GATE_MANIFESTS } from './gates.js';
@@ -4,10 +4,10 @@ import { lintError } from '../../contracts.js';
4
4
  import { contextGraphEdgeId, contextGraphNodeId } from '../../ids.js';
5
5
  import { ContextGraphPathError, isWorkspaceRelativePosixPath, resolveInsideWorkspace } from '../../paths.js';
6
6
  export const TOPOLOGY_EXTRACTOR_ID = 'topology';
7
- export const TOPOLOGY_EXTRACTOR_REVISION = '1.0.0';
7
+ export const TOPOLOGY_EXTRACTOR_REVISION = '1.1.0';
8
8
  export const TOPOLOGY_GLOB_MATCH_CAP = 48;
9
- export const TOPOLOGY_GATE_AFFECTED_CAP = 96;
10
- export const TOPOLOGY_GATE_VERIFIED_CAP = 24;
9
+ export const TOPOLOGY_GATE_AFFECTED_CAP = 256;
10
+ export const TOPOLOGY_GATE_VERIFIED_CAP = 96;
11
11
  export function createTopologyContext(params) {
12
12
  const timeout = Number.isFinite(params.limits.timeoutMs) && params.limits.timeoutMs > 0 ? params.limits.timeoutMs : 0;
13
13
  return {
@@ -16,6 +16,7 @@ export function createTopologyContext(params) {
16
16
  limits: params.limits,
17
17
  deadline: params.startedAt + timeout,
18
18
  files: params.files,
19
+ sourcePaths: params.sourcePaths,
19
20
  nodes: new Map(),
20
21
  edges: new Map(),
21
22
  issues: [],
@@ -114,6 +115,8 @@ export function addEdge(ctx, input) {
114
115
  export function ensureFileNode(ctx, relativePath, role, sourcePath) {
115
116
  if (!isWorkspaceRelativePosixPath(relativePath))
116
117
  return null;
118
+ if (!ctx.sourcePaths.has(relativePath))
119
+ return null;
117
120
  const id = contextGraphNodeId({ kind: 'file', path: relativePath });
118
121
  const classification = ctx.fileClassification.get(relativePath);
119
122
  const metadata = {
@@ -82,7 +82,7 @@ function looksHighEntropy(token) {
82
82
  return true;
83
83
  return /[A-Z]/.test(token) && /[a-z]/.test(token) && /[0-9]/.test(token);
84
84
  }
85
- function isStructuralValue(value) {
85
+ export function isStructuralValue(value) {
86
86
  if (value.length > 512)
87
87
  return false;
88
88
  for (const run of value.match(HIGH_ENTROPY_RUN_RE) ?? []) {
@@ -1 +1 @@
1
- export const PACKAGE_VERSION = '9.0.2';
1
+ export const PACKAGE_VERSION = '9.0.5';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "9.0.2",
4
+ "version": "9.0.5",
5
5
  "description": "Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",