sneakoscope 8.3.2 → 8.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +1 -1
  2. package/config/architecture-map-policy.v1.json +168 -0
  3. package/crates/sks-core/Cargo.lock +1 -1
  4. package/crates/sks-core/Cargo.toml +1 -1
  5. package/dist/commands/bridge.js +19 -1
  6. package/dist/commands/doctor-profile.js +1 -1
  7. package/dist/commands/doctor.js +33 -1
  8. package/dist/config/skills-manifest.json +1 -1
  9. package/dist/core/align/align-route.js +8 -2
  10. package/dist/core/align/code-navigation-align.js +88 -32
  11. package/dist/core/architecture-map-pipeline.js +172 -0
  12. package/dist/core/architecture-map-review.js +180 -0
  13. package/dist/core/codex-app/codex-app-fast-ui-repair.js +2 -1
  14. package/dist/core/codex-app/menubar/constants.js +1 -0
  15. package/dist/core/codex-lb/codex-lb-env.js +25 -3
  16. package/dist/core/codex-lb/combined-catalog/builder.js +7 -2
  17. package/dist/core/codex-lb/combined-catalog/model-selection.js +71 -0
  18. package/dist/core/codex-lb/combined-catalog/normalize.js +20 -3
  19. package/dist/core/codex-lb/desktop-controller-v3/catalog.js +75 -4
  20. package/dist/core/codex-lb/desktop-controller-v3.js +5 -1
  21. package/dist/core/commands/triwiki-atlas-command.js +372 -0
  22. package/dist/core/commands/triwiki-command.js +12 -1
  23. package/dist/core/fsx.js +14 -0
  24. package/dist/core/git-hygiene/git-policy.js +4 -0
  25. package/dist/core/git-hygiene/gitignore-writer.js +2 -0
  26. package/dist/core/gx-renderer.js +2 -1
  27. package/dist/core/managed-paths.js +1 -0
  28. package/dist/core/perf/release-latency-slo.js +57 -11
  29. package/dist/core/pipeline-internals/pipeline-stage-builder.js +181 -0
  30. package/dist/core/pipeline-internals/runtime-core.js +13 -172
  31. package/dist/core/pipeline-internals/runtime-gates.js +12 -1
  32. package/dist/core/release/content-digest-cache.js +70 -0
  33. package/dist/core/release/gate-manifest.js +9 -0
  34. package/dist/core/release/main-push-guard.js +4 -3
  35. package/dist/core/release/package-dist-snapshot.js +18 -2
  36. package/dist/core/release/physical-release-gates.js +26 -3
  37. package/dist/core/release/release-authorization-snapshot.js +4 -0
  38. package/dist/core/release/release-gate-cache-v2.js +27 -3
  39. package/dist/core/release/release-gate-contract.js +2 -0
  40. package/dist/core/release/release-gate-dag.js +3 -0
  41. package/dist/core/release/release-inspection-memo.js +69 -0
  42. package/dist/core/release/release-pack-content-scanner.js +5 -0
  43. package/dist/core/release/release-pack-receipt.js +28 -18
  44. package/dist/core/release/release-pack-tarball.js +10 -0
  45. package/dist/core/release/release-upgrade-baseline.js +4 -0
  46. package/dist/core/routes.js +1 -1
  47. package/dist/core/safety/ssot-authority-inventory.js +114 -0
  48. package/dist/core/safety/ssot-guard.js +2 -44
  49. package/dist/core/triwiki/context-graph/architecture/baseline.js +81 -0
  50. package/dist/core/triwiki/context-graph/architecture/capsule.js +34 -0
  51. package/dist/core/triwiki/context-graph/architecture/contracts.js +67 -0
  52. package/dist/core/triwiki/context-graph/architecture/delta.js +149 -0
  53. package/dist/core/triwiki/context-graph/architecture/finding-factory.js +28 -0
  54. package/dist/core/triwiki/context-graph/architecture/findings.js +173 -0
  55. package/dist/core/triwiki/context-graph/architecture/fingerprint.js +75 -0
  56. package/dist/core/triwiki/context-graph/architecture/index.js +15 -0
  57. package/dist/core/triwiki/context-graph/architecture/input-bundle.js +66 -0
  58. package/dist/core/triwiki/context-graph/architecture/metrics.js +137 -0
  59. package/dist/core/triwiki/context-graph/architecture/policy.js +321 -0
  60. package/dist/core/triwiki/context-graph/architecture/slice.js +100 -0
  61. package/dist/core/triwiki/context-graph/architecture/ssot-analysis.js +59 -0
  62. package/dist/core/triwiki/context-graph/architecture/topology-overlay.js +106 -0
  63. package/dist/core/triwiki/context-graph/architecture/validation.js +145 -0
  64. package/dist/core/triwiki/context-graph/architecture/voxel-context.js +39 -0
  65. package/dist/core/triwiki/context-graph/extractors/index.js +4 -0
  66. package/dist/core/triwiki/context-graph/projections/index.js +2 -0
  67. package/dist/core/triwiki/context-graph/projections/mermaid/architecture-delta.js +68 -0
  68. package/dist/core/triwiki/context-graph/projections/mermaid/ast.js +49 -0
  69. package/dist/core/triwiki/context-graph/projections/mermaid/change-impact.js +43 -0
  70. package/dist/core/triwiki/context-graph/projections/mermaid/contracts.js +5 -0
  71. package/dist/core/triwiki/context-graph/projections/mermaid/escape.js +45 -0
  72. package/dist/core/triwiki/context-graph/projections/mermaid/ids.js +73 -0
  73. package/dist/core/triwiki/context-graph/projections/mermaid/index.js +113 -0
  74. package/dist/core/triwiki/context-graph/projections/mermaid/manifest.js +37 -0
  75. package/dist/core/triwiki/context-graph/projections/mermaid/module-dependency.js +21 -0
  76. package/dist/core/triwiki/context-graph/projections/mermaid/ownership-workstream.js +21 -0
  77. package/dist/core/triwiki/context-graph/projections/mermaid/project-topology.js +13 -0
  78. package/dist/core/triwiki/context-graph/projections/mermaid/public-surface.js +17 -0
  79. package/dist/core/triwiki/context-graph/projections/mermaid/reduce.js +44 -0
  80. package/dist/core/triwiki/context-graph/projections/mermaid/risk-domains.js +23 -0
  81. package/dist/core/triwiki/context-graph/projections/mermaid/runtime-control.js +20 -0
  82. package/dist/core/triwiki/context-graph/projections/mermaid/selection-ledger.js +94 -0
  83. package/dist/core/triwiki/context-graph/projections/mermaid/serializer.js +81 -0
  84. package/dist/core/triwiki/context-graph/projections/mermaid/ssot-provenance.js +17 -0
  85. package/dist/core/triwiki/context-graph/projections/mermaid/verification-coverage.js +17 -0
  86. package/dist/core/triwiki/context-graph/projections/mermaid/view-builder-common.js +1 -0
  87. package/dist/core/triwiki/context-graph/projections/mermaid/view-builder.js +173 -0
  88. package/dist/core/triwiki/context-graph/projections/mermaid/view-specs.js +33 -0
  89. package/dist/core/triwiki/context-graph/store/architecture-map-store.js +71 -0
  90. package/dist/core/triwiki/triwiki-proof-bank-index.js +26 -1
  91. package/dist/core/triwiki/triwiki-proof-bank.js +82 -2
  92. package/dist/core/version.js +1 -1
  93. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +2 -2
  94. package/dist/native/sks-menubar/Sources/ProvidersModelExposure.swift +178 -0
  95. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +10 -1
  96. package/dist/scripts/architecture-map-check.js +299 -0
  97. package/dist/scripts/lib/ensure-dist-fresh.js +48 -1
  98. package/dist/scripts/release-metadata-check.js +2 -0
  99. package/package.json +6 -3
  100. package/release-gates.v2.json +204 -0
  101. package/schemas/architecture-baseline.v1.schema.json +47 -0
  102. package/schemas/architecture-map-manifest.v1.schema.json +40 -0
  103. package/schemas/architecture-map-policy.v1.schema.json +147 -0
  104. package/schemas/architecture-review.v1.schema.json +46 -0
  105. package/schemas/mermaid-projection.v1.schema.json +47 -0
