sneakoscope 8.3.2 → 8.6.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 +97 -16
- 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 +173 -0
- package/dist/core/architecture-map-review.js +180 -0
- package/dist/core/codex/agent-config-file-repair.js +17 -3
- package/dist/core/codex/codex-config-guard.js +3 -1
- package/dist/core/codex/codex-feature-flags.js +10 -0
- package/dist/core/codex/codex-project-config-policy.js +54 -30
- package/dist/core/codex/codex-startup-config-postcheck.js +5 -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-hooks/codex-hook-state-writer.js +2 -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-bridge/security.js +11 -8
- 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/codex-lb/request-route-resolver.js +21 -18
- package/dist/core/commands/triwiki-atlas-command.js +372 -0
- package/dist/core/commands/triwiki-command.js +12 -1
- package/dist/core/doctor/doctor-dirty-planner.js +16 -1
- package/dist/core/doctor/doctor-idempotence.js +11 -0
- package/dist/core/doctor/doctor-repair-postcheck.js +49 -0
- package/dist/core/doctor/doctor-transaction.js +47 -2
- package/dist/core/doctor/legacy-global-hook-cleanup.js +1 -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/init.js +40 -17
- 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 +18 -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/subagents/agent-catalog.js +4 -2
- package/dist/core/subagents/official-subagent-config.js +92 -15
- package/dist/core/subagents/official-subagent-preparation.js +1 -0
- 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,81 @@
|
|
|
1
|
+
import { sha256 } from '../../../fsx.js';
|
|
2
|
+
import { ARCHITECTURE_BASELINE_SCHEMA } from './contracts.js';
|
|
3
|
+
import { hashCanonical } from './fingerprint.js';
|
|
4
|
+
import { analyzeArchitectureFindings } from './findings.js';
|
|
5
|
+
import { computeArchitectureMetrics, computeSccs, buildModuleDependencyGraph } from './metrics.js';
|
|
6
|
+
import { mergeGraphWithTopology } from './topology-overlay.js';
|
|
7
|
+
export function sealArchitectureBaseline(canonicalPayloadHash) {
|
|
8
|
+
return sha256(`${ARCHITECTURE_BASELINE_SCHEMA}\n${canonicalPayloadHash}`);
|
|
9
|
+
}
|
|
10
|
+
export function buildArchitectureBaseline(input) {
|
|
11
|
+
const merged = mergeGraphWithTopology(input.bundle.graph, input.bundle.topology);
|
|
12
|
+
const moduleGraph = buildModuleDependencyGraph(merged.nodes, merged.edges);
|
|
13
|
+
const sccs = computeSccs(moduleGraph);
|
|
14
|
+
const findings = input.findings ?? analyzeArchitectureFindings({
|
|
15
|
+
bundle: input.bundle,
|
|
16
|
+
policy: input.policy,
|
|
17
|
+
baselineSccs: []
|
|
18
|
+
});
|
|
19
|
+
const metrics = input.metrics ?? computeArchitectureMetrics({
|
|
20
|
+
graph: input.bundle.graph,
|
|
21
|
+
topology: input.bundle.topology,
|
|
22
|
+
layerViolationCount: findings.filter((finding) => finding.code === 'forbidden_dependency').length,
|
|
23
|
+
ssotCollisionCount: findings.filter((finding) => finding.code === 'ssot_collision').length,
|
|
24
|
+
authorityBypassCount: findings.filter((finding) => finding.code === 'authority_bypass').length,
|
|
25
|
+
protectedVerificationGapCount: findings.filter((finding) => finding.code === 'protected_verification_gap').length
|
|
26
|
+
});
|
|
27
|
+
const viewHashes = Object.freeze({ ...(input.viewHashes ?? {}) });
|
|
28
|
+
const payload = {
|
|
29
|
+
schema: ARCHITECTURE_BASELINE_SCHEMA,
|
|
30
|
+
missionId: input.missionId,
|
|
31
|
+
routeId: input.routeId,
|
|
32
|
+
taskProfile: input.taskProfile,
|
|
33
|
+
required: true,
|
|
34
|
+
capturedBeforeMutation: true,
|
|
35
|
+
repositoryHead: input.bundle.worktree.repositoryHead,
|
|
36
|
+
worktreeFingerprintHash: input.bundle.worktree.hash,
|
|
37
|
+
preexistingDirtyPaths: input.bundle.worktree.dirtyPaths,
|
|
38
|
+
graphHash: input.bundle.graphHash,
|
|
39
|
+
topologyHash: input.bundle.topologyHash,
|
|
40
|
+
ssotInventoryHash: input.bundle.ssotInventoryHash,
|
|
41
|
+
voxelContextHash: input.bundle.voxelContextHash,
|
|
42
|
+
policyHash: input.bundle.policyHash,
|
|
43
|
+
analyzerVersion: input.bundle.analyzerVersion,
|
|
44
|
+
serializerVersion: input.bundle.serializerVersion,
|
|
45
|
+
scope: input.scope,
|
|
46
|
+
metrics,
|
|
47
|
+
findings,
|
|
48
|
+
viewHashes,
|
|
49
|
+
sccKeys: sccs.map((members) => members.join('|'))
|
|
50
|
+
};
|
|
51
|
+
const canonicalPayloadHash = hashCanonical(payload);
|
|
52
|
+
const seal = sealArchitectureBaseline(canonicalPayloadHash);
|
|
53
|
+
return Object.freeze({
|
|
54
|
+
schema: ARCHITECTURE_BASELINE_SCHEMA,
|
|
55
|
+
missionId: input.missionId,
|
|
56
|
+
routeId: input.routeId,
|
|
57
|
+
taskProfile: input.taskProfile,
|
|
58
|
+
required: true,
|
|
59
|
+
capturedAt: input.capturedAt,
|
|
60
|
+
capturedBeforeMutation: true,
|
|
61
|
+
repositoryHead: input.bundle.worktree.repositoryHead,
|
|
62
|
+
worktreeFingerprintHash: input.bundle.worktree.hash,
|
|
63
|
+
preexistingDirtyPaths: Object.freeze([...input.bundle.worktree.dirtyPaths]),
|
|
64
|
+
graphHash: input.bundle.graphHash,
|
|
65
|
+
topologyHash: input.bundle.topologyHash,
|
|
66
|
+
ssotInventoryHash: input.bundle.ssotInventoryHash,
|
|
67
|
+
voxelContextHash: input.bundle.voxelContextHash,
|
|
68
|
+
policyHash: input.bundle.policyHash,
|
|
69
|
+
analyzerVersion: input.bundle.analyzerVersion,
|
|
70
|
+
serializerVersion: input.bundle.serializerVersion,
|
|
71
|
+
scope: input.scope,
|
|
72
|
+
metrics,
|
|
73
|
+
findings: Object.freeze([...findings]),
|
|
74
|
+
viewHashes,
|
|
75
|
+
canonicalPayloadHash,
|
|
76
|
+
seal
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
export function verifyArchitectureBaselineSeal(baseline) {
|
|
80
|
+
return baseline.seal === sealArchitectureBaseline(baseline.canonicalPayloadHash);
|
|
81
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { byCodePoint } from './contracts.js';
|
|
2
|
+
import { hashCanonical } from './fingerprint.js';
|
|
3
|
+
export function renderArchitectureCapsule(input) {
|
|
4
|
+
const blocking = input.findings
|
|
5
|
+
.filter((finding) => finding.severity === 'blocking')
|
|
6
|
+
.sort((left, right) => byCodePoint(left.id, right.id));
|
|
7
|
+
const baselineShort = input.baseline ? input.baseline.seal.slice(0, 12) : 'none';
|
|
8
|
+
const lines = [
|
|
9
|
+
`schema=sks.architecture-capsule.v1`,
|
|
10
|
+
`profile=${input.profile}`,
|
|
11
|
+
`mission=${input.missionId}`,
|
|
12
|
+
`route=${input.routeId ?? 'unknown'}`,
|
|
13
|
+
`baseline=${baselineShort}`,
|
|
14
|
+
`modules=${input.metrics.moduleCount}`,
|
|
15
|
+
`scc=${input.metrics.sccCount}`,
|
|
16
|
+
`fan_in_max=${input.metrics.maxFanIn}`,
|
|
17
|
+
`fan_out_max=${input.metrics.maxFanOut}`,
|
|
18
|
+
`layer_violations=${input.metrics.layerViolationCount}`,
|
|
19
|
+
`ssot_collisions=${input.metrics.ssotCollisionCount}`,
|
|
20
|
+
`verification_gaps=${input.metrics.protectedVerificationGapCount}`,
|
|
21
|
+
`affected_gates=${(input.affectedGates ?? []).slice().sort(byCodePoint).join(',') || 'none'}`,
|
|
22
|
+
`blocking_findings=${blocking.map((finding) => `${finding.id}:${finding.code}`).join(',') || 'none'}`,
|
|
23
|
+
`seeds=${(input.seedPaths ?? []).slice().sort(byCodePoint).join(',') || 'none'}`,
|
|
24
|
+
`map=${input.mapArtifact ?? 'architecture-map-delta.mmd'}`
|
|
25
|
+
];
|
|
26
|
+
const text = `${lines.join('\n')}\n`;
|
|
27
|
+
return Object.freeze({
|
|
28
|
+
schema: 'sks.architecture-capsule.v1',
|
|
29
|
+
text,
|
|
30
|
+
byteLength: Buffer.byteLength(text, 'utf8'),
|
|
31
|
+
profile: input.profile,
|
|
32
|
+
contentHash: hashCanonical(text)
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export const ARCHITECTURE_INPUT_BUNDLE_SCHEMA = 'sks.architecture-input-bundle.v1';
|
|
2
|
+
export const ARCHITECTURE_BASELINE_SCHEMA = 'sks.architecture-baseline.v1';
|
|
3
|
+
export const ARCHITECTURE_REVIEW_SCHEMA = 'sks.architecture-review.v1';
|
|
4
|
+
export const ARCHITECTURE_MAP_MANIFEST_SCHEMA = 'sks.architecture-map-manifest.v1';
|
|
5
|
+
export const ARCHITECTURE_MAP_POLICY_SCHEMA = 'sks.architecture-map-policy.v1';
|
|
6
|
+
export const MERMAID_PROJECTION_SCHEMA = 'sks.mermaid-projection.v1';
|
|
7
|
+
export const ARCHITECTURE_ANALYZER_VERSION = 'architecture-map-analyzer.v1';
|
|
8
|
+
export const ARCHITECTURE_SERIALIZER_VERSION = 'mermaid-serializer.v1';
|
|
9
|
+
export const ARCHITECTURE_MAP_DIR_REL = '.sneakoscope/wiki/architecture-map';
|
|
10
|
+
export const ARCHITECTURE_MAP_POLICY_FILE = 'config/architecture-map-policy.v1.json';
|
|
11
|
+
export const ARCHITECTURE_MAP_VIEW_IDS = Object.freeze([
|
|
12
|
+
'project-topology',
|
|
13
|
+
'module-dependency',
|
|
14
|
+
'public-surface',
|
|
15
|
+
'ssot-provenance',
|
|
16
|
+
'runtime-control',
|
|
17
|
+
'verification-coverage',
|
|
18
|
+
'risk-domains',
|
|
19
|
+
'change-impact',
|
|
20
|
+
'architecture-delta',
|
|
21
|
+
'ownership-workstream'
|
|
22
|
+
]);
|
|
23
|
+
export const GLOBAL_ARCHITECTURE_MAP_VIEW_IDS = Object.freeze([
|
|
24
|
+
'project-topology',
|
|
25
|
+
'module-dependency',
|
|
26
|
+
'public-surface',
|
|
27
|
+
'ssot-provenance',
|
|
28
|
+
'runtime-control',
|
|
29
|
+
'verification-coverage',
|
|
30
|
+
'risk-domains'
|
|
31
|
+
]);
|
|
32
|
+
export const ARCHITECTURE_FINDING_CODES = Object.freeze([
|
|
33
|
+
'baseline_missing',
|
|
34
|
+
'baseline_late',
|
|
35
|
+
'baseline_stale',
|
|
36
|
+
'baseline_tampered',
|
|
37
|
+
'graph_stale',
|
|
38
|
+
'map_parity_failure',
|
|
39
|
+
'unaccounted_changed_file',
|
|
40
|
+
'new_cycle',
|
|
41
|
+
'cycle_expansion',
|
|
42
|
+
'forbidden_dependency',
|
|
43
|
+
'layer_skip',
|
|
44
|
+
'ssot_collision',
|
|
45
|
+
'authority_bypass',
|
|
46
|
+
'protected_verification_gap',
|
|
47
|
+
'fan_out_regression',
|
|
48
|
+
'fan_in_unverified',
|
|
49
|
+
'passthrough_chain',
|
|
50
|
+
'redundant_parallel_path',
|
|
51
|
+
'duplicate_facade',
|
|
52
|
+
'orphan_entrypoint',
|
|
53
|
+
'unowned_high_risk',
|
|
54
|
+
'test_distance_increase',
|
|
55
|
+
'architecture_depth_increase',
|
|
56
|
+
'cross_workstream_write_overlap',
|
|
57
|
+
'unknown_dynamic_relation',
|
|
58
|
+
'insufficient_graph'
|
|
59
|
+
]);
|
|
60
|
+
export function byCodePoint(left, right) {
|
|
61
|
+
if (left < right)
|
|
62
|
+
return -1;
|
|
63
|
+
return left > right ? 1 : 0;
|
|
64
|
+
}
|
|
65
|
+
export function sortStrings(values) {
|
|
66
|
+
return [...values].sort(byCodePoint);
|
|
67
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { ARCHITECTURE_REVIEW_SCHEMA, byCodePoint } from './contracts.js';
|
|
2
|
+
import { verifyArchitectureBaselineSeal } from './baseline.js';
|
|
3
|
+
import { diffWorktreePaths, hashCanonical } from './fingerprint.js';
|
|
4
|
+
import { analyzeArchitectureFindings } from './findings.js';
|
|
5
|
+
import { computeArchitectureMetrics } from './metrics.js';
|
|
6
|
+
export function metricDelta(before, after) {
|
|
7
|
+
return Object.freeze({
|
|
8
|
+
sccCountDelta: after.sccCount - before.sccCount,
|
|
9
|
+
cyclicNodeCountDelta: after.cyclicNodeCount - before.cyclicNodeCount,
|
|
10
|
+
layerViolationCountDelta: after.layerViolationCount - before.layerViolationCount,
|
|
11
|
+
ssotCollisionCountDelta: after.ssotCollisionCount - before.ssotCollisionCount,
|
|
12
|
+
protectedVerificationGapCountDelta: after.protectedVerificationGapCount - before.protectedVerificationGapCount
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function partitionFindings(before, after) {
|
|
16
|
+
const beforeIds = new Set(before.map((finding) => finding.id));
|
|
17
|
+
const afterIds = new Set(after.map((finding) => finding.id));
|
|
18
|
+
const byId = (left, right) => byCodePoint(left.id, right.id);
|
|
19
|
+
return {
|
|
20
|
+
newFindings: after.filter((finding) => !beforeIds.has(finding.id)).sort(byId),
|
|
21
|
+
resolvedFindings: before.filter((finding) => !afterIds.has(finding.id)).sort(byId),
|
|
22
|
+
persistentFindings: after.filter((finding) => beforeIds.has(finding.id)).sort(byId)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function baselineSccsFromFindings(findings) {
|
|
26
|
+
return Object.freeze(findings
|
|
27
|
+
.filter((finding) => finding.code === 'new_cycle' || finding.code === 'cycle_expansion')
|
|
28
|
+
.map((finding) => finding.subjectIds));
|
|
29
|
+
}
|
|
30
|
+
function countCodes(findings, code) {
|
|
31
|
+
return findings.filter((finding) => finding.code === code).length;
|
|
32
|
+
}
|
|
33
|
+
export function buildArchitectureDelta(input) {
|
|
34
|
+
const afterFindings = analyzeArchitectureFindings({
|
|
35
|
+
bundle: input.afterBundle,
|
|
36
|
+
policy: input.policy,
|
|
37
|
+
baselineSccs: baselineSccsFromFindings(input.baseline.findings)
|
|
38
|
+
});
|
|
39
|
+
const partitioned = partitionFindings(input.baseline.findings, afterFindings);
|
|
40
|
+
const afterMetrics = computeArchitectureMetrics({
|
|
41
|
+
graph: input.afterBundle.graph,
|
|
42
|
+
topology: input.afterBundle.topology,
|
|
43
|
+
layerViolationCount: countCodes(afterFindings, 'forbidden_dependency'),
|
|
44
|
+
ssotCollisionCount: countCodes(afterFindings, 'ssot_collision'),
|
|
45
|
+
protectedVerificationGapCount: countCodes(afterFindings, 'protected_verification_gap')
|
|
46
|
+
});
|
|
47
|
+
return Object.freeze({
|
|
48
|
+
schema: 'sks.architecture-delta.v1',
|
|
49
|
+
missionId: input.missionId,
|
|
50
|
+
baselineSeal: input.baseline.seal,
|
|
51
|
+
addedNodeIds: Object.freeze([]),
|
|
52
|
+
removedNodeIds: Object.freeze([]),
|
|
53
|
+
addedEdgeIds: Object.freeze([]),
|
|
54
|
+
removedEdgeIds: Object.freeze([]),
|
|
55
|
+
newFindings: Object.freeze(partitioned.newFindings),
|
|
56
|
+
resolvedFindings: Object.freeze(partitioned.resolvedFindings),
|
|
57
|
+
metricDelta: metricDelta(input.baseline.metrics, afterMetrics)
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export function buildArchitectureReview(input) {
|
|
61
|
+
const sealValid = verifyArchitectureBaselineSeal(input.baseline);
|
|
62
|
+
const changedPaths = [...diffWorktreePaths(input.baseline.preexistingDirtyPaths.map((path) => ({
|
|
63
|
+
path,
|
|
64
|
+
kind: 'file',
|
|
65
|
+
sha256: 'baseline',
|
|
66
|
+
size: 0,
|
|
67
|
+
executable: false
|
|
68
|
+
})), input.afterBundle.worktree.paths)];
|
|
69
|
+
const changedPathSet = new Set(changedPaths.map((entry) => entry.path));
|
|
70
|
+
const accounted = [...(input.accountedChangedPaths ?? [...changedPathSet])].sort(byCodePoint);
|
|
71
|
+
const accountedSet = new Set(accounted);
|
|
72
|
+
const unaccounted = [...changedPathSet].filter((path) => !accountedSet.has(path)).sort(byCodePoint);
|
|
73
|
+
const afterFindings = analyzeArchitectureFindings({
|
|
74
|
+
bundle: input.afterBundle,
|
|
75
|
+
policy: input.policy,
|
|
76
|
+
baselineSccs: baselineSccsFromFindings(input.baseline.findings)
|
|
77
|
+
});
|
|
78
|
+
const partitioned = partitionFindings(input.baseline.findings, afterFindings);
|
|
79
|
+
const afterMetrics = computeArchitectureMetrics({
|
|
80
|
+
graph: input.afterBundle.graph,
|
|
81
|
+
topology: input.afterBundle.topology,
|
|
82
|
+
layerViolationCount: countCodes(afterFindings, 'forbidden_dependency'),
|
|
83
|
+
ssotCollisionCount: countCodes(afterFindings, 'ssot_collision'),
|
|
84
|
+
authorityBypassCount: countCodes(afterFindings, 'authority_bypass'),
|
|
85
|
+
protectedVerificationGapCount: countCodes(afterFindings, 'protected_verification_gap')
|
|
86
|
+
});
|
|
87
|
+
const newBlocking = afterFindings
|
|
88
|
+
.filter((finding) => finding.severity === 'blocking' && finding.disposition === 'open')
|
|
89
|
+
.filter((finding) => !input.baseline.findings.some((prior) => prior.id === finding.id))
|
|
90
|
+
.map((finding) => finding.id)
|
|
91
|
+
.sort(byCodePoint);
|
|
92
|
+
const blockers = [];
|
|
93
|
+
if (!sealValid)
|
|
94
|
+
blockers.push('baseline_tampered');
|
|
95
|
+
if (input.baseline.policyHash !== input.afterBundle.policyHash ||
|
|
96
|
+
input.baseline.analyzerVersion !== input.afterBundle.analyzerVersion ||
|
|
97
|
+
input.baseline.serializerVersion !== input.afterBundle.serializerVersion) {
|
|
98
|
+
blockers.push('baseline_stale');
|
|
99
|
+
}
|
|
100
|
+
if (unaccounted.length)
|
|
101
|
+
blockers.push('unaccounted_changed_file');
|
|
102
|
+
if (newBlocking.length)
|
|
103
|
+
blockers.push('blocking_findings');
|
|
104
|
+
const verification = Object.freeze({
|
|
105
|
+
baselineValid: input.baseline.schema === 'sks.architecture-baseline.v1',
|
|
106
|
+
baselineTimely: input.baseline.capturedBeforeMutation === true,
|
|
107
|
+
baselineFresh: !blockers.includes('baseline_stale'),
|
|
108
|
+
sealValid,
|
|
109
|
+
changedPathAccounting: unaccounted.length === 0 ? 1 : 0,
|
|
110
|
+
projectionAccounting: 1,
|
|
111
|
+
blockers: Object.freeze([...blockers].sort(byCodePoint))
|
|
112
|
+
});
|
|
113
|
+
const blockingFindingIds = Object.freeze([...newBlocking, ...(unaccounted.length ? ['unaccounted_changed_file'] : [])].sort(byCodePoint));
|
|
114
|
+
const verdict = verification.baselineValid &&
|
|
115
|
+
verification.baselineTimely &&
|
|
116
|
+
verification.baselineFresh &&
|
|
117
|
+
verification.sealValid &&
|
|
118
|
+
verification.changedPathAccounting === 1 &&
|
|
119
|
+
newBlocking.length === 0 &&
|
|
120
|
+
unaccounted.length === 0
|
|
121
|
+
? 'pass'
|
|
122
|
+
: 'block';
|
|
123
|
+
const payload = {
|
|
124
|
+
schema: ARCHITECTURE_REVIEW_SCHEMA,
|
|
125
|
+
missionId: input.missionId,
|
|
126
|
+
baselineSeal: input.baseline.seal,
|
|
127
|
+
baselineHash: input.baseline.canonicalPayloadHash,
|
|
128
|
+
afterInputHash: input.afterBundle.canonicalHash,
|
|
129
|
+
changedPaths,
|
|
130
|
+
accountedChangedPaths: accounted,
|
|
131
|
+
unaccountedChangedPaths: unaccounted,
|
|
132
|
+
beforeMetrics: input.baseline.metrics,
|
|
133
|
+
afterMetrics,
|
|
134
|
+
metricDelta: metricDelta(input.baseline.metrics, afterMetrics),
|
|
135
|
+
newFindings: partitioned.newFindings,
|
|
136
|
+
resolvedFindings: partitioned.resolvedFindings,
|
|
137
|
+
persistentFindings: partitioned.persistentFindings,
|
|
138
|
+
acceptedExceptions: [],
|
|
139
|
+
projectionAccounting: input.projectionAccounting ?? {},
|
|
140
|
+
verification,
|
|
141
|
+
verdict,
|
|
142
|
+
blockingFindingIds
|
|
143
|
+
};
|
|
144
|
+
return Object.freeze({
|
|
145
|
+
...payload,
|
|
146
|
+
canonicalPayloadHash: hashCanonical(payload),
|
|
147
|
+
generatedAt: input.generatedAt
|
|
148
|
+
});
|
|
149
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { sha256 } from '../../../fsx.js';
|
|
2
|
+
import { sortStrings } from './contracts.js';
|
|
3
|
+
const FINDING_SCHEMA = 'sks.architecture-finding.v1';
|
|
4
|
+
export function findingId(input) {
|
|
5
|
+
const payload = [
|
|
6
|
+
FINDING_SCHEMA,
|
|
7
|
+
input.code,
|
|
8
|
+
...sortStrings(input.subjectIds),
|
|
9
|
+
...sortStrings(input.evidenceIds),
|
|
10
|
+
input.ruleId
|
|
11
|
+
].join('\n');
|
|
12
|
+
return `af_${sha256(payload).slice(0, 24)}`;
|
|
13
|
+
}
|
|
14
|
+
export function makeFinding(input) {
|
|
15
|
+
const subjectIds = Object.freeze(sortStrings(input.subjectIds));
|
|
16
|
+
const evidenceIds = Object.freeze(sortStrings(input.evidenceIds ?? []));
|
|
17
|
+
const ruleId = input.ruleId ?? '';
|
|
18
|
+
return Object.freeze({
|
|
19
|
+
id: findingId({ code: input.code, subjectIds, evidenceIds, ruleId }),
|
|
20
|
+
code: input.code,
|
|
21
|
+
severity: input.severity,
|
|
22
|
+
subjectIds,
|
|
23
|
+
evidenceIds,
|
|
24
|
+
ruleId,
|
|
25
|
+
message: input.message,
|
|
26
|
+
disposition: 'open'
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { byCodePoint } from './contracts.js';
|
|
2
|
+
import { findingId, makeFinding } from './finding-factory.js';
|
|
3
|
+
import { isAllowedLayerEdge, layerForModule } from './policy.js';
|
|
4
|
+
import { buildModuleDependencyGraph, computeSccs, sccKey } from './metrics.js';
|
|
5
|
+
import { mergeGraphWithTopology } from './topology-overlay.js';
|
|
6
|
+
import { canonicalSsotAuthorityInventory } from '../../../safety/ssot-authority-inventory.js';
|
|
7
|
+
import { analyzeSsotCollisions } from './ssot-analysis.js';
|
|
8
|
+
export { findingId, makeFinding };
|
|
9
|
+
const ANALYSIS_REQUIRED_KINDS = ['module'];
|
|
10
|
+
const ANALYSIS_REQUIRED_EDGES = ['imports'];
|
|
11
|
+
function severityFor(code, policy) {
|
|
12
|
+
return policy.blockingCodes.includes(code) ? 'blocking' : 'warning';
|
|
13
|
+
}
|
|
14
|
+
export function graphSufficiencyIssues(graph) {
|
|
15
|
+
const kinds = new Set(graph.nodes.map((node) => node.kind));
|
|
16
|
+
const edgeTypes = new Set(graph.edges.map((edge) => edge.type));
|
|
17
|
+
const missing = [];
|
|
18
|
+
for (const kind of ANALYSIS_REQUIRED_KINDS) {
|
|
19
|
+
if (!kinds.has(kind))
|
|
20
|
+
missing.push(`kind:${kind}`);
|
|
21
|
+
}
|
|
22
|
+
for (const type of ANALYSIS_REQUIRED_EDGES) {
|
|
23
|
+
if (!edgeTypes.has(type))
|
|
24
|
+
missing.push(`edge:${type}`);
|
|
25
|
+
}
|
|
26
|
+
if (!graph.nodes.length)
|
|
27
|
+
missing.push('nodes:empty');
|
|
28
|
+
return Object.freeze(missing.sort(byCodePoint));
|
|
29
|
+
}
|
|
30
|
+
function insufficientGraphFinding(policy, missing) {
|
|
31
|
+
return makeFinding({
|
|
32
|
+
code: 'insufficient_graph',
|
|
33
|
+
severity: severityFor('insufficient_graph', policy),
|
|
34
|
+
subjectIds: missing,
|
|
35
|
+
evidenceIds: missing,
|
|
36
|
+
ruleId: 'graph-sufficiency',
|
|
37
|
+
message: `Architecture graph lacks required kinds/edges for analysis: ${missing.join(', ')}`
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function detectNewAndExpandedCycles(input) {
|
|
41
|
+
const baselineKeys = new Map(input.baselineSccs.map((members) => [sccKey(members), members]));
|
|
42
|
+
const findings = [];
|
|
43
|
+
for (const members of input.afterSccs) {
|
|
44
|
+
const key = sccKey(members);
|
|
45
|
+
const prior = baselineKeys.get(key);
|
|
46
|
+
if (!prior) {
|
|
47
|
+
let expandedFrom = null;
|
|
48
|
+
for (const baseline of input.baselineSccs) {
|
|
49
|
+
if (baseline.every((id) => members.includes(id)) && members.length > baseline.length) {
|
|
50
|
+
expandedFrom = baseline;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (expandedFrom) {
|
|
55
|
+
findings.push(makeFinding({
|
|
56
|
+
code: 'cycle_expansion',
|
|
57
|
+
severity: severityFor('cycle_expansion', input.policy),
|
|
58
|
+
subjectIds: members,
|
|
59
|
+
evidenceIds: expandedFrom,
|
|
60
|
+
ruleId: 'cycle-expansion',
|
|
61
|
+
message: `Dependency cycle expanded from ${expandedFrom.length} to ${members.length} modules`
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
findings.push(makeFinding({
|
|
66
|
+
code: 'new_cycle',
|
|
67
|
+
severity: severityFor('new_cycle', input.policy),
|
|
68
|
+
subjectIds: members,
|
|
69
|
+
evidenceIds: members,
|
|
70
|
+
ruleId: 'new-cycle',
|
|
71
|
+
message: `New dependency cycle among ${members.length} modules`
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return findings.sort((left, right) => byCodePoint(left.id, right.id));
|
|
77
|
+
}
|
|
78
|
+
export function detectForbiddenDependencies(input) {
|
|
79
|
+
const findings = [];
|
|
80
|
+
for (const key of input.edgeKeys) {
|
|
81
|
+
const [from, to] = key.split('->');
|
|
82
|
+
if (!from || !to)
|
|
83
|
+
continue;
|
|
84
|
+
const fromLayer = layerForModule(input.policy, modulePathFromId(from));
|
|
85
|
+
const toLayer = layerForModule(input.policy, modulePathFromId(to));
|
|
86
|
+
if (!fromLayer || !toLayer)
|
|
87
|
+
continue;
|
|
88
|
+
if (!isAllowedLayerEdge(input.policy, fromLayer, toLayer)) {
|
|
89
|
+
findings.push(makeFinding({
|
|
90
|
+
code: 'forbidden_dependency',
|
|
91
|
+
severity: severityFor('forbidden_dependency', input.policy),
|
|
92
|
+
subjectIds: [from, to],
|
|
93
|
+
evidenceIds: [key],
|
|
94
|
+
ruleId: `${fromLayer}->${toLayer}`,
|
|
95
|
+
message: `Forbidden layer edge ${fromLayer} -> ${toLayer} via ${from} -> ${to}`
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return findings.sort((left, right) => byCodePoint(left.id, right.id));
|
|
100
|
+
}
|
|
101
|
+
function modulePathFromId(moduleId) {
|
|
102
|
+
return moduleId.startsWith('module:') ? moduleId.slice('module:'.length) : moduleId;
|
|
103
|
+
}
|
|
104
|
+
export function detectVerificationGaps(input) {
|
|
105
|
+
const protectedNodes = input.graph.nodes.filter((node) => node.risk === 'protected' || node.risk === 'high');
|
|
106
|
+
if (!protectedNodes.length)
|
|
107
|
+
return [];
|
|
108
|
+
const verifiedTargets = new Set();
|
|
109
|
+
for (const edge of input.graph.edges) {
|
|
110
|
+
if (edge.type === 'tests' || edge.type === 'verified_by' || edge.type === 'gated_by') {
|
|
111
|
+
verifiedTargets.add(edge.to);
|
|
112
|
+
verifiedTargets.add(edge.from);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const kinds = new Set(input.graph.nodes.map((node) => node.kind));
|
|
116
|
+
if (!kinds.has('test') && !kinds.has('gate') && !kinds.has('proof')) {
|
|
117
|
+
return [
|
|
118
|
+
makeFinding({
|
|
119
|
+
code: 'insufficient_graph',
|
|
120
|
+
severity: severityFor('insufficient_graph', input.policy),
|
|
121
|
+
subjectIds: ['kind:test', 'kind:gate', 'kind:proof'],
|
|
122
|
+
evidenceIds: protectedNodes.map((node) => node.id),
|
|
123
|
+
ruleId: 'verification-kinds',
|
|
124
|
+
message: 'Graph lacks test/gate/proof kinds required to evaluate verification gaps'
|
|
125
|
+
})
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
const findings = [];
|
|
129
|
+
for (const node of protectedNodes) {
|
|
130
|
+
if (!verifiedTargets.has(node.id)) {
|
|
131
|
+
findings.push(makeFinding({
|
|
132
|
+
code: 'protected_verification_gap',
|
|
133
|
+
severity: severityFor('protected_verification_gap', input.policy),
|
|
134
|
+
subjectIds: [node.id],
|
|
135
|
+
evidenceIds: [node.id],
|
|
136
|
+
ruleId: 'protected-verification-gap',
|
|
137
|
+
message: `Protected/high-risk node ${node.id} has no tests/gate/evidence path`
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return findings.sort((left, right) => byCodePoint(left.id, right.id));
|
|
142
|
+
}
|
|
143
|
+
export function analyzeArchitectureFindings(input) {
|
|
144
|
+
const missing = graphSufficiencyIssues(input.bundle.graph);
|
|
145
|
+
if (missing.length) {
|
|
146
|
+
return Object.freeze([insufficientGraphFinding(input.policy, missing)]);
|
|
147
|
+
}
|
|
148
|
+
const merged = mergeGraphWithTopology(input.bundle.graph, input.bundle.topology);
|
|
149
|
+
const moduleGraph = buildModuleDependencyGraph(merged.nodes, merged.edges);
|
|
150
|
+
const afterSccs = computeSccs(moduleGraph);
|
|
151
|
+
const baselineSccs = input.baselineSccs ?? [];
|
|
152
|
+
const findings = [
|
|
153
|
+
...detectNewAndExpandedCycles({
|
|
154
|
+
policy: input.policy,
|
|
155
|
+
baselineSccs,
|
|
156
|
+
afterSccs
|
|
157
|
+
}),
|
|
158
|
+
...detectForbiddenDependencies({
|
|
159
|
+
policy: input.policy,
|
|
160
|
+
moduleIds: moduleGraph.moduleIds,
|
|
161
|
+
edgeKeys: moduleGraph.edgeKeys
|
|
162
|
+
}),
|
|
163
|
+
...analyzeSsotCollisions({
|
|
164
|
+
policy: input.policy,
|
|
165
|
+
inventory: canonicalSsotAuthorityInventory()
|
|
166
|
+
}),
|
|
167
|
+
...detectVerificationGaps({
|
|
168
|
+
policy: input.policy,
|
|
169
|
+
graph: input.bundle.graph
|
|
170
|
+
})
|
|
171
|
+
];
|
|
172
|
+
return Object.freeze(findings.sort((left, right) => byCodePoint(left.id, right.id)));
|
|
173
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from '../../../json/canonical.js';
|
|
3
|
+
import { sha256 } from '../../../fsx.js';
|
|
4
|
+
import { byCodePoint } from './contracts.js';
|
|
5
|
+
export function hashCanonical(value) {
|
|
6
|
+
return sha256(canonicalJson(value));
|
|
7
|
+
}
|
|
8
|
+
export function hashWithoutKeys(value, omit) {
|
|
9
|
+
const copy = {};
|
|
10
|
+
for (const key of Object.keys(value).sort(byCodePoint)) {
|
|
11
|
+
if (omit.includes(key))
|
|
12
|
+
continue;
|
|
13
|
+
copy[key] = value[key];
|
|
14
|
+
}
|
|
15
|
+
return hashCanonical(copy);
|
|
16
|
+
}
|
|
17
|
+
export function sealBaselinePayload(canonicalPayloadHash) {
|
|
18
|
+
return sha256(`sks.architecture-baseline.v1\n${canonicalPayloadHash}`);
|
|
19
|
+
}
|
|
20
|
+
export function buildWorktreeFingerprint(input) {
|
|
21
|
+
const paths = [...input.paths].sort((a, b) => byCodePoint(a.path, b.path));
|
|
22
|
+
const fingerprintHash = hashCanonical({
|
|
23
|
+
rootId: input.rootId,
|
|
24
|
+
head: input.head,
|
|
25
|
+
paths
|
|
26
|
+
});
|
|
27
|
+
const dirtyPaths = Object.freeze(paths.filter((entry) => entry.kind !== 'missing').map((entry) => entry.path));
|
|
28
|
+
return Object.freeze({
|
|
29
|
+
rootId: input.rootId,
|
|
30
|
+
head: input.head,
|
|
31
|
+
repositoryHead: input.head,
|
|
32
|
+
paths: Object.freeze(paths),
|
|
33
|
+
fingerprintHash,
|
|
34
|
+
hash: fingerprintHash,
|
|
35
|
+
dirtyPaths
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export function emptyWorktreeFingerprint(rootId, head = null) {
|
|
39
|
+
return buildWorktreeFingerprint({ rootId, head, paths: [] });
|
|
40
|
+
}
|
|
41
|
+
export function shortHash(value, length = 12) {
|
|
42
|
+
return createHash('sha256').update(value).digest('hex').slice(0, length);
|
|
43
|
+
}
|
|
44
|
+
export function diffWorktreePaths(before, after) {
|
|
45
|
+
const beforeMap = new Map(before.map((entry) => [entry.path, entry]));
|
|
46
|
+
const afterMap = new Map(after.map((entry) => [entry.path, entry]));
|
|
47
|
+
const paths = new Set([...beforeMap.keys(), ...afterMap.keys()]);
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const path of [...paths].sort(byCodePoint)) {
|
|
50
|
+
const left = beforeMap.get(path);
|
|
51
|
+
const right = afterMap.get(path);
|
|
52
|
+
if (!left && right) {
|
|
53
|
+
out.push({ path, change: 'added' });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (left && !right) {
|
|
57
|
+
out.push({ path, change: 'removed' });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!left || !right)
|
|
61
|
+
continue;
|
|
62
|
+
if (left.kind !== right.kind) {
|
|
63
|
+
out.push({ path, change: 'type' });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (left.sha256 !== right.sha256) {
|
|
67
|
+
out.push({ path, change: 'content' });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (left.executable !== right.executable) {
|
|
71
|
+
out.push({ path, change: 'mode' });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return Object.freeze(out);
|
|
75
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './contracts.js';
|
|
2
|
+
export * from './policy.js';
|
|
3
|
+
export * from './fingerprint.js';
|
|
4
|
+
export * from './input-bundle.js';
|
|
5
|
+
export * from './topology-overlay.js';
|
|
6
|
+
export * from './voxel-context.js';
|
|
7
|
+
export * from './slice.js';
|
|
8
|
+
export * from './metrics.js';
|
|
9
|
+
export * from './finding-factory.js';
|
|
10
|
+
export * from './findings.js';
|
|
11
|
+
export * from './ssot-analysis.js';
|
|
12
|
+
export * from './baseline.js';
|
|
13
|
+
export * from './delta.js';
|
|
14
|
+
export * from './capsule.js';
|
|
15
|
+
export * from './validation.js';
|