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.
- package/README.md +1 -1
- package/config/architecture-map-policy.v1.json +168 -0
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/commands/bridge.js +19 -1
- package/dist/commands/doctor-profile.js +1 -1
- package/dist/commands/doctor.js +33 -1
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/align/align-route.js +8 -2
- package/dist/core/align/code-navigation-align.js +88 -32
- package/dist/core/architecture-map-pipeline.js +172 -0
- package/dist/core/architecture-map-review.js +180 -0
- package/dist/core/codex-app/codex-app-fast-ui-repair.js +2 -1
- package/dist/core/codex-app/menubar/constants.js +1 -0
- package/dist/core/codex-lb/codex-lb-env.js +25 -3
- package/dist/core/codex-lb/combined-catalog/builder.js +7 -2
- package/dist/core/codex-lb/combined-catalog/model-selection.js +71 -0
- package/dist/core/codex-lb/combined-catalog/normalize.js +20 -3
- package/dist/core/codex-lb/desktop-controller-v3/catalog.js +75 -4
- package/dist/core/codex-lb/desktop-controller-v3.js +5 -1
- package/dist/core/commands/triwiki-atlas-command.js +372 -0
- package/dist/core/commands/triwiki-command.js +12 -1
- package/dist/core/fsx.js +14 -0
- package/dist/core/git-hygiene/git-policy.js +4 -0
- package/dist/core/git-hygiene/gitignore-writer.js +2 -0
- package/dist/core/gx-renderer.js +2 -1
- package/dist/core/managed-paths.js +1 -0
- package/dist/core/perf/release-latency-slo.js +57 -11
- package/dist/core/pipeline-internals/pipeline-stage-builder.js +181 -0
- package/dist/core/pipeline-internals/runtime-core.js +13 -172
- package/dist/core/pipeline-internals/runtime-gates.js +12 -1
- package/dist/core/release/content-digest-cache.js +70 -0
- package/dist/core/release/gate-manifest.js +9 -0
- package/dist/core/release/main-push-guard.js +4 -3
- package/dist/core/release/package-dist-snapshot.js +18 -2
- package/dist/core/release/physical-release-gates.js +26 -3
- package/dist/core/release/release-authorization-snapshot.js +4 -0
- package/dist/core/release/release-gate-cache-v2.js +27 -3
- package/dist/core/release/release-gate-contract.js +2 -0
- package/dist/core/release/release-gate-dag.js +3 -0
- package/dist/core/release/release-inspection-memo.js +69 -0
- package/dist/core/release/release-pack-content-scanner.js +5 -0
- package/dist/core/release/release-pack-receipt.js +28 -18
- package/dist/core/release/release-pack-tarball.js +10 -0
- package/dist/core/release/release-upgrade-baseline.js +4 -0
- package/dist/core/routes.js +1 -1
- package/dist/core/safety/ssot-authority-inventory.js +114 -0
- package/dist/core/safety/ssot-guard.js +2 -44
- package/dist/core/triwiki/context-graph/architecture/baseline.js +81 -0
- package/dist/core/triwiki/context-graph/architecture/capsule.js +34 -0
- package/dist/core/triwiki/context-graph/architecture/contracts.js +67 -0
- package/dist/core/triwiki/context-graph/architecture/delta.js +149 -0
- package/dist/core/triwiki/context-graph/architecture/finding-factory.js +28 -0
- package/dist/core/triwiki/context-graph/architecture/findings.js +173 -0
- package/dist/core/triwiki/context-graph/architecture/fingerprint.js +75 -0
- package/dist/core/triwiki/context-graph/architecture/index.js +15 -0
- package/dist/core/triwiki/context-graph/architecture/input-bundle.js +66 -0
- package/dist/core/triwiki/context-graph/architecture/metrics.js +137 -0
- package/dist/core/triwiki/context-graph/architecture/policy.js +321 -0
- package/dist/core/triwiki/context-graph/architecture/slice.js +100 -0
- package/dist/core/triwiki/context-graph/architecture/ssot-analysis.js +59 -0
- package/dist/core/triwiki/context-graph/architecture/topology-overlay.js +106 -0
- package/dist/core/triwiki/context-graph/architecture/validation.js +145 -0
- package/dist/core/triwiki/context-graph/architecture/voxel-context.js +39 -0
- package/dist/core/triwiki/context-graph/extractors/index.js +4 -0
- package/dist/core/triwiki/context-graph/projections/index.js +2 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/architecture-delta.js +68 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/ast.js +49 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/change-impact.js +43 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/contracts.js +5 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/escape.js +45 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/ids.js +73 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/index.js +113 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/manifest.js +37 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/module-dependency.js +21 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/ownership-workstream.js +21 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/project-topology.js +13 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/public-surface.js +17 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/reduce.js +44 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/risk-domains.js +23 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/runtime-control.js +20 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/selection-ledger.js +94 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/serializer.js +81 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/ssot-provenance.js +17 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/verification-coverage.js +17 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/view-builder-common.js +1 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/view-builder.js +173 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/view-specs.js +33 -0
- package/dist/core/triwiki/context-graph/store/architecture-map-store.js +71 -0
- package/dist/core/triwiki/triwiki-proof-bank-index.js +26 -1
- package/dist/core/triwiki/triwiki-proof-bank.js +82 -2
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/OverviewViewController.swift +2 -2
- package/dist/native/sks-menubar/Sources/ProvidersModelExposure.swift +178 -0
- package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +10 -1
- package/dist/scripts/architecture-map-check.js +299 -0
- package/dist/scripts/lib/ensure-dist-fresh.js +48 -1
- package/dist/scripts/release-metadata-check.js +2 -0
- package/package.json +6 -3
- package/release-gates.v2.json +204 -0
- package/schemas/architecture-baseline.v1.schema.json +47 -0
- package/schemas/architecture-map-manifest.v1.schema.json +40 -0
- package/schemas/architecture-map-policy.v1.schema.json +147 -0
- package/schemas/architecture-review.v1.schema.json +46 -0
- package/schemas/mermaid-projection.v1.schema.json +47 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { layerForModule } from '../../architecture/policy.js';
|
|
2
|
+
import { projectFilteredView } from './view-builder.js';
|
|
3
|
+
function modulePath(nodeId, path) {
|
|
4
|
+
if (path)
|
|
5
|
+
return path;
|
|
6
|
+
return nodeId.startsWith('module:') ? nodeId.slice('module:'.length) : nodeId;
|
|
7
|
+
}
|
|
8
|
+
export function buildOwnershipWorkstreamView(input) {
|
|
9
|
+
return projectFilteredView({
|
|
10
|
+
viewId: 'ownership-workstream',
|
|
11
|
+
snapshot: input.snapshot,
|
|
12
|
+
policy: input.policy,
|
|
13
|
+
...(input.request ? { request: input.request } : {}),
|
|
14
|
+
filter: {
|
|
15
|
+
kinds: ['module'],
|
|
16
|
+
edgeTypes: ['owns', 'depends_on', 'imports']
|
|
17
|
+
},
|
|
18
|
+
groupByLayer: true,
|
|
19
|
+
layerOf: (node, policy) => layerForModule(policy, modulePath(node.id, node.path))
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { projectFilteredView } from './view-builder.js';
|
|
2
|
+
export function buildProjectTopologyView(input) {
|
|
3
|
+
return projectFilteredView({
|
|
4
|
+
viewId: 'project-topology',
|
|
5
|
+
snapshot: input.snapshot,
|
|
6
|
+
policy: input.policy,
|
|
7
|
+
...(input.request ? { request: input.request } : {}),
|
|
8
|
+
filter: {
|
|
9
|
+
kinds: ['module', 'file', 'command', 'route'],
|
|
10
|
+
edgeTypes: ['contains', 'depends_on', 'routes_to']
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { projectFilteredView } from './view-builder.js';
|
|
2
|
+
export function buildPublicSurfaceView(input) {
|
|
3
|
+
const edgeTypes = ['reexports', 'owns', 'routes_to'];
|
|
4
|
+
if (input.snapshot.edges.some((edge) => edge.type === 'exports')) {
|
|
5
|
+
edgeTypes.push('exports');
|
|
6
|
+
}
|
|
7
|
+
return projectFilteredView({
|
|
8
|
+
viewId: 'public-surface',
|
|
9
|
+
snapshot: input.snapshot,
|
|
10
|
+
policy: input.policy,
|
|
11
|
+
...(input.request ? { request: input.request } : {}),
|
|
12
|
+
filter: {
|
|
13
|
+
kinds: ['command', 'route', 'schema', 'config'],
|
|
14
|
+
edgeTypes
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { byCodePoint } from '../../architecture/contracts.js';
|
|
2
|
+
import { SelectionLedger } from './selection-ledger.js';
|
|
3
|
+
export function reduceForBudget(input) {
|
|
4
|
+
const ledger = new SelectionLedger();
|
|
5
|
+
const protectedIds = input.protectedNodeIds ?? new Set();
|
|
6
|
+
const sortedNodes = [...input.nodes].sort((left, right) => byCodePoint(left.id, right.id));
|
|
7
|
+
for (const node of sortedNodes)
|
|
8
|
+
ledger.selectNode(node.id);
|
|
9
|
+
const admitted = [];
|
|
10
|
+
for (const node of sortedNodes) {
|
|
11
|
+
if (protectedIds.has(node.id)) {
|
|
12
|
+
admitted.push(node);
|
|
13
|
+
ledger.emitNode(node.id);
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (admitted.length >= input.budget.maxNodes) {
|
|
17
|
+
ledger.omitNode(node.id, 'budget_exhausted_after_protected_set');
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
admitted.push(node);
|
|
21
|
+
ledger.emitNode(node.id);
|
|
22
|
+
}
|
|
23
|
+
const admittedIds = new Set(admitted.map((node) => node.id));
|
|
24
|
+
const sortedEdges = [...input.edges].sort((left, right) => byCodePoint(left.id, right.id));
|
|
25
|
+
const admittedEdges = [];
|
|
26
|
+
for (const edge of sortedEdges) {
|
|
27
|
+
ledger.selectEdge(edge.id);
|
|
28
|
+
if (!admittedIds.has(edge.from) || !admittedIds.has(edge.to)) {
|
|
29
|
+
ledger.omitEdge(edge.id, 'outside_profile_scope', 'endpoint omitted');
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (admittedEdges.length >= input.budget.maxEdges) {
|
|
33
|
+
ledger.omitEdge(edge.id, 'budget_exhausted_after_protected_set');
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
admittedEdges.push(edge);
|
|
37
|
+
ledger.emitEdge(edge.id);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
nodes: Object.freeze(admitted),
|
|
41
|
+
edges: Object.freeze(admittedEdges),
|
|
42
|
+
ledger
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { projectFilteredView } from './view-builder.js';
|
|
2
|
+
export function buildRiskDomainsView(input) {
|
|
3
|
+
const highRiskIds = input.snapshot.nodes
|
|
4
|
+
.filter((node) => node.kind === 'module' && (node.risk === 'high' || node.risk === 'protected'))
|
|
5
|
+
.map((node) => node.id);
|
|
6
|
+
const findingSubjectIds = [...(input.request?.findingSubjectIds ?? []), ...highRiskIds];
|
|
7
|
+
const request = {
|
|
8
|
+
...(input.request?.profile ? { profile: input.request.profile } : {}),
|
|
9
|
+
...(input.request?.seedNodeIds ? { seedNodeIds: input.request.seedNodeIds } : {}),
|
|
10
|
+
...(input.request?.seedPaths ? { seedPaths: input.request.seedPaths } : {}),
|
|
11
|
+
...(findingSubjectIds.length ? { findingSubjectIds } : {})
|
|
12
|
+
};
|
|
13
|
+
return projectFilteredView({
|
|
14
|
+
viewId: 'risk-domains',
|
|
15
|
+
snapshot: input.snapshot,
|
|
16
|
+
policy: input.policy,
|
|
17
|
+
request,
|
|
18
|
+
filter: {
|
|
19
|
+
kinds: ['risk_domain', 'module'],
|
|
20
|
+
edgeTypes: ['affected_by', 'owns']
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { emptyProjection, projectFilteredView } from './view-builder.js';
|
|
2
|
+
export function buildRuntimeControlView(input) {
|
|
3
|
+
const hasControl = input.snapshot.nodes.some((node) => node.kind === 'route' ||
|
|
4
|
+
node.kind === 'pipeline' ||
|
|
5
|
+
node.kind === 'gate' ||
|
|
6
|
+
node.kind === 'command');
|
|
7
|
+
if (!hasControl) {
|
|
8
|
+
return emptyProjection('runtime-control', 'Runtime control', 'TD', 'insufficient_graph: no route/pipeline/gate/command nodes');
|
|
9
|
+
}
|
|
10
|
+
return projectFilteredView({
|
|
11
|
+
viewId: 'runtime-control',
|
|
12
|
+
snapshot: input.snapshot,
|
|
13
|
+
policy: input.policy,
|
|
14
|
+
...(input.request ? { request: input.request } : {}),
|
|
15
|
+
filter: {
|
|
16
|
+
kinds: ['route', 'pipeline', 'gate', 'command'],
|
|
17
|
+
edgeTypes: ['routes_to', 'gated_by', 'owns']
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { byCodePoint } from '../../architecture/contracts.js';
|
|
2
|
+
export class SelectionLedger {
|
|
3
|
+
selectedNodes = new Set();
|
|
4
|
+
emittedNodes = new Set();
|
|
5
|
+
omittedNodes = [];
|
|
6
|
+
aggregatedNodes = new Map();
|
|
7
|
+
selectedEdges = new Set();
|
|
8
|
+
emittedEdges = new Set();
|
|
9
|
+
omittedEdges = [];
|
|
10
|
+
aggregatedEdges = new Map();
|
|
11
|
+
selectNode(id) {
|
|
12
|
+
this.selectedNodes.add(id);
|
|
13
|
+
}
|
|
14
|
+
emitNode(id) {
|
|
15
|
+
this.selectedNodes.add(id);
|
|
16
|
+
this.emittedNodes.add(id);
|
|
17
|
+
}
|
|
18
|
+
omitNode(id, reason, detail) {
|
|
19
|
+
this.selectedNodes.add(id);
|
|
20
|
+
const omission = detail === undefined
|
|
21
|
+
? { id, reason }
|
|
22
|
+
: { id, reason, detail };
|
|
23
|
+
this.omittedNodes.push(Object.freeze(omission));
|
|
24
|
+
}
|
|
25
|
+
aggregateNodes(aggregateId, memberIds) {
|
|
26
|
+
this.selectedNodes.add(aggregateId);
|
|
27
|
+
for (const id of memberIds)
|
|
28
|
+
this.selectedNodes.add(id);
|
|
29
|
+
this.emittedNodes.add(aggregateId);
|
|
30
|
+
this.aggregatedNodes.set(aggregateId, [...memberIds].sort(byCodePoint));
|
|
31
|
+
}
|
|
32
|
+
selectEdge(id) {
|
|
33
|
+
this.selectedEdges.add(id);
|
|
34
|
+
}
|
|
35
|
+
emitEdge(id) {
|
|
36
|
+
this.selectedEdges.add(id);
|
|
37
|
+
this.emittedEdges.add(id);
|
|
38
|
+
}
|
|
39
|
+
omitEdge(id, reason, detail) {
|
|
40
|
+
this.selectedEdges.add(id);
|
|
41
|
+
const omission = detail === undefined
|
|
42
|
+
? { id, reason }
|
|
43
|
+
: { id, reason, detail };
|
|
44
|
+
this.omittedEdges.push(Object.freeze(omission));
|
|
45
|
+
}
|
|
46
|
+
aggregateEdges(aggregateId, memberIds) {
|
|
47
|
+
this.selectedEdges.add(aggregateId);
|
|
48
|
+
for (const id of memberIds)
|
|
49
|
+
this.selectedEdges.add(id);
|
|
50
|
+
this.emittedEdges.add(aggregateId);
|
|
51
|
+
this.aggregatedEdges.set(aggregateId, [...memberIds].sort(byCodePoint));
|
|
52
|
+
}
|
|
53
|
+
toAccounting() {
|
|
54
|
+
return Object.freeze({
|
|
55
|
+
selectedNodeIds: Object.freeze([...this.selectedNodes].sort(byCodePoint)),
|
|
56
|
+
emittedNodeIds: Object.freeze([...this.emittedNodes].sort(byCodePoint)),
|
|
57
|
+
aggregatedNodeMembers: Object.freeze(Object.fromEntries([...this.aggregatedNodes.entries()]
|
|
58
|
+
.sort(([left], [right]) => byCodePoint(left, right))
|
|
59
|
+
.map(([key, value]) => [key, Object.freeze(value)]))),
|
|
60
|
+
omittedNodes: Object.freeze([...this.omittedNodes].sort((left, right) => byCodePoint(left.id, right.id))),
|
|
61
|
+
selectedEdgeIds: Object.freeze([...this.selectedEdges].sort(byCodePoint)),
|
|
62
|
+
emittedEdgeIds: Object.freeze([...this.emittedEdges].sort(byCodePoint)),
|
|
63
|
+
aggregatedEdgeMembers: Object.freeze(Object.fromEntries([...this.aggregatedEdges.entries()]
|
|
64
|
+
.sort(([left], [right]) => byCodePoint(left, right))
|
|
65
|
+
.map(([key, value]) => [key, Object.freeze(value)]))),
|
|
66
|
+
omittedEdges: Object.freeze([...this.omittedEdges].sort((left, right) => byCodePoint(left.id, right.id)))
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
assertBalanced() {
|
|
70
|
+
const accounting = this.toAccounting();
|
|
71
|
+
const nodeCovered = new Set([
|
|
72
|
+
...accounting.emittedNodeIds,
|
|
73
|
+
...Object.values(accounting.aggregatedNodeMembers).flat(),
|
|
74
|
+
...accounting.omittedNodes.map((entry) => entry.id)
|
|
75
|
+
]);
|
|
76
|
+
for (const id of accounting.selectedNodeIds) {
|
|
77
|
+
if (!nodeCovered.has(id) && !accounting.emittedNodeIds.includes(id)) {
|
|
78
|
+
if (!Object.prototype.hasOwnProperty.call(accounting.aggregatedNodeMembers, id)) {
|
|
79
|
+
throw new Error(`projection_node_unaccounted: ${id}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const edgeCovered = new Set([
|
|
84
|
+
...accounting.emittedEdgeIds,
|
|
85
|
+
...Object.values(accounting.aggregatedEdgeMembers).flat(),
|
|
86
|
+
...accounting.omittedEdges.map((entry) => entry.id)
|
|
87
|
+
]);
|
|
88
|
+
for (const id of accounting.selectedEdgeIds) {
|
|
89
|
+
if (!edgeCovered.has(id) && !Object.prototype.hasOwnProperty.call(accounting.aggregatedEdgeMembers, id)) {
|
|
90
|
+
throw new Error(`projection_edge_unaccounted: ${id}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { byCodePoint } from '../../architecture/contracts.js';
|
|
2
|
+
import { hashCanonical } from '../../architecture/fingerprint.js';
|
|
3
|
+
import { GENERATED_HEADER, MERMAID_TEXT_LIMIT } from './contracts.js';
|
|
4
|
+
import { MERMAID_PROJECTION_SCHEMA } from './contracts.js';
|
|
5
|
+
function nodePrimaryCanonical(node) {
|
|
6
|
+
return node.canonicalNodeIds[0] ?? node.id;
|
|
7
|
+
}
|
|
8
|
+
function edgeSortKey(edge) {
|
|
9
|
+
return [edge.fromCanonicalId, edge.relation, edge.toCanonicalId, edge.canonicalEdgeIds[0] ?? ''].join('\0');
|
|
10
|
+
}
|
|
11
|
+
function sortStatements(statements) {
|
|
12
|
+
const comments = statements.filter((entry) => entry.kind === 'comment');
|
|
13
|
+
const subgraphs = statements
|
|
14
|
+
.filter((entry) => entry.kind === 'subgraph')
|
|
15
|
+
.sort((left, right) => byCodePoint(left.canonicalId, right.canonicalId));
|
|
16
|
+
const nodes = statements
|
|
17
|
+
.filter((entry) => entry.kind === 'node')
|
|
18
|
+
.sort((left, right) => byCodePoint(nodePrimaryCanonical(left), nodePrimaryCanonical(right)));
|
|
19
|
+
const edges = statements
|
|
20
|
+
.filter((entry) => entry.kind === 'edge')
|
|
21
|
+
.sort((left, right) => byCodePoint(edgeSortKey(left), edgeSortKey(right)));
|
|
22
|
+
return [...comments, ...subgraphs, ...nodes, ...edges];
|
|
23
|
+
}
|
|
24
|
+
function renderStatement(statement, indent) {
|
|
25
|
+
if (statement.kind === 'comment')
|
|
26
|
+
return [`${indent}%% ${statement.text}`];
|
|
27
|
+
if (statement.kind === 'node')
|
|
28
|
+
return [`${indent}${statement.id}["${statement.label}"]`];
|
|
29
|
+
if (statement.kind === 'edge') {
|
|
30
|
+
const arrow = statement.style === 'dotted' ? '-.->' : '-->';
|
|
31
|
+
return [`${indent}${statement.from} ${arrow}|"${statement.relation}"| ${statement.to}`];
|
|
32
|
+
}
|
|
33
|
+
const lines = [`${indent}subgraph ${statement.id}["${statement.label}"]`];
|
|
34
|
+
for (const child of sortStatements(statement.statements)) {
|
|
35
|
+
lines.push(...renderStatement(child, `${indent} `));
|
|
36
|
+
}
|
|
37
|
+
lines.push(`${indent}end`);
|
|
38
|
+
return lines;
|
|
39
|
+
}
|
|
40
|
+
export function serializeMermaidDocument(doc) {
|
|
41
|
+
const lines = [
|
|
42
|
+
GENERATED_HEADER,
|
|
43
|
+
`%% title: ${doc.title}`,
|
|
44
|
+
`flowchart ${doc.direction}`
|
|
45
|
+
];
|
|
46
|
+
for (const statement of sortStatements(doc.statements)) {
|
|
47
|
+
lines.push(...renderStatement(statement, ''));
|
|
48
|
+
}
|
|
49
|
+
const text = `${lines.join('\n')}\n`;
|
|
50
|
+
const bytes = Buffer.byteLength(text, 'utf8');
|
|
51
|
+
if (bytes > MERMAID_TEXT_LIMIT) {
|
|
52
|
+
throw new Error(`mermaid_budget_overrun: ${bytes}>${MERMAID_TEXT_LIMIT}`);
|
|
53
|
+
}
|
|
54
|
+
return text;
|
|
55
|
+
}
|
|
56
|
+
export function emptyAccounting() {
|
|
57
|
+
return Object.freeze({
|
|
58
|
+
selectedNodeIds: Object.freeze([]),
|
|
59
|
+
emittedNodeIds: Object.freeze([]),
|
|
60
|
+
aggregatedNodeMembers: Object.freeze({}),
|
|
61
|
+
omittedNodes: Object.freeze([]),
|
|
62
|
+
selectedEdgeIds: Object.freeze([]),
|
|
63
|
+
emittedEdgeIds: Object.freeze([]),
|
|
64
|
+
aggregatedEdgeMembers: Object.freeze({}),
|
|
65
|
+
omittedEdges: Object.freeze([])
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export function toMermaidProjection(input) {
|
|
69
|
+
const text = serializeMermaidDocument(input.doc);
|
|
70
|
+
return Object.freeze({
|
|
71
|
+
schema: MERMAID_PROJECTION_SCHEMA,
|
|
72
|
+
viewId: input.viewId,
|
|
73
|
+
direction: input.doc.direction,
|
|
74
|
+
title: input.doc.title,
|
|
75
|
+
source: text,
|
|
76
|
+
accounting: input.accounting,
|
|
77
|
+
contentHash: hashCanonical(text),
|
|
78
|
+
byteLength: Buffer.byteLength(text, 'utf8'),
|
|
79
|
+
text
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { emptyProjection, projectFilteredView } from './view-builder.js';
|
|
2
|
+
export function buildSsotProvenanceView(input) {
|
|
3
|
+
const hasSsotKinds = input.snapshot.nodes.some((node) => node.kind === 'source' || node.kind === 'wiki_claim' || node.kind === 'proof');
|
|
4
|
+
if (!hasSsotKinds) {
|
|
5
|
+
return emptyProjection('ssot-provenance', 'SSOT provenance', 'TD', 'insufficient_graph: no source/wiki_claim/proof nodes');
|
|
6
|
+
}
|
|
7
|
+
return projectFilteredView({
|
|
8
|
+
viewId: 'ssot-provenance',
|
|
9
|
+
snapshot: input.snapshot,
|
|
10
|
+
policy: input.policy,
|
|
11
|
+
...(input.request ? { request: input.request } : {}),
|
|
12
|
+
filter: {
|
|
13
|
+
kinds: ['source', 'wiki_claim', 'proof'],
|
|
14
|
+
edgeTypes: ['cites', 'derived_from', 'verified_by']
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { emptyProjection, projectFilteredView } from './view-builder.js';
|
|
2
|
+
export function buildVerificationCoverageView(input) {
|
|
3
|
+
const hasVerification = input.snapshot.nodes.some((node) => node.kind === 'test' || node.kind === 'gate' || node.kind === 'proof');
|
|
4
|
+
if (!hasVerification) {
|
|
5
|
+
return emptyProjection('verification-coverage', 'Verification coverage', 'LR', 'insufficient_graph: no test/gate/proof nodes');
|
|
6
|
+
}
|
|
7
|
+
return projectFilteredView({
|
|
8
|
+
viewId: 'verification-coverage',
|
|
9
|
+
snapshot: input.snapshot,
|
|
10
|
+
policy: input.policy,
|
|
11
|
+
...(input.request ? { request: input.request } : {}),
|
|
12
|
+
filter: {
|
|
13
|
+
kinds: ['test', 'gate', 'proof', 'module'],
|
|
14
|
+
edgeTypes: ['tests', 'verified_by', 'gated_by']
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { buildMermaidView, emptyMermaidView, emptyProjection, projectFilteredView } from './view-builder.js';
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { byCodePoint } from '../../architecture/contracts.js';
|
|
2
|
+
import * as ast from './ast.js';
|
|
3
|
+
import { assertInjective, mermaidEdgeId, mermaidNodeId, mermaidSubgraphId } from './ids.js';
|
|
4
|
+
import { reduceForBudget } from './reduce.js';
|
|
5
|
+
import { serializeMermaidDocument, toMermaidProjection } from './serializer.js';
|
|
6
|
+
import { viewSpecFor } from './view-specs.js';
|
|
7
|
+
function labelFor(node) {
|
|
8
|
+
return node.label || node.path || node.id;
|
|
9
|
+
}
|
|
10
|
+
function filterGraph(snapshot, filter) {
|
|
11
|
+
const kindSet = new Set(filter.kinds);
|
|
12
|
+
const edgeSet = new Set(filter.edgeTypes);
|
|
13
|
+
const nodes = snapshot.nodes
|
|
14
|
+
.filter((node) => kindSet.has(node.kind))
|
|
15
|
+
.sort((left, right) => byCodePoint(left.id, right.id));
|
|
16
|
+
const candidateIds = new Set(nodes.map((node) => node.id));
|
|
17
|
+
const edges = snapshot.edges
|
|
18
|
+
.filter((edge) => edgeSet.has(edge.type) && candidateIds.has(edge.from) && candidateIds.has(edge.to))
|
|
19
|
+
.sort((left, right) => byCodePoint(left.id, right.id));
|
|
20
|
+
return { nodes, edges };
|
|
21
|
+
}
|
|
22
|
+
export function buildMermaidView(input) {
|
|
23
|
+
const spec = viewSpecFor(input.viewId);
|
|
24
|
+
const { nodes: candidateNodes, edges: candidateEdges } = filterGraph(input.snapshot, input.filter);
|
|
25
|
+
const protectedIds = new Set(input.protectedNodeIds ?? []);
|
|
26
|
+
const profileKey = input.profile ?? 'global';
|
|
27
|
+
const budget = input.policy.profiles[profileKey];
|
|
28
|
+
const reduced = reduceForBudget({
|
|
29
|
+
nodes: candidateNodes,
|
|
30
|
+
edges: candidateEdges,
|
|
31
|
+
budget,
|
|
32
|
+
protectedNodeIds: protectedIds
|
|
33
|
+
});
|
|
34
|
+
const idMap = assertInjective(reduced.nodes.map((node) => node.id));
|
|
35
|
+
const statements = [ast.comment(`atlas-view: ${input.viewId}`)];
|
|
36
|
+
if (input.groupByLayer && input.layerOf) {
|
|
37
|
+
const byLayer = new Map();
|
|
38
|
+
for (const node of reduced.nodes) {
|
|
39
|
+
const layer = input.layerOf(node, input.policy) ?? 'unassigned';
|
|
40
|
+
const list = byLayer.get(layer) ?? [];
|
|
41
|
+
list.push(node);
|
|
42
|
+
byLayer.set(layer, list);
|
|
43
|
+
}
|
|
44
|
+
for (const layer of [...byLayer.keys()].sort(byCodePoint)) {
|
|
45
|
+
const nodes = byLayer.get(layer) ?? [];
|
|
46
|
+
const subgraphNodes = nodes.map((node) => ast.node({
|
|
47
|
+
id: idMap.get(node.id),
|
|
48
|
+
label: labelFor(node),
|
|
49
|
+
canonicalNodeIds: [node.id]
|
|
50
|
+
}));
|
|
51
|
+
statements.push(ast.subgraph({
|
|
52
|
+
id: mermaidSubgraphId(`layer:${layer}`),
|
|
53
|
+
label: layer,
|
|
54
|
+
canonicalId: `layer:${layer}`,
|
|
55
|
+
statements: subgraphNodes
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
for (const node of reduced.nodes) {
|
|
61
|
+
statements.push(ast.node({
|
|
62
|
+
id: idMap.get(node.id),
|
|
63
|
+
label: labelFor(node),
|
|
64
|
+
canonicalNodeIds: [node.id]
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const edge of reduced.edges) {
|
|
69
|
+
const fromId = idMap.get(edge.from);
|
|
70
|
+
const toId = idMap.get(edge.to);
|
|
71
|
+
if (!fromId || !toId)
|
|
72
|
+
continue;
|
|
73
|
+
const style = protectedIds.has(edge.from) || protectedIds.has(edge.to)
|
|
74
|
+
? 'dotted'
|
|
75
|
+
: 'solid';
|
|
76
|
+
statements.push(ast.edge({
|
|
77
|
+
from: fromId,
|
|
78
|
+
to: toId,
|
|
79
|
+
relation: edge.type,
|
|
80
|
+
style,
|
|
81
|
+
canonicalEdgeIds: [edge.id],
|
|
82
|
+
fromCanonicalId: edge.from,
|
|
83
|
+
toCanonicalId: edge.to
|
|
84
|
+
}));
|
|
85
|
+
void mermaidEdgeId(edge.from, edge.to, edge.type);
|
|
86
|
+
}
|
|
87
|
+
for (const seed of protectedIds) {
|
|
88
|
+
if (!idMap.has(seed))
|
|
89
|
+
void mermaidNodeId(seed);
|
|
90
|
+
}
|
|
91
|
+
reduced.ledger.assertBalanced();
|
|
92
|
+
const doc = ast.document({
|
|
93
|
+
direction: spec.direction,
|
|
94
|
+
title: spec.title,
|
|
95
|
+
statements
|
|
96
|
+
});
|
|
97
|
+
void serializeMermaidDocument(doc);
|
|
98
|
+
const built = toMermaidProjection({
|
|
99
|
+
viewId: input.viewId,
|
|
100
|
+
doc,
|
|
101
|
+
accounting: reduced.ledger.toAccounting()
|
|
102
|
+
});
|
|
103
|
+
return Object.freeze({
|
|
104
|
+
text: built.text,
|
|
105
|
+
projection: Object.freeze({
|
|
106
|
+
schema: built.schema,
|
|
107
|
+
viewId: built.viewId,
|
|
108
|
+
direction: built.direction,
|
|
109
|
+
title: built.title,
|
|
110
|
+
source: built.source,
|
|
111
|
+
accounting: built.accounting,
|
|
112
|
+
contentHash: built.contentHash,
|
|
113
|
+
byteLength: built.byteLength
|
|
114
|
+
})
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
export function emptyMermaidView(input) {
|
|
118
|
+
const statements = [ast.comment(input.reason)];
|
|
119
|
+
const reduced = reduceForBudget({
|
|
120
|
+
nodes: [],
|
|
121
|
+
edges: [],
|
|
122
|
+
budget: { maxNodes: 1, maxEdges: 1, maxLabelChars: 56, tokenBudget: 0 }
|
|
123
|
+
});
|
|
124
|
+
const built = toMermaidProjection({
|
|
125
|
+
viewId: input.viewId,
|
|
126
|
+
doc: ast.document({
|
|
127
|
+
direction: input.direction,
|
|
128
|
+
title: input.title,
|
|
129
|
+
statements
|
|
130
|
+
}),
|
|
131
|
+
accounting: reduced.ledger.toAccounting()
|
|
132
|
+
});
|
|
133
|
+
return Object.freeze({
|
|
134
|
+
text: built.text,
|
|
135
|
+
projection: Object.freeze({
|
|
136
|
+
schema: built.schema,
|
|
137
|
+
viewId: built.viewId,
|
|
138
|
+
direction: built.direction,
|
|
139
|
+
title: built.title,
|
|
140
|
+
source: built.source,
|
|
141
|
+
accounting: built.accounting,
|
|
142
|
+
contentHash: built.contentHash,
|
|
143
|
+
byteLength: built.byteLength
|
|
144
|
+
})
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
export function projectFilteredView(input) {
|
|
148
|
+
const protectedNodeIds = [
|
|
149
|
+
...(input.request?.seedNodeIds ?? []),
|
|
150
|
+
...(input.request?.findingSubjectIds ?? [])
|
|
151
|
+
];
|
|
152
|
+
const built = buildMermaidView({
|
|
153
|
+
snapshot: input.snapshot,
|
|
154
|
+
policy: input.policy,
|
|
155
|
+
viewId: input.viewId,
|
|
156
|
+
filter: input.filter,
|
|
157
|
+
...(protectedNodeIds.length ? { protectedNodeIds } : {}),
|
|
158
|
+
...(input.groupByLayer === true ? { groupByLayer: true } : {}),
|
|
159
|
+
...(input.layerOf ? { layerOf: input.layerOf } : {}),
|
|
160
|
+
...(input.request?.profile ? { profile: input.request.profile } : {})
|
|
161
|
+
});
|
|
162
|
+
return Object.freeze({
|
|
163
|
+
...built.projection,
|
|
164
|
+
text: built.text
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
export function emptyProjection(viewId, title, direction, reason) {
|
|
168
|
+
const built = emptyMermaidView({ viewId, title, direction, reason });
|
|
169
|
+
return Object.freeze({
|
|
170
|
+
...built.projection,
|
|
171
|
+
text: built.text
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ARCHITECTURE_MAP_VIEW_IDS } from '../../architecture/contracts.js';
|
|
2
|
+
const SPECS = Object.freeze([
|
|
3
|
+
{ viewId: 'project-topology', title: 'Project topology', direction: 'LR', filename: 'project-topology.mmd' },
|
|
4
|
+
{ viewId: 'module-dependency', title: 'Module dependency', direction: 'TD', filename: 'module-dependency.mmd' },
|
|
5
|
+
{ viewId: 'public-surface', title: 'Public surface', direction: 'LR', filename: 'public-surface.mmd' },
|
|
6
|
+
{ viewId: 'ssot-provenance', title: 'SSOT provenance', direction: 'TD', filename: 'ssot-provenance.mmd' },
|
|
7
|
+
{ viewId: 'runtime-control', title: 'Runtime control', direction: 'TD', filename: 'runtime-control.mmd' },
|
|
8
|
+
{ viewId: 'verification-coverage', title: 'Verification coverage', direction: 'LR', filename: 'verification-coverage.mmd' },
|
|
9
|
+
{ viewId: 'risk-domains', title: 'Risk domains', direction: 'TD', filename: 'risk-domains.mmd' },
|
|
10
|
+
{ viewId: 'change-impact', title: 'Change impact', direction: 'LR', filename: 'change-impact.mmd' },
|
|
11
|
+
{ viewId: 'architecture-delta', title: 'Architecture delta', direction: 'TD', filename: 'architecture-delta.mmd' },
|
|
12
|
+
{ viewId: 'ownership-workstream', title: 'Ownership workstream', direction: 'LR', filename: 'ownership-workstream.mmd' }
|
|
13
|
+
]);
|
|
14
|
+
export function architectureMapViewSpecs() {
|
|
15
|
+
return SPECS;
|
|
16
|
+
}
|
|
17
|
+
export function viewSpecFor(viewId) {
|
|
18
|
+
const spec = SPECS.find((entry) => entry.viewId === viewId);
|
|
19
|
+
if (!spec)
|
|
20
|
+
throw new Error(`unknown_architecture_map_view: ${viewId}`);
|
|
21
|
+
return spec;
|
|
22
|
+
}
|
|
23
|
+
export function assertViewIdsMatchWo() {
|
|
24
|
+
const fromSpecs = SPECS.map((spec) => spec.viewId);
|
|
25
|
+
if (fromSpecs.length !== ARCHITECTURE_MAP_VIEW_IDS.length) {
|
|
26
|
+
throw new Error('architecture_map_view_id_count_mismatch');
|
|
27
|
+
}
|
|
28
|
+
for (let index = 0; index < ARCHITECTURE_MAP_VIEW_IDS.length; index += 1) {
|
|
29
|
+
if (fromSpecs[index] !== ARCHITECTURE_MAP_VIEW_IDS[index]) {
|
|
30
|
+
throw new Error(`architecture_map_view_id_mismatch: ${fromSpecs[index]}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadArchitectureMapPolicy } from '../architecture/policy.js';
|
|
4
|
+
import { buildArchitectureMapViews } from '../projections/mermaid/index.js';
|
|
5
|
+
const WIKI_PREFIX = '.sneakoscope/wiki/';
|
|
6
|
+
export const ARCHITECTURE_MAP_DIR_REL = '.sneakoscope/wiki/architecture-map';
|
|
7
|
+
export const ARCHITECTURE_MAP_GLOBAL_VIEW_FILES = Object.freeze([
|
|
8
|
+
'project-topology.mmd',
|
|
9
|
+
'module-dependency.mmd',
|
|
10
|
+
'public-surface.mmd',
|
|
11
|
+
'ssot-provenance.mmd',
|
|
12
|
+
'runtime-control.mmd',
|
|
13
|
+
'verification-coverage.mmd',
|
|
14
|
+
'risk-domains.mmd'
|
|
15
|
+
]);
|
|
16
|
+
export const ARCHITECTURE_MAP_ARTIFACT_RELS = Object.freeze([
|
|
17
|
+
`${ARCHITECTURE_MAP_DIR_REL}/manifest.json`,
|
|
18
|
+
`${ARCHITECTURE_MAP_DIR_REL}/metrics.json`,
|
|
19
|
+
`${ARCHITECTURE_MAP_DIR_REL}/findings.json`,
|
|
20
|
+
...ARCHITECTURE_MAP_GLOBAL_VIEW_FILES.map((name) => `${ARCHITECTURE_MAP_DIR_REL}/views/${name}`)
|
|
21
|
+
]);
|
|
22
|
+
function byCodePoint(left, right) {
|
|
23
|
+
if (left < right)
|
|
24
|
+
return -1;
|
|
25
|
+
return left > right ? 1 : 0;
|
|
26
|
+
}
|
|
27
|
+
function wikiRelative(artifactRel) {
|
|
28
|
+
if (!artifactRel.startsWith(WIKI_PREFIX)) {
|
|
29
|
+
throw new Error(`architecture_map_publish_artifact_outside_wiki:${artifactRel}`);
|
|
30
|
+
}
|
|
31
|
+
return artifactRel.slice(WIKI_PREFIX.length);
|
|
32
|
+
}
|
|
33
|
+
export async function publishArchitectureMapToStage(stageWikiAbs, snapshot, options) {
|
|
34
|
+
const policy = options.policy ?? loadArchitectureMapPolicy(options.root);
|
|
35
|
+
const built = buildArchitectureMapViews(snapshot, policy, {
|
|
36
|
+
rootId: options.root,
|
|
37
|
+
missionId: options.missionId ?? null
|
|
38
|
+
});
|
|
39
|
+
const written = [];
|
|
40
|
+
const writeJson = async (rel, value) => {
|
|
41
|
+
const dest = path.join(stageWikiAbs, wikiRelative(rel));
|
|
42
|
+
await fsp.mkdir(path.dirname(dest), { recursive: true });
|
|
43
|
+
await fsp.writeFile(dest, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
44
|
+
written.push(rel);
|
|
45
|
+
};
|
|
46
|
+
const writeText = async (rel, text) => {
|
|
47
|
+
const dest = path.join(stageWikiAbs, wikiRelative(rel));
|
|
48
|
+
await fsp.mkdir(path.dirname(dest), { recursive: true });
|
|
49
|
+
await fsp.writeFile(dest, text.endsWith('\n') ? text : `${text}\n`, 'utf8');
|
|
50
|
+
written.push(rel);
|
|
51
|
+
};
|
|
52
|
+
await writeJson(`${ARCHITECTURE_MAP_DIR_REL}/manifest.json`, built.manifest);
|
|
53
|
+
await writeJson(`${ARCHITECTURE_MAP_DIR_REL}/metrics.json`, built.metrics);
|
|
54
|
+
await writeJson(`${ARCHITECTURE_MAP_DIR_REL}/findings.json`, { findings: built.findings });
|
|
55
|
+
for (const view of built.views) {
|
|
56
|
+
await writeText(`${ARCHITECTURE_MAP_DIR_REL}/views/${view.filename}`, view.text);
|
|
57
|
+
}
|
|
58
|
+
const expected = new Set(ARCHITECTURE_MAP_ARTIFACT_RELS);
|
|
59
|
+
for (const rel of written) {
|
|
60
|
+
if (!expected.has(rel))
|
|
61
|
+
throw new Error(`architecture_map_publish_unexpected_artifact:${rel}`);
|
|
62
|
+
}
|
|
63
|
+
if (written.length !== ARCHITECTURE_MAP_ARTIFACT_RELS.length) {
|
|
64
|
+
throw new Error(`architecture_map_publish_incomplete:${written.length}/${ARCHITECTURE_MAP_ARTIFACT_RELS.length}`);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
written: [...written].sort(byCodePoint),
|
|
68
|
+
stageMapDir: path.join(stageWikiAbs, wikiRelative(ARCHITECTURE_MAP_DIR_REL)),
|
|
69
|
+
built
|
|
70
|
+
};
|
|
71
|
+
}
|