@@ -0,0 +1,321 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { ARCHITECTURE_FINDING_CODES, ARCHITECTURE_MAP_POLICY_FILE, ARCHITECTURE_MAP_POLICY_SCHEMA } from './contracts.js';
4
+ export const LAYER_EDGE_SEPARATOR = ' -> ';
5
+ const LAYER_ID = /^[a-z][a-z0-9_]*$/;
6
+ const EXCEPTION_ID = /^[a-z][a-z0-9-]*$/;
7
+ const MODULE_ID_SHAPE = /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/;
8
+ export const ARCHITECTURE_MAP_THRESHOLD_KEYS = [
9
+ 'max_diagram_nodes',
10
+ 'max_diagram_edges',
11
+ 'max_diagram_bytes',
12
+ 'max_module_fan_out',
13
+ 'max_module_fan_in'
14
+ ];
15
+ function fail(code, detail) {
16
+ throw new Error(`${code}: ${detail}`);
17
+ }
18
+ function asRecord(value, where) {
19
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
20
+ fail('architecture_map_policy_not_an_object', where);
21
+ }
22
+ return value;
23
+ }
24
+ function asArray(value, where) {
25
+ if (!Array.isArray(value))
26
+ fail('architecture_map_policy_not_an_array', where);
27
+ return value;
28
+ }
29
+ function asText(value, where) {
30
+ if (typeof value !== 'string' || value.trim().length === 0)
31
+ fail('architecture_map_policy_not_text', where);
32
+ return value;
33
+ }
34
+ function asCount(value, where) {
35
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) {
36
+ fail('architecture_map_policy_not_a_count', `${where} must be a positive safe integer`);
37
+ }
38
+ return value;
39
+ }
40
+ function asNonNeg(value, where) {
41
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
42
+ fail('architecture_map_policy_not_nonneg', where);
43
+ }
44
+ return value;
45
+ }
46
+ function assertDistinct(values, code, where) {
47
+ const seen = new Set();
48
+ for (const value of values) {
49
+ if (seen.has(value))
50
+ fail(code, `${where}: ${value}`);
51
+ seen.add(value);
52
+ }
53
+ }
54
+ export function parseLayerEdge(edge) {
55
+ const parts = edge.split(LAYER_EDGE_SEPARATOR);
56
+ const from = parts[0];
57
+ const to = parts[1];
58
+ if (parts.length !== 2 || !from || !to || !LAYER_ID.test(from) || !LAYER_ID.test(to)) {
59
+ fail('architecture_map_policy_malformed_edge', edge);
60
+ }
61
+ return { from, to };
62
+ }
63
+ function parseLayers(value) {
64
+ const rows = asArray(value, 'layers');
65
+ if (!rows.length)
66
+ fail('architecture_map_policy_no_layers', 'layers');
67
+ const layers = [];
68
+ for (const [index, row] of rows.entries()) {
69
+ const where = `layers[${index}]`;
70
+ const record = asRecord(row, where);
71
+ const id = asText(record.id, `${where}.id`);
72
+ if (!LAYER_ID.test(id))
73
+ fail('architecture_map_policy_bad_layer_id', id);
74
+ const modules = asArray(record.modules, `${where}.modules`).map((entry, i) => {
75
+ const moduleId = asText(entry, `${where}.modules[${i}]`);
76
+ if (!MODULE_ID_SHAPE.test(moduleId) || moduleId.split('/').includes('..')) {
77
+ fail('architecture_map_policy_bad_module_id', moduleId);
78
+ }
79
+ return moduleId;
80
+ });
81
+ if (!modules.length)
82
+ fail('architecture_map_policy_empty_layer', where);
83
+ assertDistinct(modules, 'architecture_map_policy_duplicate_module', where);
84
+ layers.push(Object.freeze({
85
+ id,
86
+ purpose: asText(record.purpose, `${where}.purpose`),
87
+ modules: Object.freeze(modules)
88
+ }));
89
+ }
90
+ assertDistinct(layers.map((layer) => layer.id), 'architecture_map_policy_duplicate_layer', 'layers');
91
+ assertDistinct(layers.flatMap((layer) => layer.modules), 'architecture_map_policy_module_in_two_layers', 'layers');
92
+ return Object.freeze(layers);
93
+ }
94
+ function parseBoundaries(value, layerIds) {
95
+ const record = asRecord(value, 'boundaries');
96
+ if (record.mode !== 'allow-list')
97
+ fail('architecture_map_policy_bad_boundary_mode', String(record.mode));
98
+ const rules = asArray(record.allow, 'boundaries.allow').map((row, index) => {
99
+ const where = `boundaries.allow[${index}]`;
100
+ const rule = asRecord(row, where);
101
+ const from = asText(rule.from, `${where}.from`);
102
+ if (!layerIds.has(from))
103
+ fail('architecture_map_policy_unknown_layer', from);
104
+ const to = asArray(rule.to, `${where}.to`).map((entry, i) => {
105
+ const target = asText(entry, `${where}.to[${i}]`);
106
+ if (!layerIds.has(target))
107
+ fail('architecture_map_policy_unknown_layer', target);
108
+ return target;
109
+ });
110
+ return Object.freeze({ from, to: Object.freeze(to) });
111
+ });
112
+ assertDistinct(rules.map((rule) => rule.from), 'architecture_map_policy_duplicate_boundary', 'boundaries.allow');
113
+ return Object.freeze({ mode: 'allow-list', allow: Object.freeze(rules) });
114
+ }
115
+ function parseThresholds(value) {
116
+ const record = asRecord(value, 'thresholds');
117
+ return Object.freeze({
118
+ max_diagram_nodes: asCount(record.max_diagram_nodes, 'thresholds.max_diagram_nodes'),
119
+ max_diagram_edges: asCount(record.max_diagram_edges, 'thresholds.max_diagram_edges'),
120
+ max_diagram_bytes: asCount(record.max_diagram_bytes, 'thresholds.max_diagram_bytes'),
121
+ max_module_fan_out: asCount(record.max_module_fan_out, 'thresholds.max_module_fan_out'),
122
+ max_module_fan_in: asCount(record.max_module_fan_in, 'thresholds.max_module_fan_in')
123
+ });
124
+ }
125
+ function parseProfileBudget(value, where) {
126
+ const record = asRecord(value, where);
127
+ return Object.freeze({
128
+ maxNodes: asCount(record.maxNodes, `${where}.maxNodes`),
129
+ maxEdges: asCount(record.maxEdges, `${where}.maxEdges`),
130
+ maxLabelChars: asCount(record.maxLabelChars, `${where}.maxLabelChars`),
131
+ tokenBudget: asNonNeg(record.tokenBudget, `${where}.tokenBudget`)
132
+ });
133
+ }
134
+ function parseProfiles(value) {
135
+ const record = asRecord(value, 'profiles');
136
+ const keys = ['global', 'planning', 'implementation', 'review', 'protected'];
137
+ const out = {};
138
+ for (const key of keys) {
139
+ if (!(key in record))
140
+ fail('architecture_map_policy_missing_profile', key);
141
+ out[key] = parseProfileBudget(record[key], `profiles.${key}`);
142
+ }
143
+ return Object.freeze(out);
144
+ }
145
+ function parseExceptions(value, layerIds, allowed) {
146
+ const record = asRecord(value, 'exceptions');
147
+ if (record.mode !== 'shrink-only')
148
+ fail('architecture_map_policy_bad_exception_mode', String(record.mode));
149
+ const claimed = new Set();
150
+ const entries = asArray(record.entries, 'exceptions.entries').map((row, index) => {
151
+ const where = `exceptions.entries[${index}]`;
152
+ const entry = asRecord(row, where);
153
+ const id = asText(entry.id, `${where}.id`);
154
+ if (!EXCEPTION_ID.test(id))
155
+ fail('architecture_map_policy_bad_exception_id', id);
156
+ const edges = asArray(entry.edges, `${where}.edges`).map((item, i) => {
157
+ const text = asText(item, `${where}.edges[${i}]`);
158
+ const edge = parseLayerEdge(text);
159
+ if (!layerIds.has(edge.from) || !layerIds.has(edge.to)) {
160
+ fail('architecture_map_policy_unknown_layer', text);
161
+ }
162
+ if (allowed.has(text))
163
+ fail('architecture_map_policy_exception_already_allowed', text);
164
+ if (claimed.has(text))
165
+ fail('architecture_map_policy_edge_claimed_twice', text);
166
+ claimed.add(text);
167
+ return text;
168
+ });
169
+ const baseline = asCount(entry.baseline_module_edges, `${where}.baseline_module_edges`);
170
+ const ceiling = asCount(entry.ceiling_module_edges, `${where}.ceiling_module_edges`);
171
+ if (ceiling > baseline)
172
+ fail('architecture_map_policy_ceiling_above_baseline', where);
173
+ return Object.freeze({
174
+ id,
175
+ reason: asText(entry.reason, `${where}.reason`),
176
+ edges: Object.freeze(edges),
177
+ baseline_module_edges: baseline,
178
+ ceiling_module_edges: ceiling
179
+ });
180
+ });
181
+ assertDistinct(entries.map((entry) => entry.id), 'architecture_map_policy_duplicate_exception', 'exceptions.entries');
182
+ return Object.freeze({ mode: 'shrink-only', entries: Object.freeze(entries) });
183
+ }
184
+ function parseBlockingCodes(value) {
185
+ if (value === undefined)
186
+ return Object.freeze([...ARCHITECTURE_FINDING_CODES]);
187
+ const known = new Set(ARCHITECTURE_FINDING_CODES);
188
+ const codes = asArray(value, 'blockingCodes').map((entry, i) => {
189
+ const code = asText(entry, `blockingCodes[${i}]`);
190
+ if (!known.has(code))
191
+ fail('architecture_map_policy_unknown_blocking_code', code);
192
+ return code;
193
+ });
194
+ return Object.freeze(codes);
195
+ }
196
+ function parseViewBudgets(value) {
197
+ if (value === undefined)
198
+ return Object.freeze({});
199
+ const record = asRecord(value, 'viewBudgets');
200
+ const out = {};
201
+ for (const [viewId, budget] of Object.entries(record)) {
202
+ const row = asRecord(budget, `viewBudgets.${viewId}`);
203
+ out[viewId] = Object.freeze({
204
+ maxNodes: asCount(row.maxNodes, `viewBudgets.${viewId}.maxNodes`),
205
+ maxEdges: asCount(row.maxEdges, `viewBudgets.${viewId}.maxEdges`)
206
+ });
207
+ }
208
+ return Object.freeze(out);
209
+ }
210
+ function parseAccuracyFloors(value) {
211
+ const defaults = {
212
+ changedPathAccounting: 1,
213
+ projectionAccounting: 1,
214
+ protectedRelationRecall: 1,
215
+ eligibleExtractionSuccess: 0.995,
216
+ deterministicHashRuns: 3
217
+ };
218
+ if (value === undefined)
219
+ return Object.freeze(defaults);
220
+ const record = asRecord(value, 'accuracyFloors');
221
+ return Object.freeze({
222
+ changedPathAccounting: asNonNeg(record.changedPathAccounting ?? 1, 'accuracyFloors.changedPathAccounting'),
223
+ projectionAccounting: asNonNeg(record.projectionAccounting ?? 1, 'accuracyFloors.projectionAccounting'),
224
+ protectedRelationRecall: asNonNeg(record.protectedRelationRecall ?? 1, 'accuracyFloors.protectedRelationRecall'),
225
+ eligibleExtractionSuccess: asNonNeg(record.eligibleExtractionSuccess ?? 0.995, 'accuracyFloors.eligibleExtractionSuccess'),
226
+ deterministicHashRuns: asCount(record.deterministicHashRuns ?? 3, 'accuracyFloors.deterministicHashRuns')
227
+ });
228
+ }
229
+ export function parseArchitectureMapPolicy(value) {
230
+ const record = asRecord(value, 'policy');
231
+ if (record.schema !== ARCHITECTURE_MAP_POLICY_SCHEMA) {
232
+ fail('architecture_map_policy_schema_mismatch', String(record.schema));
233
+ }
234
+ const layers = parseLayers(record.layers);
235
+ const layerIds = new Set(layers.map((layer) => layer.id));
236
+ const boundaries = parseBoundaries(record.boundaries, layerIds);
237
+ const allowed = new Set(boundaries.allow.flatMap((rule) => rule.to.map((target) => `${rule.from}${LAYER_EDGE_SEPARATOR}${target}`)));
238
+ return Object.freeze({
239
+ schema: ARCHITECTURE_MAP_POLICY_SCHEMA,
240
+ version: typeof record.version === 'number' ? record.version : 1,
241
+ module_id_source: asText(record.module_id_source, 'module_id_source'),
242
+ layers,
243
+ boundaries,
244
+ thresholds: parseThresholds(record.thresholds),
245
+ profiles: parseProfiles(record.profiles),
246
+ absoluteTokenCeiling: asNonNeg(record.absoluteTokenCeiling ?? 3200, 'absoluteTokenCeiling'),
247
+ globalAtlasMaxBytes: asCount(record.globalAtlasMaxBytes ?? 1572864, 'globalAtlasMaxBytes'),
248
+ missionArtifactsMaxBytes: asCount(record.missionArtifactsMaxBytes ?? 358400, 'missionArtifactsMaxBytes'),
249
+ accuracyFloors: parseAccuracyFloors(record.accuracyFloors),
250
+ viewBudgets: parseViewBudgets(record.viewBudgets),
251
+ blockingCodes: parseBlockingCodes(record.blockingCodes),
252
+ exceptions: parseExceptions(record.exceptions, layerIds, allowed)
253
+ });
254
+ }
255
+ export function loadArchitectureMapPolicy(root) {
256
+ if (typeof root !== 'string' || !root)
257
+ fail('architecture_map_policy_root_missing', 'root');
258
+ const file = path.join(root, ...ARCHITECTURE_MAP_POLICY_FILE.split('/'));
259
+ let text;
260
+ try {
261
+ text = fs.readFileSync(file, 'utf8');
262
+ }
263
+ catch (error) {
264
+ fail('architecture_map_policy_unreadable', `${file}: ${error instanceof Error ? error.message : String(error)}`);
265
+ }
266
+ let json;
267
+ try {
268
+ json = JSON.parse(text);
269
+ }
270
+ catch (error) {
271
+ fail('architecture_map_policy_invalid_json', `${file}: ${error instanceof Error ? error.message : String(error)}`);
272
+ }
273
+ return parseArchitectureMapPolicy(json);
274
+ }
275
+ export function moduleLayerMap(policy) {
276
+ const map = new Map();
277
+ for (const layer of policy.layers) {
278
+ for (const moduleId of layer.modules)
279
+ map.set(moduleId, layer.id);
280
+ }
281
+ return map;
282
+ }
283
+ export function allowedLayerEdges(policy) {
284
+ const allowed = new Set();
285
+ for (const rule of policy.boundaries.allow) {
286
+ for (const target of rule.to)
287
+ allowed.add(`${rule.from}${LAYER_EDGE_SEPARATOR}${target}`);
288
+ }
289
+ for (const entry of policy.exceptions.entries) {
290
+ for (const edge of entry.edges)
291
+ allowed.add(edge);
292
+ }
293
+ return allowed;
294
+ }
295
+ export function severityForCode(policy, code) {
296
+ if (policy.blockingCodes.includes(code))
297
+ return 'blocking';
298
+ if (code === 'insufficient_graph')
299
+ return 'blocking';
300
+ return 'warning';
301
+ }
302
+ export function layerForModule(policy, moduleDir) {
303
+ const exact = moduleLayerMap(policy).get(moduleDir);
304
+ if (exact)
305
+ return exact;
306
+ let best = null;
307
+ for (const layer of policy.layers) {
308
+ for (const moduleId of layer.modules) {
309
+ if (moduleDir === moduleId || moduleDir.startsWith(`${moduleId}/`)) {
310
+ if (!best || moduleId.length > best.length)
311
+ best = { id: layer.id, length: moduleId.length };
312
+ }
313
+ }
314
+ }
315
+ return best?.id ?? null;
316
+ }
317
+ export function isAllowedLayerEdge(policy, fromLayer, toLayer) {
318
+ if (fromLayer === toLayer)
319
+ return true;
320
+ return allowedLayerEdges(policy).has(`${fromLayer}${LAYER_EDGE_SEPARATOR}${toLayer}`);
321
+ }
@@ -0,0 +1,100 @@
1
+ import { byCodePoint } from './contracts.js';
2
+ export const SELECTION_RULE_VERSION = 'architecture-slice.v1';
3
+ function ownerIndex(nodes, edges) {
4
+ const modules = new Set(nodes.filter((node) => node.kind === 'module').map((node) => node.id));
5
+ const owner = new Map();
6
+ for (const edge of edges) {
7
+ if (edge.type !== 'contains' || !modules.has(edge.from))
8
+ continue;
9
+ if (!owner.has(edge.to))
10
+ owner.set(edge.to, edge.from);
11
+ }
12
+ let grew = true;
13
+ while (grew) {
14
+ grew = false;
15
+ for (const edge of edges) {
16
+ if (edge.type !== 'contains' && edge.type !== 'defines')
17
+ continue;
18
+ const parent = modules.has(edge.from) ? edge.from : owner.get(edge.from);
19
+ if (!parent || owner.has(edge.to))
20
+ continue;
21
+ owner.set(edge.to, parent);
22
+ grew = true;
23
+ }
24
+ }
25
+ return owner;
26
+ }
27
+ export function buildArchitectureScope(input) {
28
+ return Object.freeze({
29
+ profile: input.profile,
30
+ seedNodeIds: Object.freeze([...(input.seedNodeIds ?? [])].sort(byCodePoint)),
31
+ seedPaths: Object.freeze([...(input.seedPaths ?? [])].sort(byCodePoint)),
32
+ selectionRuleVersion: SELECTION_RULE_VERSION
33
+ });
34
+ }
35
+ export function sliceArchitectureGraph(snapshot, policy, scope) {
36
+ const budget = policy.profiles[scope.profile] ?? policy.profiles.global;
37
+ const nodesById = new Map(snapshot.nodes.map((node) => [node.id, node]));
38
+ const owner = ownerIndex(snapshot.nodes, snapshot.edges);
39
+ const selected = new Set();
40
+ const protectedIds = new Set();
41
+ for (const id of scope.seedNodeIds) {
42
+ if (nodesById.has(id)) {
43
+ selected.add(id);
44
+ protectedIds.add(id);
45
+ }
46
+ }
47
+ for (const path of scope.seedPaths) {
48
+ for (const node of snapshot.nodes) {
49
+ if (node.path === path || (typeof node.metadata.dir === 'string' && node.metadata.dir === path)) {
50
+ selected.add(node.id);
51
+ protectedIds.add(node.id);
52
+ const moduleId = node.kind === 'module' ? node.id : owner.get(node.id);
53
+ if (moduleId) {
54
+ selected.add(moduleId);
55
+ protectedIds.add(moduleId);
56
+ }
57
+ }
58
+ }
59
+ }
60
+ const expandTypes = new Set(['contains', 'defines', 'imports', 'reexports', 'references']);
61
+ for (const edge of snapshot.edges) {
62
+ if (!expandTypes.has(edge.type))
63
+ continue;
64
+ if (selected.has(edge.from) || selected.has(edge.to)) {
65
+ selected.add(edge.from);
66
+ selected.add(edge.to);
67
+ }
68
+ }
69
+ const ranked = [...snapshot.nodes]
70
+ .filter((node) => node.kind === 'module' || node.risk === 'high' || node.risk === 'protected')
71
+ .sort((a, b) => {
72
+ const riskRank = (risk) => risk === 'protected' ? 3 : risk === 'high' ? 2 : risk === 'medium' ? 1 : 0;
73
+ return riskRank(b.risk) - riskRank(a.risk) || byCodePoint(a.id, b.id);
74
+ });
75
+ for (const node of ranked) {
76
+ if (selected.size >= budget.maxNodes)
77
+ break;
78
+ selected.add(node.id);
79
+ if (node.risk === 'protected')
80
+ protectedIds.add(node.id);
81
+ }
82
+ if (selected.size === 0) {
83
+ for (const node of snapshot.nodes.filter((n) => n.kind === 'module').sort((a, b) => byCodePoint(a.id, b.id))) {
84
+ if (selected.size >= budget.maxNodes)
85
+ break;
86
+ selected.add(node.id);
87
+ }
88
+ }
89
+ const edgeIds = snapshot.edges
90
+ .filter((edge) => selected.has(edge.from) && selected.has(edge.to))
91
+ .map((edge) => edge.id)
92
+ .sort(byCodePoint)
93
+ .slice(0, budget.maxEdges);
94
+ return Object.freeze({
95
+ scope,
96
+ nodeIds: Object.freeze([...selected].sort(byCodePoint)),
97
+ edgeIds: Object.freeze(edgeIds),
98
+ protectedNodeIds: Object.freeze([...protectedIds].sort(byCodePoint))
99
+ });
100
+ }
@@ -0,0 +1,59 @@
1
+ import { byCodePoint } from './contracts.js';
2
+ import { makeFinding } from './finding-factory.js';
3
+ function severity(policy, code) {
4
+ return policy.blockingCodes.includes(code) ? 'blocking' : 'warning';
5
+ }
6
+ export function analyzeSsotCollisions(input) {
7
+ if (!input.inventory.length) {
8
+ if (!input.requireInventory)
9
+ return [];
10
+ return [
11
+ makeFinding({
12
+ code: 'insufficient_graph',
13
+ severity: severity(input.policy, 'insufficient_graph'),
14
+ subjectIds: ['ssot_inventory'],
15
+ evidenceIds: ['ssot_inventory:empty'],
16
+ ruleId: 'ssot-inventory-required',
17
+ message: 'SSOT inventory is empty; collision analysis cannot pass closed'
18
+ })
19
+ ];
20
+ }
21
+ const byPath = new Map();
22
+ for (const domain of input.inventory) {
23
+ for (const path of domain.canonicalSources) {
24
+ const owners = byPath.get(path) ?? [];
25
+ owners.push(domain.id);
26
+ byPath.set(path, owners);
27
+ }
28
+ }
29
+ const findings = [];
30
+ for (const [path, owners] of [...byPath.entries()].sort(([left], [right]) => byCodePoint(left, right))) {
31
+ const unique = [...new Set(owners)].sort(byCodePoint);
32
+ if (unique.length > 1) {
33
+ findings.push(makeFinding({
34
+ code: 'ssot_collision',
35
+ severity: severity(input.policy, 'ssot_collision'),
36
+ subjectIds: unique,
37
+ evidenceIds: [path],
38
+ ruleId: 'ssot-path-collision',
39
+ message: `Canonical path ${path} claimed by multiple SSOT domains: ${unique.join(', ')}`
40
+ }));
41
+ }
42
+ }
43
+ for (const domain of input.inventory) {
44
+ const writers = new Set(domain.allowedWriters);
45
+ for (const reader of domain.allowedReaders) {
46
+ if (writers.has(reader) && domain.canonicalSources.length === 0) {
47
+ findings.push(makeFinding({
48
+ code: 'authority_bypass',
49
+ severity: severity(input.policy, 'authority_bypass'),
50
+ subjectIds: [domain.id, reader],
51
+ evidenceIds: [domain.id],
52
+ ruleId: 'ssot-authority-bypass',
53
+ message: `SSOT domain ${domain.id} has reader/writer ${reader} without canonical sources`
54
+ }));
55
+ }
56
+ }
57
+ }
58
+ return findings.sort((left, right) => byCodePoint(left.id, right.id));
59
+ }
@@ -0,0 +1,106 @@
1
+ import { byCodePoint } from './contracts.js';
2
+ import { hashCanonical } from './fingerprint.js';
3
+ const TOPOLOGY_NODE_KINDS = new Set([
4
+ 'command',
5
+ 'route',
6
+ 'pipeline',
7
+ 'gate',
8
+ 'schema',
9
+ 'config'
10
+ ]);
11
+ const TOPOLOGY_EDGE_TYPES = new Set([
12
+ 'routes_to',
13
+ 'depends_on',
14
+ 'owns',
15
+ 'gated_by',
16
+ 'affected_by',
17
+ 'derived_from'
18
+ ]);
19
+ export function emptyTopologyOverlay() {
20
+ return Object.freeze({
21
+ nodes: Object.freeze([]),
22
+ edges: Object.freeze([]),
23
+ sourceManifestHashes: Object.freeze({}),
24
+ extractionErrors: Object.freeze([])
25
+ });
26
+ }
27
+ export function buildTopologyOverlayFromSnapshot(snapshot) {
28
+ const nodes = snapshot.nodes
29
+ .filter((node) => TOPOLOGY_NODE_KINDS.has(node.kind))
30
+ .slice()
31
+ .sort((a, b) => byCodePoint(a.id, b.id));
32
+ const nodeIds = new Set(nodes.map((node) => node.id));
33
+ const edges = snapshot.edges
34
+ .filter((edge) => TOPOLOGY_EDGE_TYPES.has(edge.type) &&
35
+ (nodeIds.has(edge.from) || nodeIds.has(edge.to) || TOPOLOGY_EDGE_TYPES.has(edge.type)))
36
+ .slice()
37
+ .sort((a, b) => byCodePoint(a.id, b.id));
38
+ return Object.freeze({
39
+ nodes: Object.freeze(nodes),
40
+ edges: Object.freeze(edges),
41
+ sourceManifestHashes: Object.freeze({
42
+ snapshot: snapshot.snapshotHash
43
+ }),
44
+ extractionErrors: Object.freeze([])
45
+ });
46
+ }
47
+ export function mergeTopologyOntoGraph(graph, overlay) {
48
+ const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
49
+ const issues = [...overlay.extractionErrors];
50
+ for (const node of overlay.nodes) {
51
+ const existing = nodesById.get(node.id);
52
+ if (!existing) {
53
+ nodesById.set(node.id, node);
54
+ continue;
55
+ }
56
+ if (existing.kind !== node.kind) {
57
+ const issue = typeof existing.path === 'string'
58
+ ? {
59
+ code: 'topology_node_kind_conflict',
60
+ message: `overlay kind ${node.kind} ignored for ${node.id}; code graph has ${existing.kind}`,
61
+ path: existing.path
62
+ }
63
+ : {
64
+ code: 'topology_node_kind_conflict',
65
+ message: `overlay kind ${node.kind} ignored for ${node.id}; code graph has ${existing.kind}`
66
+ };
67
+ issues.push(issue);
68
+ }
69
+ }
70
+ const edgeIds = new Set(graph.edges.map((edge) => edge.id));
71
+ const edges = [...graph.edges];
72
+ for (const edge of overlay.edges) {
73
+ if (edgeIds.has(edge.id))
74
+ continue;
75
+ if (!nodesById.has(edge.from) || !nodesById.has(edge.to)) {
76
+ issues.push({
77
+ code: 'topology_dangling_edge',
78
+ message: `overlay edge ${edge.id} skipped; missing endpoint`,
79
+ path: edge.provenance.path
80
+ });
81
+ continue;
82
+ }
83
+ edgeIds.add(edge.id);
84
+ edges.push(edge);
85
+ }
86
+ return {
87
+ nodes: [...nodesById.values()].sort((a, b) => byCodePoint(a.id, b.id)),
88
+ edges: edges.sort((a, b) => byCodePoint(a.id, b.id)),
89
+ issues
90
+ };
91
+ }
92
+ export function mergeGraphWithTopology(graph, overlay) {
93
+ const merged = mergeTopologyOntoGraph(graph, overlay);
94
+ return {
95
+ nodes: Object.freeze(merged.nodes),
96
+ edges: Object.freeze(merged.edges)
97
+ };
98
+ }
99
+ export function topologyOverlayHash(overlay) {
100
+ return hashCanonical({
101
+ nodes: overlay.nodes.map((node) => node.id),
102
+ edges: overlay.edges.map((edge) => edge.id),
103
+ sourceManifestHashes: overlay.sourceManifestHashes,
104
+ extractionErrors: overlay.extractionErrors
105
+ });
106
+ }